diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a46b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ +*.iml +*.log +.DS_Store diff --git a/.tmp-body.json b/.tmp-body.json new file mode 100644 index 0000000..56486aa --- /dev/null +++ b/.tmp-body.json @@ -0,0 +1 @@ +{"title":"【测试】curl-UTF8文件体","content":"
x
curl
路径前缀:{@code Config.API_PATH + "/base/"}(原硬编码已修正)。 + * 承载平台代理接口:GetUserInfo / GetUserMod / GetSchoolAll / GetGroupUser / Logout, + * 业务逻辑已下沉 {@link IUserCenterService},本控制器仅做"取参 → 调 Service → 返回"。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
路径前缀:{@code Config.API_PATH + "/contacts/"}(原硬编码已修正为 Config 拼接)。 + * 方法名已为 PascalCase 且写操作已是 POST,保持不动。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
路径前缀:{@code Config.API_PATH + "/dict/"}(原 base/ 前缀已修正)。 + * 写操作一律 POST(原 DelDept/OrdbyDicIds 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/dict/") +public class DictController { + + @Autowired + private IDictService dictService; + + /** + * 获取系统字典(下拉框用)。 + */ + @GetMapping("List") + public List> list(@RequestParam(value = "type", defaultValue = "0") Integer type) { + return dictService.getSysDic(type); + } + + /** + * 获取系统字典管理列表。 + */ + @GetMapping("ManageList") + public List> manageList(@RequestParam(value = "type", defaultValue = "dept") String type) { + return dictService.getDictMangeList(type); + } + + /** + * 添加发文单位。 + */ + @PostMapping("Add") + public void add(@RequestParam("name") String name) { + dictService.addDeptDic(name); + } + + /** + * 更新发文单位。 + */ + @PostMapping("Update") + public void update( + @RequestParam("SerID") Integer serID, + @RequestParam("Name") String name, + @RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable) { + dictService.updateDeptDic(serID, name, enable); + } + + /** + * 删除发文单位(原 GET DelDept,已 POST 化)。 + */ + @PostMapping("Delete") + public void delete(@RequestParam("serid") Integer serID) { + dictService.delDept(serID); + } + + /** + * 发文单位排序(原 GET OrdbyDicIds,已 POST 化)。 + */ + @PostMapping("Sort") + public void sort(@RequestParam("ids") String ids) { + dictService.ordbyDicIds(ids); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/FileController.java b/src/main/java/com/yunhai/campusnotice/controller/api/FileController.java new file mode 100644 index 0000000..d259efd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/FileController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.service.IFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 附件域接口(file/)。 + * 路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。 + * 方法名统一 PascalCase;写操作一律 POST。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/file/") +public class FileController { + + @Autowired + private IFileService fileService; + + /** + * 附件上传(单文件)。 + * 前端: POST file/Upload (form-data) + * 返回: { fileid, host, fileurl, name, type, size } + */ + @PostMapping("Upload") + public UpFileResultVo upload(@RequestParam("file") MultipartFile file) { + return fileService.uploadToResult(file, "notice"); + } + + /** + * 图片上传(多文件,返回数组)。 + * 前端: POST file/UploadImage (form-data, accept=image/*) + */ + @PostMapping("UploadImage") + public List uploadImage(@RequestParam("file") MultipartFile[] files) { + return fileService.uploadImages(files, "notice"); + } + /** + * 文件下载(浏览器直接 location.href 跳转)。 + */ + @GetMapping("Down") + public void down(@RequestParam("id") Integer id, HttpServletResponse response) { + fileService.download(id, response); + } + + /** + * 文件在线预览(inline 输出,前端取 Blob 交给 open-file-viewer 渲染)。 + */ + @GetMapping("Preview") + public void preview(@RequestParam("id") Integer id, + @RequestParam(value = "type", required = false) Integer type, + HttpServletResponse response) { + fileService.preview(id, response); + } + + /** + * 删除文件。 + */ + @PostMapping("Delete") + public void delete(@RequestParam("fileId") Integer fileId) { + fileService.delete(fileId); + } + + /** + * 查询某通知下的附件列表。 + */ + @GetMapping("List") + public List list(@RequestParam("topicId") Integer topicId) { + return fileService.listByTopic("notice", topicId); + } + + /** + * 批量绑定附件到通知。 + */ + @PostMapping("Bind") + public void bind(@RequestParam("fileIds") List fileIds, + @RequestParam("topicId") Integer topicId) { + fileService.bindToTopic(fileIds, topicId); + } + +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/NoticeController.java b/src/main/java/com/yunhai/campusnotice/controller/api/NoticeController.java new file mode 100644 index 0000000..77b9371 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/NoticeController.java @@ -0,0 +1,369 @@ +package com.yunhai.campusnotice.controller.api; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import com.yunhai.campusnotice.service.INoticeService; +import com.yunhai.campusnotice.utils.OtherUtil; +import com.yunhai.campusnotice.utils.PageUtils; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.print.attribute.standard.MediaSize; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * 通知公告接口(合并 NoticeApiController + 旧 NoticeController)。 + * 路径前缀:{@code Config.API_PATH + "/notice/"}(platform = api / manage / mobiapi)。 + * 方法动词式 PascalCase(对齐 UserBase);写操作一律 POST。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/notice/") +public class NoticeController { + + @Autowired + private INoticeService noticeService; + // ============================ 收件箱列表 ============================ + + /** + * 已收通知列表(用户端)。 + */ + @GetMapping("List") + public NoticePageResult list(NoticeQuery query, + @SortDefault(sort = {"topmost", "createTime"}, + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.userList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 已发/全部通知列表 ============================ + + /** + * 已发/全部通知列表(管理端)。type=user 只看自己。 + */ + @GetMapping("SendList") + public BasePageResult sendList(NoticeQuery query, + @SortDefault(sort = "createTime", + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.adminList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 查看详情(标读) ============================ + + /** + * 查看通知详情(type=1 普通查看标记已读, type=0 编辑模式不标记已读)。 + */ + @GetMapping("Detail") + public NoticeDetailVo detail(@RequestParam("serid") Integer serid, + @RequestParam(value = "type", defaultValue = "1") Integer type) { + return noticeService.detailMap(serid, type); + } + + // ============================ 编辑回显(不标读) ============================ + + /** + * 获取通知详情(编辑/转发用,不标记已读)。 + */ + @GetMapping("GetDetail") + public NoticeDetailVo getDetail(@RequestParam("serid") Integer serid) { + return noticeService.getDetailMap(serid); + } + + // ============================ 新建 ============================ + + /** + * 新建通知(JSON body)。fileIds 为附件ID逗号串,smsFlag 为短信标记。 + */ + @PostMapping("Create") + public Integer create(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) { + return noticeService.publish(request); + } + + // ============================ 编辑 ============================ + + /** + * 编辑通知(JSON body,含 id)。 + */ + @PostMapping("Edit") + public void edit(@RequestBody @Validated(ValidationGroups.Update.class) NoticeRequest request) { + noticeService.edit(request.getSourceId() != null ? request.getSourceId() : 0, request); + } + + // ============================ 批量删除 ============================ + + /** + * 批量删除通知(ids 数组)。 + */ + @PostMapping("Delete") + public void delete(@RequestBody List 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> readDetail(@RequestParam("serid") Integer serid) { + return noticeService.getReadDetailGrouped(serid); + } + + // ============================ 上一篇/下一篇 ============================ + + /** + * 上一篇/下一篇通知ID。 + */ + @GetMapping("GetNextPrev") + public Map getNextPrev(@RequestParam("serid") Integer serid, + @RequestParam("type") String type) { + return noticeService.getNextPrev(serid, type); + } + + // ============================ 附件打包下载 ============================ + + /** + * 批量下载通知全部附件(打包 zip)。 + */ + @GetMapping("AllFileDown") + public void allFileDown(@RequestParam("dataid") Integer dataid, + HttpServletResponse response) { + noticeService.downloadAllFiles(dataid, response); + } + + // ============================ 统计 ============================ + + /** + * 通知统计。 + */ + @GetMapping("Statis") + public NoticeStatisticsVo statis( + @RequestParam(value = "starttime", required = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start, + @RequestParam(value = "endtime", required = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) { + return noticeService.statistics(new NoticeQuery(), start, end); + } + + // ============================ 转发(合并旧 REST 壳 forward) ============================ + + /** + * 转发通知(基于源通知创建新通知)。sourceId 在 request body 中传递。 + */ + @PostMapping("Forward") + public Integer forward(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) { + return noticeService.forward(request.getSourceId(), request); + } + + // ============================ 推送范围树(新版发布表单,改造方案 3.1) ============================ + + /** + * 当前学校学员推送范围树:全校根 + 学段→年级→班级(数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。节点结构:{scopeType, scopeId, name, children}。 + */ + @GetMapping("ScopeTree") + public List> scopeTree() { + return noticeService.scopeTree(); + } + + // ============================ 推送范围预估 ============================ + + /** + * 按已选范围预估接收人数(学员 = 范围展开去重账号数 + 教师 = receiveList 定向名单数)。 + * scopes 为 JSON 数组:[{"scopeType":3,"scopeId":5,"scopeName":"初一"}] + */ + @GetMapping("ScopePreview") + public ScopePreviewVo scopePreview(@RequestParam(value = "scopes", required = false) String scopes, + @RequestParam(value = "receiveList", required = false) String receiveList) { + return noticeService.scopePreview(parseScopes(scopes), receiveList); + } + + // ============================ 存为草稿 ============================ + + /** + * 存为草稿(status=0):不圈人、不发短信、不落接收人,仅存内容 + 范围快照 + 附件。 + */ + @PostMapping("SaveDraft") + public Integer saveDraft(@RequestBody NoticeRequest request) { + return noticeService.saveDraft(request); + } + + // ============================ 草稿箱列表 ============================ + + /** + * 草稿箱列表(status=0 本人草稿):重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + @GetMapping("DraftList") + public BasePageResult draftList(NoticeQuery query, + @SortDefault(sort = "updateTime", + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.draftList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 已发送阅读回执 ============================ + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + @GetMapping("ReadReceipt") + public ReadReceiptVo readReceipt(@RequestParam("serid") Integer serid) { + return noticeService.readReceipt(serid); + } + + // ============================ 审批人记忆回显 ============================ + + /** + * 审批人记忆回显:当前发布人最近一次所选审批人组合(无记忆返回空)。 + */ + @GetMapping("ApproverSuggest") + public ApproverSuggestVo approverSuggest() { + return noticeService.approverSuggest(); + } + + // ============================ 审批教师绑定(教师树ID→姓名/头像) ============================ + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * ids=101,102(教师树叶子节点ID);schId 缺省取当前登录人学校; + * 未命中的ID保留、name 为 null,由前端提示。 + */ + @GetMapping("ApproverResolve") + public List approverResolve( + @RequestParam(value = "ids", required = false) String ids, + @RequestParam(value = "schId", required = false) Integer schId) { + if (ids == null || ids.trim().isEmpty()) { + throw new BizException(ErrorCode.PARAM_ERROR, "ids 不允许为空"); + } + return noticeService.approverResolve(Arrays.asList(OtherUtil.convertToArray(ids)), schId); + } + + // ============================ 提交审批 ============================ + + /** + * 提交审批(status: 0/3→2):发布人从教师树选定审批教师(可多选,或签), + * 写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + @PostMapping("Submit") + public void submit(@RequestBody SubmitRequest request) { + noticeService.submit(request.getSerid(), request.getApproverTeaIds(), request.getOpinion()); + } + + // ============================ 审批通过 ============================ + + /** + * 审批通过(或签):任一审批人通过即 status→1 并触发真实发布; + * 被他人先审时返回带实际处理人/时间的冲突提示。 + */ + @PostMapping("Approve") + public void approve(@RequestParam("serid") Integer serid, + @RequestParam(value = "opinion", required = false) String opinion) { + noticeService.approve(serid, opinion); + } + + // ============================ 审批驳回 ============================ + + /** + * 审批驳回(或签):任一审批人驳回即 status→3,意见必填;发布人可改后重新提交。 + */ + @PostMapping("Reject") + public void reject(@RequestParam("serid") Integer serid, + @RequestParam("opinion") String opinion) { + noticeService.reject(serid, opinion); + } + + // ============================ 审批列表(待审/已审合一) ============================ + + /** + * 审批列表:按审批人快照含当前账号过滤(schId 隔离)。 + * auditState=pending(默认) 待审 / reviewed 已审(含或签下他人已处理,带实际处理人)。 + */ + @GetMapping("ApprovalList") + public BasePageResult approvalList( + @RequestParam(value = "auditState", defaultValue = "pending") String auditState, + NoticeQuery query, + @SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) { + return noticeService.approvalList(auditState, query, PageUtils.toPageable(query, sort)); + } + + /** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */ + private List parseScopes(String scopes) { + if (scopes == null || scopes.trim().isEmpty()) { + return null; + } + try { + return new ObjectMapper().readValue(scopes, new TypeReference>() { + }); + } catch (IOException e) { + throw new BizException(ErrorCode.PARAM_ERROR, "scopes 参数格式错误,应为 JSON 数组:[{\"scopeType\":3,\"scopeId\":5}]"); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java b/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java new file mode 100644 index 0000000..f58015a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.service.ISmsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置接口(短信域)。 + * 路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。 + * 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sms/") +public class SmsController { + + @Autowired + private ISmsService smsService; + + /** + * 获取短信模块列表。 + */ + @GetMapping("GetModule") + public List> getModule() { + return smsService.getModuleList(); + } + + /** + * 获取学校父类组列表。 + */ + @GetMapping("GetGroup") + public List> getGroup() { + return smsService.getSchParentGroup(); + } + + /** + * 分页获取学校短信开关列表。 + */ + @GetMapping("GetSchoolSms") + public Map getSchoolSms( + @RequestParam(value = "schpid", required = false) String schpid, + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "SchName", required = false, defaultValue = "") String schName) { + return smsService.getSchIsSmsList(pageSize, pageIndex, schName); + } + + /** + * 获取学校列表。 + */ + @GetMapping("GetSchool") + public List> getSchool() { + return smsService.getSchList(); + } + + /** + * 设置单校单模块短信开关。 + */ + @PostMapping("SetSchoolSms") + public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) { + smsService.setSchSmsFromJson(itemsModuJson); + } + + /** + * 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。 + */ + @PostMapping("SetAllSchoolSms") + public void setAllSchoolSms( + @RequestParam("SchSerID") Integer schSerID, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchSmsAll(schSerID, isSms); + } + + /** + * 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。 + */ + @PostMapping("SetSchoolModuleSms") + public void setSchoolModuleSms( + @RequestParam("SchSerID") String schSerID, + @RequestParam("Module") String module, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchModuleSms(schSerID, module, isSms); + } + +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java new file mode 100644 index 0000000..d512ae1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.service.ITemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板接口(模板域)。 + * 路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。 + * 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/template/") +public class TemplateController { + + @Autowired + private ITemplateService templateService; + + /** + * 获取模板类型列表。 + */ + @GetMapping("Typelist") + public List> getTypelist() { + return templateService.getTypelist(); + } + + /** + * 新增/编辑模板类型(原 GET EditType,已 POST 化)。 + */ + @PostMapping("TypeEdit") + public void typeEdit( + @RequestParam("typeid") Integer typeid, + @RequestParam("TypeName") String typeName) { + templateService.editType(typeid, typeName); + } + + /** + * 删除模板类型(原 GET DelType,已 POST 化)。 + */ + @PostMapping("TypeDelete") + public void typeDelete(@RequestParam("typeid") Integer typeid) { + templateService.delType(typeid); + } + + /** + * 分页获取模板列表。 + */ + @GetMapping("List") + public Map list( + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "typeid", defaultValue = "0") Integer typeid) { + return templateService.getTemplatelist(pageSize, pageIndex, typeid); + } + + /** + * 新增/编辑模板。 + */ + @PostMapping("TemplateEdit") + public void templateEdit( + @RequestParam(value = "SerID", required = false) Integer serID, + @RequestParam("Title") String title, + @RequestParam("Contents") String contents, + @RequestParam(value = "Type", required = false) Integer type, + @RequestParam(value = "Cover", required = false) Integer cover) { + templateService.editTemplate(serID, title, contents, type, cover); + } + + /** + * 删除模板(原 GET DelTemplate,已 POST 化)。 + */ + @PostMapping("TemplateDelete") + public void templateDelete(@RequestParam("SerID") Integer serid) { + templateService.delTemplate(serid); + } + + /** + * 获取单个模板详情。 + */ + @GetMapping("TemplateFirst") + public Map templateFirst(@RequestParam("serid") Integer serid) { + return templateService.getTemplateFist(serid); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java new file mode 100644 index 0000000..1daf5a9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.yunhai.campusnotice.entity; + +import com.yunhai.campusnotice.common.listener.NoticeAuditListener; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。 + */ +@MappedSuperclass +@Getter +@Setter +@EntityListeners(NoticeAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 创建人登录账号 + */ + @Column(name = "create_by", updatable = false) + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time", updatable = false) + private LocalDateTime createTime; + + /** + * 更新人登录账号 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java new file mode 100644 index 0000000..c71e63d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 联系组。对应原 .NET Contacts_Group 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroup extends BaseEntity { + + /** 组名称 */ + @Column(name = "group_name", length = 200) + private String groupName; + + /** 创建人账号 */ + @Column(name = "creator_account", length = 64) + private String creatorAccount; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java new file mode 100644 index 0000000..b87dee9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 联系组-用户关联。对应原 .NET Contacts_Tea 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group_user", + uniqueConstraints = @UniqueConstraint(name = "uk_group_user", + columnNames = {"group_id", "user_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroupUser extends BaseEntity { + + /** 联系组ID */ + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** 用户ID(基础平台用户ID) */ + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** 用户账号 */ + @Column(name = "user_account", length = 64) + private String userAccount; + + /** 用户姓名 */ + @Column(name = "user_name", length = 64) + private String userName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/Notice.java b/src/main/java/com/yunhai/campusnotice/entity/Notice.java new file mode 100644 index 0000000..97c056a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/Notice.java @@ -0,0 +1,174 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 校园公告主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。 + * + * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead}, + * 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class Notice extends BaseEntity { + + /** + * 通知标题 + */ + @Column(name = "title", nullable = false, length = 200) + private String title; + + /** + * 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。 + */ + @Column(name = "sch_id") + private Integer schId; + + /** + * 通知内容 + */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** + * 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum}) + */ + @Column(name = "type", nullable = false) + private Integer type = 1; + + /** + * 发布人登录账号 + */ + @Column(name = "publisher_account", length = 64) + private String publisherAccount; + + /** + * 发布人姓名 + */ + @Column(name = "publisher_name", length = 64) + private String publisherName; + + /** + * 所属分类ID(原 DeptID) + */ + @Column(name = "dept_id", length = 64) + private String deptId; + + /** + * 所属分类名称 + */ + @Column(name = "dept_name", length = 128) + private String deptName; + + /** + * 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}: + * 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。 + */ + @Column(name = "status", nullable = false) + private Integer status = 0; + + /** + * 是否置顶:0 否 / 1 是(原 topmost) + */ + @Column(name = "topmost") + private Integer topmost = 0; + + // ==================== 迁移补全字段(对应 C# Notify_Message) ==================== + + /** + * 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。 + * 对应 C# Notify_Message.ReceiveList + */ + @Column(name = "receive_list", columnDefinition = "LONGTEXT") + private String receiveList; + + /** + * 密码发送(对应 C# Notify_Message.PwdSend) + */ + @Column(name = "pwd_send", length = 255) + private String pwdSend; + + /** + * 抄送人列表(对应 C# Notify_Message.ChaoSend) + */ + @Column(name = "chao_send", length = 2000) + private String chaoSend; + + /** + * 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。 + * 对应 C# Notify_Message.readcount + */ + @Column(name = "read_count") + private Integer readCount = 0; + + /** + * 应读数(接收人总数)。对应 C# Notify_Message.allowcount + */ + @Column(name = "allow_count") + private Integer allowCount = 0; + + /** + * 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 回复ID。对应 C# Notify_Message.reid + */ + @Column(name = "reply_id", length = 255) + private String replyId; + + /** + * 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull + */ + @Column(name = "is_pulled") + private Integer isPulled = 0; + + /** + * 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime + */ + @Column(name = "timing_send_time") + private LocalDateTime timingSendTime; + + /** + * 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit + */ + @Column(name = "sms_flag") + private Integer smsFlag = 0; + + // ==================== 新版发布表单扩展字段(改造方案 2.1) ==================== + + /** + * 是否需要学员阅读回执:0 否 / 1 是。 + * =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示; + * =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。 + */ + @Column(name = "receipt_flag") + private Integer receiptFlag = 0; + + /** + * 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。 + * ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。 + * 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。 + */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + + /** + * 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。 + * 立即发布的通知在真实发布时一并置 1。 + */ + @Column(name = "timing_published") + private Integer timingPublished = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..314e007 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + * + * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */ + @Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT") + private String assigneeTeaIds; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..41c9c17 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + * + * 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_tea_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人教师ID(用户中心用户ID,按人记忆) */ + @Column(name = "publisher_tea_id", nullable = false) + private Integer publisherTeaId; + + /** 最近一次所选审批教师ID JSON [101,102] */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + * + * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java new file mode 100644 index 0000000..81ad83e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java @@ -0,0 +1,71 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + * + * 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_read", + uniqueConstraints = @UniqueConstraint( + name = "uk_notice_reader", + columnNames = {"notice_id", "reader_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeRead extends BaseEntity { + + /** + * 通知ID(关联 notice.id) + */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** + * 读者登录账号 + */ + @Column(name = "reader_account", nullable = false, length = 64) + private String readerAccount; + + /** + * 读者姓名 + */ + @Column(name = "reader_name", length = 64) + private String readerName; + + /** + * 阅读时间 + */ + @Column(name = "read_time") + private LocalDateTime readTime; + + // ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ==================== + + /** 学校ID(取通知 sch_id) */ + @Column(name = "sch_id") + private Integer schId; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照 */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照 */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java new file mode 100644 index 0000000..225c8de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java @@ -0,0 +1,67 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。 + * + * 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。 + * 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入, + * 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_receiver", + indexes = { + @Index(name = "idx_notice_id", columnList = "notice_id"), + @Index(name = "idx_account", columnList = "account") + }) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeReceiver extends BaseEntity { + + /** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */ + public static final int TYPE_TEACHER = 1; + public static final int TYPE_STUDENT = 2; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 接收人登录账号(学员=SchStudent.account) */ + @Column(name = "account", nullable = false, length = 64) + private String account; + + /** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */ + @Column(name = "receive_type", nullable = false) + private Integer receiveType = TYPE_TEACHER; + + // ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ==================== + + /** 学员姓名快照 */ + @Column(name = "stu_name", length = 64) + private String stuName; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照(如「初二年级」) */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照(sch_class.class_id) */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java new file mode 100644 index 0000000..5ed9fba --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板。对应原 .NET Notify_Template 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_template") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeTemplate extends BaseEntity { + + /** 模板标题 */ + @Column(name = "title", length = 200) + private String title; + + /** 模板内容(HTML) */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** 封面图片文件ID */ + @Column(name = "cover") + private Integer cover; + + /** 引用次数 */ + @Column(name = "ref_count") + private Integer refCount = 0; + + /** 模板类型ID */ + @Column(name = "type_id") + private Integer typeId; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java new file mode 100644 index 0000000..a365b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 短信模块配置。对应原 .NET SchSms 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_module") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsModule extends BaseEntity { + + /** 模块名称 */ + @Column(name = "module_name", length = 100) + private String moduleName; + + /** 模块标识 */ + @Column(name = "module_code", length = 50) + private String moduleCode; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..30dbd56 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 学校短信开通配置。对应原 .NET SchSms_Set 表。 + * 记录某学校某模块是否开通短信。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_school_config") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsSchoolConfig extends BaseEntity { + + /** 学校ID */ + @Column(name = "school_id", nullable = false) + private Integer schoolId; + + /** 学校名称 */ + @Column(name = "school_name", length = 200) + private String schoolName; + + /** 模块ID */ + @Column(name = "module_id") + private Integer moduleId; + + /** 是否开通短信 */ + @Column(name = "is_sms") + private Boolean isSms = false; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SysDict.java b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java new file mode 100644 index 0000000..681acaa --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_dict") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysDict extends BaseEntity { + + /** 字典类型(如 "dept"=所属分类) */ + @Column(name = "dict_type", length = 50) + private String dictType; + + /** 字典 key(原 CodeID) */ + @Column(name = "dict_key", length = 100) + private String dictKey; + + /** 字典 value(显示名称,原 Name) */ + @Column(name = "dict_value", length = 200) + private String dictValue; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; + + /** 是否启用:0 禁用 / 1 启用(原 Enable) */ + @Column(name = "enable_flag") + private Integer enableFlag = 1; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java new file mode 100644 index 0000000..2384267 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板类型。对应原 .NET Notify_TemplateType 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "template_type") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class TemplateType extends BaseEntity { + + /** 类型名称 */ + @Column(name = "type_name", length = 100) + private String typeName; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/UpFile.java b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java new file mode 100644 index 0000000..4db89c2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java @@ -0,0 +1,47 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 附件文件记录。对应原 .NET Common_UpFiles 表。 + * + * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * application.yml 的 file.upload-path 目录下。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "up_file") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UpFile extends BaseEntity { + + /** 应用类型(如 "notice"),用于区分不同业务的附件 */ + @Column(name = "topic_type", length = 50) + private String topicType; + + /** 关联主题ID(如通知 serid) */ + @Column(name = "topic_id") + private Integer topicId; + + /** 文件真实名称(用户上传时的原始文件名) */ + @Column(name = "file_true_name", length = 255) + private String fileTrueName; + + /** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */ + @Column(name = "file_name", length = 500) + private String fileName; + + /** 文件大小(字节) */ + @Column(name = "file_size") + private Integer fileSize; + + /** 文件后缀名(如 pdf, docx, jpg) */ + @Column(name = "file_type", length = 20) + private String fileType; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..1089c9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。 + * + * 0 DRAFT 草稿(存为草稿) + * 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点) + * 2 PENDING 待审批(提交校办审批) + * 3 REJECTED 已驳回(可改后重新提交) + * + * 兼容性:.NET 原系统「已发布=2」迁移时已归一化为 1,存量数据无 0/2/3; + * 实体历史注释「2 定时发送」从未真实写入(定时靠 timingSendTime 字段表达)。 + * {@link #of(Integer)} 未知值回落 PUBLISHED 的兜底保留。 + */ +@Getter +public enum NoticeStatusEnum { + + DRAFT(0, "草稿"), + PUBLISHED(1, "已发布"), + PENDING(2, "待审批"), + REJECTED(3, "已驳回"); + + private final int code; + private final String desc; + + NoticeStatusEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeStatusEnum of(Integer code) { + if (code == null) { + return PUBLISHED; + } + for (NoticeStatusEnum e : values()) { + if (e.code == code) { + return e; + } + } + return PUBLISHED; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java new file mode 100644 index 0000000..57deb89 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知类型。对应原 .NET Notify_Message.type(原系统通知类=2,此处归一化)。 + */ +@Getter +public enum NoticeTypeEnum { + + NORMAL(1, "普通公告"), + IMPORTANT(2, "重要公告"); + + private final int code; + private final String desc; + + NoticeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeTypeEnum of(Integer code) { + if (code == null) { + return NORMAL; + } + for (NoticeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return NORMAL; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java new file mode 100644 index 0000000..228fa52 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 推送范围节点类型(notice_push_scope.scope_type)。 + * scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id; + * 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。 + */ +@Getter +public enum PushScopeTypeEnum { + + SCHOOL(1, "全校"), + SUB_SCHOOL(2, "学部"), + GRADE(3, "年级"), + CLAZZ(4, "班级"), + TEAM(5, "训练队"), + STUDENT(6, "指定学员"); + + private final int code; + private final String desc; + + PushScopeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static PushScopeTypeEnum of(Integer code) { + if (code == null) { + return null; + } + for (PushScopeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java new file mode 100644 index 0000000..b2804a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java @@ -0,0 +1,38 @@ +package com.yunhai.campusnotice.job; + +import com.yunhai.campusnotice.service.INoticeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 定时发送调度任务(改造方案 4.2)。 + * 每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。 + * 注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。 + */ +@Component +public class TimingPublishJob { + + private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class); + + @Autowired + private INoticeService noticeService; + + @Scheduled(fixedDelay = 60_000, initialDelay = 30_000) + public void publishDue() { + try { + int count = noticeService.timingPublishDue(); + if (count > 0) { + log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count); + } + } catch (Exception e) { + // 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露) + log.error("[TimingPublishJob] 定时发布调度异常", e); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java new file mode 100644 index 0000000..000eb69 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java @@ -0,0 +1,14 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +/** + * 统一分页查询入参。pageIndex 从 1 起。 + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java new file mode 100644 index 0000000..c27ad15 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java new file mode 100644 index 0000000..916b705 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java @@ -0,0 +1,106 @@ +package com.yunhai.campusnotice.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。 + * 对应 C# BestSchool.Entity.Model.SchTeaList。 + */ +public class SchTeaList { + + /** 学校 GUID(SchSerID) */ + @JsonProperty("SchSerID") + private String schSerID; + + @JsonProperty("TeaTel") + private String teaTel; + + /** 教师姓名 */ + @JsonProperty("TeaName") + private String teaName; + + /** 教师用户ID */ + @JsonProperty("UserTeaSerID") + private Integer userTeaSerID; + + @JsonProperty("TeaIDCard") + private String teaIDCard; + + /** 教师工号 */ + @JsonProperty("TeaNo") + private String teaNo; + + /** 教师所属组ID列表(兼容旧字段 GroupIds) */ + @JsonProperty("GroupIds") + private List 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 getGroupIds() { + return groupIds; + } + + public void setGroupIds(List groupIds) { + this.groupIds = groupIds; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java new file mode 100644 index 0000000..2c51b20 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java @@ -0,0 +1,29 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchClassDto { + + /** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 班级名称 */ + @JsonProperty("ClassName") + private String className; + + /** 年级ID(= gradeId) */ + @JsonProperty("Levell") + private Integer gradeId; + + /** 排序号 */ + @JsonProperty("sx") + private Integer sort; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java new file mode 100644 index 0000000..fdb75db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchGradeDto { + + /** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */ + @JsonProperty("Id") + private Integer id; + + /** 年级名称(gradeNickname,如「初一」) */ + @JsonProperty("Name") + private String name; + + /** 所属学段(学制)ID */ + @JsonProperty("OwnerId") + private Integer ownerId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java new file mode 100644 index 0000000..f683b91 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchInfoDto { + + /** 学校 GUID(bschclass 侧 SchSerId) */ + @JsonProperty("SchSerId") + private String schSerId; + + /** 用户中心学校组ID(与 CampusNotice schId 同域) */ + @JsonProperty("UserSchSerId") + private Integer userSchSerId; + + /** 学校名称 */ + @JsonProperty("SchName") + private String schName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java new file mode 100644 index 0000000..cd45856 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。 + * 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchStudentDto { + + /** 学员登录账号 */ + @JsonProperty("Account") + private String account; + + /** 学生ID */ + @JsonProperty("UserStuSerID") + private Integer stuId; + + /** 学生姓名 */ + @JsonProperty("StuName") + private String stuName; + + /** 所在班级ID(sch_class.class_id) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 所在组ID */ + @JsonProperty("GroupID") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java new file mode 100644 index 0000000..7084aa9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchSystemDto { + + /** 学制ID(scopeType=学部 的 scopeId) */ + @JsonProperty("Id") + private Integer id; + + /** 学段名称(如「小学部」「初中部」) */ + @JsonProperty("Name") + private String name; + + /** 学制类型 */ + @JsonProperty("Type") + private Integer type; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java new file mode 100644 index 0000000..0d38a76 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。 + * type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchTreeNodeDto { + + /** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */ + private Integer id; + + /** 节点名称 */ + private String name; + + /** 父节点ID(根节点为 0) */ + private Integer pid; + + /** 排序号(组取 sort,叶子固定 0) */ + private Integer no; + + /** 1=组节点,2=人员叶子 */ + private Integer type; + + /** 是否父节点(组 true,叶子 false) */ + @JsonProperty("isParent") + private Boolean isParent; + + /** 头像(当前固定空串) */ + private String photo; + + /** 子节点(叶子为 null,组节点非 null) */ + private List children; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java new file mode 100644 index 0000000..9707b2d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java @@ -0,0 +1,78 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批列表行 VO(notice/ApprovalList,待审/已审合一)。 + * 已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false), + * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。 + */ +@Data +public class ApprovalListVo { + + private Integer id; + + private String title; + + /** 通知类型:1 普通 / 2 重要 */ + private Integer type; + + private String typeDesc; + + /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */ + private Integer status; + + private String statusDesc; + + /** 发布人账号 */ + private String publisherAccount; + + /** 发布人姓名 */ + private String publisherName; + + /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */ + private List approverNames = new ArrayList<>(); + + /** 最新一轮提交时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime submitTime; + + /** 是否需要学员阅读回执 */ + private Integer receiptFlag; + + /** 定时发送时间(审批通过后到点真实发布) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + // ==================== 已审 Tab 字段(待审 Tab 为空) ==================== + + /** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */ + private boolean handled; + + /** 是否由当前登录人处理(false=或签下他人已处理) */ + private boolean handledByMe; + + /** 实际处理人账号 */ + private String handledAccount; + + /** 实际处理人姓名 */ + private String handledName; + + /** 实际处理动作:2 通过 / 3 驳回 */ + private Integer handledAction; + + /** 实际处理动作描述 */ + private String handledActionDesc; + + /** 实际处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime handledTime; + + /** 处理意见(长文本建议进详情查看) */ + private String handledOpinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java new file mode 100644 index 0000000..94c3527 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。 + */ +@Data +public class ApprovalRecordVo { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + private Integer action; + + /** 动作描述 */ + private String actionDesc; + + /** 操作人账号 */ + private String operatorAccount; + + /** 操作人姓名 */ + private String operatorName; + + /** 审批意见(提交行=提交说明) */ + private String opinion; + + /** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */ + private List assigneeTeaIds = new ArrayList<>(); + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java new file mode 100644 index 0000000..de19b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。 + * 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。 + */ +@Data +public class ApproverResolveVo { + + /** 教师用户ID(教师树叶子节点 id) */ + private Integer id; + + /** 头像 */ + private String photo; + + /** 姓名 */ + private String name; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java new file mode 100644 index 0000000..2f2d9b6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java @@ -0,0 +1,17 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 审批人记忆回显 VO(notice/ApproverSuggest)。 + * 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。 + */ +@Data +public class ApproverSuggestVo { + + /** 审批教师ID列表(教师树叶子节点ID) */ + private List teaIds = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java new file mode 100644 index 0000000..fbef6ee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 一键已读结果视图对象(替代原 Map<String,Object> 返回)。 + */ +@Data +public class InitializeRedVo { + + @JsonProperty("count") + private Integer count; + + public InitializeRedVo() {} + + public InitializeRedVo(Integer count) { + this.count = count; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java new file mode 100644 index 0000000..353e843 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java @@ -0,0 +1,112 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 通知详情视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class NoticeDetailVo { + + @JsonProperty("serid") + private Integer serid; + + @JsonProperty("title") + private String title; + + @JsonProperty("Contents") + private String contents; + + @JsonProperty("DeptID") + private String deptId; + + @JsonProperty("DeptName") + private String deptName; + + @JsonProperty("TeaName") + private String publisherName; + + @JsonProperty("EditDate") + private String editDate; + + @JsonProperty("numcount") + private Integer numCount; + + @JsonProperty("readnumcount") + private Long readNumCount; + + @JsonProperty("ReceiveList") + private String receiveList; + + @JsonProperty("DsfsTime") + private String dsfsTime; + + @JsonProperty("isDsfs") + private Boolean isDsfs; + + @JsonProperty("ChaoSend") + private List chaoSend; + + /** 接收人列表:"all" 或账号列表 */ + @JsonProperty("tlist") + private Object tlist; + + // ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ==================== + + /** 重要程度:1 普通 / 2 重要(编辑回显) */ + @JsonProperty("type") + private Integer type; + + /** 是否置顶:0 否 / 1 是(编辑回显) */ + @JsonProperty("topmost") + private Integer topmost; + + /** 短信提醒标记:0 否 / 1 是(编辑回显) */ + @JsonProperty("smsFlag") + private Integer smsFlag; + + /** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */ + @JsonProperty("status") + private Integer status; + + /** 状态描述 */ + @JsonProperty("statusDesc") + private String statusDesc; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + @JsonProperty("receiptFlag") + private Integer receiptFlag; + + /** 推送范围快照(编辑回显) */ + @JsonProperty("pushScopes") + private List pushScopes; + + /** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */ + @JsonProperty("approvalRecords") + private List approvalRecords; + + /** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */ + @JsonProperty("approverTeaIds") + private List approverTeaIds; + + /** + * 附件项(旧 ChaoSend 数组元素)。 + */ + @Data + public static class FileItem { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("size") + private Integer size; + @JsonProperty("fileid") + private Integer fileid; + @JsonProperty("filename") + private String filename; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java new file mode 100644 index 0000000..95314a7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageResult; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。 + * + * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。 + * 方法名统一 PascalCase;写操作一律 POST。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
路径前缀:{@code Config.API_PATH + "/notice/"}(platform = api / manage / mobiapi)。 + * 方法动词式 PascalCase(对齐 UserBase);写操作一律 POST。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/notice/") +public class NoticeController { + + @Autowired + private INoticeService noticeService; + // ============================ 收件箱列表 ============================ + + /** + * 已收通知列表(用户端)。 + */ + @GetMapping("List") + public NoticePageResult list(NoticeQuery query, + @SortDefault(sort = {"topmost", "createTime"}, + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.userList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 已发/全部通知列表 ============================ + + /** + * 已发/全部通知列表(管理端)。type=user 只看自己。 + */ + @GetMapping("SendList") + public BasePageResult sendList(NoticeQuery query, + @SortDefault(sort = "createTime", + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.adminList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 查看详情(标读) ============================ + + /** + * 查看通知详情(type=1 普通查看标记已读, type=0 编辑模式不标记已读)。 + */ + @GetMapping("Detail") + public NoticeDetailVo detail(@RequestParam("serid") Integer serid, + @RequestParam(value = "type", defaultValue = "1") Integer type) { + return noticeService.detailMap(serid, type); + } + + // ============================ 编辑回显(不标读) ============================ + + /** + * 获取通知详情(编辑/转发用,不标记已读)。 + */ + @GetMapping("GetDetail") + public NoticeDetailVo getDetail(@RequestParam("serid") Integer serid) { + return noticeService.getDetailMap(serid); + } + + // ============================ 新建 ============================ + + /** + * 新建通知(JSON body)。fileIds 为附件ID逗号串,smsFlag 为短信标记。 + */ + @PostMapping("Create") + public Integer create(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) { + return noticeService.publish(request); + } + + // ============================ 编辑 ============================ + + /** + * 编辑通知(JSON body,含 id)。 + */ + @PostMapping("Edit") + public void edit(@RequestBody @Validated(ValidationGroups.Update.class) NoticeRequest request) { + noticeService.edit(request.getSourceId() != null ? request.getSourceId() : 0, request); + } + + // ============================ 批量删除 ============================ + + /** + * 批量删除通知(ids 数组)。 + */ + @PostMapping("Delete") + public void delete(@RequestBody List 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> readDetail(@RequestParam("serid") Integer serid) { + return noticeService.getReadDetailGrouped(serid); + } + + // ============================ 上一篇/下一篇 ============================ + + /** + * 上一篇/下一篇通知ID。 + */ + @GetMapping("GetNextPrev") + public Map getNextPrev(@RequestParam("serid") Integer serid, + @RequestParam("type") String type) { + return noticeService.getNextPrev(serid, type); + } + + // ============================ 附件打包下载 ============================ + + /** + * 批量下载通知全部附件(打包 zip)。 + */ + @GetMapping("AllFileDown") + public void allFileDown(@RequestParam("dataid") Integer dataid, + HttpServletResponse response) { + noticeService.downloadAllFiles(dataid, response); + } + + // ============================ 统计 ============================ + + /** + * 通知统计。 + */ + @GetMapping("Statis") + public NoticeStatisticsVo statis( + @RequestParam(value = "starttime", required = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start, + @RequestParam(value = "endtime", required = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) { + return noticeService.statistics(new NoticeQuery(), start, end); + } + + // ============================ 转发(合并旧 REST 壳 forward) ============================ + + /** + * 转发通知(基于源通知创建新通知)。sourceId 在 request body 中传递。 + */ + @PostMapping("Forward") + public Integer forward(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) { + return noticeService.forward(request.getSourceId(), request); + } + + // ============================ 推送范围树(新版发布表单,改造方案 3.1) ============================ + + /** + * 当前学校学员推送范围树:全校根 + 学段→年级→班级(数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。节点结构:{scopeType, scopeId, name, children}。 + */ + @GetMapping("ScopeTree") + public List> scopeTree() { + return noticeService.scopeTree(); + } + + // ============================ 推送范围预估 ============================ + + /** + * 按已选范围预估接收人数(学员 = 范围展开去重账号数 + 教师 = receiveList 定向名单数)。 + * scopes 为 JSON 数组:[{"scopeType":3,"scopeId":5,"scopeName":"初一"}] + */ + @GetMapping("ScopePreview") + public ScopePreviewVo scopePreview(@RequestParam(value = "scopes", required = false) String scopes, + @RequestParam(value = "receiveList", required = false) String receiveList) { + return noticeService.scopePreview(parseScopes(scopes), receiveList); + } + + // ============================ 存为草稿 ============================ + + /** + * 存为草稿(status=0):不圈人、不发短信、不落接收人,仅存内容 + 范围快照 + 附件。 + */ + @PostMapping("SaveDraft") + public Integer saveDraft(@RequestBody NoticeRequest request) { + return noticeService.saveDraft(request); + } + + // ============================ 草稿箱列表 ============================ + + /** + * 草稿箱列表(status=0 本人草稿):重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + @GetMapping("DraftList") + public BasePageResult draftList(NoticeQuery query, + @SortDefault(sort = "updateTime", + direction = Sort.Direction.DESC) Sort sort) { + return noticeService.draftList(query, PageUtils.toPageable(query, sort)); + } + + // ============================ 已发送阅读回执 ============================ + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + @GetMapping("ReadReceipt") + public ReadReceiptVo readReceipt(@RequestParam("serid") Integer serid) { + return noticeService.readReceipt(serid); + } + + // ============================ 审批人记忆回显 ============================ + + /** + * 审批人记忆回显:当前发布人最近一次所选审批人组合(无记忆返回空)。 + */ + @GetMapping("ApproverSuggest") + public ApproverSuggestVo approverSuggest() { + return noticeService.approverSuggest(); + } + + // ============================ 审批教师绑定(教师树ID→姓名/头像) ============================ + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * ids=101,102(教师树叶子节点ID);schId 缺省取当前登录人学校; + * 未命中的ID保留、name 为 null,由前端提示。 + */ + @GetMapping("ApproverResolve") + public List approverResolve( + @RequestParam(value = "ids", required = false) String ids, + @RequestParam(value = "schId", required = false) Integer schId) { + if (ids == null || ids.trim().isEmpty()) { + throw new BizException(ErrorCode.PARAM_ERROR, "ids 不允许为空"); + } + return noticeService.approverResolve(Arrays.asList(OtherUtil.convertToArray(ids)), schId); + } + + // ============================ 提交审批 ============================ + + /** + * 提交审批(status: 0/3→2):发布人从教师树选定审批教师(可多选,或签), + * 写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + @PostMapping("Submit") + public void submit(@RequestBody SubmitRequest request) { + noticeService.submit(request.getSerid(), request.getApproverTeaIds(), request.getOpinion()); + } + + // ============================ 审批通过 ============================ + + /** + * 审批通过(或签):任一审批人通过即 status→1 并触发真实发布; + * 被他人先审时返回带实际处理人/时间的冲突提示。 + */ + @PostMapping("Approve") + public void approve(@RequestParam("serid") Integer serid, + @RequestParam(value = "opinion", required = false) String opinion) { + noticeService.approve(serid, opinion); + } + + // ============================ 审批驳回 ============================ + + /** + * 审批驳回(或签):任一审批人驳回即 status→3,意见必填;发布人可改后重新提交。 + */ + @PostMapping("Reject") + public void reject(@RequestParam("serid") Integer serid, + @RequestParam("opinion") String opinion) { + noticeService.reject(serid, opinion); + } + + // ============================ 审批列表(待审/已审合一) ============================ + + /** + * 审批列表:按审批人快照含当前账号过滤(schId 隔离)。 + * auditState=pending(默认) 待审 / reviewed 已审(含或签下他人已处理,带实际处理人)。 + */ + @GetMapping("ApprovalList") + public BasePageResult approvalList( + @RequestParam(value = "auditState", defaultValue = "pending") String auditState, + NoticeQuery query, + @SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) { + return noticeService.approvalList(auditState, query, PageUtils.toPageable(query, sort)); + } + + /** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */ + private List parseScopes(String scopes) { + if (scopes == null || scopes.trim().isEmpty()) { + return null; + } + try { + return new ObjectMapper().readValue(scopes, new TypeReference>() { + }); + } catch (IOException e) { + throw new BizException(ErrorCode.PARAM_ERROR, "scopes 参数格式错误,应为 JSON 数组:[{\"scopeType\":3,\"scopeId\":5}]"); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java b/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java new file mode 100644 index 0000000..f58015a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/SmsController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.service.ISmsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置接口(短信域)。 + * 路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。 + * 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sms/") +public class SmsController { + + @Autowired + private ISmsService smsService; + + /** + * 获取短信模块列表。 + */ + @GetMapping("GetModule") + public List> getModule() { + return smsService.getModuleList(); + } + + /** + * 获取学校父类组列表。 + */ + @GetMapping("GetGroup") + public List> getGroup() { + return smsService.getSchParentGroup(); + } + + /** + * 分页获取学校短信开关列表。 + */ + @GetMapping("GetSchoolSms") + public Map getSchoolSms( + @RequestParam(value = "schpid", required = false) String schpid, + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "SchName", required = false, defaultValue = "") String schName) { + return smsService.getSchIsSmsList(pageSize, pageIndex, schName); + } + + /** + * 获取学校列表。 + */ + @GetMapping("GetSchool") + public List> getSchool() { + return smsService.getSchList(); + } + + /** + * 设置单校单模块短信开关。 + */ + @PostMapping("SetSchoolSms") + public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) { + smsService.setSchSmsFromJson(itemsModuJson); + } + + /** + * 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。 + */ + @PostMapping("SetAllSchoolSms") + public void setAllSchoolSms( + @RequestParam("SchSerID") Integer schSerID, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchSmsAll(schSerID, isSms); + } + + /** + * 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。 + */ + @PostMapping("SetSchoolModuleSms") + public void setSchoolModuleSms( + @RequestParam("SchSerID") String schSerID, + @RequestParam("Module") String module, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchModuleSms(schSerID, module, isSms); + } + +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java new file mode 100644 index 0000000..d512ae1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.service.ITemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板接口(模板域)。 + * 路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。 + * 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/template/") +public class TemplateController { + + @Autowired + private ITemplateService templateService; + + /** + * 获取模板类型列表。 + */ + @GetMapping("Typelist") + public List> getTypelist() { + return templateService.getTypelist(); + } + + /** + * 新增/编辑模板类型(原 GET EditType,已 POST 化)。 + */ + @PostMapping("TypeEdit") + public void typeEdit( + @RequestParam("typeid") Integer typeid, + @RequestParam("TypeName") String typeName) { + templateService.editType(typeid, typeName); + } + + /** + * 删除模板类型(原 GET DelType,已 POST 化)。 + */ + @PostMapping("TypeDelete") + public void typeDelete(@RequestParam("typeid") Integer typeid) { + templateService.delType(typeid); + } + + /** + * 分页获取模板列表。 + */ + @GetMapping("List") + public Map list( + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "typeid", defaultValue = "0") Integer typeid) { + return templateService.getTemplatelist(pageSize, pageIndex, typeid); + } + + /** + * 新增/编辑模板。 + */ + @PostMapping("TemplateEdit") + public void templateEdit( + @RequestParam(value = "SerID", required = false) Integer serID, + @RequestParam("Title") String title, + @RequestParam("Contents") String contents, + @RequestParam(value = "Type", required = false) Integer type, + @RequestParam(value = "Cover", required = false) Integer cover) { + templateService.editTemplate(serID, title, contents, type, cover); + } + + /** + * 删除模板(原 GET DelTemplate,已 POST 化)。 + */ + @PostMapping("TemplateDelete") + public void templateDelete(@RequestParam("SerID") Integer serid) { + templateService.delTemplate(serid); + } + + /** + * 获取单个模板详情。 + */ + @GetMapping("TemplateFirst") + public Map templateFirst(@RequestParam("serid") Integer serid) { + return templateService.getTemplateFist(serid); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java new file mode 100644 index 0000000..1daf5a9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.yunhai.campusnotice.entity; + +import com.yunhai.campusnotice.common.listener.NoticeAuditListener; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。 + */ +@MappedSuperclass +@Getter +@Setter +@EntityListeners(NoticeAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 创建人登录账号 + */ + @Column(name = "create_by", updatable = false) + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time", updatable = false) + private LocalDateTime createTime; + + /** + * 更新人登录账号 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java new file mode 100644 index 0000000..c71e63d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 联系组。对应原 .NET Contacts_Group 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroup extends BaseEntity { + + /** 组名称 */ + @Column(name = "group_name", length = 200) + private String groupName; + + /** 创建人账号 */ + @Column(name = "creator_account", length = 64) + private String creatorAccount; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java new file mode 100644 index 0000000..b87dee9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 联系组-用户关联。对应原 .NET Contacts_Tea 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group_user", + uniqueConstraints = @UniqueConstraint(name = "uk_group_user", + columnNames = {"group_id", "user_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroupUser extends BaseEntity { + + /** 联系组ID */ + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** 用户ID(基础平台用户ID) */ + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** 用户账号 */ + @Column(name = "user_account", length = 64) + private String userAccount; + + /** 用户姓名 */ + @Column(name = "user_name", length = 64) + private String userName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/Notice.java b/src/main/java/com/yunhai/campusnotice/entity/Notice.java new file mode 100644 index 0000000..97c056a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/Notice.java @@ -0,0 +1,174 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 校园公告主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。 + * + * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead}, + * 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class Notice extends BaseEntity { + + /** + * 通知标题 + */ + @Column(name = "title", nullable = false, length = 200) + private String title; + + /** + * 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。 + */ + @Column(name = "sch_id") + private Integer schId; + + /** + * 通知内容 + */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** + * 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum}) + */ + @Column(name = "type", nullable = false) + private Integer type = 1; + + /** + * 发布人登录账号 + */ + @Column(name = "publisher_account", length = 64) + private String publisherAccount; + + /** + * 发布人姓名 + */ + @Column(name = "publisher_name", length = 64) + private String publisherName; + + /** + * 所属分类ID(原 DeptID) + */ + @Column(name = "dept_id", length = 64) + private String deptId; + + /** + * 所属分类名称 + */ + @Column(name = "dept_name", length = 128) + private String deptName; + + /** + * 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}: + * 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。 + */ + @Column(name = "status", nullable = false) + private Integer status = 0; + + /** + * 是否置顶:0 否 / 1 是(原 topmost) + */ + @Column(name = "topmost") + private Integer topmost = 0; + + // ==================== 迁移补全字段(对应 C# Notify_Message) ==================== + + /** + * 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。 + * 对应 C# Notify_Message.ReceiveList + */ + @Column(name = "receive_list", columnDefinition = "LONGTEXT") + private String receiveList; + + /** + * 密码发送(对应 C# Notify_Message.PwdSend) + */ + @Column(name = "pwd_send", length = 255) + private String pwdSend; + + /** + * 抄送人列表(对应 C# Notify_Message.ChaoSend) + */ + @Column(name = "chao_send", length = 2000) + private String chaoSend; + + /** + * 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。 + * 对应 C# Notify_Message.readcount + */ + @Column(name = "read_count") + private Integer readCount = 0; + + /** + * 应读数(接收人总数)。对应 C# Notify_Message.allowcount + */ + @Column(name = "allow_count") + private Integer allowCount = 0; + + /** + * 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 回复ID。对应 C# Notify_Message.reid + */ + @Column(name = "reply_id", length = 255) + private String replyId; + + /** + * 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull + */ + @Column(name = "is_pulled") + private Integer isPulled = 0; + + /** + * 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime + */ + @Column(name = "timing_send_time") + private LocalDateTime timingSendTime; + + /** + * 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit + */ + @Column(name = "sms_flag") + private Integer smsFlag = 0; + + // ==================== 新版发布表单扩展字段(改造方案 2.1) ==================== + + /** + * 是否需要学员阅读回执:0 否 / 1 是。 + * =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示; + * =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。 + */ + @Column(name = "receipt_flag") + private Integer receiptFlag = 0; + + /** + * 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。 + * ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。 + * 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。 + */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + + /** + * 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。 + * 立即发布的通知在真实发布时一并置 1。 + */ + @Column(name = "timing_published") + private Integer timingPublished = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..314e007 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + * + * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */ + @Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT") + private String assigneeTeaIds; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..41c9c17 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + * + * 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_tea_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人教师ID(用户中心用户ID,按人记忆) */ + @Column(name = "publisher_tea_id", nullable = false) + private Integer publisherTeaId; + + /** 最近一次所选审批教师ID JSON [101,102] */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + * + * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java new file mode 100644 index 0000000..81ad83e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java @@ -0,0 +1,71 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + * + * 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_read", + uniqueConstraints = @UniqueConstraint( + name = "uk_notice_reader", + columnNames = {"notice_id", "reader_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeRead extends BaseEntity { + + /** + * 通知ID(关联 notice.id) + */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** + * 读者登录账号 + */ + @Column(name = "reader_account", nullable = false, length = 64) + private String readerAccount; + + /** + * 读者姓名 + */ + @Column(name = "reader_name", length = 64) + private String readerName; + + /** + * 阅读时间 + */ + @Column(name = "read_time") + private LocalDateTime readTime; + + // ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ==================== + + /** 学校ID(取通知 sch_id) */ + @Column(name = "sch_id") + private Integer schId; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照 */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照 */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java new file mode 100644 index 0000000..225c8de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java @@ -0,0 +1,67 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。 + * + * 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。 + * 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入, + * 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_receiver", + indexes = { + @Index(name = "idx_notice_id", columnList = "notice_id"), + @Index(name = "idx_account", columnList = "account") + }) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeReceiver extends BaseEntity { + + /** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */ + public static final int TYPE_TEACHER = 1; + public static final int TYPE_STUDENT = 2; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 接收人登录账号(学员=SchStudent.account) */ + @Column(name = "account", nullable = false, length = 64) + private String account; + + /** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */ + @Column(name = "receive_type", nullable = false) + private Integer receiveType = TYPE_TEACHER; + + // ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ==================== + + /** 学员姓名快照 */ + @Column(name = "stu_name", length = 64) + private String stuName; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照(如「初二年级」) */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照(sch_class.class_id) */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java new file mode 100644 index 0000000..5ed9fba --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板。对应原 .NET Notify_Template 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_template") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeTemplate extends BaseEntity { + + /** 模板标题 */ + @Column(name = "title", length = 200) + private String title; + + /** 模板内容(HTML) */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** 封面图片文件ID */ + @Column(name = "cover") + private Integer cover; + + /** 引用次数 */ + @Column(name = "ref_count") + private Integer refCount = 0; + + /** 模板类型ID */ + @Column(name = "type_id") + private Integer typeId; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java new file mode 100644 index 0000000..a365b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 短信模块配置。对应原 .NET SchSms 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_module") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsModule extends BaseEntity { + + /** 模块名称 */ + @Column(name = "module_name", length = 100) + private String moduleName; + + /** 模块标识 */ + @Column(name = "module_code", length = 50) + private String moduleCode; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..30dbd56 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 学校短信开通配置。对应原 .NET SchSms_Set 表。 + * 记录某学校某模块是否开通短信。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_school_config") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsSchoolConfig extends BaseEntity { + + /** 学校ID */ + @Column(name = "school_id", nullable = false) + private Integer schoolId; + + /** 学校名称 */ + @Column(name = "school_name", length = 200) + private String schoolName; + + /** 模块ID */ + @Column(name = "module_id") + private Integer moduleId; + + /** 是否开通短信 */ + @Column(name = "is_sms") + private Boolean isSms = false; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SysDict.java b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java new file mode 100644 index 0000000..681acaa --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_dict") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysDict extends BaseEntity { + + /** 字典类型(如 "dept"=所属分类) */ + @Column(name = "dict_type", length = 50) + private String dictType; + + /** 字典 key(原 CodeID) */ + @Column(name = "dict_key", length = 100) + private String dictKey; + + /** 字典 value(显示名称,原 Name) */ + @Column(name = "dict_value", length = 200) + private String dictValue; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; + + /** 是否启用:0 禁用 / 1 启用(原 Enable) */ + @Column(name = "enable_flag") + private Integer enableFlag = 1; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java new file mode 100644 index 0000000..2384267 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板类型。对应原 .NET Notify_TemplateType 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "template_type") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class TemplateType extends BaseEntity { + + /** 类型名称 */ + @Column(name = "type_name", length = 100) + private String typeName; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/UpFile.java b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java new file mode 100644 index 0000000..4db89c2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java @@ -0,0 +1,47 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 附件文件记录。对应原 .NET Common_UpFiles 表。 + * + * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * application.yml 的 file.upload-path 目录下。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "up_file") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UpFile extends BaseEntity { + + /** 应用类型(如 "notice"),用于区分不同业务的附件 */ + @Column(name = "topic_type", length = 50) + private String topicType; + + /** 关联主题ID(如通知 serid) */ + @Column(name = "topic_id") + private Integer topicId; + + /** 文件真实名称(用户上传时的原始文件名) */ + @Column(name = "file_true_name", length = 255) + private String fileTrueName; + + /** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */ + @Column(name = "file_name", length = 500) + private String fileName; + + /** 文件大小(字节) */ + @Column(name = "file_size") + private Integer fileSize; + + /** 文件后缀名(如 pdf, docx, jpg) */ + @Column(name = "file_type", length = 20) + private String fileType; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..1089c9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。 + * + * 0 DRAFT 草稿(存为草稿) + * 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点) + * 2 PENDING 待审批(提交校办审批) + * 3 REJECTED 已驳回(可改后重新提交) + * + * 兼容性:.NET 原系统「已发布=2」迁移时已归一化为 1,存量数据无 0/2/3; + * 实体历史注释「2 定时发送」从未真实写入(定时靠 timingSendTime 字段表达)。 + * {@link #of(Integer)} 未知值回落 PUBLISHED 的兜底保留。 + */ +@Getter +public enum NoticeStatusEnum { + + DRAFT(0, "草稿"), + PUBLISHED(1, "已发布"), + PENDING(2, "待审批"), + REJECTED(3, "已驳回"); + + private final int code; + private final String desc; + + NoticeStatusEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeStatusEnum of(Integer code) { + if (code == null) { + return PUBLISHED; + } + for (NoticeStatusEnum e : values()) { + if (e.code == code) { + return e; + } + } + return PUBLISHED; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java new file mode 100644 index 0000000..57deb89 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知类型。对应原 .NET Notify_Message.type(原系统通知类=2,此处归一化)。 + */ +@Getter +public enum NoticeTypeEnum { + + NORMAL(1, "普通公告"), + IMPORTANT(2, "重要公告"); + + private final int code; + private final String desc; + + NoticeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeTypeEnum of(Integer code) { + if (code == null) { + return NORMAL; + } + for (NoticeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return NORMAL; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java new file mode 100644 index 0000000..228fa52 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 推送范围节点类型(notice_push_scope.scope_type)。 + * scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id; + * 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。 + */ +@Getter +public enum PushScopeTypeEnum { + + SCHOOL(1, "全校"), + SUB_SCHOOL(2, "学部"), + GRADE(3, "年级"), + CLAZZ(4, "班级"), + TEAM(5, "训练队"), + STUDENT(6, "指定学员"); + + private final int code; + private final String desc; + + PushScopeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static PushScopeTypeEnum of(Integer code) { + if (code == null) { + return null; + } + for (PushScopeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java new file mode 100644 index 0000000..b2804a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java @@ -0,0 +1,38 @@ +package com.yunhai.campusnotice.job; + +import com.yunhai.campusnotice.service.INoticeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 定时发送调度任务(改造方案 4.2)。 + * 每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。 + * 注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。 + */ +@Component +public class TimingPublishJob { + + private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class); + + @Autowired + private INoticeService noticeService; + + @Scheduled(fixedDelay = 60_000, initialDelay = 30_000) + public void publishDue() { + try { + int count = noticeService.timingPublishDue(); + if (count > 0) { + log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count); + } + } catch (Exception e) { + // 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露) + log.error("[TimingPublishJob] 定时发布调度异常", e); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java new file mode 100644 index 0000000..000eb69 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java @@ -0,0 +1,14 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +/** + * 统一分页查询入参。pageIndex 从 1 起。 + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java new file mode 100644 index 0000000..c27ad15 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java new file mode 100644 index 0000000..916b705 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java @@ -0,0 +1,106 @@ +package com.yunhai.campusnotice.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。 + * 对应 C# BestSchool.Entity.Model.SchTeaList。 + */ +public class SchTeaList { + + /** 学校 GUID(SchSerID) */ + @JsonProperty("SchSerID") + private String schSerID; + + @JsonProperty("TeaTel") + private String teaTel; + + /** 教师姓名 */ + @JsonProperty("TeaName") + private String teaName; + + /** 教师用户ID */ + @JsonProperty("UserTeaSerID") + private Integer userTeaSerID; + + @JsonProperty("TeaIDCard") + private String teaIDCard; + + /** 教师工号 */ + @JsonProperty("TeaNo") + private String teaNo; + + /** 教师所属组ID列表(兼容旧字段 GroupIds) */ + @JsonProperty("GroupIds") + private List 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 getGroupIds() { + return groupIds; + } + + public void setGroupIds(List groupIds) { + this.groupIds = groupIds; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java new file mode 100644 index 0000000..2c51b20 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java @@ -0,0 +1,29 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchClassDto { + + /** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 班级名称 */ + @JsonProperty("ClassName") + private String className; + + /** 年级ID(= gradeId) */ + @JsonProperty("Levell") + private Integer gradeId; + + /** 排序号 */ + @JsonProperty("sx") + private Integer sort; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java new file mode 100644 index 0000000..fdb75db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchGradeDto { + + /** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */ + @JsonProperty("Id") + private Integer id; + + /** 年级名称(gradeNickname,如「初一」) */ + @JsonProperty("Name") + private String name; + + /** 所属学段(学制)ID */ + @JsonProperty("OwnerId") + private Integer ownerId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java new file mode 100644 index 0000000..f683b91 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchInfoDto { + + /** 学校 GUID(bschclass 侧 SchSerId) */ + @JsonProperty("SchSerId") + private String schSerId; + + /** 用户中心学校组ID(与 CampusNotice schId 同域) */ + @JsonProperty("UserSchSerId") + private Integer userSchSerId; + + /** 学校名称 */ + @JsonProperty("SchName") + private String schName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java new file mode 100644 index 0000000..cd45856 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。 + * 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchStudentDto { + + /** 学员登录账号 */ + @JsonProperty("Account") + private String account; + + /** 学生ID */ + @JsonProperty("UserStuSerID") + private Integer stuId; + + /** 学生姓名 */ + @JsonProperty("StuName") + private String stuName; + + /** 所在班级ID(sch_class.class_id) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 所在组ID */ + @JsonProperty("GroupID") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java new file mode 100644 index 0000000..7084aa9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchSystemDto { + + /** 学制ID(scopeType=学部 的 scopeId) */ + @JsonProperty("Id") + private Integer id; + + /** 学段名称(如「小学部」「初中部」) */ + @JsonProperty("Name") + private String name; + + /** 学制类型 */ + @JsonProperty("Type") + private Integer type; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java new file mode 100644 index 0000000..0d38a76 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。 + * type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchTreeNodeDto { + + /** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */ + private Integer id; + + /** 节点名称 */ + private String name; + + /** 父节点ID(根节点为 0) */ + private Integer pid; + + /** 排序号(组取 sort,叶子固定 0) */ + private Integer no; + + /** 1=组节点,2=人员叶子 */ + private Integer type; + + /** 是否父节点(组 true,叶子 false) */ + @JsonProperty("isParent") + private Boolean isParent; + + /** 头像(当前固定空串) */ + private String photo; + + /** 子节点(叶子为 null,组节点非 null) */ + private List children; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java new file mode 100644 index 0000000..9707b2d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java @@ -0,0 +1,78 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批列表行 VO(notice/ApprovalList,待审/已审合一)。 + * 已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false), + * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。 + */ +@Data +public class ApprovalListVo { + + private Integer id; + + private String title; + + /** 通知类型:1 普通 / 2 重要 */ + private Integer type; + + private String typeDesc; + + /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */ + private Integer status; + + private String statusDesc; + + /** 发布人账号 */ + private String publisherAccount; + + /** 发布人姓名 */ + private String publisherName; + + /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */ + private List approverNames = new ArrayList<>(); + + /** 最新一轮提交时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime submitTime; + + /** 是否需要学员阅读回执 */ + private Integer receiptFlag; + + /** 定时发送时间(审批通过后到点真实发布) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + // ==================== 已审 Tab 字段(待审 Tab 为空) ==================== + + /** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */ + private boolean handled; + + /** 是否由当前登录人处理(false=或签下他人已处理) */ + private boolean handledByMe; + + /** 实际处理人账号 */ + private String handledAccount; + + /** 实际处理人姓名 */ + private String handledName; + + /** 实际处理动作:2 通过 / 3 驳回 */ + private Integer handledAction; + + /** 实际处理动作描述 */ + private String handledActionDesc; + + /** 实际处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime handledTime; + + /** 处理意见(长文本建议进详情查看) */ + private String handledOpinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java new file mode 100644 index 0000000..94c3527 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。 + */ +@Data +public class ApprovalRecordVo { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + private Integer action; + + /** 动作描述 */ + private String actionDesc; + + /** 操作人账号 */ + private String operatorAccount; + + /** 操作人姓名 */ + private String operatorName; + + /** 审批意见(提交行=提交说明) */ + private String opinion; + + /** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */ + private List assigneeTeaIds = new ArrayList<>(); + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java new file mode 100644 index 0000000..de19b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。 + * 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。 + */ +@Data +public class ApproverResolveVo { + + /** 教师用户ID(教师树叶子节点 id) */ + private Integer id; + + /** 头像 */ + private String photo; + + /** 姓名 */ + private String name; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java new file mode 100644 index 0000000..2f2d9b6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java @@ -0,0 +1,17 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 审批人记忆回显 VO(notice/ApproverSuggest)。 + * 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。 + */ +@Data +public class ApproverSuggestVo { + + /** 审批教师ID列表(教师树叶子节点ID) */ + private List teaIds = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java new file mode 100644 index 0000000..fbef6ee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 一键已读结果视图对象(替代原 Map<String,Object> 返回)。 + */ +@Data +public class InitializeRedVo { + + @JsonProperty("count") + private Integer count; + + public InitializeRedVo() {} + + public InitializeRedVo(Integer count) { + this.count = count; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java new file mode 100644 index 0000000..353e843 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java @@ -0,0 +1,112 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 通知详情视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class NoticeDetailVo { + + @JsonProperty("serid") + private Integer serid; + + @JsonProperty("title") + private String title; + + @JsonProperty("Contents") + private String contents; + + @JsonProperty("DeptID") + private String deptId; + + @JsonProperty("DeptName") + private String deptName; + + @JsonProperty("TeaName") + private String publisherName; + + @JsonProperty("EditDate") + private String editDate; + + @JsonProperty("numcount") + private Integer numCount; + + @JsonProperty("readnumcount") + private Long readNumCount; + + @JsonProperty("ReceiveList") + private String receiveList; + + @JsonProperty("DsfsTime") + private String dsfsTime; + + @JsonProperty("isDsfs") + private Boolean isDsfs; + + @JsonProperty("ChaoSend") + private List chaoSend; + + /** 接收人列表:"all" 或账号列表 */ + @JsonProperty("tlist") + private Object tlist; + + // ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ==================== + + /** 重要程度:1 普通 / 2 重要(编辑回显) */ + @JsonProperty("type") + private Integer type; + + /** 是否置顶:0 否 / 1 是(编辑回显) */ + @JsonProperty("topmost") + private Integer topmost; + + /** 短信提醒标记:0 否 / 1 是(编辑回显) */ + @JsonProperty("smsFlag") + private Integer smsFlag; + + /** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */ + @JsonProperty("status") + private Integer status; + + /** 状态描述 */ + @JsonProperty("statusDesc") + private String statusDesc; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + @JsonProperty("receiptFlag") + private Integer receiptFlag; + + /** 推送范围快照(编辑回显) */ + @JsonProperty("pushScopes") + private List pushScopes; + + /** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */ + @JsonProperty("approvalRecords") + private List approvalRecords; + + /** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */ + @JsonProperty("approverTeaIds") + private List approverTeaIds; + + /** + * 附件项(旧 ChaoSend 数组元素)。 + */ + @Data + public static class FileItem { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("size") + private Integer size; + @JsonProperty("fileid") + private Integer fileid; + @JsonProperty("filename") + private String filename; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java new file mode 100644 index 0000000..95314a7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageResult; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。 + * + * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。 + * 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sms/") +public class SmsController { + + @Autowired + private ISmsService smsService; + + /** + * 获取短信模块列表。 + */ + @GetMapping("GetModule") + public List> getModule() { + return smsService.getModuleList(); + } + + /** + * 获取学校父类组列表。 + */ + @GetMapping("GetGroup") + public List> getGroup() { + return smsService.getSchParentGroup(); + } + + /** + * 分页获取学校短信开关列表。 + */ + @GetMapping("GetSchoolSms") + public Map getSchoolSms( + @RequestParam(value = "schpid", required = false) String schpid, + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "SchName", required = false, defaultValue = "") String schName) { + return smsService.getSchIsSmsList(pageSize, pageIndex, schName); + } + + /** + * 获取学校列表。 + */ + @GetMapping("GetSchool") + public List> getSchool() { + return smsService.getSchList(); + } + + /** + * 设置单校单模块短信开关。 + */ + @PostMapping("SetSchoolSms") + public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) { + smsService.setSchSmsFromJson(itemsModuJson); + } + + /** + * 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。 + */ + @PostMapping("SetAllSchoolSms") + public void setAllSchoolSms( + @RequestParam("SchSerID") Integer schSerID, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchSmsAll(schSerID, isSms); + } + + /** + * 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。 + */ + @PostMapping("SetSchoolModuleSms") + public void setSchoolModuleSms( + @RequestParam("SchSerID") String schSerID, + @RequestParam("Module") String module, + @RequestParam("IsSms") Boolean isSms) { + smsService.setSchModuleSms(schSerID, module, isSms); + } + +} diff --git a/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java new file mode 100644 index 0000000..d512ae1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/controller/api/TemplateController.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.controller.api; + +import com.yunhai.campusnotice.config.Config; +import com.yunhai.campusnotice.service.ITemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板接口(模板域)。 + * 路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。 + * 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/template/") +public class TemplateController { + + @Autowired + private ITemplateService templateService; + + /** + * 获取模板类型列表。 + */ + @GetMapping("Typelist") + public List> getTypelist() { + return templateService.getTypelist(); + } + + /** + * 新增/编辑模板类型(原 GET EditType,已 POST 化)。 + */ + @PostMapping("TypeEdit") + public void typeEdit( + @RequestParam("typeid") Integer typeid, + @RequestParam("TypeName") String typeName) { + templateService.editType(typeid, typeName); + } + + /** + * 删除模板类型(原 GET DelType,已 POST 化)。 + */ + @PostMapping("TypeDelete") + public void typeDelete(@RequestParam("typeid") Integer typeid) { + templateService.delType(typeid); + } + + /** + * 分页获取模板列表。 + */ + @GetMapping("List") + public Map list( + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "typeid", defaultValue = "0") Integer typeid) { + return templateService.getTemplatelist(pageSize, pageIndex, typeid); + } + + /** + * 新增/编辑模板。 + */ + @PostMapping("TemplateEdit") + public void templateEdit( + @RequestParam(value = "SerID", required = false) Integer serID, + @RequestParam("Title") String title, + @RequestParam("Contents") String contents, + @RequestParam(value = "Type", required = false) Integer type, + @RequestParam(value = "Cover", required = false) Integer cover) { + templateService.editTemplate(serID, title, contents, type, cover); + } + + /** + * 删除模板(原 GET DelTemplate,已 POST 化)。 + */ + @PostMapping("TemplateDelete") + public void templateDelete(@RequestParam("SerID") Integer serid) { + templateService.delTemplate(serid); + } + + /** + * 获取单个模板详情。 + */ + @GetMapping("TemplateFirst") + public Map templateFirst(@RequestParam("serid") Integer serid) { + return templateService.getTemplateFist(serid); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java new file mode 100644 index 0000000..1daf5a9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.yunhai.campusnotice.entity; + +import com.yunhai.campusnotice.common.listener.NoticeAuditListener; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。 + */ +@MappedSuperclass +@Getter +@Setter +@EntityListeners(NoticeAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 创建人登录账号 + */ + @Column(name = "create_by", updatable = false) + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time", updatable = false) + private LocalDateTime createTime; + + /** + * 更新人登录账号 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java new file mode 100644 index 0000000..c71e63d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 联系组。对应原 .NET Contacts_Group 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroup extends BaseEntity { + + /** 组名称 */ + @Column(name = "group_name", length = 200) + private String groupName; + + /** 创建人账号 */ + @Column(name = "creator_account", length = 64) + private String creatorAccount; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java new file mode 100644 index 0000000..b87dee9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 联系组-用户关联。对应原 .NET Contacts_Tea 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group_user", + uniqueConstraints = @UniqueConstraint(name = "uk_group_user", + columnNames = {"group_id", "user_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroupUser extends BaseEntity { + + /** 联系组ID */ + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** 用户ID(基础平台用户ID) */ + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** 用户账号 */ + @Column(name = "user_account", length = 64) + private String userAccount; + + /** 用户姓名 */ + @Column(name = "user_name", length = 64) + private String userName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/Notice.java b/src/main/java/com/yunhai/campusnotice/entity/Notice.java new file mode 100644 index 0000000..97c056a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/Notice.java @@ -0,0 +1,174 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 校园公告主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。 + * + * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead}, + * 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class Notice extends BaseEntity { + + /** + * 通知标题 + */ + @Column(name = "title", nullable = false, length = 200) + private String title; + + /** + * 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。 + */ + @Column(name = "sch_id") + private Integer schId; + + /** + * 通知内容 + */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** + * 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum}) + */ + @Column(name = "type", nullable = false) + private Integer type = 1; + + /** + * 发布人登录账号 + */ + @Column(name = "publisher_account", length = 64) + private String publisherAccount; + + /** + * 发布人姓名 + */ + @Column(name = "publisher_name", length = 64) + private String publisherName; + + /** + * 所属分类ID(原 DeptID) + */ + @Column(name = "dept_id", length = 64) + private String deptId; + + /** + * 所属分类名称 + */ + @Column(name = "dept_name", length = 128) + private String deptName; + + /** + * 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}: + * 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。 + */ + @Column(name = "status", nullable = false) + private Integer status = 0; + + /** + * 是否置顶:0 否 / 1 是(原 topmost) + */ + @Column(name = "topmost") + private Integer topmost = 0; + + // ==================== 迁移补全字段(对应 C# Notify_Message) ==================== + + /** + * 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。 + * 对应 C# Notify_Message.ReceiveList + */ + @Column(name = "receive_list", columnDefinition = "LONGTEXT") + private String receiveList; + + /** + * 密码发送(对应 C# Notify_Message.PwdSend) + */ + @Column(name = "pwd_send", length = 255) + private String pwdSend; + + /** + * 抄送人列表(对应 C# Notify_Message.ChaoSend) + */ + @Column(name = "chao_send", length = 2000) + private String chaoSend; + + /** + * 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。 + * 对应 C# Notify_Message.readcount + */ + @Column(name = "read_count") + private Integer readCount = 0; + + /** + * 应读数(接收人总数)。对应 C# Notify_Message.allowcount + */ + @Column(name = "allow_count") + private Integer allowCount = 0; + + /** + * 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 回复ID。对应 C# Notify_Message.reid + */ + @Column(name = "reply_id", length = 255) + private String replyId; + + /** + * 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull + */ + @Column(name = "is_pulled") + private Integer isPulled = 0; + + /** + * 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime + */ + @Column(name = "timing_send_time") + private LocalDateTime timingSendTime; + + /** + * 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit + */ + @Column(name = "sms_flag") + private Integer smsFlag = 0; + + // ==================== 新版发布表单扩展字段(改造方案 2.1) ==================== + + /** + * 是否需要学员阅读回执:0 否 / 1 是。 + * =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示; + * =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。 + */ + @Column(name = "receipt_flag") + private Integer receiptFlag = 0; + + /** + * 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。 + * ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。 + * 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。 + */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + + /** + * 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。 + * 立即发布的通知在真实发布时一并置 1。 + */ + @Column(name = "timing_published") + private Integer timingPublished = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..314e007 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + * + * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */ + @Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT") + private String assigneeTeaIds; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..41c9c17 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + * + * 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_tea_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人教师ID(用户中心用户ID,按人记忆) */ + @Column(name = "publisher_tea_id", nullable = false) + private Integer publisherTeaId; + + /** 最近一次所选审批教师ID JSON [101,102] */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + * + * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java new file mode 100644 index 0000000..81ad83e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java @@ -0,0 +1,71 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + * + * 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_read", + uniqueConstraints = @UniqueConstraint( + name = "uk_notice_reader", + columnNames = {"notice_id", "reader_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeRead extends BaseEntity { + + /** + * 通知ID(关联 notice.id) + */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** + * 读者登录账号 + */ + @Column(name = "reader_account", nullable = false, length = 64) + private String readerAccount; + + /** + * 读者姓名 + */ + @Column(name = "reader_name", length = 64) + private String readerName; + + /** + * 阅读时间 + */ + @Column(name = "read_time") + private LocalDateTime readTime; + + // ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ==================== + + /** 学校ID(取通知 sch_id) */ + @Column(name = "sch_id") + private Integer schId; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照 */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照 */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java new file mode 100644 index 0000000..225c8de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java @@ -0,0 +1,67 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。 + * + * 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。 + * 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入, + * 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_receiver", + indexes = { + @Index(name = "idx_notice_id", columnList = "notice_id"), + @Index(name = "idx_account", columnList = "account") + }) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeReceiver extends BaseEntity { + + /** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */ + public static final int TYPE_TEACHER = 1; + public static final int TYPE_STUDENT = 2; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 接收人登录账号(学员=SchStudent.account) */ + @Column(name = "account", nullable = false, length = 64) + private String account; + + /** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */ + @Column(name = "receive_type", nullable = false) + private Integer receiveType = TYPE_TEACHER; + + // ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ==================== + + /** 学员姓名快照 */ + @Column(name = "stu_name", length = 64) + private String stuName; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照(如「初二年级」) */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照(sch_class.class_id) */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java new file mode 100644 index 0000000..5ed9fba --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板。对应原 .NET Notify_Template 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_template") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeTemplate extends BaseEntity { + + /** 模板标题 */ + @Column(name = "title", length = 200) + private String title; + + /** 模板内容(HTML) */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** 封面图片文件ID */ + @Column(name = "cover") + private Integer cover; + + /** 引用次数 */ + @Column(name = "ref_count") + private Integer refCount = 0; + + /** 模板类型ID */ + @Column(name = "type_id") + private Integer typeId; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java new file mode 100644 index 0000000..a365b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 短信模块配置。对应原 .NET SchSms 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_module") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsModule extends BaseEntity { + + /** 模块名称 */ + @Column(name = "module_name", length = 100) + private String moduleName; + + /** 模块标识 */ + @Column(name = "module_code", length = 50) + private String moduleCode; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..30dbd56 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 学校短信开通配置。对应原 .NET SchSms_Set 表。 + * 记录某学校某模块是否开通短信。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_school_config") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsSchoolConfig extends BaseEntity { + + /** 学校ID */ + @Column(name = "school_id", nullable = false) + private Integer schoolId; + + /** 学校名称 */ + @Column(name = "school_name", length = 200) + private String schoolName; + + /** 模块ID */ + @Column(name = "module_id") + private Integer moduleId; + + /** 是否开通短信 */ + @Column(name = "is_sms") + private Boolean isSms = false; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SysDict.java b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java new file mode 100644 index 0000000..681acaa --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_dict") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysDict extends BaseEntity { + + /** 字典类型(如 "dept"=所属分类) */ + @Column(name = "dict_type", length = 50) + private String dictType; + + /** 字典 key(原 CodeID) */ + @Column(name = "dict_key", length = 100) + private String dictKey; + + /** 字典 value(显示名称,原 Name) */ + @Column(name = "dict_value", length = 200) + private String dictValue; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; + + /** 是否启用:0 禁用 / 1 启用(原 Enable) */ + @Column(name = "enable_flag") + private Integer enableFlag = 1; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java new file mode 100644 index 0000000..2384267 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板类型。对应原 .NET Notify_TemplateType 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "template_type") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class TemplateType extends BaseEntity { + + /** 类型名称 */ + @Column(name = "type_name", length = 100) + private String typeName; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/UpFile.java b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java new file mode 100644 index 0000000..4db89c2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java @@ -0,0 +1,47 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 附件文件记录。对应原 .NET Common_UpFiles 表。 + * + * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * application.yml 的 file.upload-path 目录下。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "up_file") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UpFile extends BaseEntity { + + /** 应用类型(如 "notice"),用于区分不同业务的附件 */ + @Column(name = "topic_type", length = 50) + private String topicType; + + /** 关联主题ID(如通知 serid) */ + @Column(name = "topic_id") + private Integer topicId; + + /** 文件真实名称(用户上传时的原始文件名) */ + @Column(name = "file_true_name", length = 255) + private String fileTrueName; + + /** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */ + @Column(name = "file_name", length = 500) + private String fileName; + + /** 文件大小(字节) */ + @Column(name = "file_size") + private Integer fileSize; + + /** 文件后缀名(如 pdf, docx, jpg) */ + @Column(name = "file_type", length = 20) + private String fileType; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..1089c9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。 + * + * 0 DRAFT 草稿(存为草稿) + * 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点) + * 2 PENDING 待审批(提交校办审批) + * 3 REJECTED 已驳回(可改后重新提交) + * + * 兼容性:.NET 原系统「已发布=2」迁移时已归一化为 1,存量数据无 0/2/3; + * 实体历史注释「2 定时发送」从未真实写入(定时靠 timingSendTime 字段表达)。 + * {@link #of(Integer)} 未知值回落 PUBLISHED 的兜底保留。 + */ +@Getter +public enum NoticeStatusEnum { + + DRAFT(0, "草稿"), + PUBLISHED(1, "已发布"), + PENDING(2, "待审批"), + REJECTED(3, "已驳回"); + + private final int code; + private final String desc; + + NoticeStatusEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeStatusEnum of(Integer code) { + if (code == null) { + return PUBLISHED; + } + for (NoticeStatusEnum e : values()) { + if (e.code == code) { + return e; + } + } + return PUBLISHED; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java new file mode 100644 index 0000000..57deb89 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知类型。对应原 .NET Notify_Message.type(原系统通知类=2,此处归一化)。 + */ +@Getter +public enum NoticeTypeEnum { + + NORMAL(1, "普通公告"), + IMPORTANT(2, "重要公告"); + + private final int code; + private final String desc; + + NoticeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeTypeEnum of(Integer code) { + if (code == null) { + return NORMAL; + } + for (NoticeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return NORMAL; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java new file mode 100644 index 0000000..228fa52 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 推送范围节点类型(notice_push_scope.scope_type)。 + * scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id; + * 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。 + */ +@Getter +public enum PushScopeTypeEnum { + + SCHOOL(1, "全校"), + SUB_SCHOOL(2, "学部"), + GRADE(3, "年级"), + CLAZZ(4, "班级"), + TEAM(5, "训练队"), + STUDENT(6, "指定学员"); + + private final int code; + private final String desc; + + PushScopeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static PushScopeTypeEnum of(Integer code) { + if (code == null) { + return null; + } + for (PushScopeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java new file mode 100644 index 0000000..b2804a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java @@ -0,0 +1,38 @@ +package com.yunhai.campusnotice.job; + +import com.yunhai.campusnotice.service.INoticeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 定时发送调度任务(改造方案 4.2)。 + * 每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。 + * 注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。 + */ +@Component +public class TimingPublishJob { + + private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class); + + @Autowired + private INoticeService noticeService; + + @Scheduled(fixedDelay = 60_000, initialDelay = 30_000) + public void publishDue() { + try { + int count = noticeService.timingPublishDue(); + if (count > 0) { + log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count); + } + } catch (Exception e) { + // 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露) + log.error("[TimingPublishJob] 定时发布调度异常", e); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java new file mode 100644 index 0000000..000eb69 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java @@ -0,0 +1,14 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +/** + * 统一分页查询入参。pageIndex 从 1 起。 + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java new file mode 100644 index 0000000..c27ad15 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java new file mode 100644 index 0000000..916b705 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java @@ -0,0 +1,106 @@ +package com.yunhai.campusnotice.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。 + * 对应 C# BestSchool.Entity.Model.SchTeaList。 + */ +public class SchTeaList { + + /** 学校 GUID(SchSerID) */ + @JsonProperty("SchSerID") + private String schSerID; + + @JsonProperty("TeaTel") + private String teaTel; + + /** 教师姓名 */ + @JsonProperty("TeaName") + private String teaName; + + /** 教师用户ID */ + @JsonProperty("UserTeaSerID") + private Integer userTeaSerID; + + @JsonProperty("TeaIDCard") + private String teaIDCard; + + /** 教师工号 */ + @JsonProperty("TeaNo") + private String teaNo; + + /** 教师所属组ID列表(兼容旧字段 GroupIds) */ + @JsonProperty("GroupIds") + private List 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 getGroupIds() { + return groupIds; + } + + public void setGroupIds(List groupIds) { + this.groupIds = groupIds; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java new file mode 100644 index 0000000..2c51b20 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java @@ -0,0 +1,29 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchClassDto { + + /** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 班级名称 */ + @JsonProperty("ClassName") + private String className; + + /** 年级ID(= gradeId) */ + @JsonProperty("Levell") + private Integer gradeId; + + /** 排序号 */ + @JsonProperty("sx") + private Integer sort; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java new file mode 100644 index 0000000..fdb75db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchGradeDto { + + /** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */ + @JsonProperty("Id") + private Integer id; + + /** 年级名称(gradeNickname,如「初一」) */ + @JsonProperty("Name") + private String name; + + /** 所属学段(学制)ID */ + @JsonProperty("OwnerId") + private Integer ownerId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java new file mode 100644 index 0000000..f683b91 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchInfoDto { + + /** 学校 GUID(bschclass 侧 SchSerId) */ + @JsonProperty("SchSerId") + private String schSerId; + + /** 用户中心学校组ID(与 CampusNotice schId 同域) */ + @JsonProperty("UserSchSerId") + private Integer userSchSerId; + + /** 学校名称 */ + @JsonProperty("SchName") + private String schName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java new file mode 100644 index 0000000..cd45856 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。 + * 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchStudentDto { + + /** 学员登录账号 */ + @JsonProperty("Account") + private String account; + + /** 学生ID */ + @JsonProperty("UserStuSerID") + private Integer stuId; + + /** 学生姓名 */ + @JsonProperty("StuName") + private String stuName; + + /** 所在班级ID(sch_class.class_id) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 所在组ID */ + @JsonProperty("GroupID") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java new file mode 100644 index 0000000..7084aa9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchSystemDto { + + /** 学制ID(scopeType=学部 的 scopeId) */ + @JsonProperty("Id") + private Integer id; + + /** 学段名称(如「小学部」「初中部」) */ + @JsonProperty("Name") + private String name; + + /** 学制类型 */ + @JsonProperty("Type") + private Integer type; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java new file mode 100644 index 0000000..0d38a76 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。 + * type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchTreeNodeDto { + + /** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */ + private Integer id; + + /** 节点名称 */ + private String name; + + /** 父节点ID(根节点为 0) */ + private Integer pid; + + /** 排序号(组取 sort,叶子固定 0) */ + private Integer no; + + /** 1=组节点,2=人员叶子 */ + private Integer type; + + /** 是否父节点(组 true,叶子 false) */ + @JsonProperty("isParent") + private Boolean isParent; + + /** 头像(当前固定空串) */ + private String photo; + + /** 子节点(叶子为 null,组节点非 null) */ + private List children; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java new file mode 100644 index 0000000..9707b2d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java @@ -0,0 +1,78 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批列表行 VO(notice/ApprovalList,待审/已审合一)。 + * 已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false), + * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。 + */ +@Data +public class ApprovalListVo { + + private Integer id; + + private String title; + + /** 通知类型:1 普通 / 2 重要 */ + private Integer type; + + private String typeDesc; + + /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */ + private Integer status; + + private String statusDesc; + + /** 发布人账号 */ + private String publisherAccount; + + /** 发布人姓名 */ + private String publisherName; + + /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */ + private List approverNames = new ArrayList<>(); + + /** 最新一轮提交时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime submitTime; + + /** 是否需要学员阅读回执 */ + private Integer receiptFlag; + + /** 定时发送时间(审批通过后到点真实发布) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + // ==================== 已审 Tab 字段(待审 Tab 为空) ==================== + + /** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */ + private boolean handled; + + /** 是否由当前登录人处理(false=或签下他人已处理) */ + private boolean handledByMe; + + /** 实际处理人账号 */ + private String handledAccount; + + /** 实际处理人姓名 */ + private String handledName; + + /** 实际处理动作:2 通过 / 3 驳回 */ + private Integer handledAction; + + /** 实际处理动作描述 */ + private String handledActionDesc; + + /** 实际处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime handledTime; + + /** 处理意见(长文本建议进详情查看) */ + private String handledOpinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java new file mode 100644 index 0000000..94c3527 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。 + */ +@Data +public class ApprovalRecordVo { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + private Integer action; + + /** 动作描述 */ + private String actionDesc; + + /** 操作人账号 */ + private String operatorAccount; + + /** 操作人姓名 */ + private String operatorName; + + /** 审批意见(提交行=提交说明) */ + private String opinion; + + /** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */ + private List assigneeTeaIds = new ArrayList<>(); + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java new file mode 100644 index 0000000..de19b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。 + * 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。 + */ +@Data +public class ApproverResolveVo { + + /** 教师用户ID(教师树叶子节点 id) */ + private Integer id; + + /** 头像 */ + private String photo; + + /** 姓名 */ + private String name; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java new file mode 100644 index 0000000..2f2d9b6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java @@ -0,0 +1,17 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 审批人记忆回显 VO(notice/ApproverSuggest)。 + * 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。 + */ +@Data +public class ApproverSuggestVo { + + /** 审批教师ID列表(教师树叶子节点ID) */ + private List teaIds = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java new file mode 100644 index 0000000..fbef6ee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 一键已读结果视图对象(替代原 Map<String,Object> 返回)。 + */ +@Data +public class InitializeRedVo { + + @JsonProperty("count") + private Integer count; + + public InitializeRedVo() {} + + public InitializeRedVo(Integer count) { + this.count = count; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java new file mode 100644 index 0000000..353e843 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java @@ -0,0 +1,112 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 通知详情视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class NoticeDetailVo { + + @JsonProperty("serid") + private Integer serid; + + @JsonProperty("title") + private String title; + + @JsonProperty("Contents") + private String contents; + + @JsonProperty("DeptID") + private String deptId; + + @JsonProperty("DeptName") + private String deptName; + + @JsonProperty("TeaName") + private String publisherName; + + @JsonProperty("EditDate") + private String editDate; + + @JsonProperty("numcount") + private Integer numCount; + + @JsonProperty("readnumcount") + private Long readNumCount; + + @JsonProperty("ReceiveList") + private String receiveList; + + @JsonProperty("DsfsTime") + private String dsfsTime; + + @JsonProperty("isDsfs") + private Boolean isDsfs; + + @JsonProperty("ChaoSend") + private List chaoSend; + + /** 接收人列表:"all" 或账号列表 */ + @JsonProperty("tlist") + private Object tlist; + + // ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ==================== + + /** 重要程度:1 普通 / 2 重要(编辑回显) */ + @JsonProperty("type") + private Integer type; + + /** 是否置顶:0 否 / 1 是(编辑回显) */ + @JsonProperty("topmost") + private Integer topmost; + + /** 短信提醒标记:0 否 / 1 是(编辑回显) */ + @JsonProperty("smsFlag") + private Integer smsFlag; + + /** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */ + @JsonProperty("status") + private Integer status; + + /** 状态描述 */ + @JsonProperty("statusDesc") + private String statusDesc; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + @JsonProperty("receiptFlag") + private Integer receiptFlag; + + /** 推送范围快照(编辑回显) */ + @JsonProperty("pushScopes") + private List pushScopes; + + /** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */ + @JsonProperty("approvalRecords") + private List approvalRecords; + + /** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */ + @JsonProperty("approverTeaIds") + private List approverTeaIds; + + /** + * 附件项(旧 ChaoSend 数组元素)。 + */ + @Data + public static class FileItem { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("size") + private Integer size; + @JsonProperty("fileid") + private Integer fileid; + @JsonProperty("filename") + private String filename; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java new file mode 100644 index 0000000..95314a7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageResult; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。 + * + * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。 + * 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/template/") +public class TemplateController { + + @Autowired + private ITemplateService templateService; + + /** + * 获取模板类型列表。 + */ + @GetMapping("Typelist") + public List> getTypelist() { + return templateService.getTypelist(); + } + + /** + * 新增/编辑模板类型(原 GET EditType,已 POST 化)。 + */ + @PostMapping("TypeEdit") + public void typeEdit( + @RequestParam("typeid") Integer typeid, + @RequestParam("TypeName") String typeName) { + templateService.editType(typeid, typeName); + } + + /** + * 删除模板类型(原 GET DelType,已 POST 化)。 + */ + @PostMapping("TypeDelete") + public void typeDelete(@RequestParam("typeid") Integer typeid) { + templateService.delType(typeid); + } + + /** + * 分页获取模板列表。 + */ + @GetMapping("List") + public Map list( + @RequestParam("PageSize") Integer pageSize, + @RequestParam("PageIndex") Integer pageIndex, + @RequestParam(value = "typeid", defaultValue = "0") Integer typeid) { + return templateService.getTemplatelist(pageSize, pageIndex, typeid); + } + + /** + * 新增/编辑模板。 + */ + @PostMapping("TemplateEdit") + public void templateEdit( + @RequestParam(value = "SerID", required = false) Integer serID, + @RequestParam("Title") String title, + @RequestParam("Contents") String contents, + @RequestParam(value = "Type", required = false) Integer type, + @RequestParam(value = "Cover", required = false) Integer cover) { + templateService.editTemplate(serID, title, contents, type, cover); + } + + /** + * 删除模板(原 GET DelTemplate,已 POST 化)。 + */ + @PostMapping("TemplateDelete") + public void templateDelete(@RequestParam("SerID") Integer serid) { + templateService.delTemplate(serid); + } + + /** + * 获取单个模板详情。 + */ + @GetMapping("TemplateFirst") + public Map templateFirst(@RequestParam("serid") Integer serid) { + return templateService.getTemplateFist(serid); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java new file mode 100644 index 0000000..1daf5a9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.yunhai.campusnotice.entity; + +import com.yunhai.campusnotice.common.listener.NoticeAuditListener; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。 + */ +@MappedSuperclass +@Getter +@Setter +@EntityListeners(NoticeAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 创建人登录账号 + */ + @Column(name = "create_by", updatable = false) + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time", updatable = false) + private LocalDateTime createTime; + + /** + * 更新人登录账号 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java new file mode 100644 index 0000000..c71e63d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroup.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 联系组。对应原 .NET Contacts_Group 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroup extends BaseEntity { + + /** 组名称 */ + @Column(name = "group_name", length = 200) + private String groupName; + + /** 创建人账号 */ + @Column(name = "creator_account", length = 64) + private String creatorAccount; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java new file mode 100644 index 0000000..b87dee9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/ContactsGroupUser.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 联系组-用户关联。对应原 .NET Contacts_Tea 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "contacts_group_user", + uniqueConstraints = @UniqueConstraint(name = "uk_group_user", + columnNames = {"group_id", "user_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ContactsGroupUser extends BaseEntity { + + /** 联系组ID */ + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** 用户ID(基础平台用户ID) */ + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** 用户账号 */ + @Column(name = "user_account", length = 64) + private String userAccount; + + /** 用户姓名 */ + @Column(name = "user_name", length = 64) + private String userName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/Notice.java b/src/main/java/com/yunhai/campusnotice/entity/Notice.java new file mode 100644 index 0000000..97c056a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/Notice.java @@ -0,0 +1,174 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 校园公告主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。 + * + * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead}, + * 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class Notice extends BaseEntity { + + /** + * 通知标题 + */ + @Column(name = "title", nullable = false, length = 200) + private String title; + + /** + * 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。 + */ + @Column(name = "sch_id") + private Integer schId; + + /** + * 通知内容 + */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** + * 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum}) + */ + @Column(name = "type", nullable = false) + private Integer type = 1; + + /** + * 发布人登录账号 + */ + @Column(name = "publisher_account", length = 64) + private String publisherAccount; + + /** + * 发布人姓名 + */ + @Column(name = "publisher_name", length = 64) + private String publisherName; + + /** + * 所属分类ID(原 DeptID) + */ + @Column(name = "dept_id", length = 64) + private String deptId; + + /** + * 所属分类名称 + */ + @Column(name = "dept_name", length = 128) + private String deptName; + + /** + * 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}: + * 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。 + */ + @Column(name = "status", nullable = false) + private Integer status = 0; + + /** + * 是否置顶:0 否 / 1 是(原 topmost) + */ + @Column(name = "topmost") + private Integer topmost = 0; + + // ==================== 迁移补全字段(对应 C# Notify_Message) ==================== + + /** + * 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。 + * 对应 C# Notify_Message.ReceiveList + */ + @Column(name = "receive_list", columnDefinition = "LONGTEXT") + private String receiveList; + + /** + * 密码发送(对应 C# Notify_Message.PwdSend) + */ + @Column(name = "pwd_send", length = 255) + private String pwdSend; + + /** + * 抄送人列表(对应 C# Notify_Message.ChaoSend) + */ + @Column(name = "chao_send", length = 2000) + private String chaoSend; + + /** + * 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。 + * 对应 C# Notify_Message.readcount + */ + @Column(name = "read_count") + private Integer readCount = 0; + + /** + * 应读数(接收人总数)。对应 C# Notify_Message.allowcount + */ + @Column(name = "allow_count") + private Integer allowCount = 0; + + /** + * 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 回复ID。对应 C# Notify_Message.reid + */ + @Column(name = "reply_id", length = 255) + private String replyId; + + /** + * 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull + */ + @Column(name = "is_pulled") + private Integer isPulled = 0; + + /** + * 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime + */ + @Column(name = "timing_send_time") + private LocalDateTime timingSendTime; + + /** + * 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit + */ + @Column(name = "sms_flag") + private Integer smsFlag = 0; + + // ==================== 新版发布表单扩展字段(改造方案 2.1) ==================== + + /** + * 是否需要学员阅读回执:0 否 / 1 是。 + * =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示; + * =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。 + */ + @Column(name = "receipt_flag") + private Integer receiptFlag = 0; + + /** + * 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。 + * ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。 + * 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。 + */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + + /** + * 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。 + * 立即发布的通知在真实发布时一并置 1。 + */ + @Column(name = "timing_published") + private Integer timingPublished = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..314e007 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + * + * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */ + @Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT") + private String assigneeTeaIds; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..41c9c17 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + * + * 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_tea_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人教师ID(用户中心用户ID,按人记忆) */ + @Column(name = "publisher_tea_id", nullable = false) + private Integer publisherTeaId; + + /** 最近一次所选审批教师ID JSON [101,102] */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + * + * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java new file mode 100644 index 0000000..81ad83e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java @@ -0,0 +1,71 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + * + * 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_read", + uniqueConstraints = @UniqueConstraint( + name = "uk_notice_reader", + columnNames = {"notice_id", "reader_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeRead extends BaseEntity { + + /** + * 通知ID(关联 notice.id) + */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** + * 读者登录账号 + */ + @Column(name = "reader_account", nullable = false, length = 64) + private String readerAccount; + + /** + * 读者姓名 + */ + @Column(name = "reader_name", length = 64) + private String readerName; + + /** + * 阅读时间 + */ + @Column(name = "read_time") + private LocalDateTime readTime; + + // ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ==================== + + /** 学校ID(取通知 sch_id) */ + @Column(name = "sch_id") + private Integer schId; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照 */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照 */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java new file mode 100644 index 0000000..225c8de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java @@ -0,0 +1,67 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。 + * + * 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。 + * 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入, + * 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_receiver", + indexes = { + @Index(name = "idx_notice_id", columnList = "notice_id"), + @Index(name = "idx_account", columnList = "account") + }) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeReceiver extends BaseEntity { + + /** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */ + public static final int TYPE_TEACHER = 1; + public static final int TYPE_STUDENT = 2; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 接收人登录账号(学员=SchStudent.account) */ + @Column(name = "account", nullable = false, length = 64) + private String account; + + /** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */ + @Column(name = "receive_type", nullable = false) + private Integer receiveType = TYPE_TEACHER; + + // ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ==================== + + /** 学员姓名快照 */ + @Column(name = "stu_name", length = 64) + private String stuName; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照(如「初二年级」) */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照(sch_class.class_id) */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java new file mode 100644 index 0000000..5ed9fba --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板。对应原 .NET Notify_Template 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_template") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeTemplate extends BaseEntity { + + /** 模板标题 */ + @Column(name = "title", length = 200) + private String title; + + /** 模板内容(HTML) */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** 封面图片文件ID */ + @Column(name = "cover") + private Integer cover; + + /** 引用次数 */ + @Column(name = "ref_count") + private Integer refCount = 0; + + /** 模板类型ID */ + @Column(name = "type_id") + private Integer typeId; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java new file mode 100644 index 0000000..a365b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 短信模块配置。对应原 .NET SchSms 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_module") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsModule extends BaseEntity { + + /** 模块名称 */ + @Column(name = "module_name", length = 100) + private String moduleName; + + /** 模块标识 */ + @Column(name = "module_code", length = 50) + private String moduleCode; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..30dbd56 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 学校短信开通配置。对应原 .NET SchSms_Set 表。 + * 记录某学校某模块是否开通短信。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_school_config") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsSchoolConfig extends BaseEntity { + + /** 学校ID */ + @Column(name = "school_id", nullable = false) + private Integer schoolId; + + /** 学校名称 */ + @Column(name = "school_name", length = 200) + private String schoolName; + + /** 模块ID */ + @Column(name = "module_id") + private Integer moduleId; + + /** 是否开通短信 */ + @Column(name = "is_sms") + private Boolean isSms = false; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SysDict.java b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java new file mode 100644 index 0000000..681acaa --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_dict") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysDict extends BaseEntity { + + /** 字典类型(如 "dept"=所属分类) */ + @Column(name = "dict_type", length = 50) + private String dictType; + + /** 字典 key(原 CodeID) */ + @Column(name = "dict_key", length = 100) + private String dictKey; + + /** 字典 value(显示名称,原 Name) */ + @Column(name = "dict_value", length = 200) + private String dictValue; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; + + /** 是否启用:0 禁用 / 1 启用(原 Enable) */ + @Column(name = "enable_flag") + private Integer enableFlag = 1; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java new file mode 100644 index 0000000..2384267 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板类型。对应原 .NET Notify_TemplateType 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "template_type") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class TemplateType extends BaseEntity { + + /** 类型名称 */ + @Column(name = "type_name", length = 100) + private String typeName; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/UpFile.java b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java new file mode 100644 index 0000000..4db89c2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java @@ -0,0 +1,47 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 附件文件记录。对应原 .NET Common_UpFiles 表。 + * + * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * application.yml 的 file.upload-path 目录下。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "up_file") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UpFile extends BaseEntity { + + /** 应用类型(如 "notice"),用于区分不同业务的附件 */ + @Column(name = "topic_type", length = 50) + private String topicType; + + /** 关联主题ID(如通知 serid) */ + @Column(name = "topic_id") + private Integer topicId; + + /** 文件真实名称(用户上传时的原始文件名) */ + @Column(name = "file_true_name", length = 255) + private String fileTrueName; + + /** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */ + @Column(name = "file_name", length = 500) + private String fileName; + + /** 文件大小(字节) */ + @Column(name = "file_size") + private Integer fileSize; + + /** 文件后缀名(如 pdf, docx, jpg) */ + @Column(name = "file_type", length = 20) + private String fileType; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..1089c9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。 + * + * 0 DRAFT 草稿(存为草稿) + * 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点) + * 2 PENDING 待审批(提交校办审批) + * 3 REJECTED 已驳回(可改后重新提交) + * + * 兼容性:.NET 原系统「已发布=2」迁移时已归一化为 1,存量数据无 0/2/3; + * 实体历史注释「2 定时发送」从未真实写入(定时靠 timingSendTime 字段表达)。 + * {@link #of(Integer)} 未知值回落 PUBLISHED 的兜底保留。 + */ +@Getter +public enum NoticeStatusEnum { + + DRAFT(0, "草稿"), + PUBLISHED(1, "已发布"), + PENDING(2, "待审批"), + REJECTED(3, "已驳回"); + + private final int code; + private final String desc; + + NoticeStatusEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeStatusEnum of(Integer code) { + if (code == null) { + return PUBLISHED; + } + for (NoticeStatusEnum e : values()) { + if (e.code == code) { + return e; + } + } + return PUBLISHED; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java new file mode 100644 index 0000000..57deb89 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeTypeEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知类型。对应原 .NET Notify_Message.type(原系统通知类=2,此处归一化)。 + */ +@Getter +public enum NoticeTypeEnum { + + NORMAL(1, "普通公告"), + IMPORTANT(2, "重要公告"); + + private final int code; + private final String desc; + + NoticeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static NoticeTypeEnum of(Integer code) { + if (code == null) { + return NORMAL; + } + for (NoticeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return NORMAL; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java new file mode 100644 index 0000000..228fa52 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 推送范围节点类型(notice_push_scope.scope_type)。 + * scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id; + * 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。 + */ +@Getter +public enum PushScopeTypeEnum { + + SCHOOL(1, "全校"), + SUB_SCHOOL(2, "学部"), + GRADE(3, "年级"), + CLAZZ(4, "班级"), + TEAM(5, "训练队"), + STUDENT(6, "指定学员"); + + private final int code; + private final String desc; + + PushScopeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static PushScopeTypeEnum of(Integer code) { + if (code == null) { + return null; + } + for (PushScopeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java new file mode 100644 index 0000000..b2804a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java @@ -0,0 +1,38 @@ +package com.yunhai.campusnotice.job; + +import com.yunhai.campusnotice.service.INoticeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 定时发送调度任务(改造方案 4.2)。 + * 每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。 + * 注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。 + */ +@Component +public class TimingPublishJob { + + private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class); + + @Autowired + private INoticeService noticeService; + + @Scheduled(fixedDelay = 60_000, initialDelay = 30_000) + public void publishDue() { + try { + int count = noticeService.timingPublishDue(); + if (count > 0) { + log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count); + } + } catch (Exception e) { + // 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露) + log.error("[TimingPublishJob] 定时发布调度异常", e); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java new file mode 100644 index 0000000..000eb69 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageQuery.java @@ -0,0 +1,14 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +/** + * 统一分页查询入参。pageIndex 从 1 起。 + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java new file mode 100644 index 0000000..c27ad15 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/BasePageResult.java @@ -0,0 +1,33 @@ +package com.yunhai.campusnotice.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java new file mode 100644 index 0000000..916b705 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/SchTeaList.java @@ -0,0 +1,106 @@ +package com.yunhai.campusnotice.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。 + * 对应 C# BestSchool.Entity.Model.SchTeaList。 + */ +public class SchTeaList { + + /** 学校 GUID(SchSerID) */ + @JsonProperty("SchSerID") + private String schSerID; + + @JsonProperty("TeaTel") + private String teaTel; + + /** 教师姓名 */ + @JsonProperty("TeaName") + private String teaName; + + /** 教师用户ID */ + @JsonProperty("UserTeaSerID") + private Integer userTeaSerID; + + @JsonProperty("TeaIDCard") + private String teaIDCard; + + /** 教师工号 */ + @JsonProperty("TeaNo") + private String teaNo; + + /** 教师所属组ID列表(兼容旧字段 GroupIds) */ + @JsonProperty("GroupIds") + private List 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 getGroupIds() { + return groupIds; + } + + public void setGroupIds(List groupIds) { + this.groupIds = groupIds; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java new file mode 100644 index 0000000..2c51b20 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java @@ -0,0 +1,29 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchClassDto { + + /** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 班级名称 */ + @JsonProperty("ClassName") + private String className; + + /** 年级ID(= gradeId) */ + @JsonProperty("Levell") + private Integer gradeId; + + /** 排序号 */ + @JsonProperty("sx") + private Integer sort; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java new file mode 100644 index 0000000..fdb75db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchGradeDto { + + /** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */ + @JsonProperty("Id") + private Integer id; + + /** 年级名称(gradeNickname,如「初一」) */ + @JsonProperty("Name") + private String name; + + /** 所属学段(学制)ID */ + @JsonProperty("OwnerId") + private Integer ownerId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java new file mode 100644 index 0000000..f683b91 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchInfoDto { + + /** 学校 GUID(bschclass 侧 SchSerId) */ + @JsonProperty("SchSerId") + private String schSerId; + + /** 用户中心学校组ID(与 CampusNotice schId 同域) */ + @JsonProperty("UserSchSerId") + private Integer userSchSerId; + + /** 学校名称 */ + @JsonProperty("SchName") + private String schName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java new file mode 100644 index 0000000..cd45856 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。 + * 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchStudentDto { + + /** 学员登录账号 */ + @JsonProperty("Account") + private String account; + + /** 学生ID */ + @JsonProperty("UserStuSerID") + private Integer stuId; + + /** 学生姓名 */ + @JsonProperty("StuName") + private String stuName; + + /** 所在班级ID(sch_class.class_id) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 所在组ID */ + @JsonProperty("GroupID") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java new file mode 100644 index 0000000..7084aa9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchSystemDto { + + /** 学制ID(scopeType=学部 的 scopeId) */ + @JsonProperty("Id") + private Integer id; + + /** 学段名称(如「小学部」「初中部」) */ + @JsonProperty("Name") + private String name; + + /** 学制类型 */ + @JsonProperty("Type") + private Integer type; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java new file mode 100644 index 0000000..0d38a76 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。 + * type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SchTreeNodeDto { + + /** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */ + private Integer id; + + /** 节点名称 */ + private String name; + + /** 父节点ID(根节点为 0) */ + private Integer pid; + + /** 排序号(组取 sort,叶子固定 0) */ + private Integer no; + + /** 1=组节点,2=人员叶子 */ + private Integer type; + + /** 是否父节点(组 true,叶子 false) */ + @JsonProperty("isParent") + private Boolean isParent; + + /** 头像(当前固定空串) */ + private String photo; + + /** 子节点(叶子为 null,组节点非 null) */ + private List children; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java new file mode 100644 index 0000000..9707b2d --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalListVo.java @@ -0,0 +1,78 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批列表行 VO(notice/ApprovalList,待审/已审合一)。 + * 已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false), + * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。 + */ +@Data +public class ApprovalListVo { + + private Integer id; + + private String title; + + /** 通知类型:1 普通 / 2 重要 */ + private Integer type; + + private String typeDesc; + + /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */ + private Integer status; + + private String statusDesc; + + /** 发布人账号 */ + private String publisherAccount; + + /** 发布人姓名 */ + private String publisherName; + + /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */ + private List approverNames = new ArrayList<>(); + + /** 最新一轮提交时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime submitTime; + + /** 是否需要学员阅读回执 */ + private Integer receiptFlag; + + /** 定时发送时间(审批通过后到点真实发布) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + // ==================== 已审 Tab 字段(待审 Tab 为空) ==================== + + /** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */ + private boolean handled; + + /** 是否由当前登录人处理(false=或签下他人已处理) */ + private boolean handledByMe; + + /** 实际处理人账号 */ + private String handledAccount; + + /** 实际处理人姓名 */ + private String handledName; + + /** 实际处理动作:2 通过 / 3 驳回 */ + private Integer handledAction; + + /** 实际处理动作描述 */ + private String handledActionDesc; + + /** 实际处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime handledTime; + + /** 处理意见(长文本建议进详情查看) */ + private String handledOpinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java new file mode 100644 index 0000000..94c3527 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。 + */ +@Data +public class ApprovalRecordVo { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + private Integer action; + + /** 动作描述 */ + private String actionDesc; + + /** 操作人账号 */ + private String operatorAccount; + + /** 操作人姓名 */ + private String operatorName; + + /** 审批意见(提交行=提交说明) */ + private String opinion; + + /** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */ + private List assigneeTeaIds = new ArrayList<>(); + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java new file mode 100644 index 0000000..de19b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。 + * 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。 + */ +@Data +public class ApproverResolveVo { + + /** 教师用户ID(教师树叶子节点 id) */ + private Integer id; + + /** 头像 */ + private String photo; + + /** 姓名 */ + private String name; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java new file mode 100644 index 0000000..2f2d9b6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java @@ -0,0 +1,17 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 审批人记忆回显 VO(notice/ApproverSuggest)。 + * 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。 + */ +@Data +public class ApproverSuggestVo { + + /** 审批教师ID列表(教师树叶子节点ID) */ + private List teaIds = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java new file mode 100644 index 0000000..fbef6ee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 一键已读结果视图对象(替代原 Map<String,Object> 返回)。 + */ +@Data +public class InitializeRedVo { + + @JsonProperty("count") + private Integer count; + + public InitializeRedVo() {} + + public InitializeRedVo(Integer count) { + this.count = count; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java new file mode 100644 index 0000000..353e843 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java @@ -0,0 +1,112 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 通知详情视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class NoticeDetailVo { + + @JsonProperty("serid") + private Integer serid; + + @JsonProperty("title") + private String title; + + @JsonProperty("Contents") + private String contents; + + @JsonProperty("DeptID") + private String deptId; + + @JsonProperty("DeptName") + private String deptName; + + @JsonProperty("TeaName") + private String publisherName; + + @JsonProperty("EditDate") + private String editDate; + + @JsonProperty("numcount") + private Integer numCount; + + @JsonProperty("readnumcount") + private Long readNumCount; + + @JsonProperty("ReceiveList") + private String receiveList; + + @JsonProperty("DsfsTime") + private String dsfsTime; + + @JsonProperty("isDsfs") + private Boolean isDsfs; + + @JsonProperty("ChaoSend") + private List chaoSend; + + /** 接收人列表:"all" 或账号列表 */ + @JsonProperty("tlist") + private Object tlist; + + // ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ==================== + + /** 重要程度:1 普通 / 2 重要(编辑回显) */ + @JsonProperty("type") + private Integer type; + + /** 是否置顶:0 否 / 1 是(编辑回显) */ + @JsonProperty("topmost") + private Integer topmost; + + /** 短信提醒标记:0 否 / 1 是(编辑回显) */ + @JsonProperty("smsFlag") + private Integer smsFlag; + + /** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */ + @JsonProperty("status") + private Integer status; + + /** 状态描述 */ + @JsonProperty("statusDesc") + private String statusDesc; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + @JsonProperty("receiptFlag") + private Integer receiptFlag; + + /** 推送范围快照(编辑回显) */ + @JsonProperty("pushScopes") + private List pushScopes; + + /** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */ + @JsonProperty("approvalRecords") + private List approvalRecords; + + /** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */ + @JsonProperty("approverTeaIds") + private List approverTeaIds; + + /** + * 附件项(旧 ChaoSend 数组元素)。 + */ + @Data + public static class FileItem { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("size") + private Integer size; + @JsonProperty("fileid") + private Integer fileid; + @JsonProperty("filename") + private String filename; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java new file mode 100644 index 0000000..95314a7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageResult; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。 + * + * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
+ * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead}, + * 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class Notice extends BaseEntity { + + /** + * 通知标题 + */ + @Column(name = "title", nullable = false, length = 200) + private String title; + + /** + * 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。 + */ + @Column(name = "sch_id") + private Integer schId; + + /** + * 通知内容 + */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** + * 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum}) + */ + @Column(name = "type", nullable = false) + private Integer type = 1; + + /** + * 发布人登录账号 + */ + @Column(name = "publisher_account", length = 64) + private String publisherAccount; + + /** + * 发布人姓名 + */ + @Column(name = "publisher_name", length = 64) + private String publisherName; + + /** + * 所属分类ID(原 DeptID) + */ + @Column(name = "dept_id", length = 64) + private String deptId; + + /** + * 所属分类名称 + */ + @Column(name = "dept_name", length = 128) + private String deptName; + + /** + * 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}: + * 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。 + */ + @Column(name = "status", nullable = false) + private Integer status = 0; + + /** + * 是否置顶:0 否 / 1 是(原 topmost) + */ + @Column(name = "topmost") + private Integer topmost = 0; + + // ==================== 迁移补全字段(对应 C# Notify_Message) ==================== + + /** + * 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。 + * 对应 C# Notify_Message.ReceiveList + */ + @Column(name = "receive_list", columnDefinition = "LONGTEXT") + private String receiveList; + + /** + * 密码发送(对应 C# Notify_Message.PwdSend) + */ + @Column(name = "pwd_send", length = 255) + private String pwdSend; + + /** + * 抄送人列表(对应 C# Notify_Message.ChaoSend) + */ + @Column(name = "chao_send", length = 2000) + private String chaoSend; + + /** + * 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。 + * 对应 C# Notify_Message.readcount + */ + @Column(name = "read_count") + private Integer readCount = 0; + + /** + * 应读数(接收人总数)。对应 C# Notify_Message.allowcount + */ + @Column(name = "allow_count") + private Integer allowCount = 0; + + /** + * 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 回复ID。对应 C# Notify_Message.reid + */ + @Column(name = "reply_id", length = 255) + private String replyId; + + /** + * 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull + */ + @Column(name = "is_pulled") + private Integer isPulled = 0; + + /** + * 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime + */ + @Column(name = "timing_send_time") + private LocalDateTime timingSendTime; + + /** + * 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit + */ + @Column(name = "sms_flag") + private Integer smsFlag = 0; + + // ==================== 新版发布表单扩展字段(改造方案 2.1) ==================== + + /** + * 是否需要学员阅读回执:0 否 / 1 是。 + * =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示; + * =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。 + */ + @Column(name = "receipt_flag") + private Integer receiptFlag = 0; + + /** + * 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。 + * ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。 + * 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。 + */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + + /** + * 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。 + * 立即发布的通知在真实发布时一并置 1。 + */ + @Column(name = "timing_published") + private Integer timingPublished = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..314e007 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + *
+ * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */ + @Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT") + private String assigneeTeaIds; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..41c9c17 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + *
+ * 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_tea_id"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人教师ID(用户中心用户ID,按人记忆) */ + @Column(name = "publisher_tea_id", nullable = false) + private Integer publisherTeaId; + + /** 最近一次所选审批教师ID JSON [101,102] */ + @Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT") + private String approverTeaIds; + +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + *
+ * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java new file mode 100644 index 0000000..81ad83e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeRead.java @@ -0,0 +1,71 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + *
+ * 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_read", + uniqueConstraints = @UniqueConstraint( + name = "uk_notice_reader", + columnNames = {"notice_id", "reader_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeRead extends BaseEntity { + + /** + * 通知ID(关联 notice.id) + */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** + * 读者登录账号 + */ + @Column(name = "reader_account", nullable = false, length = 64) + private String readerAccount; + + /** + * 读者姓名 + */ + @Column(name = "reader_name", length = 64) + private String readerName; + + /** + * 阅读时间 + */ + @Column(name = "read_time") + private LocalDateTime readTime; + + // ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ==================== + + /** 学校ID(取通知 sch_id) */ + @Column(name = "sch_id") + private Integer schId; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照 */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照 */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java new file mode 100644 index 0000000..225c8de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeReceiver.java @@ -0,0 +1,67 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。 + *
+ * 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。 + * 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入, + * 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_receiver", + indexes = { + @Index(name = "idx_notice_id", columnList = "notice_id"), + @Index(name = "idx_account", columnList = "account") + }) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeReceiver extends BaseEntity { + + /** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */ + public static final int TYPE_TEACHER = 1; + public static final int TYPE_STUDENT = 2; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 接收人登录账号(学员=SchStudent.account) */ + @Column(name = "account", nullable = false, length = 64) + private String account; + + /** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */ + @Column(name = "receive_type", nullable = false) + private Integer receiveType = TYPE_TEACHER; + + // ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ==================== + + /** 学员姓名快照 */ + @Column(name = "stu_name", length = 64) + private String stuName; + + /** 年级ID快照 */ + @Column(name = "grade_id") + private Integer gradeId; + + /** 年级名称快照(如「初二年级」) */ + @Column(name = "grade_name", length = 128) + private String gradeName; + + /** 班级ID快照(sch_class.class_id) */ + @Column(name = "class_id") + private Integer classId; + + /** 班级名称快照 */ + @Column(name = "class_name", length = 128) + private String className; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java new file mode 100644 index 0000000..5ed9fba --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeTemplate.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板。对应原 .NET Notify_Template 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_template") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeTemplate extends BaseEntity { + + /** 模板标题 */ + @Column(name = "title", length = 200) + private String title; + + /** 模板内容(HTML) */ + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + /** 封面图片文件ID */ + @Column(name = "cover") + private Integer cover; + + /** 引用次数 */ + @Column(name = "ref_count") + private Integer refCount = 0; + + /** 模板类型ID */ + @Column(name = "type_id") + private Integer typeId; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java new file mode 100644 index 0000000..a365b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsModule.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 短信模块配置。对应原 .NET SchSms 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_module") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsModule extends BaseEntity { + + /** 模块名称 */ + @Column(name = "module_name", length = 100) + private String moduleName; + + /** 模块标识 */ + @Column(name = "module_code", length = 50) + private String moduleCode; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..30dbd56 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SmsSchoolConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 学校短信开通配置。对应原 .NET SchSms_Set 表。 + * 记录某学校某模块是否开通短信。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sms_school_config") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SmsSchoolConfig extends BaseEntity { + + /** 学校ID */ + @Column(name = "school_id", nullable = false) + private Integer schoolId; + + /** 学校名称 */ + @Column(name = "school_name", length = 200) + private String schoolName; + + /** 模块ID */ + @Column(name = "module_id") + private Integer moduleId; + + /** 是否开通短信 */ + @Column(name = "is_sms") + private Boolean isSms = false; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/SysDict.java b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java new file mode 100644 index 0000000..681acaa --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/SysDict.java @@ -0,0 +1,40 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_dict") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysDict extends BaseEntity { + + /** 字典类型(如 "dept"=所属分类) */ + @Column(name = "dict_type", length = 50) + private String dictType; + + /** 字典 key(原 CodeID) */ + @Column(name = "dict_key", length = 100) + private String dictKey; + + /** 字典 value(显示名称,原 Name) */ + @Column(name = "dict_value", length = 200) + private String dictValue; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; + + /** 是否启用:0 禁用 / 1 启用(原 Enable) */ + @Column(name = "enable_flag") + private Integer enableFlag = 1; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java new file mode 100644 index 0000000..2384267 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/TemplateType.java @@ -0,0 +1,28 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 通知模板类型。对应原 .NET Notify_TemplateType 表。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "template_type") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class TemplateType extends BaseEntity { + + /** 类型名称 */ + @Column(name = "type_name", length = 100) + private String typeName; + + /** 排序号 */ + @Column(name = "sort_order") + private Integer sortOrder = 0; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/UpFile.java b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java new file mode 100644 index 0000000..4db89c2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/UpFile.java @@ -0,0 +1,47 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 附件文件记录。对应原 .NET Common_UpFiles 表。 + *
+ * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * application.yml 的 file.upload-path 目录下。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "up_file") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UpFile extends BaseEntity { + + /** 应用类型(如 "notice"),用于区分不同业务的附件 */ + @Column(name = "topic_type", length = 50) + private String topicType; + + /** 关联主题ID(如通知 serid) */ + @Column(name = "topic_id") + private Integer topicId; + + /** 文件真实名称(用户上传时的原始文件名) */ + @Column(name = "file_true_name", length = 255) + private String fileTrueName; + + /** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */ + @Column(name = "file_name", length = 500) + private String fileName; + + /** 文件大小(字节) */ + @Column(name = "file_size") + private Integer fileSize; + + /** 文件后缀名(如 pdf, docx, jpg) */ + @Column(name = "file_type", length = 20) + private String fileType; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..1089c9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/NoticeStatusEnum.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。 + *
+ * 0 DRAFT 草稿(存为草稿) + * 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点) + * 2 PENDING 待审批(提交校办审批) + * 3 REJECTED 已驳回(可改后重新提交) + *
每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。
注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。
已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false), + * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。 + */ +@Data +public class ApprovalListVo { + + private Integer id; + + private String title; + + /** 通知类型:1 普通 / 2 重要 */ + private Integer type; + + private String typeDesc; + + /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */ + private Integer status; + + private String statusDesc; + + /** 发布人账号 */ + private String publisherAccount; + + /** 发布人姓名 */ + private String publisherName; + + /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */ + private List approverNames = new ArrayList<>(); + + /** 最新一轮提交时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime submitTime; + + /** 是否需要学员阅读回执 */ + private Integer receiptFlag; + + /** 定时发送时间(审批通过后到点真实发布) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + // ==================== 已审 Tab 字段(待审 Tab 为空) ==================== + + /** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */ + private boolean handled; + + /** 是否由当前登录人处理(false=或签下他人已处理) */ + private boolean handledByMe; + + /** 实际处理人账号 */ + private String handledAccount; + + /** 实际处理人姓名 */ + private String handledName; + + /** 实际处理动作:2 通过 / 3 驳回 */ + private Integer handledAction; + + /** 实际处理动作描述 */ + private String handledActionDesc; + + /** 实际处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime handledTime; + + /** 处理意见(长文本建议进详情查看) */ + private String handledOpinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java new file mode 100644 index 0000000..94c3527 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApprovalRecordVo.java @@ -0,0 +1,37 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。 + */ +@Data +public class ApprovalRecordVo { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + private Integer action; + + /** 动作描述 */ + private String actionDesc; + + /** 操作人账号 */ + private String operatorAccount; + + /** 操作人姓名 */ + private String operatorName; + + /** 审批意见(提交行=提交说明) */ + private String opinion; + + /** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */ + private List assigneeTeaIds = new ArrayList<>(); + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java new file mode 100644 index 0000000..de19b0c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverResolveVo.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。 + * 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。 + */ +@Data +public class ApproverResolveVo { + + /** 教师用户ID(教师树叶子节点 id) */ + private Integer id; + + /** 头像 */ + private String photo; + + /** 姓名 */ + private String name; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java new file mode 100644 index 0000000..2f2d9b6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ApproverSuggestVo.java @@ -0,0 +1,17 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 审批人记忆回显 VO(notice/ApproverSuggest)。 + * 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。 + */ +@Data +public class ApproverSuggestVo { + + /** 审批教师ID列表(教师树叶子节点ID) */ + private List teaIds = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java new file mode 100644 index 0000000..fbef6ee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/InitializeRedVo.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 一键已读结果视图对象(替代原 Map<String,Object> 返回)。 + */ +@Data +public class InitializeRedVo { + + @JsonProperty("count") + private Integer count; + + public InitializeRedVo() {} + + public InitializeRedVo(Integer count) { + this.count = count; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java new file mode 100644 index 0000000..353e843 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeDetailVo.java @@ -0,0 +1,112 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 通知详情视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class NoticeDetailVo { + + @JsonProperty("serid") + private Integer serid; + + @JsonProperty("title") + private String title; + + @JsonProperty("Contents") + private String contents; + + @JsonProperty("DeptID") + private String deptId; + + @JsonProperty("DeptName") + private String deptName; + + @JsonProperty("TeaName") + private String publisherName; + + @JsonProperty("EditDate") + private String editDate; + + @JsonProperty("numcount") + private Integer numCount; + + @JsonProperty("readnumcount") + private Long readNumCount; + + @JsonProperty("ReceiveList") + private String receiveList; + + @JsonProperty("DsfsTime") + private String dsfsTime; + + @JsonProperty("isDsfs") + private Boolean isDsfs; + + @JsonProperty("ChaoSend") + private List chaoSend; + + /** 接收人列表:"all" 或账号列表 */ + @JsonProperty("tlist") + private Object tlist; + + // ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ==================== + + /** 重要程度:1 普通 / 2 重要(编辑回显) */ + @JsonProperty("type") + private Integer type; + + /** 是否置顶:0 否 / 1 是(编辑回显) */ + @JsonProperty("topmost") + private Integer topmost; + + /** 短信提醒标记:0 否 / 1 是(编辑回显) */ + @JsonProperty("smsFlag") + private Integer smsFlag; + + /** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */ + @JsonProperty("status") + private Integer status; + + /** 状态描述 */ + @JsonProperty("statusDesc") + private String statusDesc; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + @JsonProperty("receiptFlag") + private Integer receiptFlag; + + /** 推送范围快照(编辑回显) */ + @JsonProperty("pushScopes") + private List pushScopes; + + /** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */ + @JsonProperty("approvalRecords") + private List approvalRecords; + + /** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */ + @JsonProperty("approverTeaIds") + private List approverTeaIds; + + /** + * 附件项(旧 ChaoSend 数组元素)。 + */ + @Data + public static class FileItem { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("size") + private Integer size; + @JsonProperty("fileid") + private Integer fileid; + @JsonProperty("filename") + private String filename; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java new file mode 100644 index 0000000..95314a7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticePageResult.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageResult; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。 + * + * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储; + * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。 + * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。
+ * 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageResult extends BasePageResult { + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticePageResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + super(content, total, pageIndex, pageSize, totalPages, hasNext); + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..d31692b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeQuery.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.model.notice; + +import com.yunhai.campusnotice.model.BasePageQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +/** + * 通知列表查询条件。 + * + * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
+ * - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤; + * - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeQuery extends BasePageQuery { + + /** 状态过滤:null 不过滤;1 已发布 */ + private Integer status; + + /** 类型过滤:1 普通 / 2 重要 */ + private Integer type; + + /** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */ + private String scope; + + /** 发布部门ID */ + private String deptId; + + /** 发布人账号(管理端按发布人查) */ + private String publisherAccount; + + /** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + /** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + private Integer readType = 0; + + /** 排序:1 时间倒序(默认)/ 2 时间正序 */ + private String orderByTime = "1"; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..0c351f1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeRequest.java @@ -0,0 +1,68 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import com.yunhai.campusnotice.common.validation.ValidationGroups; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。 + */ +@Data +public class NoticeRequest { + + /** 通知标题 */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空") + private String title; + + /** 通知内容 */ + private String content; + + /** 通知类型:1 普通 / 2 重要 */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空") + private Integer type = 1; + + /** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空") + private String deptId; + + /** 发布部门名称 */ + private String deptName; + + /** 是否置顶:0 否 / 1 是 */ + private Integer topmost = 0; + + /** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */ + private String receiveList; + + /** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */ + private String fileIds; + + /** 定时发送时间(为空表示立即发布) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timingSendTime; + + /** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */ + private Integer smsFlag = 0; + + /** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */ + private Integer sourceId; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */ + private List pushScopes; + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag = 0; + + /** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */ + private String publishType; + + /** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */ + private List approverTeaIds; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3ac36eb --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeStatisticsVo.java @@ -0,0 +1,54 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。 + */ +@Data +public class NoticeStatisticsVo { + + /** 通知总数(全部已发布) */ + private long total; + + /** 已发布数 */ + private long publishedCount; + + /** 阅读记录总数(累计已读人次) */ + private long totalReadRecords; + + /** 当前用户未读数(已到点的已发布 - 当前用户已读) */ + private long unreadForCurrentUser; + + /** 按发布人统计 */ + private List byPublisher; + + /** 按月统计(已发布通知数) */ + private List byMonth; + + @Data + public static class PublisherStat { + private String publisherName; + private long count; + + public PublisherStat(String publisherName, long count) { + this.publisherName = publisherName; + this.count = count; + } + } + + @Data + public static class MonthStat { + private int year; + private int month; + private long count; + + public MonthStat(int year, int month, long count) { + this.year = year; + this.month = month; + this.count = count; + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..02edeec --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeUserListResult.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」, + * 供前端展示未读角标(对应原系统 OtherData.unReadCount)。 + */ +@Data +public class NoticeUserListResult { + + private int total; + private List content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List content, int total, int pageIndex, int pageSize, + int totalPages, boolean hasNext, long unreadCount) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java new file mode 100644 index 0000000..755b9f7 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/NoticeVo.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 通知视图对象(列表/详情返回)。 + */ +@Data +public class NoticeVo { + + private Integer id; + private String title; + private String content; + private Integer type; + private String typeDesc; + private String publisherAccount; + private String publisherName; + private String deptId; + private String deptName; + private Integer status; + private String statusDesc; + private Integer topmost; + + /** 已读人数(基于阅读记录实时统计) */ + private Long readCount = 0L; + + /** 应读人数 */ + private Integer allowCount; + + /** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */ + private boolean read = false; + + /** 是否撤回:0 否 / 1 是 */ + private Integer isPulled; + + /** 定时发送时间 */ + private LocalDateTime timingSendTime; + + /** 父通知ID(转发时关联原通知) */ + private Integer parentId; + + /** 短信发送标记 */ + private Integer smsFlag; + + /** 接收人列表(JSON) */ + private String receiveList; + + // ==================== 新版发布表单扩展(改造方案 3.2) ==================== + + /** 是否需要学员阅读回执:0 否 / 1 是 */ + private Integer receiptFlag; + + /** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */ + private Integer timingPublished; + + /** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */ + private String approverTeaIds; + + /** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */ + private String UpFile; + + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; + + /** + * 阅读明细视图(阅读列表查询用)。 + */ + @Data + public static class ReadDetail { + private String account; + private String name; + private boolean read; + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java new file mode 100644 index 0000000..f18aba6 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/PushScopeItem.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围项(发布/编辑入参与详情回显)。 + * scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。 + */ +@Data +public class PushScopeItem { + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */ + private Integer scopeType; + + /** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */ + private Integer scopeId; + + /** 节点名称快照 */ + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java new file mode 100644 index 0000000..4c368e9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ReadReceiptVo.java @@ -0,0 +1,66 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 已发送通知阅读回执 VO(notice/ReadReceipt)。 + * 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。 + * 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。 + */ +@Data +public class ReadReceiptVo { + + /** 通知ID */ + private Integer serid; + + /** 应读人数(notice.allow_count) */ + private Integer allowCount; + + /** 已读人数(notice_read 实时统计) */ + private Long readCount; + + /** 年级分组 */ + private List grades = new ArrayList<>(); + + /** 教师接收行(无年级/班级维度) */ + private List teachers = new ArrayList<>(); + + /** + * 年级分组。 + */ + @Data + public static class GradeGroup { + private Integer gradeId; + private String gradeName; + private List classes = new ArrayList<>(); + } + + /** + * 班级分组。 + */ + @Data + public static class ClassGroup { + private Integer classId; + private String className; + private List students = new ArrayList<>(); + } + + /** + * 学生/教师阅读项。 + */ + @Data + public static class ReaderItem { + private String account; + private String name; + /** 是否已读 */ + private boolean read; + /** 阅读时间(未读为 null) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime readTime; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java new file mode 100644 index 0000000..4a3fbee --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/ScopePreviewVo.java @@ -0,0 +1,19 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 推送范围预估 VO(notice/ScopePreview)。 + */ +@Data +public class ScopePreviewVo { + + /** 范围展开学员数(按账号去重) */ + private int studentCount; + + /** 教师定向名单数(receiveList 解析) */ + private int teacherCount; + + /** 合计预估接收人数 */ + private int totalCount; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java new file mode 100644 index 0000000..a44750f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/SubmitRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +import java.util.List; + +/** + * 提交审批入参(notice/Submit)。 + * 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。 + */ +@Data +public class SubmitRequest { + + /** 通知ID */ + private Integer serid; + + /** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */ + private List approverTeaIds; + + /** 提交说明(可选) */ + private String opinion; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java new file mode 100644 index 0000000..12fcd73 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileResultVo.java @@ -0,0 +1,30 @@ +package com.yunhai.campusnotice.model.notice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 文件上传结果视图对象(替代原 Map<String,Object> 返回)。 + * 使用 @JsonProperty 保持旧字段名兼容前端渲染。 + */ +@Data +public class UpFileResultVo { + + @JsonProperty("fileid") + private Integer fileid; + + @JsonProperty("name") + private String name; + + @JsonProperty("fileurl") + private String fileurl; + + @JsonProperty("host") + private String host; + + @JsonProperty("type") + private String type; + + @JsonProperty("size") + private Integer size; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UpFileVo.java @@ -0,0 +1,31 @@ +package com.yunhai.campusnotice.model.notice; + +import lombok.Data; + +/** + * 附件视图对象(对应原 .NET FileObj)。 + */ +@Data +public class UpFileVo { + + /** 文件记录ID */ + private Integer id; + + /** 文件真实名称 */ + private String fileTrueName; + + /** 文件存储路径(相对路径) */ + private String fileName; + + /** 文件大小(字节) */ + private Integer fileSize; + + /** 文件后缀名 */ + private String fileType; + + /** 关联主题ID */ + private Integer topicId; + + /** 应用类型 */ + private String topicType; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java new file mode 100644 index 0000000..4d448e4 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/notice/UserModuleVo.java @@ -0,0 +1,122 @@ +package com.yunhai.campusnotice.model.notice; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户可访问模块(左菜单)视图对象。 + * 将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。 + */ +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 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 getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..e9f963c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ContactsGroupRepository extends JpaRepository { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..82061f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/ContactsGroupUserRepository.java @@ -0,0 +1,42 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContactsGroupUserRepository extends JpaRepository { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + + /** + * 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。 + * 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行, + * 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按多个组ID批量删除成员。 + */ + @Modifying(clearAutomatically = true) + @Query("delete from ContactsGroupUser g where g.groupId in :groupIds") + void deleteByGroupIdInBulk(@Param("groupIds") List groupIds); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java new file mode 100644 index 0000000..677f98b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApprovalRepository.java @@ -0,0 +1,32 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproval; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 通知审批记录仓储。 + */ +public interface NoticeApprovalRepository extends JpaRepository { + + /** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc") + List findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId); + + /** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */ + @Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc") + List findByNoticeIds(@Param("noticeIds") Collection noticeIds); + + /** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */ + @Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " + + "order by a.id desc") + List findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId, + @Param("actions") Collection actions); + + /** 按通知ID删除全部审批记录(通知删除时级联清理) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java new file mode 100644 index 0000000..d391f9b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeApproverMemoryRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeApproverMemory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。 + */ +public interface NoticeApproverMemoryRepository extends JpaRepository { + + /** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */ + Optional findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java new file mode 100644 index 0000000..33abcfc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticePushScopeRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticePushScope; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * 通知推送范围仓储。 + */ +public interface NoticePushScopeRepository extends JpaRepository { + + /** 按通知ID查询全部范围快照 */ + List findByNoticeId(Integer noticeId); + + /** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */ + @Query("select s from NoticePushScope s where s.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + /** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */ + boolean existsByNoticeId(Integer noticeId); + + /** 按通知ID删除全部范围(编辑全量替换) */ + void deleteByNoticeId(Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..9e948f0 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReadRepository.java @@ -0,0 +1,52 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知阅读记录仓储(懒标记模型)。 + */ +public interface NoticeReadRepository extends JpaRepository { + + /** + * 某用户是否已读某通知。 + */ + boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 某通知的总已读人数。 + */ + long countByNoticeId(Integer noticeId); + + /** + * 删除某通知的全部阅读记录(通知删除时级联清理)。 + */ + void deleteByNoticeId(Integer noticeId); + + /** + * 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。 + */ + long countByReaderAccount(String readerAccount); + + /** + * 查询某用户已读的通知ID列表(用于列表页批量判定未读)。 + */ + @Query("select r.noticeId from NoticeRead r where r.readerAccount = :account") + List findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java new file mode 100644 index 0000000..cb9c89c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeReceiverRepository.java @@ -0,0 +1,48 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * 通知接收人关联表仓储。 + */ +public interface NoticeReceiverRepository extends JpaRepository { + + /** 按通知ID查询全部接收人 */ + List findByNoticeId(Integer noticeId); + + /** 按通知ID删除全部接收人 */ + void deleteByNoticeId(Integer noticeId); + + /** 按通知ID和接收类型查询接收人 */ + List findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** + * 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。 + * @param noticeIds 通知ID列表 + * @return 接收人关联记录列表 + */ + @Query("select r from NoticeReceiver r where r.noticeId in :noticeIds") + List findByNoticeIds(@Param("noticeIds") List noticeIds); + + // ==================== 新版发布流程(改造方案 2.3 / 3.2) ==================== + + /** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */ + @Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account") + List findNoticeIdsByAccount(@Param("account") String account); + + /** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */ + Optional findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc( + Integer noticeId, String account); + + /** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */ + void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); + + /** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */ + long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java new file mode 100644 index 0000000..bc4adef --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeRepository.java @@ -0,0 +1,136 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 通知公告仓储。 + */ +public interface NoticeRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计已发布总数)。 + */ + long countByStatus(Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。 + */ + @Query("select count(n) from Notice n where n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDue(@Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findCreateTimesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。 + */ + @Query("select n.publisherName from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List findPublisherNamesByStatusAndRange(@Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + /** + * 按学校和状态计数(多租户隔离统计)。 + */ + long countBySchIdAndStatus(Integer schId, Integer status); + + /** + * 按学校计数(多租户隔离统计:全部状态)。 + */ + long countBySchId(Integer schId); + + /** + * 排除指定状态计数(统计"累计发布"时剔除草稿)。 + */ + long countByStatusNot(Integer status); + + /** + * 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。 + */ + long countBySchIdAndStatusNot(Integer schId, Integer status); + + /** + * 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。 + */ + @Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " + + "and (n.timingSendTime is null or n.timingSendTime <= :now)") + long countPublishedDueBySchId(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("now") LocalDateTime now); + + /** + * 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。 + */ + @Query("select n.createTime from Notice n " + + "where n.schId = :schId and n.status = :status " + + "and n.createTime >= :start and n.createTime <= :end") + List 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 findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId, + @Param("status") Integer status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + // ==================== 新版发布流程(改造方案 4.2 / 4.3) ==================== + + /** + * 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。 + * TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。 + */ + @Query("select n from Notice n where n.status = 1 " + + "and n.timingSendTime is not null and n.timingSendTime <= :now " + + "and n.timingPublished = 0") + List findDueTimingPublish(@Param("now") LocalDateTime now); + + /** + * 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。 + */ + @Modifying + @Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " + + "where n.id = :id and n.timingPublished = 0") + int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。 + * 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。 + */ + @Modifying + @Query("update Notice n set n.status = 1, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now); + + /** + * 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。 + */ + @Modifying + @Query("update Notice n set n.status = 3, n.updateTime = :now " + + "where n.id = :id and n.status = 2") + int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..b4ceb7c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/NoticeTemplateRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.NoticeTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeTemplateRepository extends JpaRepository { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..2042986 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsModuleRepository.java @@ -0,0 +1,10 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsModuleRepository extends JpaRepository { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..f883e4b --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SmsSchoolConfigRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SmsSchoolConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SmsSchoolConfigRepository extends JpaRepository { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java new file mode 100644 index 0000000..5ceaa3a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/SysDictRepository.java @@ -0,0 +1,15 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.SysDict; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SysDictRepository extends JpaRepository { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); + + /** 按类型+key 取单条字典(发文部门名称解析用) */ + Optional findByDictTypeAndDictKey(String dictType, String dictKey); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..9a181bd --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/TemplateTypeRepository.java @@ -0,0 +1,20 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.TemplateType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TemplateTypeRepository extends JpaRepository { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java new file mode 100644 index 0000000..d46831c --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/repository/UpFileRepository.java @@ -0,0 +1,26 @@ +package com.yunhai.campusnotice.repository; + +import com.yunhai.campusnotice.entity.UpFile; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +/** + * 附件文件仓储。 + */ +public interface UpFileRepository extends JpaRepository { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和多个主题ID批量查询附件(消除 N+1)。 + */ + List findByTopicTypeAndTopicIdIn(String topicType, List topicIds); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IClassService.java b/src/main/java/com/yunhai/campusnotice/service/IClassService.java new file mode 100644 index 0000000..24e1a31 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IClassService.java @@ -0,0 +1,88 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; + +import java.util.List; + +/** + * 班级管理(bschclass)开放接口客户端门面。 + * 基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
将基础平台返回的扁平 {@code List} 转换为前端 layout(参考 UserBase)期望的树结构: + * {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。
基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getStuTree(Integer schId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IContactsService.java b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java new file mode 100644 index 0000000..95971db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IContactsService.java @@ -0,0 +1,79 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 联系组业务接口。 + * 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。 + */ +public interface IContactsService { + + /** + * 添加联系组。 + * + * @param name 组名 + * @param type 类型(1 自定义组, 2 明信片) + */ + void addGroup(String name, Integer type); + + /** + * 获取联系组列表(含每组人数,分页)。 + * + * @param page 页码(从1起) + * @param count 每页条数 + * @param type 类型(1 自定义组, 2 明信片) + * @return Map: { list, total } + */ + Map getGroup(Integer page, Integer count, Integer type); + + /** + * 编辑联系组。 + * + * @param id 组ID + * @param name 组名 + * @param type 类型 + */ + void editGroup(Integer id, String name, Integer type); + + /** + * 删除联系组(支持批量,同时删除组内成员)。 + * + * @param ids 逗号分隔的组ID + */ + void delGroup(String ids); + + /** + * 设置联系组教师(全量替换:先删后增)。 + * + * @param gid 组ID + * @param ids 逗号分隔的用户ID + */ + void setContactsTea(Integer gid, String ids); + + /** + * 获取联系组教师列表(分页)。 + * + * @param gid 组ID + * @param page 页码 + * @param count 每页条数 + * @return Map: { list, total } + */ + Map getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IDictService.java b/src/main/java/com/yunhai/campusnotice/service/IDictService.java new file mode 100644 index 0000000..c76d2c5 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IDictService.java @@ -0,0 +1,74 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 系统字典业务接口。 + * 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。 + */ +public interface IDictService { + + /** + * 获取系统字典(下拉框用)。 + * C# GetSysDic: Type=1 部门类别, 其余类型略。 + * + * @param type 字典类型编号(1=部门类别) + * @return List<Map> 每项 {k, v} + */ + List> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> getDictMangeList(String type); + + /** + * 添加发文单位。 + * + * @param name 单位名称 + */ + void addDeptDic(String name); + + /** + * 更新发文单位。 + * + * @param serID 记录ID + * @param name 名称 + * @param enable 启用状态 + */ + void updateDeptDic(Integer serID, String name, Integer enable); + + /** + * 删除发文单位(校验是否被通知引用)。 + * + * @param serID 记录ID + */ + void delDept(Integer serID); + + /** + * 发文单位排序。 + * + * @param ids 逗号分隔的 SerID 列表(按前端顺序) + */ + void ordbyDicIds(String ids); + + /** + * 是否发送短信(查询当前学校模块短信开关)。 + * + * @return Boolean + */ + Boolean getSchModuleIsSms(); + + /** + * 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。 + * 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。 + * + * @param deptId 通知 deptId(存的是 sys_dict.dict_key) + * @return 部门名称;无匹配时返回 null + */ + String resolveDeptName(String deptId); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IFileService.java b/src/main/java/com/yunhai/campusnotice/service/IFileService.java new file mode 100644 index 0000000..c221cd2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.UpFile; +import com.yunhai.campusnotice.model.notice.UpFileVo; +import com.yunhai.campusnotice.model.notice.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "notice") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(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 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 uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/INoticeService.java b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java new file mode 100644 index 0000000..62c7d77 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/INoticeService.java @@ -0,0 +1,222 @@ +package com.yunhai.campusnotice.service; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.model.BasePageResult; +import com.yunhai.campusnotice.model.notice.*; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 通知公告业务接口。 + */ +public interface INoticeService { + + /** + * 发通知:以当前登录用户为发布人入库。 + * 按 request.publishType 分流(改造方案 4.1): + * direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度); + * draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。 + * + * @return 新建通知 ID(用于后续回填附件 TopicID 等) + */ + Integer publish(NoticeRequest request); + + /** + * 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。 + */ + Integer saveDraft(NoticeRequest request); + + /** + * 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。 + */ + BasePageResult draftList(NoticeQuery query, Pageable pageable); + + /** + * 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签), + * 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。 + */ + void submit(Integer serid, List approverTeaIds, String opinion); + + /** + * 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布; + * 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。 + */ + void approve(Integer serid, String opinion); + + /** + * 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。 + */ + void reject(Integer serid, String opinion); + + /** + * 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。 + * + * @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理) + */ + BasePageResult approvalList(String auditState, NoticeQuery query, Pageable pageable); + + /** + * 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。 + */ + ApproverSuggestVo approverSuggest(); + + /** + * 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。 + * schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。 + */ + List approverResolve(List ids, Integer schId); + + /** + * 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑; + * 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。 + */ + void edit(Integer id, NoticeRequest request); + + /** + * 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删; + * 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。 + */ + void deleteBatch(List ids); + + /** + * 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。 + */ + void pull(Integer id); + + /** + * 置顶/取消置顶。 + */ + void setTopmost(Integer id, Integer topmost); + + /** + * 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。 + */ + Integer forward(Integer sourceId, NoticeRequest request); + + /** + * 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。 + * 可见性(改造方案 3.2):无推送范围维持「全校广播」现状; + * 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。 + */ + NoticePageResult userList(NoticeQuery query, Pageable pageable); + + /** + * 管理端列表:按条件查询全部通知(管理员查看全部)。 + */ + BasePageResult adminList(NoticeQuery query, Pageable pageable); + + /** + * 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass), + * 供表单「推送范围」胶囊按钮渲染。 + */ + List> scopeTree(); + + /** + * 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。 + */ + ScopePreviewVo scopePreview(List scopes, String receiveList); + + /** + * 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照), + * 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。 + */ + ReadReceiptVo readReceipt(Integer serid); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + * 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。 + */ + NoticeVo detail(Integer id); + + /** + * 编辑回显详情(不标记已读,访问校验同 {@link #detail})。 + */ + NoticeVo getDetail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录, + * 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。 + */ + void markRead(Integer id); + + /** + * 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。 + * + * @return 已读条数 + */ + int initializeRed(); + + /** + * 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。 + */ + void receiveUsers(Integer noticeId, String users); + + /** + * 上一篇/下一篇通知ID(SQL 相邻查询)。 + * + * @param serid 当前通知ID + * @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列) + * @return {prevId, nextId} + */ + Map getNextPrev(Integer serid, String type); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。 + */ + List> getReadDetailGrouped(Integer serid); + + /** + * 催收未读:仅 receiptFlag=1 的通知可用; + * 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。 + */ + void expedite(Integer serid); + + /** + * 定时发送调度(TimingPublishJob 每分钟触发): + * 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(幂等)后执行真实发布。 + * + * @return 本次真实发布条数 + */ + int timingPublishDue(); + + /** + * 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); + + /** + * 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。 + */ + NoticeDetailVo getDetailMap(Integer serid); + + /** + * 已发列表附件批量查询(消除 N+1)。 + */ + Map> batchFileIds(List noticeIds); + + /** + * 查看通知详情视图(含附件、接收人)。 + * type=1 时先标记已读再返回,type=0 时直接返回不标记。 + */ + NoticeDetailVo detailMap(Integer serid, Integer type); + + /** + * 一键全部已读并返回结果(count = 已读条数)。 + */ + InitializeRedVo initializeRedResult(); + + /** + * 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。 + */ + void downloadAllFiles(Integer dataid, HttpServletResponse response); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ISmsService.java b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java new file mode 100644 index 0000000..3365c71 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ISmsService.java @@ -0,0 +1,77 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 短信设置业务接口。 + * 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。 + */ +public interface ISmsService { + + /** + * 获取短信模块列表。 + * C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchList(); + + /** + * 设置单校单模块短信开关。 + * + * @param schSerID 学校ID + * @param moduleId 模块ID + * @param isSms 是否开通 + */ + void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms); + + /** + * 设置某校全部模块短信开关。 + * + * @param schSerID 学校ID + * @param isSms 是否开通 + */ + void setSchSmsAll(Integer schSerID, Boolean isSms); + + /** + * 批量设置多校多模块短信开关。 + * + * @param schSerIDs 逗号分隔的学校ID + * @param modules 逗号分隔的模块ID + * @param isSms 是否开通 + */ + void setSchModuleSms(String schSerIDs, String modules, Boolean isSms); + + /** + * 从 JSON 字符串解析并设置单校单模块短信开关。 + * 解析 SchSerID/ModuID/IsSms 三个字段。 + */ + void setSchSmsFromJson(String itemsModuJson); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java new file mode 100644 index 0000000..95c46a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/ITemplateService.java @@ -0,0 +1,69 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 通知模板业务接口。 + * 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。 + */ +public interface ITemplateService { + + /** + * 获取模板类型列表(首项固定为"全部")。 + * + * @return List<Map> 每项 {TypeID, TypeName} + */ + List> getTypelist(); + + /** + * 新增/编辑模板类型。 + * + * @param typeid 类型ID(0=新增) + * @param typeName 类型名称 + */ + void editType(Integer typeid, String typeName); + + /** + * 删除模板类型(校验是否有模板引用此类型)。 + * + * @param typeid 类型ID + */ + void delType(Integer typeid); + + /** + * 分页获取模板列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param typeid 类型ID(0=全部) + * @return Map: { list, total } + */ + Map getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid); + + /** + * 新增/编辑模板。 + * + * @param serID 模板ID(null=新增) + * @param title 标题 + * @param content 内容 + * @param typeId 类型ID + * @param cover 封面文件ID + */ + void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover); + + /** + * 删除模板。 + * + * @param serID 模板ID + */ + void delTemplate(Integer serID); + + /** + * 获取单个模板详情。 + * + * @param serID 模板ID + * @return Map 模板详情 + */ + Map getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java new file mode 100644 index 0000000..fd4e555 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.campusnotice.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java new file mode 100644 index 0000000..32de273 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ClassService.java @@ -0,0 +1,176 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.model.SchTeaList; +import com.yunhai.campusnotice.model.bsch.SchClassDto; +import com.yunhai.campusnotice.model.bsch.SchGradeDto; +import com.yunhai.campusnotice.model.bsch.SchInfoDto; +import com.yunhai.campusnotice.model.bsch.SchStudentDto; +import com.yunhai.campusnotice.model.bsch.SchSystemDto; +import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto; +import com.yunhai.campusnotice.service.IClassService; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 班级管理(bschclass)开放接口实现。 + * bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。 + * 推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List
封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。
组装规则(从原 BaseApiController.getSchoolAll 下沉):
对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。
从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。
推送范围树/圈人/身份快照统一经本门面(改造方案 2.0): + * 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、 + * 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、 + * 学生树 SchGroup/GetStuTree。 + */ +@Service +public class ClassService implements IClassService { + + private static final Logger log = LoggerFactory.getLogger(ClassService.class); + + /** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */ + @Value("${yh.open.bschclass:}") + private String bschClassHost; + + private final RestTemplate restTemplate; + + public ClassService() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + this.restTemplate = new RestTemplate(factory); + } + + @Override + public List getSchTeaList(int schId) { + return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId, + new ParameterizedTypeReference>() { + }, "获取教师数据", "schId=" + schId); + } + + @Override + public SchInfoDto getSchFirst(int schId) { + requireHost(); + String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId; + try { + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class); + return resp.getBody(); + } catch (RestClientException e) { + log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败"); + } + } + + @Override + public List getSchLenList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学段列表", "schSerId=" + schSerId); + } + + @Override + public List getSchGradeList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取年级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchClassList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取班级列表", "schSerId=" + schSerId); + } + + @Override + public List getSchStuList(String schSerId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId, + new ParameterizedTypeReference>() { + }, "获取学校学生", "schSerId=" + schSerId); + } + + @Override + public List getGradeStuList(String schSerId, Integer gradeId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId, + new ParameterizedTypeReference>() { + }, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId); + } + + @Override + public List getSubStuList(String schSerId, Integer schSystemId) { + return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId, + new ParameterizedTypeReference>() { + }, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId); + } + + @Override + public List getClaArrStuList(List classIds) { + String csv = joinIds(classIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv, + new ParameterizedTypeReference>() { + }, "获取班级学生", "classIds=" + csv); + } + + @Override + public List getStuIdsList(List stuIds) { + String csv = joinIds(stuIds); + return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv, + new ParameterizedTypeReference>() { + }, "获取指定学生", "stuIds=" + csv); + } + + @Override + public List getStuTree(Integer schId) { + return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId, + new ParameterizedTypeReference>() { + }, "获取学生树", "schId=" + schId); + } + + // ============================ 通用调用 ============================ + + private List exchangeList(String url, ParameterizedTypeReference> type, + String action, String arg) { + requireHost(); + try { + // GET + Accept: application/json,保持与既有 GetSchteaList 调用一致 + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), type); + List list = resp.getBody(); + log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size()); + return list != null ? list : Collections.emptyList(); + } catch (RestClientException e) { + log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败"); + } + } + + private void requireHost() { + if (!StringUtils.hasText(bschClassHost)) { + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)"); + } + } + + private String joinIds(List ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + return ids.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java new file mode 100644 index 0000000..ea640de --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/ContactsService.java @@ -0,0 +1,413 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.ContactsGroup; +import com.yunhai.campusnotice.entity.ContactsGroupUser; +import com.yunhai.campusnotice.repository.ContactsGroupRepository; +import com.yunhai.campusnotice.repository.ContactsGroupUserRepository; +import com.yunhai.campusnotice.service.IContactsService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 联系组服务实现。 + * 对应 C# ContactsController + ManageContacts.cs。 + */ +@Service +public class ContactsService implements IContactsService { + + private static final Logger log = LoggerFactory.getLogger(ContactsService.class); + + @Autowired + private ContactsGroupRepository groupRepository; + + @Autowired + private ContactsGroupUserRepository groupUserRepository; + + // ============================ AddGroup ============================ + + @Override + @Transactional + public void addGroup(String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字"); + } + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查重:同创建人下同名同类型组不重复 + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!"); + } + + ContactsGroup group = new ContactsGroup(); + group.setGroupName(name); + group.setCreatorAccount(account); + groupRepository.save(group); + } + + // ============================ GetGroup ============================ + + @Override + public Map getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map item = new LinkedHashMap<>(); + item.put("serid", g.getId()); + item.put("GroupName", g.getGroupName()); + item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue()); + return item; + }).collect(Collectors.toList()); + + // 分页 + int total = listData.size(); + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + if (fromIndex < total) { + listData = listData.subList(fromIndex, toIndex); + } else { + listData = Collections.emptyList(); + } + } + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ EditGroup ============================ + + @Override + @Transactional + public void editGroup(Integer id, String name, Integer type) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空"); + } + if (id == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!"); + } + String account = currentAccount(); + + // 查重(排除自身) + List existing = groupRepository.findByCreatorAccount(account); + boolean duplicate = existing.stream().anyMatch(g -> + !g.getId().equals(id) && name.equals(g.getGroupName())); + if (duplicate) { + throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组"); + } + + ContactsGroup group = groupRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在")); + group.setGroupName(name); + groupRepository.save(group); + } + + // ============================ DelGroup ============================ + + @Override + @Transactional + public void delGroup(String ids) { + if (!StringUtils.hasText(ids)) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败"); + } + List idList = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + // 先删成员再删组 + groupUserRepository.deleteByGroupIdInBulk(idList); + groupRepository.findAllById(idList).forEach(groupRepository::delete); + } + + // ============================ SetContactsTea ============================ + + @Override + @Transactional + public void setContactsTea(Integer gid, String ids) { + if (gid == null || gid == 0) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + // 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush + // 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit) + groupUserRepository.deleteByGroupIdBulk(gid); + + if (StringUtils.hasText(ids)) { + List userIds = Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .distinct() + .collect(Collectors.toList()); + for (Integer userId : userIds) { + ContactsGroupUser member = new ContactsGroupUser(); + member.setGroupId(gid); + member.setUserId(userId); + // 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法), + // 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用 + try { + BoaUser user = YschBaseOpenApi.getUserInfo(userId); + if (user != null) { + member.setUserAccount(user.getAccount()); + member.setUserName(user.getName()); + } + } catch (Exception e) { + log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e); + } + groupUserRepository.save(member); + } + } + } + + // ============================ GetContactsTea ============================ + + @Override + public Map getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + // 先分页再补全用户信息(避免为未分页数据调用用户中心) + List pageMembers = allMembers; + if (count != null && count > 0 && page != null && page > 0) { + int fromIndex = (page - 1) * count; + int toIndex = Math.min(fromIndex + count, total); + pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList(); + } + + List> listData = pageMembers.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("sid", m.getId()); + item.put("id", m.getUserId()); + fillUserInfo(item, m); + return item; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + /** + * 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。 + * 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。 + * 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。 + */ + private void fillUserInfo(Map item, ContactsGroupUser m) { + String name = m.getUserName(); + String account = m.getUserAccount(); + if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) { + BoaUser user = null; + try { + user = YschBaseOpenApi.getUserInfo(m.getUserId()); + } catch (Exception e) { + log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e); + } + if (user == null) { + log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId()); + } else { + if (!StringUtils.hasText(account)) { + account = user.getAccount(); + } + if (!StringUtils.hasText(name)) { + name = user.getName(); + } + // 回写落库:下次读取直接命中快照 + if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account)) + || (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) { + if (StringUtils.hasText(account)) { + m.setUserAccount(account); + } + if (StringUtils.hasText(name)) { + m.setUserName(name); + } + try { + groupUserRepository.save(m); + } catch (Exception e) { + log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e); + } + } + } + } + item.put("name", name != null ? name : ""); + item.put("account", account != null ? account : ""); + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = currentAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map node = new LinkedHashMap<>(); + node.put("id", g.getId()); + node.put("name", g.getGroupName()); + node.put("pid", 0); + node.put("no", 0); + node.put("type", 1); + node.put("isParent", true); + node.put("photo", ""); + node.put("children", new ArrayList>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全 + for (ContactsGroupUser m : allMembers) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getUserId()); + node.put("name", ""); + node.put("pid", m.getGroupId()); + node.put("no", 0); + node.put("type", 2); + node.put("isParent", false); + node.put("photo", ""); + fillUserInfo(node, m); + treeNodes.add(node); + } + + // 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林 + return formatUserTree(treeNodes, 0); + } + + // ============================ GetDeptRoleGroupTree ============================ + + @Override + public List> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set allIds = treeList.stream() + .map(n -> (Integer) n.get("id")) + .collect(Collectors.toSet()); + data = treeList.stream() + .filter(n -> { + int p = (Integer) n.get("pid"); + return p == 0 || !allIds.contains(p); + }) + .collect(Collectors.toList()); + } else { + data = treeList.stream() + .filter(n -> (Integer) n.get("pid") == pid) + .collect(Collectors.toList()); + } + + List> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map node = new LinkedHashMap<>(item); + // 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent + if (itemType == 1) { + node.put("children", children); + } + result.add(node); + } + + // 按 no 排序 + result.sort(Comparator.comparingInt(n -> { + Object no = n.get("no"); + if (no instanceof Integer) return (Integer) no; + if (no instanceof String) { + try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; } + } + return 9999; + })); + + return result; + } + + /** + * 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。 + */ + private String currentAccount() { + LoginUser user = UserContextHolder.get(); + return user == null ? null : user.getLoginName(); + } +} diff --git a/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java new file mode 100644 index 0000000..5ea097e --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/service/impl/DictService.java @@ -0,0 +1,253 @@ +package com.yunhai.campusnotice.service.impl; + +import com.yunhai.campusnotice.entity.Notice; +import com.yunhai.campusnotice.entity.SysDict; +import com.yunhai.campusnotice.repository.NoticeRepository; +import com.yunhai.campusnotice.repository.SysDictRepository; +import com.yunhai.campusnotice.service.IDictService; +import com.yunhai.platform.auth.LoginUser; +import com.yunhai.platform.auth.UserContextHolder; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 系统字典服务实现。 + * 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。 + */ +@Service +public class DictService implements IDictService { + + private static final Logger log = LoggerFactory.getLogger(DictService.class); + + /** 字典类型常量:发文部门 */ + private static final String DICT_TYPE_DEPT = "dept"; + + @Autowired + private SysDictRepository sysDictRepository; + + @Autowired + private NoticeRepository noticeRepository; + + // ============================ GetSysDic ============================ + + @Override + public List> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "参数错误"); + } + List list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1); + if (list.isEmpty()) { + throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!"); + } + // 按 sortOrder 升序返回 {k, v} + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map item = new LinkedHashMap<>(); + item.put("SerID", d.getId()); + item.put("CodeID", d.getDictKey()); + item.put("Name", d.getDictValue()); + item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : ""); + item.put("Enable", d.getEnableFlag()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ AddDeptDic ============================ + + @Override + @Transactional + public void addDeptDic(String name) { + if (!StringUtils.hasText(name)) { + throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!"); + } + // 校验重名 + List