通知
build / build-push (push) Failing after 1m18s

This commit is contained in:
wanghu
2026-09-10 15:39:41 +08:00
parent 2c8125a549
commit 3016a97f1b
86 changed files with 3427 additions and 1669 deletions
@@ -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 直连。
* <p>推送范围树/圈人/身份快照统一经本门面(改造方案 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<SchTeaList> getSchTeaList(int schId) {
return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId,
new ParameterizedTypeReference<List<SchTeaList>>() {
}, "获取教师数据", "schId=" + schId);
}
@Override
public SchInfoDto getSchFirst(int schId) {
requireHost();
String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId;
try {
ResponseEntity<SchInfoDto> 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<SchSystemDto> getSchLenList(String schSerId) {
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId,
new ParameterizedTypeReference<List<SchSystemDto>>() {
}, "获取学段列表", "schSerId=" + schSerId);
}
@Override
public List<SchGradeDto> getSchGradeList(String schSerId) {
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId,
new ParameterizedTypeReference<List<SchGradeDto>>() {
}, "获取年级列表", "schSerId=" + schSerId);
}
@Override
public List<SchClassDto> getSchClassList(String schSerId) {
return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId,
new ParameterizedTypeReference<List<SchClassDto>>() {
}, "获取班级列表", "schSerId=" + schSerId);
}
@Override
public List<SchStudentDto> getSchStuList(String schSerId) {
return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId,
new ParameterizedTypeReference<List<SchStudentDto>>() {
}, "获取学校学生", "schSerId=" + schSerId);
}
@Override
public List<SchStudentDto> getGradeStuList(String schSerId, Integer gradeId) {
return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId,
new ParameterizedTypeReference<List<SchStudentDto>>() {
}, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId);
}
@Override
public List<SchStudentDto> getSubStuList(String schSerId, Integer schSystemId) {
return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId,
new ParameterizedTypeReference<List<SchStudentDto>>() {
}, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId);
}
@Override
public List<SchStudentDto> getClaArrStuList(List<Integer> classIds) {
String csv = joinIds(classIds);
return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv,
new ParameterizedTypeReference<List<SchStudentDto>>() {
}, "获取班级学生", "classIds=" + csv);
}
@Override
public List<SchStudentDto> getStuIdsList(List<Integer> stuIds) {
String csv = joinIds(stuIds);
return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv,
new ParameterizedTypeReference<List<SchStudentDto>>() {
}, "获取指定学生", "stuIds=" + csv);
}
@Override
public List<SchTreeNodeDto> getStuTree(Integer schId) {
return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId,
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
}, "获取学生树", "schId=" + schId);
}
// ============================ 通用调用 ============================
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
String action, String arg) {
requireHost();
try {
// GET + Accept: application/json,保持与既有 GetSchteaList 调用一致
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
ResponseEntity<List<T>> resp = restTemplate.exchange(
url, HttpMethod.GET, new HttpEntity<>(headers), type);
List<T> 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<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return "";
}
return ids.stream().map(String::valueOf).collect(Collectors.joining(","));
}
}
@@ -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<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
boolean duplicate = existing.stream().anyMatch(g ->
name.equals(g.getGroupName()));
if (duplicate) {
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!");
}
ContactsGroup group = new ContactsGroup();
group.setGroupName(name);
group.setCreatorAccount(account);
groupRepository.save(group);
}
// ============================ GetGroup ============================
@Override
public Map<String, Object> getGroup(Integer page, Integer count, Integer type) {
if (type == null) {
type = 1;
}
String account = currentAccount();
// 查询当前用户创建的全部联系组
List<ContactsGroup> allGroups = groupRepository.findByCreatorAccount(account);
// 统计每组人数
List<Integer> groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
: groupUserRepository.findByGroupIdIn(groupIds);
Map<Integer, Long> countByGroup = allMembers.stream()
.collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting()));
// 构建列表数据
List<Map<String, Object>> listData = allGroups.stream().map(g -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("serid", g.getId());
item.put("GroupName", g.getGroupName());
item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue());
return item;
}).collect(Collectors.toList());
// 分页
int total = listData.size();
if (count != null && count > 0 && page != null && page > 0) {
int fromIndex = (page - 1) * count;
int toIndex = Math.min(fromIndex + count, total);
if (fromIndex < total) {
listData = listData.subList(fromIndex, toIndex);
} else {
listData = Collections.emptyList();
}
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("list", listData);
result.put("total", total);
return result;
}
// ============================ EditGroup ============================
@Override
@Transactional
public void editGroup(Integer id, String name, Integer type) {
if (!StringUtils.hasText(name)) {
throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空");
}
if (id == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
}
String account = currentAccount();
// 查重(排除自身)
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
boolean duplicate = existing.stream().anyMatch(g ->
!g.getId().equals(id) && name.equals(g.getGroupName()));
if (duplicate) {
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组");
}
ContactsGroup group = groupRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在"));
group.setGroupName(name);
groupRepository.save(group);
}
// ============================ DelGroup ============================
@Override
@Transactional
public void delGroup(String ids) {
if (!StringUtils.hasText(ids)) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败");
}
List<Integer> idList = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.collect(Collectors.toList());
// 先删成员再删组
groupUserRepository.deleteByGroupIdInBulk(idList);
groupRepository.findAllById(idList).forEach(groupRepository::delete);
}
// ============================ SetContactsTea ============================
@Override
@Transactional
public void setContactsTea(Integer gid, String ids) {
if (gid == null || gid == 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
}
// 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush
// 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit)
groupUserRepository.deleteByGroupIdBulk(gid);
if (StringUtils.hasText(ids)) {
List<Integer> userIds = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.distinct()
.collect(Collectors.toList());
for (Integer userId : userIds) {
ContactsGroupUser member = new ContactsGroupUser();
member.setGroupId(gid);
member.setUserId(userId);
// 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法),
// 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用
try {
BoaUser user = YschBaseOpenApi.getUserInfo(userId);
if (user != null) {
member.setUserAccount(user.getAccount());
member.setUserName(user.getName());
}
} catch (Exception e) {
log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e);
}
groupUserRepository.save(member);
}
}
}
// ============================ GetContactsTea ============================
@Override
public Map<String, Object> getContactsTea(String gid, Integer page, Integer count) {
Integer groupId = 0;
try {
groupId = Integer.parseInt(gid);
} catch (NumberFormatException e) {
groupId = 0;
}
if (groupId == 0) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("list", Collections.emptyList());
empty.put("total", 0);
return empty;
}
List<ContactsGroupUser> allMembers = groupUserRepository.findByGroupId(groupId);
int total = allMembers.size();
// 先分页再补全用户信息(避免为未分页数据调用用户中心)
List<ContactsGroupUser> pageMembers = allMembers;
if (count != null && count > 0 && page != null && page > 0) {
int fromIndex = (page - 1) * count;
int toIndex = Math.min(fromIndex + count, total);
pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList();
}
List<Map<String, Object>> listData = pageMembers.stream().map(m -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("sid", m.getId());
item.put("id", m.getUserId());
fillUserInfo(item, m);
return item;
}).collect(Collectors.toList());
Map<String, Object> result = new LinkedHashMap<>();
result.put("list", listData);
result.put("total", total);
return result;
}
/**
* 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。
* 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。
* 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。
*/
private void fillUserInfo(Map<String, Object> item, ContactsGroupUser m) {
String name = m.getUserName();
String account = m.getUserAccount();
if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) {
BoaUser user = null;
try {
user = YschBaseOpenApi.getUserInfo(m.getUserId());
} catch (Exception e) {
log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e);
}
if (user == null) {
log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId());
} else {
if (!StringUtils.hasText(account)) {
account = user.getAccount();
}
if (!StringUtils.hasText(name)) {
name = user.getName();
}
// 回写落库:下次读取直接命中快照
if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account))
|| (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) {
if (StringUtils.hasText(account)) {
m.setUserAccount(account);
}
if (StringUtils.hasText(name)) {
m.setUserName(name);
}
try {
groupUserRepository.save(m);
} catch (Exception e) {
log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e);
}
}
}
}
item.put("name", name != null ? name : "");
item.put("account", account != null ? account : "");
}
// ============================ GetGroupTree ============================
@Override
public List<Map<String, Object>> getGroupTree(Integer type) {
if (type == null) {
type = 1;
}
String account = currentAccount();
// 查询当前用户的联系组
List<ContactsGroup> groups = groupRepository.findByCreatorAccount(account);
List<Integer> groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
// 查询全部成员
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
: groupUserRepository.findByGroupIdIn(groupIds);
// 构建树节点
List<Map<String, Object>> treeNodes = new ArrayList<>();
// 组节点 (type=1, pid=0)
for (ContactsGroup g : groups) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", g.getId());
node.put("name", g.getGroupName());
node.put("pid", 0);
node.put("no", 0);
node.put("type", 1);
node.put("isParent", true);
node.put("photo", "");
node.put("children", new ArrayList<Map<String, Object>>());
treeNodes.add(node);
}
// 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全
for (ContactsGroupUser m : allMembers) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", m.getUserId());
node.put("name", "");
node.put("pid", m.getGroupId());
node.put("no", 0);
node.put("type", 2);
node.put("isParent", false);
node.put("photo", "");
fillUserInfo(node, m);
treeNodes.add(node);
}
// 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林
return formatUserTree(treeNodes, 0);
}
// ============================ GetDeptRoleGroupTree ============================
@Override
public List<Map<String, Object>> getDeptRoleGroupTree() {
// C# 从外部API获取部门树+用户角色映射。
// Java版暂未接入用户中心OpenAPI,返回空列表。
log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表");
return Collections.emptyList();
}
// ============================ 辅助方法 ============================
/**
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
*/
@SuppressWarnings("unchecked")
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
List<Map<String, Object>> data;
if (pid == 0) {
// 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的)
Set<Integer> allIds = treeList.stream()
.map(n -> (Integer) n.get("id"))
.collect(Collectors.toSet());
data = treeList.stream()
.filter(n -> {
int p = (Integer) n.get("pid");
return p == 0 || !allIds.contains(p);
})
.collect(Collectors.toList());
} else {
data = treeList.stream()
.filter(n -> (Integer) n.get("pid") == pid)
.collect(Collectors.toList());
}
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> item : data) {
int itemId = (Integer) item.get("id");
int itemType = (Integer) item.get("type");
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
if (itemType == 1 && children.isEmpty()) {
continue;
}
Map<String, Object> node = new LinkedHashMap<>(item);
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
if (itemType == 1) {
node.put("children", children);
}
result.add(node);
}
// 按 no 排序
result.sort(Comparator.comparingInt(n -> {
Object no = n.get("no");
if (no instanceof Integer) return (Integer) no;
if (no instanceof String) {
try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; }
}
return 9999;
}));
return result;
}
/**
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
*/
private String currentAccount() {
LoginUser user = UserContextHolder.get();
return user == null ? null : user.getLoginName();
}
}
@@ -0,0 +1,253 @@
package com.yunhai.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<Map<String, Object>> getSysDic(Integer type) {
// C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及
String dictType = mapDictType(type);
if (dictType == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
}
List<SysDict> list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1);
if (list.isEmpty()) {
throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!");
}
// 按 sortOrder 升序返回 {k, v}
return list.stream()
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder()))
.map(d -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("k", d.getDictKey());
item.put("v", d.getDictValue());
return item;
})
.collect(Collectors.toList());
}
// ============================ GetDictMangeList ============================
@Override
public List<Map<String, Object>> getDictMangeList(String type) {
// type="dept" 表示部门类别(DEPT)
String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null;
if (dictType == null) {
// 非 DEPT 类型需超管权限,本系统暂只支持 DEPT
throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!");
}
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType);
return list.stream()
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()))
.map(d -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("SerID", d.getId());
item.put("CodeID", d.getDictKey());
item.put("Name", d.getDictValue());
item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : "");
item.put("Enable", d.getEnableFlag());
return item;
})
.collect(Collectors.toList());
}
// ============================ AddDeptDic ============================
@Override
@Transactional
public void addDeptDic(String name) {
if (!StringUtils.hasText(name)) {
throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!");
}
// 校验重名
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
boolean duplicate = existing.stream().anyMatch(d ->
DICT_TYPE_DEPT.equals(d.getDictType()) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
if (duplicate) {
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
}
// 计算 sortOrder = max + 1
int maxSort = existing.stream()
.filter(d -> d.getEnableFlag() != 3)
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : 0)
.max().orElse(0);
SysDict dict = new SysDict();
dict.setDictType(DICT_TYPE_DEPT);
dict.setDictValue(name);
dict.setSortOrder(maxSort + 1);
dict.setDictKey(DICT_TYPE_DEPT + (maxSort + 1));
dict.setEnableFlag(1);
sysDictRepository.save(dict);
}
// ============================ UpdateDeptDic ============================
@Override
@Transactional
public void updateDeptDic(Integer serID, String name, Integer enable) {
if (serID == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
}
// 校验重名(排除自身)
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
boolean duplicate = existing.stream().anyMatch(d ->
!d.getId().equals(serID) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
if (duplicate) {
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
}
SysDict dict = sysDictRepository.findById(serID)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
if (StringUtils.hasText(name)) {
dict.setDictValue(name);
}
if (enable != null) {
dict.setEnableFlag(enable);
}
sysDictRepository.save(dict);
}
// ============================ DelDept ============================
@Override
@Transactional
public void delDept(Integer serID) {
SysDict dict = sysDictRepository.findById(serID)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
// 校验是否被通知引用(按 deptId = dictKey 匹配)
// C# 中检查 Notify_Message.DeptID == dict.CodeID
// Java 中 Notice.deptId 存的是 dictKey
List<Notice> referenced = noticeRepository.findAll((root, query, cb) ->
cb.equal(root.get("deptId"), dict.getDictKey()));
if (!referenced.isEmpty()) {
throw new BizException(ErrorCode.FORBIDDEN, "已引用该部门,无法删除!");
}
sysDictRepository.delete(dict);
}
// ============================ OrdbyDicIds ============================
@Override
@Transactional
public void ordbyDicIds(String ids) {
if (!StringUtils.hasText(ids)) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
}
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
if (list.isEmpty()) {
throw new BizException(ErrorCode.FORBIDDEN, "字典类型错误,未找到该字典类型");
}
// C# 逻辑:取当前最大 Sort 为起始序号,逆序遍历 ids 递减赋值 Des
int[] idArr = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.mapToInt(Integer::parseInt)
.toArray();
int maxSort = list.stream()
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : idArr.length)
.max().orElse(idArr.length);
// lambda 中不能修改局部变量,用数组包装
final int[] indexOrd = { maxSort };
// 逆序遍历,从最大序号递减
for (int i = idArr.length - 1; i >= 0; i--) {
int id = idArr[i];
sysDictRepository.findById(id).ifPresent(dict -> {
dict.setSortOrder(indexOrd[0]);
sysDictRepository.save(dict);
indexOrd[0]--;
});
}
}
// ============================ GetSchModuleIsSms ============================
@Override
public Boolean getSchModuleIsSms() {
// C# 通过外部 ISchIsSmsBLL 查询学校短信开关。
// Java 版暂不接入外部短信平台,默认返回 false。
String account = currentAccount();
if (account == null) {
return false;
}
// 后续接入短信平台后补全查询逻辑
return false;
}
// ============================ 辅助方法 ============================
/**
* 按发文部门字典 key 解析部门名称(对应 C# GetDeptName)。
*/
@Override
public String resolveDeptName(String deptId) {
if (!StringUtils.hasText(deptId)) {
return null;
}
return sysDictRepository.findByDictTypeAndDictKey(DICT_TYPE_DEPT, deptId.trim())
.map(SysDict::getDictValue)
.orElse(null);
}
/**
* 将前端 Type 参数映射到字典类型字符串。
* C# 中使用枚举,Java 版仅支持 DEPT(1)。
*/
private String mapDictType(Integer type) {
if (type == null) return null;
switch (type) {
case 1: return DICT_TYPE_DEPT;
// 其余类型(文印/请假/物品等)本系统不涉及
default: return null;
}
}
/**
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
*/
private String currentAccount() {
LoginUser user = UserContextHolder.get();
return user == null ? null : user.getLoginName();
}
}
@@ -0,0 +1,326 @@
package com.yunhai.campusnotice.service.impl;
import com.yunhai.campusnotice.entity.UpFile;
import com.yunhai.campusnotice.model.notice.UpFileVo;
import com.yunhai.campusnotice.model.notice.UpFileResultVo;
import com.yunhai.campusnotice.repository.UpFileRepository;
import com.yunhai.campusnotice.service.IFileService;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 文件上传/下载/删除业务实现。
* <p>
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
*/
@Service
public class FileService implements IFileService {
private static final Logger log = LoggerFactory.getLogger(FileService.class);
@Autowired
private UpFileRepository upFileRepository;
@Value("${file.upload-path}")
private String uploadPath;
private static final String DEFAULT_APP_TYPE = "notice";
// ============================ 上传 ============================
@Override
@Transactional
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
if (file == null || file.isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, "上传文件不得为空");
}
String originalName = file.getOriginalFilename();
if (!StringUtils.hasText(originalName)) {
throw new BizException(ErrorCode.PARAM_ERROR, "文件名为空");
}
// 提取后缀
String ext = "";
int dotIdx = originalName.lastIndexOf('.');
if (dotIdx >= 0) {
ext = originalName.substring(dotIdx + 1).toLowerCase();
}
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
// 物理写入
Path fullPath = Paths.get(uploadPath, relativePath);
try {
Files.createDirectories(fullPath.getParent());
file.transferTo(fullPath.toFile());
} catch (Exception e) {
log.error("[upload] 文件保存失败: {}", fullPath, e);
throw new BizException(ErrorCode.ERROR, "文件保存失败", e);
}
// 入库
UpFile entity = new UpFile();
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType);
entity.setTopicId(topicId);
entity.setFileTrueName(originalName);
entity.setFileName(relativePath);
entity.setFileSize((int) file.getSize());
entity.setFileType(ext);
upFileRepository.save(entity);
return toVo(entity);
}
// ============================ 绑定主题 ============================
@Override
@Transactional
public void bindToTopic(List<Integer> fileIds, Integer topicId) {
if (fileIds == null || fileIds.isEmpty() || topicId == null) {
return;
}
for (Integer fileId : fileIds) {
upFileRepository.findById(fileId).ifPresent(f -> {
f.setTopicId(topicId);
upFileRepository.save(f);
});
}
}
// ============================ 查询附件列表 ============================
@Override
public List<UpFileVo> listByTopic(String appType, Integer topicId) {
String type = appType == null ? DEFAULT_APP_TYPE : appType;
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
.map(this::toVo)
.collect(Collectors.toList());
}
// ============================ 下载 ============================
@Override
public void download(Integer fileId, HttpServletResponse response) {
writeFile(fileId, response, false);
}
@Override
public void preview(Integer fileId, HttpServletResponse response) {
writeFile(fileId, response, true);
}
/**
* 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type
* (浏览器/前端组件可直接渲染),否则 attachment 强制下载。
*/
private void writeFile(Integer fileId, HttpServletResponse response, boolean inline) {
UpFile file = upFileRepository.findById(fileId)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
Path fullPath = Paths.get(uploadPath, file.getFileName());
if (!Files.exists(fullPath)) {
throw new BizException(ErrorCode.FORBIDDEN, "物理文件不存在: " + file.getFileName());
}
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
OutputStream os = response.getOutputStream()) {
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
.replaceAll("\\+", "%20");
if (inline) {
response.setContentType(resolveContentType(file.getFileName()));
response.setHeader("Content-Disposition", "inline; filename=\"" + encodedName + "\"");
} else {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\"");
}
response.setContentLengthLong(file.getFileSize());
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
} catch (Exception e) {
log.error("[writeFile] 文件输出失败: {}", file.getFileName(), e);
throw new BizException(ErrorCode.ERROR, "文件下载失败", e);
}
}
/**
* 按扩展名推断 Content-Type(JDK 自带表 + 常见办公/图片格式兜底),未知类型回退流格式。
*/
private String resolveContentType(String fileName) {
String name = String.valueOf(fileName).toLowerCase();
Map<String, String> table = new HashMap<>();
table.put("pdf", "application/pdf");
table.put("png", "image/png");
table.put("jpg", "image/jpeg");
table.put("jpeg", "image/jpeg");
table.put("gif", "image/gif");
table.put("bmp", "image/bmp");
table.put("webp", "image/webp");
table.put("svg", "image/svg+xml");
table.put("txt", "text/plain;charset=UTF-8");
table.put("mp4", "video/mp4");
table.put("webm", "video/webm");
table.put("ogg", "video/ogg");
table.put("doc", "application/msword");
table.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
table.put("xls", "application/vnd.ms-excel");
table.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
table.put("ppt", "application/vnd.ms-powerpoint");
table.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
String type = table.get(name.substring(name.lastIndexOf('.') + 1));
if (type != null) {
return type;
}
String guessed = URLConnection.guessContentTypeFromName(name);
return guessed != null ? guessed : "application/octet-stream";
}
// ============================ 删除 ============================
@Override
@Transactional
public void delete(Integer fileId) {
UpFile file = upFileRepository.findById(fileId)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
// 删物理文件
Path fullPath = Paths.get(uploadPath, file.getFileName());
try {
Files.deleteIfExists(fullPath);
} catch (Exception e) {
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
}
// 删数据库记录
upFileRepository.delete(file);
}
// ============================ 打包下载 ZIP ============================
@Override
public void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response) {
if (files == null || files.isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, "无附件可下载");
}
try {
response.setContentType("application/zip");
String name = URLEncoder.encode(zipName == null ? "附件" : zipName, "UTF-8")
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip\"");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
Set<String> used = new HashSet<>();
for (UpFile f : files) {
Path p = Paths.get(uploadPath, f.getFileName());
if (!Files.exists(p)) {
log.warn("[downloadZip] 物理文件缺失,跳过: {}", f.getFileName());
continue;
}
String entryName = sanitizeEntryName(f.getFileTrueName());
if (used.contains(entryName)) {
entryName = f.getId() + "_" + entryName;
}
used.add(entryName);
zos.putNextEntry(new ZipEntry(entryName));
Files.copy(p, zos);
zos.closeEntry();
}
}
} catch (Exception e) {
log.error("[downloadZip] 打包下载失败", e);
throw new BizException(ErrorCode.ERROR, "打包下载失败", e);
}
}
/**
* 净化 ZIP 条目名:去除路径分隔符等非法字符,防止 Zip Slip。
*/
private String sanitizeEntryName(String name) {
if (name == null || name.trim().isEmpty()) {
return "file";
}
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
}
// ============================ 按主题打包下载 ============================
@Override
public void downloadZipByTopic(String topicType, Integer topicId, String zipName,
HttpServletResponse response) {
String type = topicType == null ? DEFAULT_APP_TYPE : topicType;
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(type, topicId);
downloadZip(files, zipName, response);
}
// ============================ 批量上传图片 ============================
@Override
@Transactional
public List<UpFileVo> uploadImages(MultipartFile[] files, String appType) {
List<UpFileVo> results = new ArrayList<>();
if (files == null || files.length == 0) {
return results;
}
String type = appType != null ? appType : DEFAULT_APP_TYPE;
for (MultipartFile file : files) {
results.add(upload(file, type, null));
}
return results;
}
// ============================ 上传(返回前端兼容 VO) ============================
@Override
@Transactional
public UpFileResultVo uploadToResult(MultipartFile file, String appType) {
UpFileVo vo = upload(file, appType, null);
UpFileResultVo result = new UpFileResultVo();
result.setFileid(vo.getId());
result.setName(vo.getFileTrueName());
result.setFileurl(vo.getFileName());
result.setHost("");
result.setType(vo.getFileType());
result.setSize(vo.getFileSize());
return result;
}
// ============================ 转换 ============================
private UpFileVo toVo(UpFile entity) {
UpFileVo vo = new UpFileVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
package com.yunhai.campusnotice.service.impl;
import com.yunhai.campusnotice.entity.SmsModule;
import com.yunhai.campusnotice.entity.SmsSchoolConfig;
import com.yunhai.campusnotice.repository.SmsModuleRepository;
import com.yunhai.campusnotice.repository.SmsSchoolConfigRepository;
import com.yunhai.campusnotice.service.ISmsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* 短信设置服务实现。
* 对应 C# SchModuleSmsController + SchModuleSms.cs。
* 注意:C# 原版依赖外部DLL(ISchIsSmsBLL/ISchoolBLL/ISchGroupBLL)获取学校和组数据,
* Java版改为从本地 sms_module + sms_school_config 表管理。
*/
@Service
public class SmsService implements ISmsService {
private static final Logger log = LoggerFactory.getLogger(SmsService.class);
@Autowired
private SmsModuleRepository smsModuleRepository;
@Autowired
private SmsSchoolConfigRepository smsSchoolConfigRepository;
// ============================ getmodule ============================
@Override
public List<Map<String, Object>> getModuleList() {
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
return modules.stream().map(m -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("ID", m.getId());
item.put("Name", m.getModuleName());
return item;
}).collect(Collectors.toList());
}
// ============================ getgroup ============================
@Override
public List<Map<String, Object>> getSchParentGroup() {
// C# 从外部BLL获取学校组数据。Java版暂不接入用户中心API。
// 返回空列表,前端可按需处理。
log.info("[getSchParentGroup] 暂未接入用户中心API,返回空列表");
return Collections.emptyList();
}
// ============================ getschsms ============================
@Override
public Map<String, Object> getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName) {
if (pageSize == null || pageSize <= 0) pageSize = 10;
// 前端 PageIndex 从0开始(page-1),后端按0基处理
if (pageIndex == null || pageIndex < 0) pageIndex = 0;
// 查询学校配置(按学校名模糊搜索或全部)
Page<SmsSchoolConfig> page;
int offset = pageIndex; // 0基
Sort sort = Sort.by(Sort.Direction.DESC, "schoolName");
if (StringUtils.hasText(schName)) {
page = smsSchoolConfigRepository.findBySchoolNameContaining(schName,
PageRequest.of(offset, pageSize, sort));
} else {
page = smsSchoolConfigRepository.findAll(PageRequest.of(offset, pageSize, sort));
}
// 加载全部模块列表
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
// 按 schoolId 分组配置
Map<Integer, List<SmsSchoolConfig>> configBySchool = page.getContent().stream()
.collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId));
// 构建返回数据:每个学校一行,包含各模块的开关状态
List<Map<String, Object>> listData = page.getContent().stream()
.collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId))
.entrySet().stream()
.map(entry -> {
Integer schoolId = entry.getKey();
List<SmsSchoolConfig> configs = entry.getValue();
String schoolName = configs.get(0).getSchoolName();
// 构建模块开关矩阵
List<Map<String, Object>> moduleList = new ArrayList<>();
boolean isAll = true;
for (SmsModule module : modules) {
Map<String, Object> modu = new LinkedHashMap<>();
SmsSchoolConfig config = configs.stream()
.filter(c -> module.getId().equals(c.getModuleId()))
.findFirst().orElse(null);
modu.put("SerID", config != null ? config.getId() : -1);
modu.put("ModuID", module.getId());
modu.put("IsSms", config != null && Boolean.TRUE.equals(config.getIsSms()));
modu.put("SchSerID", schoolId);
moduleList.add(modu);
if (config == null || !Boolean.TRUE.equals(config.getIsSms())) {
isAll = false;
}
}
Map<String, Object> schoolItem = new LinkedHashMap<>();
schoolItem.put("SchSerID", schoolId);
schoolItem.put("SchName", schoolName);
schoolItem.put("IsAll", isAll);
schoolItem.put("itemsModu", moduleList);
return schoolItem;
})
.collect(Collectors.toList());
// 如果没有配置数据,返回空列表(但保留total)
if (listData.isEmpty() && page.hasContent()) {
// 这种情况不应发生,但防御性处理
listData = Collections.emptyList();
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("list", listData);
result.put("total", page.getTotalElements());
return result;
}
// ============================ getsch ============================
@Override
public List<Map<String, Object>> getSchList() {
// C# 从外部BLL获取学校列表。Java版从 sms_school_config 表提取已配置学校。
// 获取所有不重复的学校
List<SmsSchoolConfig> allConfigs = smsSchoolConfigRepository.findAll();
Map<Integer, String> schoolMap = new LinkedHashMap<>();
for (SmsSchoolConfig config : allConfigs) {
schoolMap.putIfAbsent(config.getSchoolId(), config.getSchoolName());
}
return schoolMap.entrySet().stream().map(entry -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("ID", entry.getKey());
item.put("Name", entry.getValue());
return item;
}).collect(Collectors.toList());
}
// ============================ setschsms ============================
@Override
@Transactional
public void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms) {
if (schSerID == null || moduleId == null) {
return;
}
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schSerID, moduleId);
if (config == null) {
config = new SmsSchoolConfig();
config.setSchoolId(schSerID);
config.setModuleId(moduleId);
// 学校名称从已有配置中获取
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schSerID);
if (!existing.isEmpty()) {
config.setSchoolName(existing.get(0).getSchoolName());
}
}
config.setIsSms(isSms != null ? isSms : false);
smsSchoolConfigRepository.save(config);
}
// ============================ setschallsms ============================
@Override
@Transactional
public void setSchSmsAll(Integer schSerID, Boolean isSms) {
if (schSerID == null) {
return;
}
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
boolean sms = Boolean.TRUE.equals(isSms);
for (SmsModule module : modules) {
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schSerID, module.getId());
if (config == null) {
config = new SmsSchoolConfig();
config.setSchoolId(schSerID);
config.setModuleId(module.getId());
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schSerID);
if (!existing.isEmpty()) {
config.setSchoolName(existing.get(0).getSchoolName());
}
}
config.setIsSms(sms);
smsSchoolConfigRepository.save(config);
}
}
// ============================ setschmodulesms ============================
@Override
@Transactional
public void setSchModuleSms(String schSerIDs, String modules, Boolean isSms) {
if (!StringUtils.hasText(schSerIDs) || !StringUtils.hasText(modules)) {
return;
}
List<Integer> schoolIds = Arrays.stream(schSerIDs.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.collect(Collectors.toList());
List<Integer> moduleIds = Arrays.stream(modules.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.collect(Collectors.toList());
boolean sms = Boolean.TRUE.equals(isSms);
for (Integer schoolId : schoolIds) {
for (Integer moduleId : moduleIds) {
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schoolId, moduleId);
if (config == null) {
config = new SmsSchoolConfig();
config.setSchoolId(schoolId);
config.setModuleId(moduleId);
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schoolId);
if (!existing.isEmpty()) {
config.setSchoolName(existing.get(0).getSchoolName());
}
}
config.setIsSms(sms);
smsSchoolConfigRepository.save(config);
}
}
}
// ============================ setschsms (JSON 解析) ============================
@Override
@Transactional
public void setSchSmsFromJson(String itemsModuJson) {
Integer schSerID = extractJsonField(itemsModuJson, "SchSerID");
Integer moduID = extractJsonField(itemsModuJson, "ModuID");
Boolean isSms = extractJsonBool(itemsModuJson, "IsSms");
setSchSms(schSerID, moduID, isSms);
}
// ============================ 辅助方法 ============================
/**
* 从 JSON 字符串中提取整数字段。
*/
private Integer extractJsonField(String json, String field) {
if (json == null) return null;
String pattern = "\"" + field + "\"\\s*:\\s*\"?(-?\\d+)\"?";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher m = p.matcher(json);
if (m.find()) {
return Integer.parseInt(m.group(1));
}
return null;
}
/**
* 从 JSON 字符串中提取布尔字段。
*/
private Boolean extractJsonBool(String json, String field) {
if (json == null) return null;
String pattern = "\"" + field + "\"\\s*:\\s*(true|false)";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher m = p.matcher(json);
if (m.find()) {
return Boolean.parseBoolean(m.group(1));
}
return null;
}
}
@@ -0,0 +1,264 @@
package com.yunhai.campusnotice.service.impl;
import com.yunhai.campusnotice.entity.NoticeTemplate;
import com.yunhai.campusnotice.entity.TemplateType;
import com.yunhai.campusnotice.entity.UpFile;
import com.yunhai.campusnotice.repository.NoticeTemplateRepository;
import com.yunhai.campusnotice.repository.TemplateTypeRepository;
import com.yunhai.campusnotice.repository.UpFileRepository;
import com.yunhai.campusnotice.service.ITemplateService;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* 通知模板服务实现。
* 对应 C# TemplateController + Template.cs。
*/
@Service
public class TemplateService implements ITemplateService {
@Autowired
private TemplateTypeRepository templateTypeRepository;
@Autowired
private NoticeTemplateRepository noticeTemplateRepository;
@Autowired
private UpFileRepository upFileRepository;
// ============================ GetTypelist ============================
@Override
public List<Map<String, Object>> getTypelist() {
List<TemplateType> types = templateTypeRepository.findAllByOrderBySortOrderAsc();
List<Map<String, Object>> result = new ArrayList<>();
// 首项固定为"全部"
Map<String, Object> allItem = new LinkedHashMap<>();
allItem.put("TypeID", 0);
allItem.put("TypeName", "全部");
result.add(allItem);
for (TemplateType t : types) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("TypeID", t.getId());
item.put("TypeName", t.getTypeName());
result.add(item);
}
return result;
}
// ============================ EditType ============================
@Override
@Transactional
public void editType(Integer typeid, String typeName) {
if (!StringUtils.hasText(typeName)) {
throw new BizException(ErrorCode.PARAM_ERROR, "类型名称不能为空!");
}
// 重名校验
boolean exists;
if (typeid == null || typeid == 0) {
exists = templateTypeRepository.existsByTypeName(typeName);
} else {
exists = templateTypeRepository.existsByTypeNameAndIdNot(typeName, typeid);
}
if (exists) {
throw new BizException(ErrorCode.FORBIDDEN, "类型名称已存在!");
}
if (typeid == null || typeid == 0) {
// 新增
TemplateType type = new TemplateType();
type.setTypeName(typeName);
// sortOrder = max + 1
List<TemplateType> all = templateTypeRepository.findAllByOrderBySortOrderAsc();
int maxSort = all.stream()
.mapToInt(t -> t.getSortOrder() != null ? t.getSortOrder() : 0)
.max().orElse(0);
type.setSortOrder(maxSort + 1);
templateTypeRepository.save(type);
} else {
// 编辑
TemplateType type = templateTypeRepository.findById(typeid)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
type.setTypeName(typeName);
templateTypeRepository.save(type);
}
}
// ============================ DelType ============================
@Override
@Transactional
public void delType(Integer typeid) {
if (typeid == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "类型不存在!");
}
TemplateType type = templateTypeRepository.findById(typeid)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
// 校验是否有模板引用此类型
long refCount = noticeTemplateRepository.countByTypeId(typeid);
if (refCount > 0) {
throw new BizException(ErrorCode.FORBIDDEN, "类型存在模板!");
}
templateTypeRepository.delete(type);
}
// ============================ GetTemplatelist ============================
@Override
public Map<String, Object> getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid) {
if (pageSize == null || pageSize <= 0) pageSize = 10;
if (pageIndex == null || pageIndex <= 0) pageIndex = 1;
if (typeid == null) typeid = 0;
Page<NoticeTemplate> page;
int offset = pageIndex - 1;
if (typeid > 0) {
page = noticeTemplateRepository.findByTypeId(typeid,
PageRequest.of(offset, pageSize, Sort.by(Sort.Direction.DESC, "refCount")));
} else {
page = noticeTemplateRepository.findAllByOrderByRefCountDesc(PageRequest.of(offset, pageSize));
}
// 加载类型名映射
List<TemplateType> allTypes = templateTypeRepository.findAllByOrderBySortOrderAsc();
Map<Integer, String> typeNameMap = allTypes.stream()
.collect(Collectors.toMap(TemplateType::getId, TemplateType::getTypeName, (a, b) -> a));
// 加载封面图
List<Integer> coverIds = page.getContent().stream()
.map(NoticeTemplate::getCover)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Integer, String> coverUrlMap = new HashMap<>();
if (!coverIds.isEmpty()) {
for (Integer fid : coverIds) {
upFileRepository.findById(fid).ifPresent(f -> coverUrlMap.put(fid, f.getFileName()));
}
}
List<Map<String, Object>> listData = page.getContent().stream().map(t -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("SerID", t.getId());
item.put("Title", t.getTitle());
item.put("TypeName", typeNameMap.getOrDefault(t.getTypeId(), ""));
item.put("Contents", t.getContent());
item.put("ContentsFilter", filterHtml(t.getContent()));
item.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
item.put("imgurl", t.getCover() != null ? coverUrlMap.get(t.getCover()) : null);
return item;
}).collect(Collectors.toList());
Map<String, Object> result = new LinkedHashMap<>();
result.put("list", listData);
result.put("total", page.getTotalElements());
return result;
}
// ============================ EditTemplate ============================
@Override
@Transactional
public void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover) {
if (!StringUtils.hasText(title)) {
throw new BizException(ErrorCode.PARAM_ERROR, "模板标题不能为空!");
}
if (!StringUtils.hasText(content)) {
throw new BizException(ErrorCode.PARAM_ERROR, "模板内容不能为空!");
}
if (serID == null || serID == 0) {
// 新增
NoticeTemplate template = new NoticeTemplate();
template.setTitle(title);
template.setContent(content);
template.setTypeId(typeId);
template.setCover(cover);
template.setRefCount(0);
noticeTemplateRepository.save(template);
} else {
// 编辑
NoticeTemplate template = noticeTemplateRepository.findById(serID)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
template.setTitle(title);
template.setContent(content);
template.setTypeId(typeId);
template.setCover(cover);
noticeTemplateRepository.save(template);
}
}
// ============================ DelTemplate ============================
@Override
@Transactional
public void delTemplate(Integer serID) {
if (serID == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
}
noticeTemplateRepository.findById(serID).ifPresent(noticeTemplateRepository::delete);
}
// ============================ GetTemplateFist ============================
@Override
public Map<String, Object> getTemplateFist(Integer serID) {
if (serID == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
}
NoticeTemplate t = noticeTemplateRepository.findById(serID)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
// 加载类型名
String typeName = "";
if (t.getTypeId() != null) {
Optional<TemplateType> type = templateTypeRepository.findById(t.getTypeId());
if (type.isPresent()) {
typeName = type.get().getTypeName();
}
}
// 加载封面图
String imgurl = null;
if (t.getCover() != null) {
Optional<UpFile> file = upFileRepository.findById(t.getCover());
if (file.isPresent()) {
imgurl = file.get().getFileName();
}
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("SerID", t.getId());
result.put("Title", t.getTitle());
result.put("Type", t.getTypeId());
result.put("TypeName", typeName);
result.put("Contents", t.getContent());
result.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
result.put("imgurl", imgurl);
return result;
}
// ============================ 辅助方法 ============================
/**
* 过滤HTML标签,返回纯文本(对应 C# WebHelper.FilterHtml)。
*/
private String filterHtml(String html) {
if (html == null) return "";
return html.replaceAll("<[^>]+>", "").trim();
}
}
@@ -0,0 +1,268 @@
package com.yunhai.campusnotice.service.impl;
import com.yunhai.campusnotice.model.SchTeaList;
import com.yunhai.campusnotice.service.IClassService;
import com.yunhai.campusnotice.service.IUserCenterService;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用户中心门面服务实现。
* <p>原 BaseApiController.getSchoolAll 的组树组装、教师挂组、递归建树、排序逻辑整体下沉至此;
* GetGroupUser 空桩改为显式报错(未接入用户中心对应开放接口时提示,而非静默返回空列表)。
* 跨仓调用统一收敛到 {@link YschBaseOpenApi} 门面,Controller 不再直接静态调用。</p>
*/
@Service
public class UserCenterService implements IUserCenterService {
private static final Logger log = LoggerFactory.getLogger(UserCenterService.class);
/** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),组树中剔除 */
private static final int GROUP_TYPE_STUDENT = 8;
@Autowired
private IClassService classService;
// ============================ 学校组织树 ============================
@Override
public List<Map<String, Object>> getSchoolAllTree(Integer schId) {
if (schId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
}
// 1. userbase 组树(整棵学校子树),剔除学生组
List<Map<String, Object>> flatNodes = new ArrayList<>();
BoaGroup root = YschBaseOpenApi.getGroupById(schId, true);
if (root == null) {
log.warn("[getSchoolAll] 用户中心未找到学校组, schId={}", schId);
return Collections.emptyList();
}
flattenGroup(root, 0, flatNodes);
// 2. bschclass 全部教师,按 GroupIds 挂到对应组下;无有效组的兜底挂学校根节点
Set<Integer> treeGroupIds = new HashSet<>();
for (Map<String, Object> node : flatNodes) {
treeGroupIds.add((Integer) node.get("id"));
}
Map<Integer, SchTeaList> teacherMap = new LinkedHashMap<>();
Map<Integer, Set<Integer>> teacherPids = new LinkedHashMap<>();
List<SchTeaList> teachers = classService.getSchTeaList(schId);
for (SchTeaList tea : teachers) {
if (tea.getUserTeaSerID() == null) {
continue;
}
// 同一教师多行(多组/接口重复数据)合并,按 (教师, 组) 去重
teacherMap.putIfAbsent(tea.getUserTeaSerID(), tea);
// 接口现行字段为单个 GroupID,兼容旧字段 GroupIds
List<Integer> teaGroupIds = tea.getGroupIds();
if ((teaGroupIds == null || teaGroupIds.isEmpty()) && tea.getGroupId() != null) {
teaGroupIds = Collections.singletonList(tea.getGroupId());
}
if (teaGroupIds != null) {
for (Integer gid : teaGroupIds) {
if (gid != null && treeGroupIds.contains(gid)) {
teacherPids.computeIfAbsent(tea.getUserTeaSerID(), k -> new HashSet<>()).add(gid);
}
}
}
}
for (Map.Entry<Integer, SchTeaList> entry : teacherMap.entrySet()) {
int teaId = entry.getKey();
SchTeaList tea = entry.getValue();
Set<Integer> pids = teacherPids.get(teaId);
if (pids == null || pids.isEmpty()) {
// 教师所属组均被剔除(如学生组)或无组信息,挂学校根节点
pids = new HashSet<>(Collections.singletonList(treeGroupIds.contains(schId) ? schId : 0));
}
for (Integer pid : pids) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", teaId);
node.put("name", tea.getTeaName() != null ? tea.getTeaName() : "");
node.put("pid", pid);
node.put("no", tea.getTeaNo() != null ? tea.getTeaNo() : 0);
node.put("type", 2);
node.put("isParent", false);
node.put("photo", "");
flatNodes.add(node);
}
}
// 3. 扁平节点递归组装为树
return formatUserTree(flatNodes, 0);
}
/**
* 将 BoaGroup 子树扁平化为组节点列表(type=1);学生组(type=8)整棵剔除。
* isParent=true 标识组节点,前端 ztree 据此渲染为文件夹。
*/
private void flattenGroup(BoaGroup group, int pid, List<Map<String, Object>> nodes) {
if (group == null || group.getId() == null) {
return;
}
if (group.getType() != null && group.getType() == GROUP_TYPE_STUDENT) {
return;
}
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", group.getId());
node.put("name", group.getName() != null ? group.getName() : "");
node.put("pid", pid);
node.put("no", group.getSort() != null ? group.getSort() : 0);
node.put("type", 1);
node.put("isParent", true);
node.put("photo", "");
nodes.add(node);
if (group.getChildren() != null) {
for (BoaGroup child : group.getChildren()) {
flattenGroup(child, group.getId(), nodes);
}
}
}
/**
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
* 根层取 pid=0 或父节点不在列表中的节点;type=1 且无子节点的空组剔除。
*/
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
List<Map<String, Object>> data;
if (pid == 0) {
Set<Integer> allIds = new HashSet<>();
for (Map<String, Object> n : treeList) {
allIds.add((Integer) n.get("id"));
}
data = new ArrayList<>();
for (Map<String, Object> n : treeList) {
int p = (Integer) n.get("pid");
if (p == 0 || !allIds.contains(p)) {
data.add(n);
}
}
} else {
data = new ArrayList<>();
for (Map<String, Object> n : treeList) {
if ((Integer) n.get("pid") == pid) {
data.add(n);
}
}
}
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> item : data) {
int itemId = (Integer) item.get("id");
int itemType = (Integer) item.get("type");
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
if (itemType == 1 && children.isEmpty()) {
continue;
}
Map<String, Object> node = new LinkedHashMap<>(item);
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
if (itemType == 1) {
node.put("children", children);
}
result.add(node);
}
// 按 no 排序(数字优先,无法解析的排最后)
result.sort(Comparator.comparingInt(n -> {
Object no = n.get("no");
if (no instanceof Integer) {
return (Integer) no;
}
if (no instanceof String) {
try {
return Integer.parseInt((String) no);
} catch (NumberFormatException e) {
return 9999;
}
}
return 9999;
}));
return result;
}
// ============================ 组内用户 ============================
@Override
public List<Map<String, Object>> getGroupUser(Integer gid, Integer role) {
if (gid == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "组ID不得为空");
}
log.info("[getGroupUser] gid={}, role={}, 用户中心组内用户开放接口尚未接入", gid, role);
// 显式报错而非静默返回空列表:原空桩会让前端误以为组内无用户
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "组内用户查询接口暂未接入用户中心,请联系管理员配置后重试");
}
// ============================ 用户模块树 ============================
@Override
public List<Map<String, Object>> getUserModuleTree(Integer userId, Integer schId) {
if (userId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "用户ID不得为空");
}
List<BoaModule> modules = YschBaseOpenApi.getUserModule(userId, schId);
if (modules == null || modules.isEmpty()) {
return Collections.emptyList();
}
// 扁平 BoaModule → 树结构(modId/page/children),对齐前端 layout 期望
return buildModuleTree(modules);
}
/**
* 将扁平模块列表组装为树(对应原 NoticeController.buildModuleTree)。
* 顶层取 parentId=0 或父节点不在列表中的节点,子节点按 orderNumber 排序。
*/
private List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
Map<Integer, BoaModule> moduleMap = modules.stream()
.collect(Collectors.toMap(BoaModule::getId, m -> m, (a, b) -> a));
// 每个模块节点(含 children 占位)
Map<Integer, Map<String, Object>> nodeMap = new LinkedHashMap<>();
for (BoaModule m : modules) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", m.getId());
node.put("modId", m.getId());
node.put("name", m.getName());
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
node.put("page", m.getPath() != null ? m.getPath() : "");
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
node.put("usable", true);
node.put("children", new ArrayList<>());
nodeMap.put(m.getId(), node);
}
// 挂父子关系
List<Map<String, Object>> roots = new ArrayList<>();
for (BoaModule m : modules) {
Map<String, Object> node = nodeMap.get(m.getId());
int parentId = m.getParentId() != null ? m.getParentId() : 0;
if (parentId == 0 || !moduleMap.containsKey(parentId)) {
roots.add(node);
} else {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) nodeMap.get(parentId).get("children");
children.add(node);
}
}
// 保持 SDK(用户中心)返回顺序,不再按 orderNumber 重排
return roots;
}
}