bug修改
build / build-push (push) Successful in 2m35s

This commit is contained in:
wanghu
2026-10-08 18:29:46 +08:00
parent 3b3eeb9f87
commit 305f1c6940
11 changed files with 362 additions and 284 deletions
@@ -63,37 +63,8 @@ public class BaseApiController {
* 返回 {id, name, role: "teacher", profilePicture, school: {id, name}, currentRole} * 返回 {id, name, role: "teacher", profilePicture, school: {id, name}, currentRole}
*/ */
@GetMapping("GetUserInfo") @GetMapping("GetUserInfo")
public Map<String, Object> getUserInfo(BoaUser authUserWithDept) { public BoaUser getUserInfo(BoaUser authUserWithDept) {
// 开发联调兜底:无 CAS 会话时从 UserContext(currentrole)取身份;生产 CAS 下 BoaUser 非空 return authUserWithDept;
Integer uid = authUserWithDept != null && authUserWithDept.getId() != null
? authUserWithDept.getId() : UserContext.getUserId();
Integer schId = schIdOf(authUserWithDept);
String teacherName = uid != null ? teacherNameOf(uid) : "";
Map<String, Object> result = new LinkedHashMap<>();
result.put("id", uid);
result.put("name", authUserWithDept != null && authUserWithDept.getName() != null
? authUserWithDept.getName() : teacherName);
result.put("role", "teacher");
result.put("profilePicture", "");
Map<String, Object> school = new LinkedHashMap<>();
if (authUserWithDept != null && authUserWithDept.getSchool() != null) {
school.put("id", authUserWithDept.getSchool().getId());
school.put("name", authUserWithDept.getSchool().getName());
} else if (schId != null) {
school.put("id", schId);
school.put("name", "测试学校");
}
result.put("school", school);
UserSysRole role = new UserSysRole();
role.setSchId(schId);
role.setUserId(uid);
if (authUserWithDept != null && authUserWithDept.getRoleIds() != null && !authUserWithDept.getRoleIds().isEmpty()) {
role.setRoleId(authUserWithDept.getRoleIds().get(0));
}
Integer childId = UserContext.getChildId();
role.setChildId(childId == null ? 0 : childId);
result.put("currentRole", role);
return result;
} }
// ============================ GetUserMod ============================ // ============================ GetUserMod ============================
@@ -5,7 +5,6 @@ import com.yunhai.profession.service.IAreaService;
import com.yunhai.profession.service.IProfessionService; import com.yunhai.profession.service.IProfessionService;
import com.yunhai.profession.service.IStatsService; import com.yunhai.profession.service.IStatsService;
import com.yunhai.profession.utils.ProfessionContext; import com.yunhai.profession.utils.ProfessionContext;
import com.yunhai.profession.utils.ProfessionDicts;
import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.platform.utils.model.BoaUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -74,10 +73,10 @@ public class ProfessionController {
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex, @RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize) { @RequestParam(value = "PageSize", defaultValue = "10") int pageSize) {
return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, null, subid, grade, searchname, return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, null, subid, grade, searchname,
starttime, endtime, pageIndex, pageSize, false); starttime, endtime, pageIndex, pageSize, false, null);
} }
/** 记录详情(阅读数 +1,含字段定义/附件/点赞态) */ /** 记录详情(纯读;阅读数在 VisitCreate 记足迹时 +1,含字段定义/附件/点赞态) */
@GetMapping("ProfessionDetail") @GetMapping("ProfessionDetail")
public Map<String, Object> professionDetail(BoaUser authUserWithDept, @RequestParam("SerID") Integer serId) { public Map<String, Object> professionDetail(BoaUser authUserWithDept, @RequestParam("SerID") Integer serId) {
return professionService.recordDetail(currentSchId(authUserWithDept), serId, teacherId(authUserWithDept)); return professionService.recordDetail(currentSchId(authUserWithDept), serId, teacherId(authUserWithDept));
@@ -97,7 +96,7 @@ public class ProfessionController {
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex, @RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize) { @RequestParam(value = "PageSize", defaultValue = "10") int pageSize) {
return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, grouid, subject, grade, return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, grouid, subject, grade,
searchname, starttime, endtime, pageIndex, pageSize, true); searchname, starttime, endtime, pageIndex, pageSize, true, teacherId(authUserWithDept));
} }
/** 某教师某栏目文章分页(个人空间) */ /** 某教师某栏目文章分页(个人空间) */
@@ -110,21 +109,21 @@ public class ProfessionController {
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex, @RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize) { @RequestParam(value = "PageSize", defaultValue = "10") int pageSize) {
return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, null, null, grade, searchname, return professionService.pageRecords(currentSchId(authUserWithDept), mouid, userid, null, null, grade, searchname,
null, null, pageIndex, pageSize, false); null, null, pageIndex, pageSize, false, teacherId(authUserWithDept));
} }
/** 首页各栏目最新记录块 */ /** 首页各栏目最新记录块 */
@GetMapping("HomeModuleRecords") @GetMapping("HomeModuleRecords")
public List<Map<String, Object>> homeModuleRecords(BoaUser authUserWithDept, public List<Map<String, Object>> homeModuleRecords(BoaUser authUserWithDept,
@RequestParam(value = "size", defaultValue = "6") int size) { @RequestParam(value = "size", defaultValue = "6") int size) {
return professionService.homeModuleRecords(currentSchId(authUserWithDept), size); return professionService.homeModuleRecords(currentSchId(authUserWithDept), size, teacherId(authUserWithDept));
} }
/** 个人空间按年时间轴 */ /** 个人空间按年时间轴 */
@GetMapping("MyTimeline") @GetMapping("MyTimeline")
public List<Map<String, Object>> myTimeline(BoaUser authUserWithDept, public List<Map<String, Object>> myTimeline(BoaUser authUserWithDept,
@RequestParam("userid") Integer userid) { @RequestParam("userid") Integer userid) {
return professionService.myTimeline(currentSchId(authUserWithDept), userid); return professionService.myTimeline(currentSchId(authUserWithDept), userid, teacherId(authUserWithDept));
} }
// ==================== 评论 ==================== // ==================== 评论 ====================
@@ -193,6 +192,17 @@ public class ProfessionController {
return professionService.myPoints(currentSchId(authUserWithDept), teacherId, teacherName); return professionService.myPoints(currentSchId(authUserWithDept), teacherId, teacherName);
} }
/** 积分来源明细(发布/评论逐条带分值,分页;传 userid = 查看该教师空间,明细按登录人可见性过滤) */
@GetMapping("PointsSourceList")
public Map<String, Object> pointsSourceList(BoaUser authUserWithDept,
@RequestParam(value = "userid", required = false) Integer userid,
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize) {
Integer teacherId = userid != null ? userid : teacherId(authUserWithDept);
return professionService.pointsSourceList(currentSchId(authUserWithDept), teacherId,
teacherId(authUserWithDept), pageIndex, pageSize);
}
/** 本校发布排行(前 N) */ /** 本校发布排行(前 N) */
@GetMapping("ReleaseRanking") @GetMapping("ReleaseRanking")
public List<Map<String, Object>> releaseRanking(BoaUser authUserWithDept, public List<Map<String, Object>> releaseRanking(BoaUser authUserWithDept,
@@ -242,16 +252,16 @@ public class ProfessionController {
return rows.subList(0, Math.min(Math.max(top, 1), rows.size())); return rows.subList(0, Math.min(Math.max(top, 1), rows.size()));
} }
/** 年级段字典(对齐老 GetGradeList) */ /** 年级段字典(按当前学校过滤:班级管理该校学制的年级;不可用时回退全量) */
@GetMapping("GetGradeList") @GetMapping("GetGradeList")
public List<Map<String, Object>> gradeList() { public List<Map<String, Object>> gradeList(BoaUser authUserWithDept) {
return ProfessionDicts.gradeList(); return professionService.schoolGradeList(currentSchId(authUserWithDept));
} }
/** 学科字典([{subId,subName}],subId 对齐班级管理 SUB01 编码) */ /** 学科字典(按当前学校过滤:班级管理该校启用学科,[{subId,subName}];不可用时回退全量) */
@GetMapping("GetSubList") @GetMapping("GetSubList")
public List<Map<String, Object>> subjectList() { public List<Map<String, Object>> subjectList(BoaUser authUserWithDept) {
return ProfessionDicts.subjectList(); return professionService.schoolSubjectList(currentSchId(authUserWithDept));
} }
/** 行政区划子级(省市级联懒加载,parentCode 不传默认取省;对齐自定义字段 province_city) */ /** 行政区划子级(省市级联懒加载,parentCode 不传默认取省;对齐自定义字段 province_city) */
@@ -15,6 +15,9 @@ public interface ProfessionDiscussRepository extends JpaRepository<ProfessionDis
List<ProfessionDiscuss> findByRecordIdOrderByCreateTimeAsc(Integer recordId); List<ProfessionDiscuss> findByRecordIdOrderByCreateTimeAsc(Integer recordId);
/** 某教师的全部评论(积分来源明细用,新在前) */
List<ProfessionDiscuss> findBySchIdAndTeacherIdOrderByCreateTimeDescIdDesc(Integer schId, Integer teacherId);
long deleteByRecordId(Integer recordId); long deleteByRecordId(Integer recordId);
// ==================== 校级聚合 ==================== // ==================== 校级聚合 ====================
@@ -70,8 +73,9 @@ public interface ProfessionDiscussRepository extends JpaRepository<ProfessionDis
// ===== 教师维度聚合(个人空间概览) ===== // ===== 教师维度聚合(个人空间概览) =====
/** 教师在时间段内的评论数 */ /** 教师新获评论数:他人在该教师的记录下的评论(排除本人自评),时间边界传 null 表示不限 */
@Query("select count(d) from ProfessionDiscuss d where d.schId = :schId and d.teacherId = :uid " + @Query("select count(d) from ProfessionDiscuss d where d.schId = :schId and d.teacherId <> :uid " +
"and d.recordId in (select r.id from ProfessionRecord r where r.schId = :schId and r.teacherId = :uid) " +
"and (:start is null or d.createTime >= :start) and (:end is null or d.createTime < :end)") "and (:start is null or d.createTime >= :start) and (:end is null or d.createTime < :end)")
long countByTeacherRange(@Param("schId") Integer schId, @Param("uid") Integer uid, long countByTeacherRange(@Param("schId") Integer schId, @Param("uid") Integer uid,
@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
@@ -56,6 +56,16 @@ public interface ProfessionRecordRepository extends JpaRepository<ProfessionReco
@Param("start") LocalDateTime start, @Param("start") LocalDateTime start,
@Param("end") LocalDateTime end); @Param("end") LocalDateTime end);
/** 按栏目聚合(仅查看者可见:公开记录 + 本人发布的记录):栏目id、记录数 */
@Query("select r.moduleId, count(r) from ProfessionRecord r "
+ "where r.schId = :schId and (:start is null or r.createTime >= :start) and (:end is null or r.createTime < :end) "
+ "and (r.ifOpen = 1 or r.teacherId = :viewerId) "
+ "group by r.moduleId")
List<Object[]> countGroupByModuleVisible(@Param("schId") Integer schId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("viewerId") Integer viewerId);
/** 按学科聚合:学科值、记录数 */ /** 按学科聚合:学科值、记录数 */
@Query("select r.subject, r.subjectName, count(r) from ProfessionRecord r " @Query("select r.subject, r.subjectName, count(r) from ProfessionRecord r "
+ "where r.schId = :schId and (:start is null or r.createTime >= :start) and (:end is null or r.createTime < :end) " + "where r.schId = :schId and (:start is null or r.createTime >= :start) and (:end is null or r.createTime < :end) "
@@ -21,13 +21,18 @@ public interface ProfessionVisitRepository extends JpaRepository<ProfessionVisit
List<ProfessionVisit> findByRecordId(Integer recordId); List<ProfessionVisit> findByRecordId(Integer recordId);
// ===== 聚合计数(个人空间概览) ===== // ===== 聚合计数(个人空间概览,按访客+记录去重:刷新/回看不重复计数) =====
/** 被访问总次数(作为被访问者) */ /** 同一访客是否已看过该记录(看过即不再新增足迹/阅读数) */
long countByAuthorId(Integer authorId); boolean existsByVisitorIdAndRecordId(Integer visitorId, Integer recordId);
/** 时间段内被访问次数(start/end 传 null 表示不限) */ /** 被访问总数(同一访客看同一篇只算一次;历史刷新产生的重复足迹不计入) */
@Query("select count(v) from ProfessionVisit v where v.authorId = :uid " + @Query("select count(distinct concat(v.visitorId, ':', v.recordId)) from ProfessionVisit v " +
"where v.authorId = :uid")
long countDistinctVisitsByAuthor(@Param("uid") Integer uid);
/** 时间段内被访问总数,按访客+记录去重(start/end 传 null 表示不限) */
@Query("select count(distinct concat(v.visitorId, ':', v.recordId)) from ProfessionVisit v where v.authorId = :uid " +
"and (:start is null or v.createTime >= :start) and (:end is null or v.createTime < :end)") "and (:start is null or v.createTime >= :start) and (:end is null or v.createTime < :end)")
long countVisitsByAuthor(@Param("uid") Integer uid, long countVisitsByAuthor(@Param("uid") Integer uid,
@Param("start") java.time.LocalDateTime start, @Param("start") java.time.LocalDateTime start,
@@ -26,20 +26,21 @@ public interface IProfessionService {
* @param teacherId 按教师过滤(可空) * @param teacherId 按教师过滤(可空)
* @param groupId 按教师分组过滤(可空;组员=组长+组员教师集合) * @param groupId 按教师分组过滤(可空;组员=组长+组员教师集合)
* @param withOtherModules true 时附带同校其他启用栏目(对齐老契约 OtherData) * @param withOtherModules true 时附带同校其他启用栏目(对齐老契约 OtherData)
* @param viewerId 当前登录教师 id;非空时过滤掉他人不公开记录(ifOpen=0 仅发布人可见),空则不过滤(后台)
*/ */
Map<String, Object> pageRecords(Integer schId, Integer moduleId, Integer teacherId, Integer groupId, Map<String, Object> pageRecords(Integer schId, Integer moduleId, Integer teacherId, Integer groupId,
String subject, String grade, String subject, String grade,
String keyword, String startTime, String endTime, String keyword, String startTime, String endTime,
int pageIndex, int pageSize, boolean withOtherModules); int pageIndex, int pageSize, boolean withOtherModules, Integer viewerId);
/** 记录详情(阅读数 +1;含栏目绑定字段定义、附件、动态字段值原文、点赞数与当前用户点赞态) */ /** 记录详情(阅读数 +1;含栏目绑定字段定义、附件、动态字段值原文、点赞数与当前用户点赞态;不公开记录仅发布人可看) */
Map<String, Object> recordDetail(Integer schId, Integer recordId, Integer currentUserId); Map<String, Object> recordDetail(Integer schId, Integer recordId, Integer currentUserId);
/** 首页各栏目最新记录块 */ /** 首页各栏目最新记录块(过滤他人不公开记录,计数同口径) */
List<Map<String, Object>> homeModuleRecords(Integer schId, int sizePerModule); List<Map<String, Object>> homeModuleRecords(Integer schId, int sizePerModule, Integer viewerId);
/** 个人空间按年时间轴 */ /** 个人空间按年时间轴(viewerId 非空时过滤掉他人不公开记录) */
List<Map<String, Object>> myTimeline(Integer schId, Integer teacherId); List<Map<String, Object>> myTimeline(Integer schId, Integer teacherId, Integer viewerId);
// ==================== 评论 ==================== // ==================== 评论 ====================
@@ -70,6 +71,9 @@ public interface IProfessionService {
/** 我的积分(发布 + 评论) */ /** 我的积分(发布 + 评论) */
Map<String, Object> myPoints(Integer schId, Integer teacherId, String teacherName); Map<String, Object> myPoints(Integer schId, Integer teacherId, String teacherName);
/** 积分来源明细分页:发布/评论逐条带分值(合计口径同 myPoints;明细按查看者可见性过滤) */
Map<String, Object> pointsSourceList(Integer schId, Integer teacherId, Integer viewerId, Integer pageIndex, Integer pageSize);
/** 发布排行(默认前 10) */ /** 发布排行(默认前 10) */
List<Map<String, Object>> releaseRanking(Integer schId, Integer topN); List<Map<String, Object>> releaseRanking(Integer schId, Integer topN);
@@ -84,4 +88,10 @@ public interface IProfessionService {
/** 个人空间聚合:姓名/积分/累计记录/获赞/被访问/各栏目篇数/本月概览(发布/评论/阅读/访客) */ /** 个人空间聚合:姓名/积分/累计记录/获赞/被访问/各栏目篇数/本月概览(发布/评论/阅读/访客) */
Map<String, Object> spaceOverview(Integer schId, Integer userid); Map<String, Object> spaceOverview(Integer schId, Integer userid);
/** 年级段字典(按校过滤:取班级管理该校学制的年级并映射回本字典;班级管理不可用/无数据时回退全量) */
List<Map<String, Object>> schoolGradeList(Integer schId);
/** 学科字典(按校过滤:取班级管理该校启用学科并按名映射回本字典;班级管理不可用/无数据时回退全量) */
List<Map<String, Object>> schoolSubjectList(Integer schId);
} }
@@ -1,8 +1,8 @@
package com.yunhai.profession.service.impl; package com.yunhai.profession.service.impl;
import com.yunhai.profession.entity.UpFile; import com.yunhai.profession.entity.UpFile;
import com.yunhai.profession.model.file.UpFileVo;
import com.yunhai.profession.model.file.UpFileResultVo; import com.yunhai.profession.model.file.UpFileResultVo;
import com.yunhai.profession.model.file.UpFileVo;
import com.yunhai.profession.repository.UpFileRepository; import com.yunhai.profession.repository.UpFileRepository;
import com.yunhai.profession.service.IFileService; import com.yunhai.profession.service.IFileService;
import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.apistd.BizException;
@@ -18,30 +18,14 @@ import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URLConnection; import java.net.URLConnection;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDate; import java.util.*;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
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.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -50,7 +34,7 @@ import java.util.zip.ZipOutputStream;
* 文件上传/下载/删除业务实现。 * 文件上传/下载/删除业务实现。
* <p> * <p>
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext} * 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传(预览走本服务转 PDF,见 preview)。 * 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
*/ */
@Service @Service
public class FileService implements IFileService { public class FileService implements IFileService {
@@ -60,20 +44,9 @@ public class FileService implements IFileService {
@Autowired @Autowired
private UpFileRepository upFileRepository; private UpFileRepository upFileRepository;
@Value("${file.upload-path}") @Value("${FILE_PATH}")
private String uploadPath; private String uploadPath;
private static final String DEFAULT_APP_TYPE = "profession";
/** Office 类型扩展名:预览前需转 PDF(浏览器无法内联渲染 msword 等原始格式) */
private static final Set<String> OFFICE_EXTS = new HashSet<>(Arrays.asList(
"doc", "docx", "wps", "xls", "xlsx", "et", "ppt", "pptx", "dps"));
/** Office→PDF 全局锁:COM 自动化不宜并发,转换一次后复用同名 .pdf 缓存 */
private static final Object PDF_CONVERT_LOCK = new Object();
private static volatile Path wpsScript;
// ============================ 上传 ============================ // ============================ 上传 ============================
@Override @Override
@@ -93,13 +66,10 @@ public class FileService implements IFileService {
ext = originalName.substring(dotIdx + 1).toLowerCase(); 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 savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
// 物理写入 // 物理写入
Path fullPath = Paths.get(uploadPath, relativePath); Path fullPath = Paths.get(uploadPath, savedName);
try { try {
Files.createDirectories(fullPath.getParent()); Files.createDirectories(fullPath.getParent());
file.transferTo(fullPath.toFile()); file.transferTo(fullPath.toFile());
@@ -110,10 +80,10 @@ public class FileService implements IFileService {
// 入库 // 入库
UpFile entity = new UpFile(); UpFile entity = new UpFile();
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType); entity.setTopicType(appType);
entity.setTopicId(topicId); entity.setTopicId(topicId);
entity.setFileTrueName(originalName); entity.setFileTrueName(originalName);
entity.setFileName(relativePath); entity.setFileName(savedName);
entity.setFileSize((int) file.getSize()); entity.setFileSize((int) file.getSize());
entity.setFileType(ext); entity.setFileType(ext);
upFileRepository.save(entity); upFileRepository.save(entity);
@@ -141,8 +111,7 @@ public class FileService implements IFileService {
@Override @Override
public List<UpFileVo> listByTopic(String appType, Integer topicId) { public List<UpFileVo> listByTopic(String appType, Integer topicId) {
String type = appType == null ? DEFAULT_APP_TYPE : appType; return upFileRepository.findByTopicTypeAndTopicId(appType, topicId).stream()
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
.map(this::toVo) .map(this::toVo)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@@ -156,174 +125,9 @@ public class FileService implements IFileService {
@Override @Override
public void preview(Integer fileId, HttpServletResponse response) { public void preview(Integer fileId, HttpServletResponse response) {
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());
}
if (OFFICE_EXTS.contains(extOf(file.getFileName()))) {
// Office 原始格式浏览器无法内联渲染:转 PDF 后输出(与老系统 Aspose 转 PDF 链路对齐)
Path pdfPath = siblingPdf(fullPath);
try {
ensureOfficePdf(fullPath, pdfPath);
streamFile(pdfPath, "application/pdf", pdfDisplayName(file.getFileTrueName()), response);
} catch (Exception e) {
log.warn("[preview] Office 转 PDF 失败,输出提示页: {}", file.getFileName(), e);
writeUnsupportedHtml(response);
}
return;
}
writeFile(fileId, response, true); writeFile(fileId, response, true);
} }
// ============================ Office 转 PDF ============================
/** 转换并缓存:{uuid}.doc → 同目录 {uuid}.pdf,已存在直接复用 */
private void ensureOfficePdf(Path officePath, Path pdfPath) throws Exception {
synchronized (PDF_CONVERT_LOCK) {
if (Files.exists(pdfPath) && Files.size(pdfPath) > 0) {
return;
}
Files.deleteIfExists(pdfPath);
runWpsConvert(officePath, pdfPath);
if (!Files.exists(pdfPath) || Files.size(pdfPath) == 0) {
throw new IllegalStateException("转换未产出 PDF: " + pdfPath);
}
}
}
/**
* 通过 PowerShell 调用 WPS 的 Office COM 接口导出 PDF(本机/服务器需安装 WPS,
* ProgID:KWPS=文字 KET=表格 KWPP=演示,API 兼容 MS Office)。
* 未安装 WPS 或转换失败时由调用方降级为提示页。
*/
private static final String WPS_CONVERT_SCRIPT =
"param([string]$Src,[string]$Dst)\n"
+ "$ErrorActionPreference='Stop'\n"
+ "$ext=[IO.Path]::GetExtension($Src).ToLowerInvariant()\n"
+ "$app=$null; $doc=$null\n"
+ "try {\n"
+ " if($ext -eq '.doc' -or $ext -eq '.docx' -or $ext -eq '.wps') {\n"
+ " $app=New-Object -ComObject KWPS.Application\n"
+ " $doc=$app.Documents.Open($Src,$false,$true)\n"
+ " try { $doc.ExportAsFixedFormat($Dst,17) } catch { $doc.SaveAs2($Dst,17) }\n"
+ " } elseif($ext -eq '.xls' -or $ext -eq '.xlsx' -or $ext -eq '.et') {\n"
+ " $app=New-Object -ComObject KET.Application\n"
+ " $doc=$app.Workbooks.Open($Src,0,$true)\n"
+ " $doc.ExportAsFixedFormat(0,$Dst)\n"
+ " } elseif($ext -eq '.ppt' -or $ext -eq '.pptx' -or $ext -eq '.dps') {\n"
+ " $app=New-Object -ComObject KWPP.Application\n"
+ " $doc=$app.Presentations.Open($Src,$true,$false,$false)\n"
+ " $doc.SaveAs($Dst,32)\n"
+ " } else { throw ('unsupported ext: '+$ext) }\n"
+ " if(-not (Test-Path -LiteralPath $Dst)) { throw 'pdf not produced' }\n"
+ " exit 0\n"
+ "} finally {\n"
+ " if($doc){ try{ $doc.Close($false) }catch{} }\n"
+ " if($app){ try{ $app.Quit() }catch{} }\n"
+ "}\n";
private Path wpsScript() throws Exception {
Path script = wpsScript;
if (script != null && Files.exists(script)) {
return script;
}
synchronized (PDF_CONVERT_LOCK) {
if (wpsScript == null || !Files.exists(wpsScript)) {
Path f = Files.createTempFile("wps2pdf", ".ps1");
Files.write(f, WPS_CONVERT_SCRIPT.getBytes(StandardCharsets.US_ASCII));
f.toFile().deleteOnExit();
wpsScript = f;
}
return wpsScript;
}
}
private void runWpsConvert(Path officePath, Path pdfPath) throws Exception {
Path script = wpsScript();
ProcessBuilder pb = new ProcessBuilder("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", script.toString(), "-Src", officePath.toString(), "-Dst", pdfPath.toString());
pb.redirectErrorStream(true);
Process p = pb.start();
StringBuilder output = new StringBuilder();
Thread drain = new Thread(() -> {
try (BufferedReader r = new BufferedReader(
new InputStreamReader(p.getInputStream(), Charset.defaultCharset()))) {
String line;
while ((line = r.readLine()) != null) {
output.append(line).append('\n');
}
} catch (Exception ignored) {
}
});
drain.setDaemon(true);
drain.start();
if (!p.waitFor(120, TimeUnit.SECONDS)) {
p.destroyForcibly();
throw new IllegalStateException("WPS 转换超时: " + officePath);
}
drain.join(3000);
if (p.exitValue() != 0) {
throw new IllegalStateException("WPS 转换失败 exit=" + p.exitValue() + " output=" + output);
}
}
private String extOf(String fileName) {
String n = String.valueOf(fileName).toLowerCase();
int dot = n.lastIndexOf('.');
return dot >= 0 ? n.substring(dot + 1) : "";
}
private Path siblingPdf(Path officePath) {
String name = officePath.getFileName().toString();
int dot = name.lastIndexOf('.');
String base = dot > 0 ? name.substring(0, dot) : name;
return officePath.resolveSibling(base + ".pdf");
}
private String pdfDisplayName(String trueName) {
String n = String.valueOf(trueName);
int dot = n.lastIndexOf('.');
return (dot > 0 ? n.substring(0, dot) : n) + ".pdf";
}
/** 内联输出物理文件(预览用) */
private void streamFile(Path path, String contentType, String displayName, HttpServletResponse response) {
try (InputStream is = Files.newInputStream(path);
OutputStream os = response.getOutputStream()) {
String encodedName = URLEncoder.encode(displayName, "UTF-8").replaceAll("\\+", "%20");
response.setContentType(contentType);
response.setHeader("Content-Disposition", "inline; filename=\"" + encodedName + "\"");
response.setContentLengthLong(Files.size(path));
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
} catch (Exception e) {
log.error("[streamFile] 文件输出失败: {}", path, e);
throw new BizException(ErrorCode.ERROR, "文件下载失败", e);
}
}
/** 转换不可用时返回给 iframe 的提示页,避免整块空白 */
private void writeUnsupportedHtml(HttpServletResponse response) {
try {
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(
"<!doctype html><html><head><meta charset=\"utf-8\">"
+ "<style>body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;"
+ "background:#f7f8fa;color:#8fa0b6;font:14px/1.8 'Microsoft YaHei',sans-serif}</style></head>"
+ "<body>该文件暂不支持在线预览,请点击右上角\u201c下载\u201d按钮查看</body></html>");
response.getWriter().flush();
} catch (Exception e) {
log.warn("[preview] 提示页输出失败", e);
}
}
/** /**
* 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type * 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type
* (浏览器/前端组件可直接渲染),否则 attachment 强制下载。 * (浏览器/前端组件可直接渲染),否则 attachment 强制下载。
@@ -398,11 +202,10 @@ public class FileService implements IFileService {
public void delete(Integer fileId) { public void delete(Integer fileId) {
UpFile file = upFileRepository.findById(fileId) UpFile file = upFileRepository.findById(fileId)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId)); .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
// 删物理文件(连同同名预览 PDF 缓存) // 删物理文件
Path fullPath = Paths.get(uploadPath, file.getFileName()); Path fullPath = Paths.get(uploadPath, file.getFileName());
try { try {
Files.deleteIfExists(fullPath); Files.deleteIfExists(fullPath);
Files.deleteIfExists(siblingPdf(fullPath));
} catch (Exception e) { } catch (Exception e) {
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e); log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
} }
@@ -461,8 +264,7 @@ public class FileService implements IFileService {
@Override @Override
public void downloadZipByTopic(String topicType, Integer topicId, String zipName, public void downloadZipByTopic(String topicType, Integer topicId, String zipName,
HttpServletResponse response) { HttpServletResponse response) {
String type = topicType == null ? DEFAULT_APP_TYPE : topicType; List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(topicType, topicId);
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(type, topicId);
downloadZip(files, zipName, response); downloadZip(files, zipName, response);
} }
@@ -475,9 +277,8 @@ public class FileService implements IFileService {
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
return results; return results;
} }
String type = appType != null ? appType : DEFAULT_APP_TYPE;
for (MultipartFile file : files) { for (MultipartFile file : files) {
results.add(upload(file, type, null)); results.add(upload(file, appType, null));
} }
return results; return results;
} }
@@ -112,6 +112,11 @@ public class MouduleService implements IMouduleService {
@Override @Override
public List<Map<String, Object>> myModuleList(Integer currentSchId, Integer teacherId) { public List<Map<String, Object>> myModuleList(Integer currentSchId, Integer teacherId) {
// 行内带上上级栏目 id+名称:栏目条按上级栏目归组做悬停下拉(无上级时 topModuleName 为 null)
Map<Integer, String> topNames = new HashMap<>();
for (TopModule t : topModuleRepository.findBySchIdOrderBySortOrderAscIdAsc(currentSchId)) {
topNames.put(t.getId(), t.getName());
}
List<Map<String, Object>> rows = new ArrayList<>(); List<Map<String, Object>> rows = new ArrayList<>();
if (teacherId != null) { if (teacherId != null) {
// 指定教师:各启用栏目下该教师的篇数(含 0,对齐老 MouduleMyList) // 指定教师:各启用栏目下该教师的篇数(含 0,对齐老 MouduleMyList)
@@ -126,6 +131,7 @@ public class MouduleService implements IMouduleService {
row.put("id", m.getId()); row.put("id", m.getId());
row.put("name", m.getName()); row.put("name", m.getName());
row.put("recordCount", countByModule.getOrDefault(m.getId(), 0L)); row.put("recordCount", countByModule.getOrDefault(m.getId(), 0L));
putTopModuleInfo(row, m, topNames);
rows.add(row); rows.add(row);
} }
return rows; return rows;
@@ -136,11 +142,20 @@ public class MouduleService implements IMouduleService {
row.put("id", m.getId()); row.put("id", m.getId());
row.put("name", m.getName()); row.put("name", m.getName());
row.put("recordCount", countByModule.getOrDefault(m.getId(), 0L)); row.put("recordCount", countByModule.getOrDefault(m.getId(), 0L));
putTopModuleInfo(row, m, topNames);
rows.add(row); rows.add(row);
} }
return rows; return rows;
} }
/** 栏目行补充上级栏目信息(topModuleId=0/null 视为无上级,名称置 null) */
private void putTopModuleInfo(Map<String, Object> row, ProfessionModule m, Map<Integer, String> topNames) {
Integer topId = m.getTopModuleId();
boolean hasTop = topId != null && topId != 0;
row.put("topModuleId", hasTop ? topId : null);
row.put("topModuleName", hasTop ? topNames.get(topId) : null);
}
@Override @Override
@Transactional @Transactional
public Integer createModule(Integer currentSchId, boolean districtAccount, Map<String, Object> req) { public Integer createModule(Integer currentSchId, boolean districtAccount, Map<String, Object> req) {
@@ -236,9 +251,12 @@ public class MouduleService implements IMouduleService {
result.put("openUserIds", m.getOpenUserIds()); result.put("openUserIds", m.getOpenUserIds());
result.put("remark", m.getRemark()); result.put("remark", m.getRemark());
result.put("fieldIds", m.getFieldIds()); result.put("fieldIds", m.getFieldIds());
// 发布表单只渲染已绑定字段(按绑定顺序,对齐老 Getdatelist 的 itemlist)
List<Map<String, Object>> fields = new ArrayList<>(); List<Map<String, Object>> fields = new ArrayList<>();
for (ProfessionField f : fieldRepository.findBySchIdOrderByFieldSortAscIdAsc(m.getSchId())) { for (Integer fieldId : boundIds) {
fields.add(fieldRow(f, boundIds.contains(f.getId()))); fieldRepository.findById(fieldId)
.filter(f -> f.getSchId().equals(m.getSchId()))
.ifPresent(f -> fields.add(fieldRow(f, true)));
} }
result.put("fields", fields); result.put("fields", fields);
return result; return result;
@@ -7,6 +7,9 @@ import com.yunhai.profession.entity.ProfessionModule;
import com.yunhai.profession.entity.ProfessionRecord; import com.yunhai.profession.entity.ProfessionRecord;
import com.yunhai.profession.entity.ProfessionVisit; import com.yunhai.profession.entity.ProfessionVisit;
import com.yunhai.profession.entity.UpFile; import com.yunhai.profession.entity.UpFile;
import com.yunhai.profession.model.bsch.SchGradeDto;
import com.yunhai.profession.model.bsch.SchInfoDto;
import com.yunhai.profession.model.bsch.SchSubjectDto;
import com.yunhai.profession.model.bsch.TeacherCustomGroupDto; import com.yunhai.profession.model.bsch.TeacherCustomGroupDto;
import com.yunhai.profession.repository.ProfessionDiscussRepository; import com.yunhai.profession.repository.ProfessionDiscussRepository;
import com.yunhai.profession.repository.ProfessionFieldRepository; import com.yunhai.profession.repository.ProfessionFieldRepository;
@@ -20,6 +23,7 @@ import com.yunhai.profession.service.IPointsService;
import com.yunhai.profession.service.IProfessionService; import com.yunhai.profession.service.IProfessionService;
import com.yunhai.profession.service.IClassService; import com.yunhai.profession.service.IClassService;
import com.yunhai.profession.utils.ProfessionContext; import com.yunhai.profession.utils.ProfessionContext;
import com.yunhai.profession.utils.ProfessionDicts;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -170,10 +174,10 @@ public class ProfessionService implements IProfessionService {
public Map<String, Object> pageRecords(Integer schId, Integer moduleId, Integer teacherId, Integer groupId, public Map<String, Object> pageRecords(Integer schId, Integer moduleId, Integer teacherId, Integer groupId,
String subject, String grade, String subject, String grade,
String keyword, String startTime, String endTime, String keyword, String startTime, String endTime,
int pageIndex, int pageSize, boolean withOtherModules) { int pageIndex, int pageSize, boolean withOtherModules, Integer viewerId) {
Specification<ProfessionRecord> spec = recordSpec(schId, moduleId, teacherId, Specification<ProfessionRecord> spec = recordSpec(schId, moduleId, teacherId,
groupId != null ? groupTeacherIds(schId, groupId) : null, groupId != null ? groupTeacherIds(schId, groupId) : null,
subject, grade, keyword, startTime, endTime); subject, grade, keyword, startTime, endTime, viewerId);
Page<ProfessionRecord> page = recordRepository.findAll(spec, Page<ProfessionRecord> page = recordRepository.findAll(spec,
PageRequest.of(Math.max(pageIndex - 1, 0), pageSize <= 0 ? 10 : pageSize, PageRequest.of(Math.max(pageIndex - 1, 0), pageSize <= 0 ? 10 : pageSize,
Sort.by(Sort.Direction.DESC, "createTime", "id"))); Sort.by(Sort.Direction.DESC, "createTime", "id")));
@@ -210,7 +214,11 @@ public class ProfessionService implements IProfessionService {
if (!r.getSchId().equals(schId)) { if (!r.getSchId().equals(schId)) {
throw new IllegalArgumentException("无权查看该记录"); throw new IllegalArgumentException("无权查看该记录");
} }
recordRepository.increaseReadCount(recordId); // 不公开记录仅发布人本人可看
if (Integer.valueOf(0).equals(r.getIfOpen()) && !r.getTeacherId().equals(currentUserId)) {
throw new IllegalArgumentException("该记录不公开,仅发布人可见");
}
// 纯读:阅读数统一在 visitCreate(浏览足迹)时 +1,避免详情+足迹各加一次导致重复计数
Map<String, Object> row = recordRow(r, moduleNameMap(schId).get(r.getModuleId()), null); Map<String, Object> row = recordRow(r, moduleNameMap(schId).get(r.getModuleId()), null);
row.put("remark", r.getRemark()); row.put("remark", r.getRemark());
row.put("contents", r.getContents()); row.put("contents", r.getContents());
@@ -250,15 +258,15 @@ public class ProfessionService implements IProfessionService {
} }
@Override @Override
public List<Map<String, Object>> homeModuleRecords(Integer schId, int sizePerModule) { public List<Map<String, Object>> homeModuleRecords(Integer schId, int sizePerModule, Integer viewerId) {
Map<Integer, Long> countByModule = new HashMap<>(); Map<Integer, Long> countByModule = new HashMap<>();
for (Object[] row : recordRepository.countGroupByModule(schId, null, null)) { for (Object[] row : recordRepository.countGroupByModuleVisible(schId, null, null, viewerId)) {
countByModule.put((Integer) row[0], (Long) row[1]); countByModule.put((Integer) row[0], (Long) row[1]);
} }
List<Map<String, Object>> blocks = new ArrayList<>(); List<Map<String, Object>> blocks = new ArrayList<>();
for (ProfessionModule m : moduleRepository.findVisibleEnabled(schId)) { for (ProfessionModule m : moduleRepository.findVisibleEnabled(schId)) {
Page<ProfessionRecord> page = recordRepository.findAll( Page<ProfessionRecord> page = recordRepository.findAll(
recordSpec(schId, m.getId(), null, null, null, null, null, null, null), recordSpec(schId, m.getId(), null, null, null, null, null, null, null, viewerId),
PageRequest.of(0, Math.max(sizePerModule, 1), Sort.by(Sort.Direction.DESC, "createTime", "id"))); PageRequest.of(0, Math.max(sizePerModule, 1), Sort.by(Sort.Direction.DESC, "createTime", "id")));
List<Map<String, Object>> items = new ArrayList<>(); List<Map<String, Object>> items = new ArrayList<>();
for (ProfessionRecord r : page.getContent()) { for (ProfessionRecord r : page.getContent()) {
@@ -281,8 +289,9 @@ public class ProfessionService implements IProfessionService {
} }
@Override @Override
public List<Map<String, Object>> myTimeline(Integer schId, Integer teacherId) { public List<Map<String, Object>> myTimeline(Integer schId, Integer teacherId, Integer viewerId) {
Specification<ProfessionRecord> spec = recordSpec(schId, null, teacherId, null, null, null, null, null, null); Specification<ProfessionRecord> spec = recordSpec(schId, null, teacherId, null, null, null, null, null, null,
viewerId);
List<ProfessionRecord> records = recordRepository.findAll(spec, List<ProfessionRecord> records = recordRepository.findAll(spec,
PageRequest.of(0, 500, Sort.by(Sort.Direction.DESC, "createTime", "id"))).getContent(); PageRequest.of(0, 500, Sort.by(Sort.Direction.DESC, "createTime", "id"))).getContent();
Map<Integer, String> moduleName = moduleNameMap(schId); Map<Integer, String> moduleName = moduleNameMap(schId);
@@ -395,6 +404,10 @@ public class ProfessionService implements IProfessionService {
@Override @Override
@Transactional @Transactional
public void visitCreate(Integer schId, Integer visitorId, String visitorName, Integer recordId) { public void visitCreate(Integer schId, Integer visitorId, String visitorName, Integer recordId) {
// 同一访客同一篇记录只记一次足迹:刷新/回看不新增足迹、不加阅读数(防刷新刷访问量)
if (visitRepository.existsByVisitorIdAndRecordId(visitorId, recordId)) {
return;
}
ProfessionVisit v = new ProfessionVisit(); ProfessionVisit v = new ProfessionVisit();
v.setSchId(schId); v.setSchId(schId);
// 开发兜底名(teacher-xxx)替换为该教师记录冗余的真实姓名 // 开发兜底名(teacher-xxx)替换为该教师记录冗余的真实姓名
@@ -508,6 +521,77 @@ public class ProfessionService implements IProfessionService {
return result; return result;
} }
@Override
public Map<String, Object> pointsSourceList(Integer schId, Integer teacherId, Integer viewerId,
Integer pageIndex, Integer pageSize) {
// 合计口径与 myPoints 完全一致(含未公开记录),明细行按查看者可见性过滤
Map<String, Object> totals = myPoints(schId, teacherId, null);
List<Map<String, Object>> merged = new ArrayList<>();
// 发布来源:本人记录(可见性与时间轴一致:ifOpen=0 仅发布人本人可见)
Map<Integer, String> moduleName = moduleNameMap(schId);
List<ProfessionRecord> records = recordRepository.findAll(
recordSpec(schId, null, teacherId, null, null, null, null, null, null, viewerId),
Sort.by(Sort.Direction.DESC, "createTime", "id"));
for (ProfessionRecord r : records) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("type", "publish");
row.put("id", r.getId());
row.put("recordId", r.getId());
row.put("title", r.getTitle());
row.put("moduleName", moduleName.get(r.getModuleId()));
row.put("points", r.getSendPoints());
row.put("createTime", r.getCreateTime());
merged.add(row);
}
// 评论来源:本人评论 → 关联记录标题;记录对查看者不可见时跳过(避免借标题泄露未公开记录)
List<ProfessionDiscuss> discusses =
discussRepository.findBySchIdAndTeacherIdOrderByCreateTimeDescIdDesc(schId, teacherId);
Map<Integer, ProfessionRecord> ref = new HashMap<>();
if (!discusses.isEmpty()) {
Set<Integer> recordIds = new HashSet<>();
for (ProfessionDiscuss d : discusses) {
recordIds.add(d.getRecordId());
}
for (ProfessionRecord r : recordRepository.findAllById(recordIds)) {
ref.put(r.getId(), r);
}
}
for (ProfessionDiscuss d : discusses) {
ProfessionRecord r = ref.get(d.getRecordId());
if (r == null || (Integer.valueOf(0).equals(r.getIfOpen()) && !r.getTeacherId().equals(viewerId))) {
continue;
}
Map<String, Object> row = new LinkedHashMap<>();
row.put("type", "comment");
row.put("id", d.getId());
row.put("recordId", r.getId());
row.put("title", r.getTitle());
row.put("contents", d.getContents());
row.put("points", d.getCommPoints());
row.put("createTime", d.getCreateTime());
merged.add(row);
}
// 新在前(createTime/id 双键,与记录列表排序同规);内存分页(明细量级小,免两条 offset 查询)
merged.sort(Comparator.comparing((Map<String, Object> row) -> (LocalDateTime) row.get("createTime"),
Comparator.nullsLast(Comparator.reverseOrder())));
int total = merged.size();
int size = pageSize == null || pageSize <= 0 ? 10 : Math.min(pageSize, 100);
int pages = (total + size - 1) / size;
int idx = pageIndex == null || pageIndex < 1 ? 1 : Math.min(pageIndex, Math.max(pages, 1));
int from = Math.min((idx - 1) * size, total);
int to = Math.min(from + size, total);
Map<String, Object> data = new LinkedHashMap<>();
data.put("teacherId", teacherId);
data.put("sendPoints", totals.get("sendPoints"));
data.put("commPoints", totals.get("commPoints"));
data.put("pointsnum", totals.get("pointsnum"));
data.put("total", total);
data.put("pageIndex", idx);
data.put("pageSize", size);
data.put("rows", merged.subList(from, to));
return data;
}
@Override @Override
public List<Map<String, Object>> releaseRanking(Integer schId, Integer topN) { public List<Map<String, Object>> releaseRanking(Integer schId, Integer topN) {
List<Map<String, Object>> rows = new ArrayList<>(); List<Map<String, Object>> rows = new ArrayList<>();
@@ -702,7 +786,7 @@ public class ProfessionService implements IProfessionService {
private Specification<ProfessionRecord> recordSpec(Integer schId, Integer moduleId, Integer teacherId, private Specification<ProfessionRecord> recordSpec(Integer schId, Integer moduleId, Integer teacherId,
Collection<Integer> teacherIds, Collection<Integer> teacherIds,
String subject, String grade, String keyword, String subject, String grade, String keyword,
String startTime, String endTime) { String startTime, String endTime, Integer viewerId) {
final LocalDateTime start = parseStart(startTime); final LocalDateTime start = parseStart(startTime);
final LocalDateTime end = parseEnd(endTime); final LocalDateTime end = parseEnd(endTime);
return (root, cq, cb) -> { return (root, cq, cb) -> {
@@ -714,6 +798,10 @@ public class ProfessionService implements IProfessionService {
if (teacherId != null) { if (teacherId != null) {
ps.add(cb.equal(root.get("teacherId"), teacherId)); ps.add(cb.equal(root.get("teacherId"), teacherId));
} }
if (viewerId != null) {
// 不公开记录(ifOpen=0)仅发布人本人可见
ps.add(cb.or(cb.equal(root.get("ifOpen"), 1), cb.equal(root.get("teacherId"), viewerId)));
}
if (teacherIds != null) { if (teacherIds != null) {
// 组过滤:空集合(组不存在/组服务不可用)时恒假,避免退化为不过滤 // 组过滤:空集合(组不存在/组服务不可用)时恒假,避免退化为不过滤
ps.add(teacherIds.isEmpty() ? cb.disjunction() : root.get("teacherId").in(teacherIds)); ps.add(teacherIds.isEmpty() ? cb.disjunction() : root.get("teacherId").in(teacherIds));
@@ -896,7 +984,7 @@ public class ProfessionService implements IProfessionService {
} }
m.put("recordTotal", recordTotal); m.put("recordTotal", recordTotal);
m.put("likeTotal", recordRepository.sumLikesByTeacher(schId, userid)); m.put("likeTotal", recordRepository.sumLikesByTeacher(schId, userid));
m.put("visitTotal", visitRepository.countByAuthorId(userid)); m.put("visitTotal", visitRepository.countDistinctVisitsByAuthor(userid));
// 各栏目篇数(启用栏目,含 0) // 各栏目篇数(启用栏目,含 0)
Map<Integer, Long> byModule = new HashMap<>(); Map<Integer, Long> byModule = new HashMap<>();
for (Object[] row : recordRepository.countGroupByTeacherModule(schId, null, null)) { for (Object[] row : recordRepository.countGroupByTeacherModule(schId, null, null)) {
@@ -924,5 +1012,92 @@ public class ProfessionService implements IProfessionService {
return m; return m;
} }
// ==================== 按校字典 ====================
/** 按校字典缓存 TTL:班级管理数据改动低频,60 秒内直接复用 */
private static final long DICT_CACHE_MS = 60_000L;
/** 缓存键(kind:schId)→ 快照 */
private final Map<String, CachedDict> dictCache = new HashMap<>();
@Override
public List<Map<String, Object>> schoolGradeList(Integer schId) {
return schoolDict("grade", schId, () -> {
Set<String> values = new LinkedHashSet<>();
for (SchGradeDto g : classService.getSchGradeList(schoolSerId(schId))) {
String v = ProfessionDicts.gradeValueByNickname(g.getName());
if (v != null) {
values.add(v);
}
}
return ProfessionDicts.gradeListOf(values);
});
}
@Override
public List<Map<String, Object>> schoolSubjectList(Integer schId) {
return schoolDict("subject", schId, () -> {
Set<String> subIds = new LinkedHashSet<>();
for (SchSubjectDto s : classService.getSchSubList(schoolSerId(schId))) {
String id = ProfessionDicts.subjectIdByName(s.getSubName());
if (id != null) {
subIds.add(id);
}
}
return ProfessionDicts.subjectListOf(subIds);
});
}
/**
* 按校范围取字典:loader 调班级管理并映射回本字典编码;
* schId 为空、班级管理不可用或映射结果为空(如局端账号无校)时回退全量字典(下拉不空)。
*/
private List<Map<String, Object>> schoolDict(String kind, Integer schId, java.util.function.Supplier<List<Map<String, Object>>> loader) {
if (schId == null) {
return fullDict(kind);
}
String key = kind + ":" + schId;
synchronized (dictCache) {
CachedDict c = dictCache.get(key);
if (c != null && System.currentTimeMillis() - c.at < DICT_CACHE_MS) {
return c.data;
}
}
List<Map<String, Object>> data;
try {
data = loader.get();
} catch (Exception e) {
log.warn("[schoolDict] 班级管理取{}失败, schId={}", kind, schId, e);
data = new ArrayList<>();
}
if (data == null || data.isEmpty()) {
data = fullDict(kind);
}
synchronized (dictCache) {
dictCache.put(key, new CachedDict(System.currentTimeMillis(), data));
}
return data;
}
private List<Map<String, Object>> fullDict(String kind) {
return "grade".equals(kind) ? ProfessionDicts.gradeList() : ProfessionDicts.subjectList();
}
/** 学校 GUID(班级管理侧 SchSerId);查不到返回 null(loader 得空列表 → 回退全量) */
private String schoolSerId(Integer schId) {
SchInfoDto info = classService.getSchFirst(schId);
return info == null ? null : info.getSchSerId();
}
private static final class CachedDict {
final long at;
final List<Map<String, Object>> data;
CachedDict(long at, List<Map<String, Object>> data) {
this.at = at;
this.data = data;
}
}
// ==================== 聚合辅助 ==================== // ==================== 聚合辅助 ====================
} }
@@ -71,6 +71,76 @@ public class ProfessionDicts {
return value; return value;
} }
/**
* 班级管理年级昵称 → 本字典 value(如「初一」→ 8),无对应项(幼儿园/大学)返回 null。
* 班级管理 gradeId 与本字典错位一位(6=预备/7=初一…),故按昵称映射而非按 Id。
*/
public static String gradeValueByNickname(String nickname) {
if (nickname == null) {
return null;
}
String n = nickname.trim();
String[][] grades = {
{"小一", "一年级", "1"}, {"小二", "二年级", "2"}, {"小三", "三年级", "3"},
{"小四", "四年级", "4"}, {"小五", "五年级", "5"}, {"小六", "六年级", "6"},
{"预备", "7"}, {"初一", "8"}, {"初二", "9"}, {"初三", "10"},
{"高一", "11"}, {"高二", "12"}, {"高三", "13"},
};
for (String[] g : grades) {
if (n.equals(g[0]) || (g.length > 2 && n.equals(g[1]))) {
return g[g.length - 1];
}
}
return null;
}
/**
* 班级管理学科名 → 本字典 subId(如「道法」→ SUB06 道德与法治),无对应项返回 null。
* 班级管理学科编码与本字典自 SUB06 起不一致(SUB06=生物),故按名称映射而非按编码。
*/
public static String subjectIdByName(String name) {
if (name == null) {
return null;
}
String n = name.trim();
String[][] subs = {
{"语文", "SUB01"}, {"数学", "SUB02"}, {"英语", "SUB03"}, {"物理", "SUB04"},
{"化学", "SUB05"}, {"道德与法治", "SUB06"}, {"道法", "SUB06"}, {"政治", "SUB06"},
{"思想政治", "SUB06"}, {"历史", "SUB07"}, {"地理", "SUB08"}, {"生物", "SUB09"},
{"音乐", "SUB10"}, {"体育与健康", "SUB11"}, {"体育", "SUB11"},
{"美术", "SUB12"}, {"信息技术", "SUB13"}, {"信息", "SUB13"},
{"劳动", "SUB14"}, {"劳技", "SUB14"}, {"劳动与技术", "SUB14"},
};
for (String[] s : subs) {
if (n.equals(s[0])) {
return s[1];
}
}
return null;
}
/** 按字典顺序取 value 子集的年级段(去重;用于按校范围过滤) */
public static List<Map<String, Object>> gradeListOf(java.util.Collection<String> values) {
List<Map<String, Object>> list = new ArrayList<>();
for (Map<String, Object> it : gradeList()) {
if (values != null && values.contains(it.get("value"))) {
list.add(it);
}
}
return list;
}
/** 按字典顺序取 subId 子集的学科(去重;用于按校范围过滤) */
public static List<Map<String, Object>> subjectListOf(java.util.Collection<String> subIds) {
List<Map<String, Object>> list = new ArrayList<>();
for (Map<String, Object> it : subjectList()) {
if (subIds != null && subIds.contains(it.get("subId"))) {
list.add(it);
}
}
return list;
}
private static Map<String, Object> item(String value, String label) { private static Map<String, Object> item(String value, String label) {
Map<String, Object> m = new LinkedHashMap<>(); Map<String, Object> m = new LinkedHashMap<>();
m.put("value", value); m.put("value", value);
+9 -5
View File
@@ -46,9 +46,7 @@ spring:
simple: simple:
auto-startup: false auto-startup: false
# 文件上传存储路径(Docker 部署时与 volume 挂载一致)
file:
upload-path: D:/data/upload
# 教师专业发展应用配置 # 教师专业发展应用配置
profession: profession:
@@ -63,7 +61,9 @@ sms:
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth) # 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
yh: yh:
sso: sso:
server-url-prefix: http://sso.yunhaixingtong.com # 单点登录服务地址:本地运行 CasService(E:\YHXT\java\user-cas-service\CasService,8443 根路径)。
# 远程地址为 http://sso.yunhaixingtong.com(生产/联调环境)
server-url-prefix: http://localhost:8443
client-host-url: http://localhost:8107 client-host-url: http://localhost:8107
allow-headers: all allow-headers: all
allow-origins: all allow-origins: all
@@ -78,5 +78,9 @@ yh:
enabled: true enabled: true
base-host: http://base.yunhaixingtong.com base-host: http://base.yunhaixingtong.com
user-center-url: http://base.yunhaixingtong.com/userbase user-center-url: http://base.yunhaixingtong.com/userbase
# 班级管理(bschclass)服务基址 # 班级管理(bschclass)服务基址:本地运行 UserclassService(E:\YHXT\java\UserclassService,8080 根路径,
# IDEA 启动;/oapi/** 免签名免登录)。远程为 http://base.yunhaixingtong.com/userclass
bschclass: http://base.yunhaixingtong.com/userclass bschclass: http://base.yunhaixingtong.com/userclass
FILE_PATH: D:/data/upload
FILE_WEB_PATH: http://base.yunhaixingtong.com/userbase/upload/