diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a46b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ +*.iml +*.log +.DS_Store diff --git a/docker/application-prod.yml b/docker/application-prod.yml new file mode 100644 index 0000000..5e13ef7 --- /dev/null +++ b/docker/application-prod.yml @@ -0,0 +1,55 @@ +# Profession 生产配置(线上以 nacos 的 profession.yaml 为准,本文件是 nacos 不可达时的兜底) +# 真正上线请在 Nacos 新建 dataId=profession.yaml、group=OA_APP 的配置覆盖以下项 +# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。 +server: + port: 8071 + servlet: + # 对外经 caddy 暴露为 http://base.yunhaixingtong.com/profession + context-path: /profession + +spring: + mvc: + date-format: yyyy-MM-dd HH:mm:ss + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + datasource: + # 与 UserBase 共用同一台 mysql8;库名为 bsch_profession(需先在 mysql8 上建库) + # ⚠️ 建库必须显式指定字符集,否则表继承 MySQL 默认值,中文写入报 SQL 1366: + # CREATE DATABASE bsch_profession DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + url: jdbc:mysql://mysql8:3306/bsch_profession?useUnicode=true&characterEncoding=utf-8&connectionCollation=utf8mb4_general_ci&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: 'CVf^x$sW*b6CGHxh' + driver-class-name: com.mysql.cj.jdbc.Driver + min-idle: 10 + max-active: 30 + jpa: + database: MySQL + show-sql: false + hibernate: + ddl-auto: update + # MySQL8Dialect 建表会带 DEFAULT CHARSET=utf8mb4(MySQL5InnoDBDialect 不会) + database-platform: org.hibernate.dialect.MySQL8Dialect + properties: + hibernate.order_by.default_null_ordering: last + open-in-view: true + +# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth) +yh: + sso: + server-url-prefix: http://sso.yunhaixingtong.com + client-host-url: http://base.yunhaixingtong.com/profession + allow-headers: all + allow-origins: all + url-pattern: /api/* + app-auth: + enabled: true + # 本应用自身身份(客户端签名用) + app-key: 100617 + app-secret: 2a3b4c5d6e7f8091a2b3c4d5e6f70819 + # 受信应用(服务端验签用):appKey -> appSecret + trusted-apps: + 100617: 2a3b4c5d6e7f8091a2b3c4d5e6f70819 + +# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data +FILE_PATH: /data/upload diff --git a/docker/run-app.sh b/docker/run-app.sh new file mode 100644 index 0000000..61dd254 --- /dev/null +++ b/docker/run-app.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Profession 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml) +# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到 +# 镜像内烤好的 application-prod.yml 兜底默认。 +set -e + +echo "[run-app] starting Profession (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..." +exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar \ + --spring.profiles.active=prod diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..afb7643 --- /dev/null +++ b/pom.xml @@ -0,0 +1,204 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.yunhai + profession + 0.0.1 + Profession + 教师专业发展子应用 + + 1.8 + + + + + + + com.yunhai.platform + yh-platform-dependencies + 1.0.0-SNAPSHOT + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2021.0.5.0 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.security + spring-security-crypto + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.8 + + + + com.mysql + mysql-connector-j + runtime + + + + org.projectlombok + lombok + true + + + + + com.alibaba + fastjson + 1.2.80 + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + com.yunhai.platform + yh-spring-boot-starter + + + + + org.jasig.cas.client + cas-client-core + 3.5.1 + + + + + com.alibaba + easyexcel + 3.3.2 + + + + + + + + profession + + + org.apache.maven.plugins + maven-compiler-plugin + + + false + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.yunhai.profession.ProfessionApplication + + + + + repackage + + + + + + + + + + + nexus-public + bsch/nexus public + http://nexus.yunhaixingtong.com/repository/maven-public/ + true + true + + + + + + ci + + C:/notice-build/profession + + + + + diff --git a/src/main/java/com/yunhai/profession/ProfessionApplication.java b/src/main/java/com/yunhai/profession/ProfessionApplication.java new file mode 100644 index 0000000..c107539 --- /dev/null +++ b/src/main/java/com/yunhai/profession/ProfessionApplication.java @@ -0,0 +1,20 @@ +package com.yunhai.profession; + +import com.alibaba.fastjson.parser.ParserConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication(scanBasePackages = "com.yunhai.profession") +@EnableTransactionManagement +@EnableAsync +public class ProfessionApplication { + + public static void main(String[] args) { + // 开启 fastjson 安全模式 + ParserConfig.getGlobalInstance().setSafeMode(true); + SpringApplication.run(ProfessionApplication.class, args); + } + +} diff --git a/src/main/java/com/yunhai/profession/aspect/ControllerUserContextAspect.java b/src/main/java/com/yunhai/profession/aspect/ControllerUserContextAspect.java new file mode 100644 index 0000000..7be9d20 --- /dev/null +++ b/src/main/java/com/yunhai/profession/aspect/ControllerUserContextAspect.java @@ -0,0 +1,161 @@ +package com.yunhai.profession.aspect; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yunhai.profession.enums.UserType; +import com.yunhai.profession.model.UserSysRole; +import com.yunhai.profession.utils.IpUtils; +import com.yunhai.profession.utils.UserContext; +import com.yunhai.platform.auth.BoaUserContext; +import com.yunhai.platform.utils.model.BoaUser; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +/** + * 当前用户上下文填充切面(移植自 UserclassService ControllerUserContextAspect)。 + *
@Order(1) 先于审计日志切面(Order 3)执行,保证日志能取到 UserContext。 + * 取用户优先级:方法参数 BoaUser → BoaUserContext 静态持有者 → request attribute; + * 角色切换信息从 currentrole(Header/Query,URL 编码 JSON)解析。
全量拦截 controller 包所有方法,不依赖注解;排除日志查询控制器自身(防递归); + * 用户信息来自 {@link ControllerUserContextAspect}(Order 1)填充的 UserContext; + * 响应体 GZIP 压缩落库,落库走 @Async 不阻塞请求。
承载:FileUpload / GetUserInfo / GetUserMod / GetSchoolAll / Logout。 + * 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
数据从基础平台用户中心透传(BoaModule.id 即 modId); + * modId 分配需与基础平台菜单树一致(见 重构任务计划.md 任务5)。
本控制器被 SysOperationLogAspect 排除,避免日志查询自身再产生日志(防递归)。
对齐历史后台 ProfessionOld 契约(方法名沿用),返回裸对象由平台包装 {code,msg,data}。
对齐历史后台契约(pointstoOne/pointsEdit/TreePointsList/TermSendMessage 系列沿用)。
对齐历史前台 ProfessionPageOld 契约(方法名沿用)。
校级统计限本校;局端接口(School 前缀/GetSchool 系)需超管或区局账号。
编码采用 GB/T 2260(6 位):省 2 位有效(补 0000,如 440000)、市 4 位有效(440300)、 + * 区县 6 位(440305);直辖市的市级节点用「市辖区」占位(code 为 XX0100)。
注意:本表是行政区划字典,与项目里的 districtSchId(区级教育局账号)无关,勿混用。
层级 level:1 省 / 2 市 / 3 区县;parent_code 为上级 code(省为 "0")。
只存评论教师 id,不保存账号。
sch_id 即创建方(区局账号创建的栏目 sch_id 为区局);is_shared=1 时对全部学校可见, + * 查看方通过 ownerSchId != 当前 schId 判定只读(对齐老契约 SchSerID != userSchSerID)。
只存教师 id(teacher_id = 平台用户 id),不保存账号;teacher_name 仅为展示冗余。
数据源为 profession_area 表(GB/T 2260),非多租户数据(全校共用一份)。
基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。 + * 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。 + */ +public interface IClassService { + + /** + * 获取指定学校全部教师(含所属组 GroupIds)。 + * 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}} + * + * @param schId 学校ID(即用户当前身份的 schId) + */ + List getSchTeaList(int schId); + + /** + * 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。 + * 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}} + */ + SchInfoDto getSchFirst(int schId); + + /** + * 学校学段(学制)列表:小学部 / 初中部…。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=} + */ + List getSchLenList(String schSerId); + + /** + * 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。 + * 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=} + */ + List getSchGradeList(String schSerId); + + /** + * 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。 + * 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=} + */ + List getSchClassList(String schSerId); + + /** + * 按校全部学生(圈人:全校范围展开)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=} + */ + List getSchStuList(String schSerId); + + /** + * 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=} + */ + List getGradeStuList(String schSerId, Integer gradeId); + + /** + * 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=} + */ + List getSubStuList(String schSerId, Integer schSystemId); + + /** + * 按班级批量查学生(圈人:班级范围一次多班)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3} + */ + List getClaArrStuList(List classIds); + + /** + * 按学生ID批量查(圈人:指定学员范围)。 + * 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3} + */ + List getStuIdsList(List stuIds); + + /** + * 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。 + * 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。 + * 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=} + */ + List getTeaTree(Integer schId, boolean toUser); + + List getStuTree(Integer schId, boolean toUser); + + /** + * 学校学科列表(含学科编码、名称、排序号)。 + * 对应:GET {@code {bschclass}/oapi/SubInfo/GetSchSublist?SchID=} + */ + List getSchSubList(String schSerId); + + /** + * 指定教师的全部任教(校验教师归属该校;SchID 过滤学校、UserTeaSerID 过滤教师)。 + * 对应:GET {@code {bschclass}/oapi/TeachInfo/GetSchTeachlist?SchID=&UserTeaSerID=} + */ + List getSchTeachList(String schSerId, Integer teaId); + + /** + * 指定班级的全部任教(班级维度:该班所有教师与学科)。 + * 对应:GET {@code {bschclass}/oapi/TeachInfo/GetSchClaTeachlist?ClassID=} + */ + List getSchClaTeachList(Integer classId); + + /** + * 学校全部任教(全校所有教师的班级任教关系)。 + * 对应:GET {@code {bschclass}/oapi/TeachInfo/GetSchTeachlist?SchID=} + */ + List getSchAllTeachList(String schSerId); + + /** + * 学校全部班级(含班主任 firstTeacherId / 副班主任 secondTeacherId,不含毕业班)。 + * 请假模块用于识别教师所带班级(班主任 = 审批/登记数据范围)。 + * 对应:GET {@code {bschclass}/oapi/sch/getSchClassList?schId=}(schId 为用户中心学校ID) + */ + List getSchClassMasterList(Integer schId); + + /** + * 学校教师分组列表(组/组员/组长合一;组员teaId=用户中心ID,可与记录 teacher_id 直接关联)。 + * 发展首页/栏目列表左侧学科组卡片取数:组教师人数、组类型、教师发布数量。 + * 对应:GET {@code {bschclass}/oapi/sch/getTeaCustomGroupList?schId=&groupType=} + * (groupType:1年级组 2教研组 3备课组,不传返回全部类型) + */ + List getTeaCustomGroupList(int schId, Integer groupType); +} diff --git a/src/main/java/com/yunhai/profession/service/IFileService.java b/src/main/java/com/yunhai/profession/service/IFileService.java new file mode 100644 index 0000000..1c19b2f --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IFileService.java @@ -0,0 +1,94 @@ +package com.yunhai.profession.service; + +import com.yunhai.profession.entity.UpFile; +import com.yunhai.profession.model.file.UpFileVo; +import com.yunhai.profession.model.file.UpFileResultVo; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 文件上传/下载/删除业务接口。 + */ +public interface IFileService { + /** + * 上传单个文件。 + * + * @param file 上传的文件 + * @param appType 应用类型(如 "profession") + * @param topicId 关联主题ID(可为空,通知保存后再绑定) + * @return 文件视图对象 + */ + UpFileVo upload(MultipartFile file, String appType, Integer topicId); + + /** + * 将已上传但未绑定主题的附件绑定到指定通知。 + * + * @param fileIds 文件记录ID列表 + * @param topicId 通知ID + */ + void bindToTopic(List fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 在线预览文件(Content-Disposition: inline,按扩展名输出真实 Content-Type, + * 供前端 open-file-viewer 取 Blob / video 播放)。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void preview(Integer fileId, HttpServletResponse response); + + /** + * 删除文件(同时删除物理文件和数据库记录)。 + * + * @param fileId 文件记录ID + */ + void delete(Integer fileId); + + /** + * 打包下载多个附件为 ZIP。 + * + * @param files 附件记录列表(调用方按 fileIds 查好) + * @param zipName 压缩包显示名(自动追加 .zip) + * @param response HTTP 响应 + */ + void downloadZip(List files, String zipName, HttpServletResponse response); + + /** + * 按主题类型和主题ID打包下载附件为 ZIP(Controller 便捷方法,内部自行查附件列表)。 + * + * @param topicType 主题类型(如 "profession") + * @param topicId 主题ID + * @param zipName 压缩包显示名 + * @param response HTTP 响应 + */ + void downloadZipByTopic(String topicType, Integer topicId, String zipName, HttpServletResponse response); + + /** + * 批量上传图片(循环上传,返回每个文件的视图对象列表)。 + */ + List uploadImages(MultipartFile[] files, String appType); + + /** + * 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。 + */ + UpFileResultVo uploadToResult(MultipartFile file, String appType); +} diff --git a/src/main/java/com/yunhai/profession/service/IMouduleService.java b/src/main/java/com/yunhai/profession/service/IMouduleService.java new file mode 100644 index 0000000..a6cf316 --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IMouduleService.java @@ -0,0 +1,67 @@ +package com.yunhai.profession.service; + +import java.util.List; +import java.util.Map; + +/** + * 栏目配置域业务接口:上级栏目、栏目、自定义字段(对齐历史后台 ProfessionOld 契约)。 + */ +public interface IMouduleService { + + // ==================== 栏目 ==================== + + /** 后台栏目列表(含 ownerSchId/currentSchId 判只读、recordCount) */ + List> listModules(Integer currentSchId); + + /** 前台级联栏目树:上级栏目 → 栏目,附篇数 */ + List> moduleTree(Integer currentSchId); + + /** 前台某教师可见栏目(含篇数) */ + List> myModuleList(Integer currentSchId, Integer teacherId); + + /** 新建栏目(区局账号可设 isShared) */ + Integer createModule(Integer currentSchId, boolean districtAccount, Map req); + + /** 编辑栏目(仅归属校可改) */ + void editModule(Integer currentSchId, boolean districtAccount, Map req); + + /** 删除栏目(ids CSV,仅归属校) */ + void deleteModules(Integer currentSchId, String ids); + + /** 批量排序 + 改名 + 勾选一次提交(对齐老 OrderBy) */ + void orderModules(Integer currentSchId, String strIds, String strSorts, String names, String checks); + + /** 批量排序 + 改名 + 勾选 + 上级栏目一次提交(对齐老 OrderBy 含 TopModuleTypes) */ + void orderModules(Integer currentSchId, String strIds, String strSorts, String names, String checks, String topModuleTypes); + + /** 栏目动态字段定义(发布表单渲染用,对齐老 Getdatelist):栏目信息 + fields[ischeck] */ + Map moduleFieldList(Integer currentSchId, Integer moduleId); + + // ==================== 上级栏目 ==================== + + List> listTopModules(Integer currentSchId, String searchName, Integer isUsing); + + Integer createTopModule(Integer currentSchId, String name); + + void editTopModule(Integer currentSchId, Integer id, String name); + + void deleteTopModules(Integer currentSchId, String ids); + + void orderTopModules(Integer currentSchId, String strIds); + + void topModuleIsUsing(Integer currentSchId, Integer id, Integer isUsing); + + // ==================== 自定义字段 ==================== + + /** 字段分页(fieldType 过滤 + 分页) */ + Map pageFields(Integer currentSchId, String fieldType, int pageIndex, int pageSize); + + /** 全部字段(绑定选择用) */ + List> listFields(Integer currentSchId); + + Integer createField(Integer currentSchId, Map req); + + void editField(Integer currentSchId, Map req); + + void deleteFields(Integer currentSchId, String ids); +} diff --git a/src/main/java/com/yunhai/profession/service/IPointsService.java b/src/main/java/com/yunhai/profession/service/IPointsService.java new file mode 100644 index 0000000..0277d7d --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IPointsService.java @@ -0,0 +1,55 @@ +package com.yunhai.profession.service; + +import com.yunhai.profession.entity.ProfessionModule; + +import java.util.List; +import java.util.Map; + +/** + * 积分与学期设置域业务接口:全局积分、积分指标树、学期设置(对齐历史 Points/Term 契约)。 + */ +public interface IPointsService { + + // ==================== 全局积分 ==================== + + /** 全局默认积分(无则懒创建默认行) */ + Map globalPoints(Integer schId); + + /** 修改全局默认积分 */ + void globalPointsEdit(Integer schId, Integer sendPointsNum, Integer commPointsNum); + + // ==================== 积分计算(发布/评论时点) ==================== + + /** 发布积分:栏目覆盖 → 全局默认 */ + int resolveSendPoints(Integer schId, ProfessionModule module); + + /** 评论积分:栏目覆盖 → 全局默认 */ + int resolveCommPoints(Integer schId, ProfessionModule module); + + // ==================== 积分指标树 ==================== + + /** 指标树(zTree 结构 id/pid/name/children) */ + List> pointsTree(Integer schId); + + /** 积分来源下拉(静态) */ + List> pointsSourceList(); + + /** 节点详情 */ + Map pointsDetail(Integer schId, Integer id); + + /** 新增/编辑节点(id 空 = 新增) */ + Integer pointsEdit(Integer schId, Map req); + + /** 删除节点(ids CSV,连带子节点) */ + void deletePoints(Integer schId, String ids); + + // ==================== 学期设置 ==================== + + List> termList(Integer schId); + + Map termPage(Integer schId, int pageIndex, int pageSize); + + Map termDetail(Integer schId, Integer id); + + Integer termEdit(Integer schId, Map req); +} diff --git a/src/main/java/com/yunhai/profession/service/IProfessionService.java b/src/main/java/com/yunhai/profession/service/IProfessionService.java new file mode 100644 index 0000000..9a3d9c1 --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IProfessionService.java @@ -0,0 +1,87 @@ +package com.yunhai.profession.service; + +import java.util.List; +import java.util.Map; + +/** + * 专业发展记录域业务接口:记录(教案/学习资料)、评论、足迹、积分展示、排行 + * (对齐历史前台 ProfessionPageOld + 后台明细契约)。 + */ +public interface IProfessionService { + + // ==================== 记录 ==================== + + /** 教师发布记录,返回记录 id(写入时点计算发布积分) */ + Integer createRecord(Integer schId, Integer teacherId, String teacherName, Map req); + + /** 编辑记录(仅作者) */ + void editRecord(Integer schId, Integer teacherId, Map req); + + /** 删除记录(ids CSV;仅作者或超管) */ + void deleteRecords(Integer schId, Integer teacherId, boolean superAdmin, String ids); + + /** + * 记录分页列表。 + * + * @param teacherId 按教师过滤(可空) + * @param groupId 按教师分组过滤(可空;组员=组长+组员教师集合) + * @param withOtherModules true 时附带同校其他启用栏目(对齐老契约 OtherData) + */ + Map pageRecords(Integer schId, Integer moduleId, Integer teacherId, Integer groupId, + String subject, String grade, + String keyword, String startTime, String endTime, + int pageIndex, int pageSize, boolean withOtherModules); + + /** 记录详情(阅读数 +1;含栏目绑定字段定义、附件、动态字段值原文、点赞数与当前用户点赞态) */ + Map recordDetail(Integer schId, Integer recordId, Integer currentUserId); + + /** 首页各栏目最新记录块 */ + List> homeModuleRecords(Integer schId, int sizePerModule); + + /** 个人空间按年时间轴 */ + List> myTimeline(Integer schId, Integer teacherId); + + // ==================== 评论 ==================== + + Integer createDiscuss(Integer schId, Integer teacherId, String teacherName, Integer recordId, String contents); + + void editDiscuss(Integer schId, Integer teacherId, Integer discussId, String contents); + + void deleteDiscuss(Integer schId, Integer teacherId, boolean superAdmin, Integer discussId); + + /** 评论列表(canManage=本人/记录作者/超管) */ + List> discussList(Integer schId, Integer currentUserId, boolean superAdmin, Integer recordId); + + // ==================== 足迹 ==================== + + /** 记录浏览足迹(同时阅读数 +1) */ + void visitCreate(Integer schId, Integer visitorId, String visitorName, Integer recordId); + + /** 最近访客:recordId 优先,否则看个人空间 */ + List> visitList(Integer recordId, Integer authorId, int limit); + + // ==================== 点赞 ==================== + + /** 点赞/取消点赞(toggle),返回 {liked, likeCount} */ + Map likeCreate(Integer schId, Integer teacherId, String teacherName, Integer recordId); + + // ==================== 积分/排行 ==================== + + /** 我的积分(发布 + 评论) */ + Map myPoints(Integer schId, Integer teacherId, String teacherName); + + /** 发布排行(默认前 10) */ + List> releaseRanking(Integer schId, Integer topN); + + /** 学科组树(用户中心组树 + 教师发布篇数;组节点 count=子树合计,叶子=教师;moduleId 非空时篇数只统计该栏目) */ + List> subjectGroupTree(Integer schId, Integer moduleId); + + /** 首页刊头统计:累计记录 / 活跃教师 / 发展栏目 / 本周新增(按学校) */ + Map homeStats(Integer schId); + + /** 积分排行(发布+评论合计,前 N) */ + List> pointsRank(Integer schId, Integer topN); + + /** 个人空间聚合:姓名/积分/累计记录/获赞/被访问/各栏目篇数/本月概览(发布/评论/阅读/访客) */ + Map spaceOverview(Integer schId, Integer userid); +} diff --git a/src/main/java/com/yunhai/profession/service/IStatsService.java b/src/main/java/com/yunhai/profession/service/IStatsService.java new file mode 100644 index 0000000..cc115e4 --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IStatsService.java @@ -0,0 +1,59 @@ +package com.yunhai.profession.service; + +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * 统计域业务接口:校级 5 类统计 + 明细 + 导出 + 附件打包 + 局端跨校统计。 + * 时间维度 type:0 累计 / 1 今日 / 2 本周 / 3 本月 / 4 本学期 / 5 自定义(对齐老契约)。 + */ +public interface IStatsService { + + /** 教师个人统计(动态列 = 各栏目记录数 + 合计 + 排名) */ + Map teacherStat(Integer schId, Integer type, String start, String end, String searchName); + + /** 模块统计(各栏目记录数) */ + Map moduleStat(Integer schId, Integer type, String start, String end, String searchName); + + /** 学科组统计(grade 可选过滤) */ + Map subjectStat(Integer schId, Integer type, String start, String end, String grade); + + /** 评论统计(按记录) */ + Map discussStat(Integer schId, Integer type, String start, String end, String searchName); + + /** 积分统计(动态列 = 各栏目发布积分;评论积分单列) */ + Map pointsStat(Integer schId, Integer type, String start, String end, String searchName); + + /** 明细分页(mouid/userid/subid 过滤) */ + Map statDetail(Integer schId, Integer type, String start, String end, String searchName, + Integer moduleId, Integer teacherId, String subject, + int pageIndex, int pageSize); + + /** Excel 导出(excelType 1 教师/2 模块/3 学科/4 评论/5 积分,对齐老 Exceldaochu1~5) */ + void exportExcel(Integer schId, int excelType, Integer type, String start, String end, + String searchName, String grade, HttpServletResponse response); + + /** 明细附件打包下载(zip) */ + void attachmentZipDown(Integer schId, Integer type, String start, String end, String searchName, + Integer moduleId, Integer teacherId, String subject, HttpServletResponse response); + + /** 自定义统计区间的默认值(对齐老 GetTime) */ + Map defaultRange(); + + // ==================== 局端跨校(需超管/区局) ==================== + + /** 学校总览(记录数 + 评论数按校分组,对齐 StatisticsMap/GetSchoolType 归并) */ + Map schoolOverview(Integer type, String start, String end); + + /** 学校×教师统计(schId 空 = 全部学校) */ + Map schoolTeacherStat(Integer schId, Integer type, String start, String end); + + /** 学校×栏目统计 */ + Map schoolModuleStat(Integer schId, Integer type, String start, String end); + + /** 学校×学科统计 */ + Map schoolSubjectStat(Integer schId, Integer type, String start, String end); + + /** 学校×教师积分统计(发布 + 评论合计) */ + Map schoolPointsStat(Integer schId, Integer type, String start, String end); +} diff --git a/src/main/java/com/yunhai/profession/service/ISysOperationLogService.java b/src/main/java/com/yunhai/profession/service/ISysOperationLogService.java new file mode 100644 index 0000000..c1abb7a --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/ISysOperationLogService.java @@ -0,0 +1,17 @@ +package com.yunhai.profession.service; + +import com.yunhai.profession.entity.SysOperationLog; +import com.yunhai.profession.model.BasePageQuery; +import com.yunhai.profession.model.BasePageResult; + +/** + * 系统审计日志业务接口:异步落库 + 分页查询。 + */ +public interface ISysOperationLogService { + + /** 异步保存(失败仅记应用日志,不影响业务请求) */ + void saveAsync(SysOperationLog entity); + + /** 分页查询(responseData 不返回) */ + BasePageResult page(BasePageQuery query, String operationType, Integer statusCode); +} diff --git a/src/main/java/com/yunhai/profession/service/IUserCenterService.java b/src/main/java/com/yunhai/profession/service/IUserCenterService.java new file mode 100644 index 0000000..d3054bd --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/IUserCenterService.java @@ -0,0 +1,51 @@ +package com.yunhai.profession.service; + +import java.util.List; +import java.util.Map; + +/** + * 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。 + * 封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。 + */ +public interface IUserCenterService { + + /** + * 获取当前用户学校组及用户(树形结构)。 + * 组装规则(从原 BaseApiController.getSchoolAll 下沉): + * + * userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组; + * bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下, + * 无有效组(如所属组全被剔除)的教师兜底挂学校根节点; + * 扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。 + * + * + * @param schId 学校ID(用户当前角色所属学校) + * @return 用户树形结构(组节点 type=1 / 教师节点 type=2) + */ + List> getSchoolAllTree(Integer schId); + + /** + * 获取指定组内用户(教师 + 管理员)。 + * 对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。 + * + * @param gid 组ID + * @param role 角色(默认 2=教师;1=管理员等) + * @return 组内用户列表 + */ + List> getGroupUser(Integer gid, Integer role); + + /** + * 获取当前用户可访问的模块树(左侧菜单)。 + * 从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。 + * + * @param userId 用户ID + * @param schId 学校ID(可为 null) + * @return 模块树(顶层 List,含 children 嵌套) + */ + List> getUserModuleTree(Integer userId, Integer schId); +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/profession/service/impl/AreaService.java b/src/main/java/com/yunhai/profession/service/impl/AreaService.java new file mode 100644 index 0000000..ea43781 --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/impl/AreaService.java @@ -0,0 +1,124 @@ +package com.yunhai.profession.service.impl; + +import com.yunhai.profession.entity.ProfessionArea; +import com.yunhai.profession.repository.ProfessionAreaRepository; +import com.yunhai.profession.service.IAreaService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 行政区划字典业务实现。 + * 三级数据量约 3400 行,整树在内存组装(一次 findAll 足够,无需递归查库)。 + * 若后续数据量增大,可改为按 parentCode 懒加载(listChildren 已是懒加载语义)。 + */ +@Service +public class AreaService implements IAreaService { + + /** 省级节点的固定上级编码 */ + private static final String ROOT_PARENT = "0"; + + @Autowired + private ProfessionAreaRepository areaRepository; + + @Override + public List> listChildren(String parentCode) { + String parent = StringUtils.hasText(parentCode) ? parentCode.trim() : ROOT_PARENT; + List> rows = new ArrayList<>(); + for (ProfessionArea a : areaRepository.findByParentCodeOrderBySortOrderAscCodeAsc(parent)) { + rows.add(node(a)); + } + return rows; + } + + @Override + public List> tree(Integer level) { + int maxLevel = (level == null || level < 1) ? 3 : Math.min(level, 3); + List all = areaRepository.findByLevelOrderBySortOrderAscCodeAsc(1); + // 省 + List> root = new ArrayList<>(); + if (maxLevel < 2) { + for (ProfessionArea a : all) { + root.add(node(a)); + } + return root; + } + // 市:按 parentCode 分组 + Map> cityByProvince = groupBy(areaRepository.findByLevelOrderBySortOrderAscCodeAsc(2)); + // 区县:按 parentCode 分组 + Map> districtByCity = maxLevel >= 3 + ? groupBy(areaRepository.findByLevelOrderBySortOrderAscCodeAsc(3)) + : new HashMap<>(); + + for (ProfessionArea p : all) { + Map pNode = node(p); + List> cities = new ArrayList<>(); + for (ProfessionArea c : cityByProvince.getOrDefault(p.getCode(), new ArrayList<>())) { + Map cNode = node(c); + if (maxLevel >= 3) { + List> districts = new ArrayList<>(); + for (ProfessionArea d : districtByCity.getOrDefault(c.getCode(), new ArrayList<>())) { + districts.add(node(d)); + } + cNode.put("children", districts); + } + cities.add(cNode); + } + pNode.put("children", cities); + root.add(pNode); + } + return root; + } + + @Override + public String namePath(String codesCSV) { + if (!StringUtils.hasText(codesCSV)) { + return ""; + } + List codes = new ArrayList<>(); + for (String s : codesCSV.split(",")) { + String t = s.trim(); + if (!t.isEmpty()) { + codes.add(t); + } + } + if (codes.isEmpty()) { + return ""; + } + Map nameByCode = new HashMap<>(); + for (ProfessionArea a : areaRepository.findByCodeIn(new HashSet<>(codes))) { + nameByCode.put(a.getCode(), a.getName()); + } + List names = new ArrayList<>(); + for (String c : codes) { + names.add(nameByCode.getOrDefault(c, c)); + } + return String.join("/", names); + } + + // ==================== 内部方法 ==================== + + private static Map node(ProfessionArea a) { + Map m = new LinkedHashMap<>(); + m.put("value", a.getCode()); + m.put("label", a.getName()); + m.put("level", a.getLevel()); + m.put("parentCode", a.getParentCode()); + return m; + } + + private static Map> groupBy(List list) { + Map> map = new HashMap<>(); + for (ProfessionArea a : list) { + map.computeIfAbsent(a.getParentCode(), k -> new ArrayList<>()).add(a); + } + return map; + } +} diff --git a/src/main/java/com/yunhai/profession/service/impl/ClassService.java b/src/main/java/com/yunhai/profession/service/impl/ClassService.java new file mode 100644 index 0000000..ae516fa --- /dev/null +++ b/src/main/java/com/yunhai/profession/service/impl/ClassService.java @@ -0,0 +1,221 @@ +package com.yunhai.profession.service.impl; + +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.profession.model.SchTeaList; +import com.yunhai.profession.model.bsch.*; +import com.yunhai.profession.service.IClassService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +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 直连。 + *
时间维度 type:0 累计 / 1 今日 / 2 本周 / 3 本月 / 4 本学期 / 5 自定义(对齐老契约)。
封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用, + * 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层, + * Controller 只保留"取参 → 调 Service → 返回"的薄壳。
组装规则(从原 BaseApiController.getSchoolAll 下沉):
对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义 + * (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。 + * 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。
从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构 + * ({@code modId/page/children}),对应前端 GetUserMod 接口。
三级数据量约 3400 行,整树在内存组装(一次 findAll 足够,无需递归查库)。 + * 若后续数据量增大,可改为按 parentCode 懒加载(listChildren 已是懒加载语义)。