@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user