diff --git a/backend/roomroot-admin/target/classes/application-druid.yml b/backend/roomroot-admin/target/classes/application-druid.yml deleted file mode 100644 index 742526c8c..000000000 --- a/backend/roomroot-admin/target/classes/application-druid.yml +++ /dev/null @@ -1,56 +0,0 @@ -# 数据源配置 -spring: - datasource: - type: com.alibaba.druid.pool.DruidDataSource - # 达梦连接串:ip:端口,schema指定默认模式 - url: jdbc:dm://10.1.1.17:5236?schema=JIAOWU&useUnicode=true&characterEncoding=utf-8 - username: JIAOWU - password: Jiaowu123 - driver-class-name: dm.jdbc.driver.DmDriver - - # Druid 基础参数 - druid: - # 初始连接数 - initialSize: 5 - # 最小连接池数量 - minIdle: 10 - # 最大连接池数量 - maxActive: 20 - # 配置获取连接等待超时的时间 - maxWait: 60000 - # 配置连接超时时间 - connectTimeout: 30000 - # 配置网络超时时间 - socketTimeout: 60000 - # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 - timeBetweenEvictionRunsMillis: 60000 - # 配置一个连接在池中最小生存的时间,单位是毫秒 - minEvictableIdleTimeMillis: 300000 - # 配置一个连接在池中最大生存的时间,单位是毫秒 - maxEvictableIdleTimeMillis: 900000 - # 配置检测连接是否有效 - validationQuery: SELECT 1 FROM DUAL - testWhileIdle: true - testOnBorrow: false - testOnReturn: false - webStatFilter: - enabled: true - statViewServlet: - enabled: true - # 设置白名单,不填则允许所有访问 - allow: - url-pattern: /druid/* - # 控制台管理用户名和密码 - login-username: roomroot - login-password: 123456 - filter: - stat: - enabled: true - # 慢SQL记录 - log-slow-sql: true - slow-sql-millis: 1000 - merge-sql: true - wall: - config: - multi-statement-allow: true - db-type: dm \ No newline at end of file diff --git a/backend/roomroot-admin/target/classes/application.yml b/backend/roomroot-admin/target/classes/application.yml deleted file mode 100644 index 0352a5cca..000000000 --- a/backend/roomroot-admin/target/classes/application.yml +++ /dev/null @@ -1,149 +0,0 @@ -# 项目相关配置 -roomroot: - # 名称 - name: roomroot - # 版本 - version: 3.9.2 - # 版权年份 - copyrightYear: 2026 - # 文件路径 示例( Windows配置D:/roomroot/uploadPath,Linux配置 /home/roomroot/uploadPath) - profile: D:/roomroot/uploadPath - # 获取ip地址开关 - addressEnabled: false - # 验证码类型 math 数字计算 char 字符验证 - captchaType: math - -# 开发环境配置 -server: - # 服务器的HTTP端口,默认为8080 - port: 8080 - servlet: - # 应用的访问路径 - context-path: /api - tomcat: - # tomcat的URI编码 - uri-encoding: UTF-8 - # 连接数满后的排队数,默认为100 - accept-count: 1000 - threads: - # tomcat最大线程数,默认为200 - max: 800 - # Tomcat启动初始化的线程数,默认值10 - min-spare: 100 - -# 日志配置 -logging: - level: - com.roomroot: debug - #org.springframework: warn - -# 用户配置 -user: - password: - # 密码最大错误次数 - maxRetryCount: 5 - # 密码锁定时间(默认10分钟) - lockTime: 10 - -# Spring配置 -spring: - # 资源信息 - messages: - # 国际化资源文件路径 - basename: i18n/messages - profiles: - active: druid - # 文件上传 - servlet: - multipart: - # 单个文件大小 - max-file-size: 10MB - # 设置总上传的文件大小 - max-request-size: 20MB - jackson: - time-zone: GMT+8 - date-format: yyyy-MM-dd HH:mm:ss - # 服务模块 - devtools: - restart: - # 热部署开关 - enabled: true - data: - # redis 配置 - redis: - # 地址 - host: localhost - # 端口,默认为6379 - port: 6379 - # 数据库索引 - database: 0 - # 密码 - password: - # 连接超时时间 - timeout: 10s - lettuce: - pool: - # 连接池中的最小空闲连接 - min-idle: 0 - # 连接池中的最大空闲连接 - max-idle: 8 - # 连接池的最大数据库连接数 - max-active: 8 - # #连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - -# token配置 -token: - # 令牌自定义标识 - header: Authorization - # 令牌密钥 - secret: abcdefghijklmnopqrstuvwxyz - # 令牌有效期(默认30分钟) - expireTime: 30 - -# MyBatis-Plus配置 -mybatis-plus: - # 搜索指定包别名 - typeAliasesPackage: com.roomroot.**.domain - # 配置mapper的扫描,找到所有的mapper.xml映射文件 - mapperLocations: classpath*:mapper/**/*Mapper.xml,classpath*:com/roomroot/**/mapper/*.xml - # 加载全局的配置文件 - configLocation: classpath:mybatis/mybatis-config.xml - -# Springdoc配置 -springdoc: - api-docs: - path: /v3/api-docs - swagger-ui: - enabled: true - path: /swagger-ui.html - tags-sorter: alpha - group-configs: - - group: 'default' - display-name: '测试模块' - paths-to-match: '/**' - packages-to-scan: com.roomroot.web.controller.tool - -# 防盗链配置 -referer: - # 防盗链开关 - enabled: false - # 允许的域名列表 - allowed-domains: localhost,127.0.0.1,roomroot.vip,www.roomroot.vip - -# 防止XSS攻击 -xss: - # 过滤开关 - enabled: true - # 排除链接(多个用逗号分隔) - excludes: /system/notice - # 匹配链接 - urlPatterns: /system/*,/monitor/*,/tool/* - -# 自定义内嵌Redis开关 -custom: - embedded-redis: - # dev环境true自动启动内嵌Redis;prod改为false使用外部Redis - enable: false - port: 6379 - max-memory: 128M \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/ks/HourStatisticsQuery.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/ks/HourStatisticsQuery.java new file mode 100644 index 000000000..a05ec94f0 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/ks/HourStatisticsQuery.java @@ -0,0 +1,25 @@ +package com.roomroot.jwgl.dto.ks; + +import lombok.Data; + +/** + * 课时统计 / 课时费统计查询条件。 + */ +@Data +public class HourStatisticsQuery { + + /** 学期年度(对应课时统计表.年份、学期.年度;空则默认当前学期) */ + private Integer nd; + + /** 学期序号(与 nd 一起标识学期;课时统计表按年存储,查询按年度过滤) */ + private Integer xq; + + /** 教员姓名(模糊匹配) */ + private String jyxm; + + /** 教员编号(教员登录时强制为本人) */ + private String jybh; + + /** 课时费标准编号(课时费统计可选;空则取最新一条方案) */ + private String ksfbzbh; +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.java new file mode 100644 index 000000000..06338ed9d --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.java @@ -0,0 +1,12 @@ +package com.roomroot.jwgl.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.roomroot.jwgl.entity.KB; +import org.apache.ibatis.annotations.Mapper; + +/** + * 课表 Mapper + */ +@Mapper +public interface KBMapper extends BaseMapper { +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.xml b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.xml new file mode 100644 index 000000000..b6bfe5acc --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/KBMapper.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KBService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KBService.java new file mode 100644 index 000000000..a327e3197 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KBService.java @@ -0,0 +1,49 @@ +package com.roomroot.jwgl.service; + +import com.roomroot.jwgl.entity.KB; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; + +/** + * 课表(课程科目)服务。 + */ +public interface KBService { + + /** + * 新增课表科目。 + * + * @param entity 课表 + */ + void add(KB entity); + + /** + * 删除课表科目。 + * + * @param kbh 课编号 + */ + void delete(String kbh); + + /** + * 更新课表科目。 + * + * @param entity 课表(须含课编号) + */ + void update(KB entity); + + /** + * 按课编号查询。 + * + * @param kbh 课编号 + * @return 课表 + */ + KB getByKbh(String kbh); + + /** + * 分页查询课表科目。 + * + * @param query 分页参数 + * @param cond 查询条件 + * @return 分页结果 + */ + PageResult pageList(PageQuery query, KB cond); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KJService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KJService.java new file mode 100644 index 000000000..1943bc009 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KJService.java @@ -0,0 +1,52 @@ +package com.roomroot.jwgl.service; + +import com.roomroot.jwgl.entity.KSFBZ; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; + +/** + * 课时管理服务。 + *

+ * 当前提供课时费标准方案(表:课时费标准)的增删改查。 + *

+ */ +public interface KJService { + + /** + * 新增课时费标准方案。 + * + * @param entity 课时费标准 + */ + void addFeeStandard(KSFBZ entity); + + /** + * 删除课时费标准方案。 + * + * @param bh 编号 + */ + void deleteFeeStandard(String bh); + + /** + * 更新课时费标准方案。 + * + * @param entity 课时费标准(须含编号) + */ + void updateFeeStandard(KSFBZ entity); + + /** + * 按编号查询课时费标准方案。 + * + * @param bh 编号 + * @return 课时费标准,不存在时返回 null + */ + KSFBZ getFeeStandardById(String bh); + + /** + * 分页查询课时费标准方案。 + * + * @param query 分页参数 + * @param cond 查询条件(名称模糊匹配) + * @return 分页结果 + */ + PageResult pageFeeStandard(PageQuery query, KSFBZ cond); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java new file mode 100644 index 000000000..9840f91a6 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java @@ -0,0 +1,35 @@ +package com.roomroot.jwgl.service; + +import com.roomroot.jwgl.dto.ks.HourStatisticsQuery; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.vo.ks.HourStatisticsVO; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 课时统计、课时费统计。 + */ +public interface KSService { + + /** + * 分页查询课时统计。 + *

规则见课时统计表联查 SQL:标准课时量 180,超课时及职称单价计算过程和结果。

+ */ + PageResult pageHourStatistics(PageQuery query, HourStatisticsQuery cond); + + /** + * 导出课时统计。 + */ + void exportHourStatistics(HourStatisticsQuery cond, HttpServletResponse response); + + /** + * 分页查询课时费统计。 + *

课时量同源,单价取课时费标准(默认最新方案)。

+ */ + PageResult pageFeeStatistics(PageQuery query, HourStatisticsQuery cond); + + /** + * 导出课时费统计。 + */ + void exportFeeStatistics(HourStatisticsQuery cond, HttpServletResponse response); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KBServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KBServiceImpl.java new file mode 100644 index 000000000..6aeeb94a3 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KBServiceImpl.java @@ -0,0 +1,180 @@ +package com.roomroot.jwgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.common.utils.StringUtils; +import com.roomroot.jwgl.entity.KB; +import com.roomroot.jwgl.entity.KCBZ; +import com.roomroot.jwgl.entity.XYDRWB; +import com.roomroot.jwgl.entity.ZYJXJHB; +import com.roomroot.jwgl.mapper.KBMapper; +import com.roomroot.jwgl.mapper.KCBZMapper; +import com.roomroot.jwgl.mapper.StudentTeamTaskMapper; +import com.roomroot.jwgl.mapper.ZYJXJHBMapper; +import com.roomroot.jwgl.service.KBService; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.utils.UuidUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.time.LocalDateTime; + +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; +import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND; + +/** + * 课表(课程科目)服务实现。 + */ +@Service +public class KBServiceImpl implements KBService { + + @Resource + private KBMapper kbMapper; + + @Resource + private KCBZMapper kcbzMapper; + + @Resource + private StudentTeamTaskMapper studentTeamTaskMapper; + + @Resource + private ZYJXJHBMapper zyjxjhbMapper; + + @Override + @Transactional + public void add(KB entity) { + if (entity == null) { + throw new ServiceException("课表信息不能为空", BAD_REQUEST); + } + validate(entity, true); + if (StringUtils.isEmpty(entity.getKbh())) { + entity.setKbh(UuidUtil.getOriginalUUID()); + } + assertKbhUnique(entity.getKbh()); + if (StringUtils.isEmpty(entity.getGfmc())) { + entity.setGfmc(entity.getKmc()); + } + if (entity.getXdrq() == null) { + entity.setXdrq(LocalDateTime.now()); + } + kbMapper.insert(entity); + } + + @Override + @Transactional + public void delete(String kbh) { + if (StringUtils.isEmpty(kbh)) { + throw new ServiceException("课编号不能为空", BAD_REQUEST); + } + KB existing = kbMapper.selectById(kbh); + if (existing == null) { + throw new ServiceException("课表科目不存在", NOT_FOUND); + } + Long standardCount = kcbzMapper.selectCount( + new LambdaQueryWrapper().eq(KCBZ::getKbh, kbh)); + if (standardCount != null && standardCount > 0) { + throw new ServiceException("该科目已被课程标准引用,无法删除", CONFLICT); + } + Long taskCount = studentTeamTaskMapper.selectCount( + new LambdaQueryWrapper().eq(XYDRWB::getKbh, kbh)); + if (taskCount != null && taskCount > 0) { + throw new ServiceException("该科目已被学员队任务引用,无法删除", CONFLICT); + } + Long planCount = zyjxjhbMapper.selectCount( + new LambdaQueryWrapper().eq(ZYJXJHB::getKbh, kbh)); + if (planCount != null && planCount > 0) { + throw new ServiceException("该科目已被专业教学计划引用,无法删除", CONFLICT); + } + kbMapper.deleteById(kbh); + } + + @Override + @Transactional + public void update(KB entity) { + if (entity == null || StringUtils.isEmpty(entity.getKbh())) { + throw new ServiceException("课编号不能为空", BAD_REQUEST); + } + validate(entity, false); + KB existing = kbMapper.selectById(entity.getKbh()); + if (existing == null) { + throw new ServiceException("课表科目不存在", NOT_FOUND); + } + entity.setXdrq(LocalDateTime.now()); + kbMapper.updateById(entity); + } + + @Override + public KB getByKbh(String kbh) { + if (StringUtils.isEmpty(kbh)) { + throw new ServiceException("课编号不能为空", BAD_REQUEST); + } + return kbMapper.selectById(kbh); + } + + @Override + public PageResult pageList(PageQuery query, KB cond) { + int pageNum = query.getPageNum() == null ? 1 : query.getPageNum(); + int pageSize = query.getPageSize() == null ? 20 : query.getPageSize(); + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (cond != null) { + wrapper.like(StringUtils.isNotEmpty(cond.getKmc()), KB::getKmc, cond.getKmc()); + wrapper.eq(StringUtils.isNotEmpty(cond.getJysdh()), KB::getJysdh, cond.getJysdh()); + wrapper.eq(StringUtils.isNotEmpty(cond.getKclx()), KB::getKclx, cond.getKclx()); + wrapper.eq(StringUtils.isNotEmpty(cond.getPxlx()), KB::getPxlx, cond.getPxlx()); + wrapper.eq(StringUtils.isNotEmpty(cond.getPxcc()), KB::getPxcc, cond.getPxcc()); + wrapper.like(StringUtils.isNotEmpty(cond.getKmdm()), KB::getKmdm, cond.getKmdm()); + wrapper.like(StringUtils.isNotEmpty(cond.getJc()), KB::getJc, cond.getJc()); + } + wrapper.orderByAsc(KB::getXhbs); + Page result = kbMapper.selectPage(page, wrapper); + return new PageResult<>(result.getRecords(), result.getTotal(), pageNum, pageSize); + } + + private void validate(KB entity, boolean creating) { + if (creating && StringUtils.isEmpty(entity.getKmc())) { + throw new ServiceException("课名称不能为空", BAD_REQUEST); + } + assertMaxLength(entity.getKbh(), 50, "课编号"); + assertMaxLength(entity.getKmc(), 250, "课名称"); + assertMaxLength(entity.getJysdh(), 50, "教研室代号"); + assertMaxLength(entity.getBz(), 500, "备注"); + assertMaxLength(entity.getXh(), 50, "序号"); + assertMaxLength(entity.getJc(), 50, "简称"); + assertMaxLength(entity.getPxcc(), 50, "培训层次"); + assertMaxLength(entity.getPxlx(), 50, "培训类型"); + assertMaxLength(entity.getKclx(), 50, "课程类型"); + assertMaxLength(entity.getPy(), 50, "拼音"); + assertMaxLength(entity.getGfmc(), 250, "规范名称"); + assertMaxLength(entity.getCjfz(), 50, "成绩分制"); + assertMaxLength(entity.getJxglbmbh(), 50, "教学管理部门编号"); + assertMaxLength(entity.getKmdm(), 50, "科目代码"); + assertMaxLength(entity.getKlx(), 50, "课类型"); + assertMaxLength(entity.getKctybh(), 50, "课程统一编号"); + assertMaxLength(entity.getSydx(), 50, "适用对象"); + assertMaxLength(entity.getBb(), 50, "版本"); + assertMaxLength(entity.getKczcfs(), 50, "课程支持方式"); + assertMaxLength(entity.getKcjsfs(), 50, "课程建设方式"); + assertMaxLength(entity.getJsonzd(), 4000, "json字典"); + assertMaxLength(entity.getKhfs(), 50, "考核方式"); + assertMaxLength(entity.getMj(), 50, "密级"); + assertMaxLength(entity.getPxqk(), 50, "评选情况"); + } + + private void assertMaxLength(String value, int max, String fieldName) { + if (value != null && value.length() > max) { + throw new ServiceException(fieldName + "长度不能超过" + max + "个字符", BAD_REQUEST); + } + } + + private void assertKbhUnique(String kbh) { + Long count = kbMapper.selectCount(new LambdaQueryWrapper().eq(KB::getKbh, kbh)); + if (count != null && count > 0) { + throw new ServiceException("课编号已存在", CONFLICT); + } + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KJServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KJServiceImpl.java new file mode 100644 index 000000000..da4990434 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KJServiceImpl.java @@ -0,0 +1,151 @@ +package com.roomroot.jwgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.common.utils.StringUtils; +import com.roomroot.jwgl.entity.JSZWKSFBZ; +import com.roomroot.jwgl.entity.KSFBZ; +import com.roomroot.jwgl.entity.NZKSF; +import com.roomroot.jwgl.mapper.JSZWKSFBZMapper; +import com.roomroot.jwgl.mapper.KSFBZMapper; +import com.roomroot.jwgl.mapper.NZKSFMapper; +import com.roomroot.jwgl.service.KJService; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.utils.UuidUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.time.LocalDateTime; + +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; +import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND; + +/** + * 课时管理服务实现。 + */ +@Service +public class KJServiceImpl implements KJService { + + private static final int NAME_MAX_LENGTH = 50; + private static final int TEXT_MAX_LENGTH = 500; + + @Resource + private KSFBZMapper ksfbzMapper; + + @Resource + private JSZWKSFBZMapper jszwksfbzMapper; + + @Resource + private NZKSFMapper nzksfMapper; + + @Override + @Transactional + public void addFeeStandard(KSFBZ entity) { + if (entity == null) { + throw new ServiceException("课时费标准方案不能为空", BAD_REQUEST); + } + validateFeeStandard(entity, true); + if (StringUtils.isEmpty(entity.getBh())) { + entity.setBh(UuidUtil.getOriginalUUID()); + } + assertNameUnique(entity.getMc(), null); + entity.setCjsj(LocalDateTime.now()); + ksfbzMapper.insert(entity); + } + + @Override + @Transactional + public void deleteFeeStandard(String bh) { + if (StringUtils.isEmpty(bh)) { + throw new ServiceException("编号不能为空", BAD_REQUEST); + } + KSFBZ existing = ksfbzMapper.selectById(bh); + if (existing == null) { + throw new ServiceException("课时费标准方案不存在", NOT_FOUND); + } + Long techPosCount = jszwksfbzMapper.selectCount( + new LambdaQueryWrapper().eq(JSZWKSFBZ::getKsfbzbh, bh)); + if (techPosCount != null && techPosCount > 0) { + throw new ServiceException("该方案已被技术职务课时费标准引用,无法删除", CONFLICT); + } + Long yearEndCount = nzksfMapper.selectCount( + new LambdaQueryWrapper().eq(NZKSF::getKsfbzbh, bh)); + if (yearEndCount != null && yearEndCount > 0) { + throw new ServiceException("该方案已被年终课时费引用,无法删除", CONFLICT); + } + ksfbzMapper.deleteById(bh); + } + + @Override + @Transactional + public void updateFeeStandard(KSFBZ entity) { + if (entity == null || StringUtils.isEmpty(entity.getBh())) { + throw new ServiceException("编号不能为空", BAD_REQUEST); + } + validateFeeStandard(entity, false); + KSFBZ existing = ksfbzMapper.selectById(entity.getBh()); + if (existing == null) { + throw new ServiceException("课时费标准方案不存在", NOT_FOUND); + } + assertNameUnique(entity.getMc(), entity.getBh()); + entity.setCjsj(existing.getCjsj()); + entity.setXgsj(LocalDateTime.now()); + ksfbzMapper.updateById(entity); + } + + @Override + public KSFBZ getFeeStandardById(String bh) { + if (StringUtils.isEmpty(bh)) { + throw new ServiceException("编号不能为空", BAD_REQUEST); + } + return ksfbzMapper.selectById(bh); + } + + @Override + public PageResult pageFeeStandard(PageQuery query, KSFBZ cond) { + int pageNum = query.getPageNum() == null ? 1 : query.getPageNum(); + int pageSize = query.getPageSize() == null ? 20 : query.getPageSize(); + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (cond != null && StringUtils.isNotEmpty(cond.getMc())) { + wrapper.like(KSFBZ::getMc, cond.getMc()); + } + wrapper.orderByDesc(KSFBZ::getCjsj); + Page result = ksfbzMapper.selectPage(page, wrapper); + return new PageResult<>(result.getRecords(), result.getTotal(), pageNum, pageSize); + } + + private void validateFeeStandard(KSFBZ entity, boolean creating) { + if (creating && StringUtils.isEmpty(entity.getMc())) { + throw new ServiceException("名称不能为空", BAD_REQUEST); + } + if (entity.getMc() != null && entity.getMc().length() > NAME_MAX_LENGTH) { + throw new ServiceException("名称长度不能超过" + NAME_MAX_LENGTH + "个字符", BAD_REQUEST); + } + if (entity.getSm() != null && entity.getSm().length() > TEXT_MAX_LENGTH) { + throw new ServiceException("说明长度不能超过" + TEXT_MAX_LENGTH + "个字符", BAD_REQUEST); + } + if (entity.getBz() != null && entity.getBz().length() > TEXT_MAX_LENGTH) { + throw new ServiceException("备注长度不能超过" + TEXT_MAX_LENGTH + "个字符", BAD_REQUEST); + } + } + + private void assertNameUnique(String mc, String excludeBh) { + if (StringUtils.isEmpty(mc)) { + return; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(KSFBZ::getMc, mc); + if (StringUtils.isNotEmpty(excludeBh)) { + wrapper.ne(KSFBZ::getBh, excludeBh); + } + Long count = ksfbzMapper.selectCount(wrapper); + if (count != null && count > 0) { + throw new ServiceException("课时费标准方案名称已存在", CONFLICT); + } + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java new file mode 100644 index 000000000..4f54dc646 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java @@ -0,0 +1,301 @@ +package com.roomroot.jwgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.constant.Constants; +import com.roomroot.common.core.domain.entity.SysRole; +import com.roomroot.common.core.domain.entity.SysUser; +import com.roomroot.common.core.domain.model.LoginUser; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.common.utils.SecurityUtils; +import com.roomroot.common.utils.StringUtils; +import com.roomroot.common.utils.poi.ExcelUtil; +import com.roomroot.jwgl.dto.ks.HourStatisticsQuery; +import com.roomroot.jwgl.entity.JYB; +import com.roomroot.jwgl.entity.KSFBZ; +import com.roomroot.jwgl.mapper.JYBMapper; +import com.roomroot.jwgl.mapper.KSFBZMapper; +import com.roomroot.jwgl.mapper.KSTJBMapper; +import com.roomroot.jwgl.mapper.SystemInitializationMapper; +import com.roomroot.jwgl.service.KSService; +import com.roomroot.jwgl.unit.PageQuery; +import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.vo.ks.HourStatisticsVO; +import com.roomroot.jwgl.vo.systeminitialization.SemesterInitializationVO; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.List; + +import static com.roomroot.jwgl.utils.AccountManagementConstants.ROLE_DEPARTMENT_PERSONNEL; +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.FORBIDDEN; +import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND; + +/** + * 课时统计、课时费统计实现。 + */ +@Service +public class KSServiceImpl implements KSService { + + private static final double DEFAULT_STANDARD_HOURS = 180D; + private static final double DEFAULT_BASE_RATE = 13D; + + @Resource + private KSTJBMapper kstjbMapper; + + @Resource + private KSFBZMapper ksfbzMapper; + + @Resource + private JYBMapper jybMapper; + + @Resource + private SystemInitializationMapper systemInitializationMapper; + + @Override + public PageResult pageHourStatistics(PageQuery query, HourStatisticsQuery cond) { + Page page = queryHours(query, cond); + for (HourStatisticsVO row : page.getRecords()) { + fillHourRule(row); + } + return new PageResult<>(page.getRecords(), page.getTotal(), + (int) page.getCurrent(), (int) page.getSize()); + } + + @Override + public void exportHourStatistics(HourStatisticsQuery cond, HttpServletResponse response) { + List list = listHours(cond); + for (HourStatisticsVO row : list) { + fillHourRule(row); + } + ExcelUtil util = new ExcelUtil<>(HourStatisticsVO.class); + util.exportExcel(response, list, "课时统计"); + } + + @Override + public PageResult pageFeeStatistics(PageQuery query, HourStatisticsQuery cond) { + KSFBZ standard = resolveFeeStandard(cond); + Page page = queryHours(query, cond); + for (HourStatisticsVO row : page.getRecords()) { + fillFeeRule(row, standard); + } + return new PageResult<>(page.getRecords(), page.getTotal(), + (int) page.getCurrent(), (int) page.getSize()); + } + + @Override + public void exportFeeStatistics(HourStatisticsQuery cond, HttpServletResponse response) { + KSFBZ standard = resolveFeeStandard(cond); + List list = listHours(cond); + for (HourStatisticsVO row : list) { + fillFeeRule(row, standard); + } + ExcelUtil util = new ExcelUtil<>(HourStatisticsVO.class); + util.exportExcel(response, list, "课时费统计"); + } + + private Page queryHours(PageQuery query, HourStatisticsQuery cond) { + HourStatisticsQuery filter = prepareFilter(cond); + int pageNum = query.getPageNum() == null ? 1 : query.getPageNum(); + int pageSize = query.getPageSize() == null ? 20 : query.getPageSize(); + Page page = new Page<>(pageNum, pageSize); + return kstjbMapper.selectHourStatistics(page, filter); + } + + private List listHours(HourStatisticsQuery cond) { + return kstjbMapper.selectHourStatisticsList(prepareFilter(cond)); + } + + private HourStatisticsQuery prepareFilter(HourStatisticsQuery cond) { + HourStatisticsQuery filter = cond == null ? new HourStatisticsQuery() : cond; + applyScopeAndDefaultSemester(filter); + return filter; + } + + /** + * 未传学期年度时默认当前学期;非管理员强制只查登录教员本人。 + */ + private void applyScopeAndDefaultSemester(HourStatisticsQuery filter) { + if (filter.getNd() == null) { + SemesterInitializationVO current = resolveCurrentSemester(); + if (current != null && current.getYear() != null) { + filter.setNd(current.getYear()); + if (filter.getXq() == null) { + filter.setXq(current.getSequenceNumber()); + } + } else { + filter.setNd(LocalDate.now().getYear()); + } + } + if (isAdminViewer()) { + return; + } + String teacherId = resolveLoginTeacherId(); + filter.setJybh(teacherId); + filter.setJyxm(null); + } + + private boolean isAdminViewer() { + try { + if (SecurityUtils.isAdmin()) { + return true; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser.getPermissions() != null + && loginUser.getPermissions().contains(Constants.ALL_PERMISSION)) { + return true; + } + SysUser user = loginUser.getUser(); + if (user == null || user.getRoles() == null) { + return false; + } + for (SysRole role : user.getRoles()) { + if (role == null || StringUtils.isEmpty(role.getRoleKey())) { + continue; + } + String key = role.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(key) + || ROLE_DEPARTMENT_PERSONNEL.equalsIgnoreCase(key)) { + return true; + } + } + return false; + } catch (Exception ex) { + return false; + } + } + + private String resolveLoginTeacherId() { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser user = loginUser.getUser(); + String username = loginUser.getUsername(); + if (StringUtils.isNotEmpty(username)) { + JYB byId = jybMapper.selectById(username); + if (byId != null) { + return byId.getJybh(); + } + } + String nickName = user == null ? null : user.getNickName(); + if (StringUtils.isNotEmpty(nickName)) { + List matched = jybMapper.selectList( + new LambdaQueryWrapper().eq(JYB::getJyxm, nickName)); + if (matched != null && matched.size() == 1) { + return matched.get(0).getJybh(); + } + } + throw new ServiceException("当前登录用户不是教员,无法查看课时统计", FORBIDDEN); + } + + private SemesterInitializationVO resolveCurrentSemester() { + return systemInitializationMapper.selectCurrentSemester(); + } + + private KSFBZ resolveFeeStandard(HourStatisticsQuery cond) { + if (cond != null && StringUtils.isNotEmpty(cond.getKsfbzbh())) { + KSFBZ specified = ksfbzMapper.selectById(cond.getKsfbzbh()); + if (specified == null) { + throw new ServiceException("课时费标准不存在", NOT_FOUND); + } + return specified; + } + Page page = new Page<>(1, 1); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .orderByDesc(KSFBZ::getCjsj); + Page result = ksfbzMapper.selectPage(page, wrapper); + if (result.getRecords() == null || result.getRecords().isEmpty()) { + throw new ServiceException("尚未配置课时费标准", BAD_REQUEST); + } + return result.getRecords().get(0); + } + + /** + * 课时统计:标准课时量 180;未超课时按 13 元;超课时部分按职称 15/16/17/18。 + */ + private void fillHourRule(HourStatisticsVO row) { + double total = nvl(row.getSbn()) + nvl(row.getXbn()); + double standardHours = DEFAULT_STANDARD_HOURS; + double overtime = Math.max(0D, total - standardHours); + row.setZj(round(total)); + row.setBzksl(standardHours); + row.setCks(round(overtime)); + Double overtimeRate = overtimeRateByTitle(row.getZc()); + if (overtimeRate == null) { + return; + } + fillFormula(row, total, standardHours, overtime, DEFAULT_BASE_RATE, overtimeRate); + } + + /** + * 课时费统计:标准课时量、课时费、超量课时费取课时费标准;超量单价为空时回退职称规则。 + */ + private void fillFeeRule(HourStatisticsVO row, KSFBZ standard) { + double total = nvl(row.getSbn()) + nvl(row.getXbn()); + double standardHours = standard.getMrbzksl() != null + ? standard.getMrbzksl() : DEFAULT_STANDARD_HOURS; + double baseRate = standard.getMrksfbz() != null + ? standard.getMrksfbz() : DEFAULT_BASE_RATE; + Double overtimeRate = standard.getMrclksfbz(); + if (overtimeRate == null) { + overtimeRate = overtimeRateByTitle(row.getZc()); + } + if (overtimeRate == null) { + overtimeRate = baseRate; + } + double overtime = Math.max(0D, total - standardHours); + row.setZj(round(total)); + row.setBzksl(round(standardHours)); + row.setCks(round(overtime)); + row.setKsfbzmc(standard.getMc()); + row.setKsfbz(round(baseRate)); + row.setClksfbz(round(overtimeRate)); + fillFormula(row, total, standardHours, overtime, baseRate, overtimeRate); + } + + private void fillFormula(HourStatisticsVO row, double total, double standardHours, + double overtime, double baseRate, double overtimeRate) { + if (overtime > 0D) { + row.setJsgs(formatNumber(standardHours) + "×" + formatNumber(baseRate) + + "+" + formatNumber(overtime) + "×" + formatNumber(overtimeRate)); + row.setJg(round(standardHours * baseRate + overtime * overtimeRate)); + } else { + row.setJsgs(formatNumber(total) + "×" + formatNumber(baseRate)); + row.setJg(round(total * baseRate)); + } + } + + private Double overtimeRateByTitle(String title) { + if (StringUtils.isEmpty(title)) { + return null; + } + switch (title.trim()) { + case "助教": + return 15D; + case "讲师": + return 16D; + case "副教授": + return 17D; + case "教授": + return 18D; + default: + return null; + } + } + + private double nvl(Double value) { + return value == null ? 0D : value; + } + + private double round(double value) { + return BigDecimal.valueOf(value).setScale(1, RoundingMode.HALF_UP).doubleValue(); + } + + private String formatNumber(double value) { + return BigDecimal.valueOf(value).setScale(1, RoundingMode.HALF_UP) + .stripTrailingZeros().toPlainString(); + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/ks/HourStatisticsVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/ks/HourStatisticsVO.java new file mode 100644 index 000000000..41e44ca8d --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/ks/HourStatisticsVO.java @@ -0,0 +1,59 @@ +package com.roomroot.jwgl.vo.ks; + +import com.roomroot.common.annotation.Excel; +import lombok.Data; + +/** + * 课时统计 / 课时费统计行。 + */ +@Data +public class HourStatisticsVO { + + @Excel(name = "教研室代号", sort = 1) + private String jysdh; + + @Excel(name = "教研室名称", sort = 2) + private String jysmc; + + @Excel(name = "教员编号", sort = 3) + private String jybh; + + @Excel(name = "教员姓名", sort = 4) + private String jyxm; + + @Excel(name = "职称", sort = 5) + private String zc; + + @Excel(name = "年份", sort = 6) + private Integer nf; + + @Excel(name = "上半年", sort = 7) + private Double sbn; + + @Excel(name = "下半年", sort = 8) + private Double xbn; + + @Excel(name = "总计", sort = 9) + private Double zj; + + @Excel(name = "标准课时量", sort = 10) + private Double bzksl; + + @Excel(name = "超课时", sort = 11) + private Double cks; + + @Excel(name = "计算过程", sort = 12, width = 28) + private String jsgs; + + @Excel(name = "结果", sort = 13) + private Double jg; + + @Excel(name = "课时费标准", sort = 14) + private String ksfbzmc; + + @Excel(name = "课时费单价", sort = 15) + private Double ksfbz; + + @Excel(name = "超量课时费单价", sort = 16) + private Double clksfbz; +}