diff --git a/.gitignore b/.gitignore index 1ec16190..298ef0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -331,3 +331,7 @@ scripts/ 学生调宿、退宿功能.md 学生动向信息界面(宿管员).md prompt/ +backend/fix-all-errors.sh +.gitignore +backend/fix-mapstruct-error.sh +backend/fix-compilation-errors.sh diff --git a/backend/ruoyi-admin/pom.xml b/backend/ruoyi-admin/pom.xml index e8ab5226..78cfdea1 100644 --- a/backend/ruoyi-admin/pom.xml +++ b/backend/ruoyi-admin/pom.xml @@ -111,7 +111,7 @@ org.dromara ruoyi-xg - + @@ -147,7 +147,7 @@ org.dromara ruoyi-jcbt ${revision} - + @@ -161,7 +161,7 @@ org.dromara ruoyi-stgl ${revision} - + de.codecentric diff --git a/backend/ruoyi-admin/src/main/resources/application.yml b/backend/ruoyi-admin/src/main/resources/application.yml index 86b03355..f6bde346 100644 --- a/backend/ruoyi-admin/src/main/resources/application.yml +++ b/backend/ruoyi-admin/src/main/resources/application.yml @@ -12,10 +12,16 @@ server: # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 max-http-post-size: -1 # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 - # 每块buffer的空间大小,越小的空间被利用越充分 - buffer-size: 512 + # 每块buffer的空间大小(标准配置:16KB,提升文件上传性能) + buffer-size: 16384 # 是否分配的直接内存 direct-buffers: true + # 请求处理超时时间(毫秒),0表示无限制,建议设置合理超时避免长时间阻塞 + no-request-timeout: 600000 + # 🔧 修复文件上传超时:阻塞写操作超时时间(毫秒),适用于大文件上传或慢网络环境 + options: + # 阻塞写操作超时时间设置为10分钟,避免上传大文件时触发默认300秒超时 + blocking-write-timeout: 600000 threads: # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 io: 8 @@ -75,10 +81,16 @@ spring: # 文件上传 servlet: multipart: + # 是否启用文件上传 + enabled: true # 单个文件大小 max-file-size: 10MB # 设置总上传的文件大小 max-request-size: 20MB + # 上传文件的临时目录 + location: ${java.io.tmpdir} + # 文件大小阈值,超过该阈值将写入磁盘而非内存(0表示始终写入磁盘) + file-size-threshold: 0 mvc: # 设置静态资源路径 防止所有请求都去查静态资源 static-path-pattern: /static/** @@ -193,7 +205,7 @@ mybatis-encryptor: # api接口加密 api-decrypt: # 是否开启全局接口加密 - enabled: true # 恢复为true,保持其他接口的安全性 + enabled: false # 临时关闭用于测试权限修复 # AES 加密头标识 headerFlag: encrypt-key # 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换 diff --git a/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java b/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java index 8c690e34..ebbd0d02 100644 --- a/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java +++ b/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java @@ -13,7 +13,11 @@ import org.dromara.common.core.utils.sql.SqlUtil; import java.io.Serial; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * 分页查询实体类 @@ -46,6 +50,16 @@ public class PageQuery implements Serializable { */ private String isAsc; + /** + * 默认排序列 + */ + private String defaultOrderByColumn = "create_time"; + + /** + * 默认排序方向 + */ + private String defaultIsAsc = "desc"; + /** * 当前记录起始索引 默认值 */ @@ -66,10 +80,14 @@ public class PageQuery implements Serializable { pageNum = DEFAULT_PAGE_NUM; } Page page = new Page<>(pageNum, pageSize); - List orderItems = buildOrderItem(); + List orderItems = buildOrderItem(orderByColumn, isAsc); if (CollUtil.isNotEmpty(orderItems)) { page.addOrder(orderItems); } + List defaultOrderItems = buildDefaultOrderItem(orderItems); + if (CollUtil.isNotEmpty(defaultOrderItems)) { + page.addOrder(defaultOrderItems); + } return page; } @@ -79,18 +97,21 @@ public class PageQuery implements Serializable { * 支持的用法如下: * {isAsc:"asc",orderByColumn:"id"} order by id asc * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc - * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc - * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc + * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time + * desc + * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time + * desc */ - private List buildOrderItem() { + private List buildOrderItem(String orderByColumn, String isAsc) { if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) { - return null; + return Collections.emptyList(); } String orderBy = SqlUtil.escapeOrderBySql(orderByColumn); orderBy = StringUtils.toUnderScoreCase(orderBy); // 兼容前端排序类型 - isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"}); + isAsc = StringUtils.replaceEach(isAsc, new String[] { "ascending", "descending" }, + new String[] { "asc", "desc" }); String[] orderByArr = orderBy.split(StringUtils.SEPARATOR); String[] isAscArr = isAsc.split(StringUtils.SEPARATOR); @@ -114,6 +135,29 @@ public class PageQuery implements Serializable { return list; } + private List buildDefaultOrderItem(List existingOrders) { + if (StringUtils.isBlank(defaultOrderByColumn) || StringUtils.isBlank(defaultIsAsc)) { + return Collections.emptyList(); + } + List defaultItems = buildOrderItem(defaultOrderByColumn, defaultIsAsc); + if (CollUtil.isEmpty(defaultItems)) { + return defaultItems; + } + Set existing = CollUtil.isNotEmpty(existingOrders) + ? existingOrders.stream() + .map(item -> StringUtils.blankToDefault(item.getColumn(), StringUtils.EMPTY).trim() + .toLowerCase()) + .collect(Collectors.toCollection(HashSet::new)) + : Collections.emptySet(); + return defaultItems.stream() + .filter(item -> { + String column = StringUtils.blankToDefault(item.getColumn(), StringUtils.EMPTY).trim() + .toLowerCase(); + return !existing.contains(column); + }) + .collect(Collectors.toList()); + } + @JsonIgnore public Integer getFirstNum() { return (pageNum - 1) * pageSize; @@ -130,4 +174,20 @@ public class PageQuery implements Serializable { this.pageNum = pageNum; } + public PageQuery withDefaultOrder(String column, String asc) { + this.defaultOrderByColumn = column; + this.defaultIsAsc = asc; + return this; + } + + public PageQuery withDefaultOrder(String column) { + return withDefaultOrder(column, "desc"); + } + + public PageQuery disableDefaultOrder() { + this.defaultOrderByColumn = null; + this.defaultIsAsc = null; + return this; + } + } diff --git a/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index e4ebc6ec..4a8913da 100644 --- a/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -21,6 +21,8 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.*; @@ -32,6 +34,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; /** @@ -93,10 +96,10 @@ public class OssClient { .httpClient(NettyNioAsyncHttpClient.builder() // 🚀 优化:连接超时时间(60秒) .connectionTimeout(Duration.ofSeconds(60)) - // 🚀 优化:读取超时时间(300秒,适合大文件下载) - .readTimeout(Duration.ofSeconds(300)) - // 🚀 优化:写入超时时间(300秒,适合大文件上传) - .writeTimeout(Duration.ofSeconds(300)) + // 🚀 优化:读取超时时间(600秒,适合大文件下载和慢网络环境) + .readTimeout(Duration.ofSeconds(600)) + // 🚀 优化:写入超时时间(600秒,适合大文件上传和慢网络环境) + .writeTimeout(Duration.ofSeconds(600)) .build()) .build(); @@ -184,31 +187,31 @@ public class OssClient { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { - // 创建异步请求体(length如果为空会报错) + // 🔧 修复:创建异步请求体,设置超时时间为600秒(10分钟),适应慢网络环境 BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() .contentLength(length) - .subscribeTimeout(Duration.ofSeconds(30)) + .subscribeTimeout(Duration.ofSeconds(600)) .build(); - // 使用 transferManager 进行上传 - Upload upload = transferManager.upload( - x -> x.requestBody(body) - .putObjectRequest( - y -> y.bucket(properties.getBucketName()) - .key(key) - .contentType(contentType) - // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, - // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) - // .acl(getAccessPolicy().getObjectCannedACL()) - .build()) - .build()); + // 🔧 修复:构建上传请求对象 + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(key) + .contentType(contentType) + // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, + // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) + // .acl(getAccessPolicy().getObjectCannedACL()) + .build(); - // 将输入流写入请求体 + // 🔧 修复:先发起上传请求(重要!必须在writeInputStream之前) + CompletableFuture future = client.putObject(putObjectRequest, body); + + // 🔧 修复:在发起请求后立即写入数据流(正确顺序) body.writeInputStream(inputStream); // 等待文件上传操作完成 - CompletedUpload uploadResult = upload.completionFuture().join(); - String eTag = uploadResult.response().eTag(); + PutObjectResponse response = future.join(); + String eTag = response.eTag(); // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); diff --git a/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application-prod.yml b/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application-prod.yml new file mode 100644 index 00000000..108a203e --- /dev/null +++ b/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application-prod.yml @@ -0,0 +1,48 @@ +server: + port: 9090 +spring: + application: + name: ruoyi-monitor-admin + profiles: + active: @profiles.active@ + +logging: + config: classpath:logback-plus.xml + +--- # 监控中心服务端配置 +spring: + security: + user: + name: @monitor.username@ + password: @monitor.password@ + boot: + admin: + ui: + title: RuoYi-Vue-Plus服务监控中心 + context-path: /admin + +--- # Actuator 监控端点的配置项 +management: + endpoints: + web: + exposure: + include: '*' + endpoint: + health: + show-details: ALWAYS + logfile: + external-file: ./logs/ruoyi-monitor-admin.log + +--- # 监控配置 +spring.boot.admin.client: + # 增加客户端开关 + enabled: true + # 设置 Spring Boot Admin Server 地址 + url: http://172.16.0.67:9090/admin + instance: + service-host-type: IP + metadata: + username: ${spring.boot.admin.client.username} + userpassword: ${spring.boot.admin.client.password} + username: @monitor.username@ + password: @monitor.password@ diff --git a/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml b/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml index 108a203e..86766d7a 100644 --- a/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml +++ b/backend/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml @@ -38,7 +38,7 @@ spring.boot.admin.client: # 增加客户端开关 enabled: true # 设置 Spring Boot Admin Server 地址 - url: http://172.16.0.67:9090/admin + url: http://127.0.0.1:9090/admin instance: service-host-type: IP metadata: diff --git a/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/domain/bo/BatchApprovalBo.java b/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/domain/bo/BatchApprovalBo.java index 0444104a..da374a9c 100644 --- a/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/domain/bo/BatchApprovalBo.java +++ b/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/domain/bo/BatchApprovalBo.java @@ -24,5 +24,8 @@ public class BatchApprovalBo { /** * 拒绝原因(批量拒绝时必填) */ + private String rejectReason; + + } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwMonitoringManagementController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwMonitoringManagementController.java new file mode 100644 index 00000000..b7b1b4b8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwMonitoringManagementController.java @@ -0,0 +1,102 @@ +package org.dromara.cw.controller; + +import java.util.Arrays; +import java.util.List; + +import jakarta.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; +import org.dromara.cw.service.ICwMonitoringManagementService; + +/** + * 运行监控管理 + * 财务管理员和学校领导使用,用于查看所有人提交的绩效自评,并进行锁定/解锁操作 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/monitoringManagement") +public class CwMonitoringManagementController extends BaseController { + + private final ICwMonitoringManagementService monitoringManagementService; + + /** + * 查询绩效自评列表(运行监控管理) + * 财务管理员和学校领导可以看到所有人的数据 + */ + @SaCheckPermission("cw:monitoringManagement:list") + @GetMapping("/list") + public TableDataInfo list(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery) { + return monitoringManagementService.queryPageList(bo, pageQuery); + } + + /** + * 获取绩效自评详细信息(运行监控管理) + */ + @SaCheckPermission("cw:monitoringManagement:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(monitoringManagementService.queryById(id)); + } + + /** + * 锁定绩效自评(单个) + */ + @SaCheckPermission("cw:monitoringManagement:lock") + @Log(title = "运行监控管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/lock/{id}") + public R lock(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return toAjax(monitoringManagementService.lockEvaluation(id)); + } + + /** + * 解锁绩效自评(单个) + */ + @SaCheckPermission("cw:monitoringManagement:unlock") + @Log(title = "运行监控管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/unlock/{id}") + public R unlock(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return toAjax(monitoringManagementService.unlockEvaluation(id)); + } + + /** + * 批量锁定绩效自评 + */ + @SaCheckPermission("cw:monitoringManagement:lock") + @Log(title = "运行监控管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/lock/batch") + public R lockBatch(@NotEmpty(message = "主键不能为空") @RequestBody Long[] ids) { + return toAjax(monitoringManagementService.lockBatch(Arrays.asList(ids))); + } + + /** + * 批量解锁绩效自评 + */ + @SaCheckPermission("cw:monitoringManagement:unlock") + @Log(title = "运行监控管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/unlock/batch") + public R unlockBatch(@NotEmpty(message = "主键不能为空") @RequestBody Long[] ids) { + return toAjax(monitoringManagementService.unlockBatch(Arrays.asList(ids))); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwPerformanceMonitoringController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwPerformanceMonitoringController.java new file mode 100644 index 00000000..1b68a5a8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwPerformanceMonitoringController.java @@ -0,0 +1,85 @@ +package org.dromara.cw.controller; + +import java.util.List; + +import jakarta.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.cw.domain.bo.CwPerformanceMonitoringBo; +import org.dromara.cw.domain.vo.CwPerformanceMonitoringVo; +import org.dromara.cw.service.ICwPerformanceMonitoringService; + +/** + * 绩效运行监控 + * + * @author ruoyi + * @date 2025-11-04 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/performanceMonitoring") +public class CwPerformanceMonitoringController extends BaseController { + + private final ICwPerformanceMonitoringService performanceMonitoringService; + + /** + * 根据自评ID查询运行监控列表 + */ + @SaCheckPermission("cw:performanceMonitoring:query") + @GetMapping("/list/{evaluationId}") + public R> listByEvaluationId(@NotNull(message = "自评ID不能为空") @PathVariable Long evaluationId) { + return R.ok(performanceMonitoringService.queryByEvaluationId(evaluationId)); + } + + /** + * 获取绩效运行监控详细信息 + */ + @SaCheckPermission("cw:performanceMonitoring:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(performanceMonitoringService.queryById(id)); + } + + /** + * 新增绩效运行监控 + */ + @SaCheckPermission("cw:performanceMonitoring:add") + @Log(title = "绩效运行监控", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CwPerformanceMonitoringBo bo) { + return toAjax(performanceMonitoringService.insertByBo(bo)); + } + + /** + * 修改绩效运行监控 + */ + @SaCheckPermission("cw:performanceMonitoring:edit") + @Log(title = "绩效运行监控", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CwPerformanceMonitoringBo bo) { + return toAjax(performanceMonitoringService.updateByBo(bo)); + } + + /** + * 删除绩效运行监控 + */ + @SaCheckPermission("cw:performanceMonitoring:remove") + @Log(title = "绩效运行监控", businessType = BusinessType.DELETE) + @DeleteMapping("/{id}") + public R remove(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return toAjax(performanceMonitoringService.deleteWithValidByIds(List.of(id), true)); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectEvaluationIndicatorController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectEvaluationIndicatorController.java new file mode 100644 index 00000000..32b1a16a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectEvaluationIndicatorController.java @@ -0,0 +1,132 @@ +package org.dromara.cw.controller; + +import java.util.List; +import java.util.Arrays; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectEvaluationIndicatorBo; +import org.dromara.cw.domain.vo.CwProjectEvaluationIndicatorVo; +import org.dromara.cw.service.ICwProjectEvaluationIndicatorService; + +/** + * 项目绩效自评指标明细 + * + * @author ruoyi + * @date 2025-11-03 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/indicator") +public class CwProjectEvaluationIndicatorController extends BaseController { + + private final ICwProjectEvaluationIndicatorService indicatorService; + + /** + * 查询项目绩效自评指标明细列表 + */ + @SaCheckPermission("cw:indicator:list") + @GetMapping("/list") + public TableDataInfo list(CwProjectEvaluationIndicatorBo bo, PageQuery pageQuery) { + return indicatorService.queryPageList(bo, pageQuery); + } + + /** + * 获取项目绩效自评指标明细详细信息 + */ + @SaCheckPermission("cw:indicator:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(indicatorService.queryById(id)); + } + + /** + * 根据自评ID查询指标明细列表 + */ + @SaCheckPermission("cw:indicator:query") + @GetMapping("/evaluation/{evaluationId}") + public R> getByEvaluationId(@NotNull(message = "自评ID不能为空") @PathVariable Long evaluationId) { + return R.ok(indicatorService.queryByEvaluationId(evaluationId)); + } + + /** + * 根据自评ID和指标类型查询指标明细列表 + */ + @SaCheckPermission("cw:indicator:query") + @GetMapping("/evaluation/{evaluationId}/type/{indicatorType}") + public R> getByEvaluationIdAndType( + @NotNull(message = "自评ID不能为空") @PathVariable Long evaluationId, + @NotBlank(message = "指标类型不能为空") @PathVariable String indicatorType) { + return R.ok(indicatorService.queryByEvaluationIdAndType(evaluationId, indicatorType)); + } + + /** + * 新增项目绩效自评指标明细 + */ + @SaCheckPermission("cw:indicator:add") + @Log(title = "项目绩效自评指标明细", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CwProjectEvaluationIndicatorBo bo) { + return toAjax(indicatorService.insertByBo(bo)); + } + + /** + * 修改项目绩效自评指标明细 + */ + @SaCheckPermission("cw:indicator:edit") + @Log(title = "项目绩效自评指标明细", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CwProjectEvaluationIndicatorBo bo) { + return toAjax(indicatorService.updateByBo(bo)); + } + + /** + * 批量保存指标明细 + */ + @SaCheckPermission("cw:indicator:edit") + @Log(title = "批量保存指标明细", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/batch") + public R batchSave(@RequestParam Long evaluationId, @RequestBody List boList) { + return toAjax(indicatorService.batchSave(evaluationId, boList)); + } + + /** + * 删除项目绩效自评指标明细 + */ + @SaCheckPermission("cw:indicator:remove") + @Log(title = "项目绩效自评指标明细", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return toAjax(indicatorService.deleteWithValidByIds(Arrays.asList(ids), true)); + } + + /** + * 导出项目绩效自评指标明细列表 + */ + @SaCheckPermission("cw:indicator:export") + @Log(title = "项目绩效自评指标明细", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CwProjectEvaluationIndicatorBo bo, HttpServletResponse response) { + List list = indicatorService.queryList(bo); + ExcelUtil.exportExcel(list, "项目绩效自评指标明细", CwProjectEvaluationIndicatorVo.class, response); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectMonthlyMonitoringController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectMonthlyMonitoringController.java new file mode 100644 index 00000000..e05e2e6d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectMonthlyMonitoringController.java @@ -0,0 +1,144 @@ +package org.dromara.cw.controller; + +import java.util.List; +import java.util.Arrays; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectMonthlyMonitoringBo; +import org.dromara.cw.domain.vo.CwProjectMonthlyMonitoringVo; +import org.dromara.cw.service.ICwProjectMonthlyMonitoringService; + +/** + * 项目月度运行监控 + * + * @author ruoyi + * @date 2025-11-03 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/monitoring") +public class CwProjectMonthlyMonitoringController extends BaseController { + + private final ICwProjectMonthlyMonitoringService monitoringService; + + /** + * 查询项目月度运行监控列表 + */ + @SaCheckPermission("cw:monitoring:list") + @GetMapping("/list") + public TableDataInfo list(CwProjectMonthlyMonitoringBo bo, PageQuery pageQuery) { + return monitoringService.queryPageList(bo, pageQuery); + } + + /** + * 获取项目月度运行监控详细信息 + */ + @SaCheckPermission("cw:monitoring:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(monitoringService.queryById(id)); + } + + /** + * 根据自评ID查询月度监控列表 + */ + @SaCheckPermission("cw:monitoring:query") + @GetMapping("/evaluation/{evaluationId}") + public R> getByEvaluationId(@NotNull(message = "自评ID不能为空") @PathVariable Long evaluationId) { + return R.ok(monitoringService.queryByEvaluationId(evaluationId)); + } + + /** + * 查询某个自评的全年监控数据 + */ + @SaCheckPermission("cw:monitoring:query") + @GetMapping("/year/{evaluationId}/{year}") + public R> getYearlyMonitoring( + @NotNull(message = "自评ID不能为空") @PathVariable Long evaluationId, + @NotNull(message = "年份不能为空") @PathVariable Integer year) { + return R.ok(monitoringService.queryYearlyMonitoring(evaluationId, year)); + } + + /** + * 新增项目月度运行监控 + */ + @SaCheckPermission("cw:monitoring:add") + @Log(title = "项目月度运行监控", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CwProjectMonthlyMonitoringBo bo) { + return toAjax(monitoringService.insertByBo(bo)); + } + + /** + * 修改项目月度运行监控 + */ + @SaCheckPermission("cw:monitoring:edit") + @Log(title = "项目月度运行监控", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CwProjectMonthlyMonitoringBo bo) { + return toAjax(monitoringService.updateByBo(bo)); + } + + /** + * 填报月度数据 + */ + @SaCheckPermission("cw:monitoring:fill") + @Log(title = "填报月度数据", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/fill") + public R fillMonthlyData(@Validated @RequestBody CwProjectMonthlyMonitoringBo bo) { + return toAjax(monitoringService.fillMonthlyData(bo)); + } + + /** + * 批量保存月度监控数据 + */ + @SaCheckPermission("cw:monitoring:edit") + @Log(title = "批量保存月度监控数据", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/batch") + public R batchSave(@RequestParam Long evaluationId, @RequestBody List boList) { + return toAjax(monitoringService.batchSave(evaluationId, boList)); + } + + /** + * 删除项目月度运行监控 + */ + @SaCheckPermission("cw:monitoring:remove") + @Log(title = "项目月度运行监控", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return toAjax(monitoringService.deleteWithValidByIds(Arrays.asList(ids), true)); + } + + /** + * 导出项目月度运行监控列表 + */ + @SaCheckPermission("cw:monitoring:export") + @Log(title = "项目月度运行监控", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CwProjectMonthlyMonitoringBo bo, HttpServletResponse response) { + List list = monitoringService.queryList(bo); + ExcelUtil.exportExcel(list, "项目月度运行监控", CwProjectMonthlyMonitoringVo.class, response); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectPerformanceSelfEvaluationController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectPerformanceSelfEvaluationController.java new file mode 100644 index 00000000..fda1e72a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectPerformanceSelfEvaluationController.java @@ -0,0 +1,161 @@ +package org.dromara.cw.controller; + +import java.util.List; +import java.util.Arrays; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; +import org.dromara.cw.service.ICwProjectPerformanceSelfEvaluationService; +import org.dromara.common.satoken.utils.LoginHelper; + +/** + * 项目绩效自评 + * + * @author ruoyi + * @date 2025-11-03 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/evaluation") +public class CwProjectPerformanceSelfEvaluationController extends BaseController { + + private final ICwProjectPerformanceSelfEvaluationService evaluationService; + + /** + * 查询项目绩效自评列表 + */ + @SaCheckPermission("cw:evaluation:list") + @GetMapping("/list") + public TableDataInfo list(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery) { + return evaluationService.queryPageList(bo, pageQuery); + } + + /** + * 获取项目绩效自评详细信息 + */ + @SaCheckPermission("cw:evaluation:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(evaluationService.queryById(id)); + } + + /** + * 新增项目绩效自评 + */ + @SaCheckPermission("cw:evaluation:add") + @Log(title = "项目绩效自评", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CwProjectPerformanceSelfEvaluationBo bo) { + boolean result = evaluationService.insertByBo(bo); + if (result) { + return R.ok("新增成功", bo.getId()); + } + return R.fail("新增失败"); + } + + /** + * 修改项目绩效自评 + */ + @SaCheckPermission("cw:evaluation:edit") + @Log(title = "项目绩效自评", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CwProjectPerformanceSelfEvaluationBo bo) { + return toAjax(evaluationService.updateByBo(bo)); + } + + /** + * 保存草稿 + */ + @SaCheckPermission("cw:evaluation:add") + @Log(title = "保存自评草稿", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/saveDraft") + public R saveDraft(@RequestBody CwProjectPerformanceSelfEvaluationBo bo) { + return toAjax(evaluationService.saveDraft(bo)); + } + + /** + * 提交自评 + */ + @SaCheckPermission("cw:evaluation:submit") + @Log(title = "提交自评", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/submit/{id}") + public R submit(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return toAjax(evaluationService.submitEvaluation(id)); + } + + /** + * 审核自评 + */ + @SaCheckPermission("cw:evaluation:approve") + @Log(title = "审核自评", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/approve/{id}") + public R approve(@NotNull(message = "主键不能为空") @PathVariable Long id, + @RequestParam String opinion, + @RequestParam Boolean approved) { + return toAjax(evaluationService.approveEvaluation(id, opinion, approved)); + } + + /** + * 删除项目绩效自评 + */ + @SaCheckPermission("cw:evaluation:remove") + @Log(title = "项目绩效自评", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return toAjax(evaluationService.deleteWithValidByIds(Arrays.asList(ids), true)); + } + + /** + * 查询我的自评列表 + */ + @SaCheckPermission("cw:evaluation:list") + @GetMapping("/my") + public R> myEvaluations() { + return R.ok(evaluationService.queryMyEvaluations(LoginHelper.getUserId())); + } + + /** + * 查询待审核自评列表 + */ + @SaCheckPermission("cw:evaluation:approve") + @GetMapping("/pending") + public R> pendingApprovals() { + return R.ok(evaluationService.queryPendingApprovals()); + } + + /** + * 导出项目绩效自评列表 + */ + @SaCheckPermission("cw:evaluation:export") + @Log(title = "项目绩效自评", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CwProjectPerformanceSelfEvaluationBo bo, HttpServletResponse response) { + List list = evaluationService.queryList(bo); + ExcelUtil.exportExcel(list, "项目绩效自评", CwProjectPerformanceSelfEvaluationVo.class, response); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwPerformanceMonitoring.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwPerformanceMonitoring.java new file mode 100644 index 00000000..6b8b80bb --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwPerformanceMonitoring.java @@ -0,0 +1,82 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +/** + * 绩效运行监控对象 cw_performance_monitoring + * + * @author ruoyi + * @date 2025-11-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_performance_monitoring") +public class CwPerformanceMonitoring extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 绩效自评ID + */ + private Long evaluationId; + + /** + * 项目申报ID + */ + private Long projectApplicationId; + + /** + * 监控周期 + */ + private String monitoringPeriod; + + /** + * 资金使用情况 + */ + private String fundUsage; + + /** + * 绩效目标完成情况 + */ + private String performanceTargetCompletion; + + /** + * 绩效指标完成情况(JSON数组) + */ + private String indicators; + + /** + * 存在问题 + */ + private String existingProblems; + + /** + * 指标差异率分析和说明 + */ + private String indicatorVarianceAnalysis; + + /** + * 下一步工作建议和打算 + */ + private String nextStepSuggestions; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + private String fillByName; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectEvaluationIndicator.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectEvaluationIndicator.java new file mode 100644 index 00000000..6684d20b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectEvaluationIndicator.java @@ -0,0 +1,90 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +import java.math.BigDecimal; + +/** + * 项目绩效自评指标明细对象 cw_project_evaluation_indicator + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_project_evaluation_indicator") +public class CwProjectEvaluationIndicator extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 绩效自评ID + */ + private Long evaluationId; + + /** + * 指标类别(decision-决策/process-过程/output-产出/benefit-效益) + */ + private String indicatorCategory; + + /** + * 指标名称 + */ + private String indicatorName; + + /** + * 指标类型(quantity-数量/quality-质量/time-时效/cost-成本) + */ + private String indicatorType; + + /** + * 目标值 + */ + private String targetValue; + + /** + * 实际完成值 + */ + private String actualValue; + + /** + * 完成率(%) + */ + private BigDecimal completionRate; + + /** + * 指标分值 + */ + private BigDecimal standardScore; + + /** + * 实际得分 + */ + private BigDecimal actualScore; + + /** + * 完成情况说明 + */ + private String completionDescription; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 逻辑删除标志(0-正常 1-删除) + */ + @TableLogic + private Integer deleted; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectMonthlyMonitoring.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectMonthlyMonitoring.java new file mode 100644 index 00000000..5d8dfaae --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectMonthlyMonitoring.java @@ -0,0 +1,146 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目月度运行监控对象 cw_project_monthly_monitoring + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_project_monthly_monitoring") +public class CwProjectMonthlyMonitoring extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 绩效自评ID + */ + private Long evaluationId; + + /** + * 项目申报ID + */ + private Long projectApplicationId; + + /** + * 监控年度 + */ + private Integer monitoringYear; + + /** + * 监控月份(1-12) + */ + private Integer monitoringMonth; + + /** + * 计划进度(%) + */ + private BigDecimal plannedProgress; + + /** + * 实际进度(%) + */ + private BigDecimal actualProgress; + + /** + * 进度偏差(%) + */ + private BigDecimal progressVariance; + + /** + * 本月预算(万元) + */ + private BigDecimal monthlyBudget; + + /** + * 本月实际支出(万元) + */ + private BigDecimal monthlyExpenditure; + + /** + * 累计预算(万元) + */ + private BigDecimal cumulativeBudget; + + /** + * 累计实际支出(万元) + */ + private BigDecimal cumulativeExpenditure; + + /** + * 资金执行率(%) + */ + private BigDecimal fundExecutionRate; + + /** + * 本月完成的关键节点 + */ + private String keyMilestones; + + /** + * 节点完成情况(completed-已完成/delayed-延期/notStarted-未开始) + */ + private String milestoneCompletion; + + /** + * 本月遇到的问题 + */ + private String problemsEncountered; + + /** + * 风险描述 + */ + private String riskDescription; + + /** + * 应对措施 + */ + private String countermeasures; + + /** + * 下月工作计划 + */ + private String nextMonthPlan; + + /** + * 填报状态(not_filled-未填报/filled-已填报) + */ + private String fillStatus; + + /** + * 填报时间 + */ + private Date fillTime; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + private String fillByName; + + /** + * 逻辑删除标志(0-正常 1-删除) + */ + @TableLogic + private Integer deleted; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectPerformanceSelfEvaluation.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectPerformanceSelfEvaluation.java new file mode 100644 index 00000000..2f16eee9 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectPerformanceSelfEvaluation.java @@ -0,0 +1,591 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目绩效自评对象 cw_project_performance_self_evaluation + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_project_performance_self_evaluation") +public class CwProjectPerformanceSelfEvaluation extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 项目申报ID + */ + private Long projectApplicationId; + + /** + * 申报编号 + */ + private String applicationNo; + + /** + * 项目名称 + */ + private String projectName; + + /** + * 主管部门 + */ + private String supervisoryDept; + + /** + * 实施单位 + */ + private String implementUnit; + + /** + * 自评年度 + */ + private Integer evaluationYear; + + /** + * 年初预算数(万元) + */ + private BigDecimal initialBudget; + + /** + * 项目负责人 + */ + private String principal; + + /** + * 联系电话 + */ + private String contactPhone; + + /** + * 项目实施开始时间 + */ + private Date startDate; + + /** + * 项目实施结束时间 + */ + private Date endDate; + + /** + * 项目总投资(万元) + */ + private BigDecimal totalInvestment; + + /** + * 本年度预算(万元) + */ + private BigDecimal annualBudget; + + /** + * 本年度实际支出(万元) + */ + private BigDecimal actualExpenditure; + + /** + * 预算执行率(%) + */ + private BigDecimal budgetExecutionRate; + + /** + * 资金到位分值 + */ + private BigDecimal fundValue; + + /** + * 资金执行率(%) + */ + private BigDecimal fundExecutionRate; + + /** + * 资金到位得分 + */ + private BigDecimal fundScore; + + /** + * 当年财政拨款-年初预算数(万元) + */ + private BigDecimal currentYearBudgetInitial; + + /** + * 当年财政拨款-全年预算数(万元) + */ + private BigDecimal currentYearBudgetAnnual; + + /** + * 当年财政拨款-全年执行数(万元) + */ + private BigDecimal currentYearBudgetActual; + + /** + * 当年财政拨款-分值 + */ + private BigDecimal currentYearBudgetValue; + + /** + * 当年财政拨款-执行率(%) + */ + private BigDecimal currentYearBudgetExecutionRate; + + /** + * 当年财政拨款-得分 + */ + private BigDecimal currentYearBudgetScore; + + /** + * 上年结转资金-年初预算数(万元) + */ + private BigDecimal lastYearCarryoverInitial; + + /** + * 上年结转资金-全年预算数(万元) + */ + private BigDecimal lastYearCarryoverAnnual; + + /** + * 上年结转资金-全年执行数(万元) + */ + private BigDecimal lastYearCarryoverActual; + + /** + * 上年结转资金-分值 + */ + private BigDecimal lastYearCarryoverValue; + + /** + * 上年结转资金-执行率(%) + */ + private BigDecimal lastYearCarryoverExecutionRate; + + /** + * 上年结转资金-得分 + */ + private BigDecimal lastYearCarryoverScore; + + /** + * 其他资金-年初预算数(万元) + */ + private BigDecimal otherFundsInitial; + + /** + * 其他资金-全年预算数(万元) + */ + private BigDecimal otherFundsAnnual; + + /** + * 其他资金-全年执行数(万元) + */ + private BigDecimal otherFundsActual; + + /** + * 其他资金-分值 + */ + private BigDecimal otherFundsValue; + + /** + * 其他资金-执行率(%) + */ + private BigDecimal otherFundsExecutionRate; + + /** + * 其他资金-得分 + */ + private BigDecimal otherFundsScore; + + /** + * 年度绩效目标 + */ + private String performanceGoal; + + /** + * 目标完成情况说明 + */ + private String targetCompletion; + + /** + * 数量指标名称 + */ + private String quantityIndicator; + + /** + * 数量指标-指标值 + */ + private String quantityTarget; + + /** + * 数量指标-实际完成值 + */ + private String quantityActual; + + /** + * 数量指标-分值 + */ + private BigDecimal quantityValue; + + /** + * 数量指标-得分 + */ + private BigDecimal quantityScore; + + /** + * 数量指标-偏差原因分析及改进措施 + */ + private String quantityDeviation; + + /** + * 质量指标名称 + */ + private String qualityIndicator; + + /** + * 质量指标-指标值 + */ + private String qualityTarget; + + /** + * 质量指标-实际完成值 + */ + private String qualityActual; + + /** + * 质量指标-分值 + */ + private BigDecimal qualityValue; + + /** + * 质量指标-得分 + */ + private BigDecimal qualityScore; + + /** + * 质量指标-偏差原因分析及改进措施 + */ + private String qualityDeviation; + + /** + * 时效指标名称 + */ + private String timelinessIndicator; + + /** + * 时效指标-指标值 + */ + private String timelinessTarget; + + /** + * 时效指标-实际完成值 + */ + private String timelinessActual; + + /** + * 时效指标-分值 + */ + private BigDecimal timelinessValue; + + /** + * 时效指标-得分 + */ + private BigDecimal timelinessScore; + + /** + * 时效指标-偏差原因分析及改进措施 + */ + private String timelinessDeviation; + + /** + * 成本指标名称 + */ + private String costIndicator; + + /** + * 成本指标-指标值 + */ + private String costTarget; + + /** + * 成本指标-实际完成值 + */ + private String costActual; + + /** + * 成本指标-分值 + */ + private BigDecimal costValue; + + /** + * 成本指标-得分 + */ + private BigDecimal costScore; + + /** + * 成本指标-偏差原因分析及改进措施 + */ + private String costDeviation; + + /** + * 经济效益指标名称 + */ + private String economicIndicator; + + /** + * 经济效益指标-指标值 + */ + private String economicTarget; + + /** + * 经济效益指标-实际完成值 + */ + private String economicActual; + + /** + * 经济效益指标-分值 + */ + private BigDecimal economicValue; + + /** + * 经济效益指标-得分 + */ + private BigDecimal economicScore; + + /** + * 经济效益指标-偏差原因分析及改进措施 + */ + private String economicDeviation; + + /** + * 社会效益指标名称 + */ + private String socialIndicator; + + /** + * 社会效益指标-指标值 + */ + private String socialTarget; + + /** + * 社会效益指标-实际完成值 + */ + private String socialActual; + + /** + * 社会效益指标-分值 + */ + private BigDecimal socialValue; + + /** + * 社会效益指标-得分 + */ + private BigDecimal socialScore; + + /** + * 社会效益指标-偏差原因分析及改进措施 + */ + private String socialDeviation; + + /** + * 生态效益指标名称 + */ + private String ecologicalIndicator; + + /** + * 生态效益指标-指标值 + */ + private String ecologicalTarget; + + /** + * 生态效益指标-实际完成值 + */ + private String ecologicalActual; + + /** + * 生态效益指标-分值 + */ + private BigDecimal ecologicalValue; + + /** + * 生态效益指标-得分 + */ + private BigDecimal ecologicalScore; + + /** + * 生态效益指标-偏差原因分析及改进措施 + */ + private String ecologicalDeviation; + + /** + * 满意度指标名称 + */ + private String satisfactionIndicator; + + /** + * 满意度指标-指标值 + */ + private String satisfactionTarget; + + /** + * 满意度指标-实际完成值 + */ + private String satisfactionActual; + + /** + * 满意度指标-分值 + */ + private BigDecimal satisfactionValue; + + /** + * 满意度指标-得分 + */ + private BigDecimal satisfactionScore; + + /** + * 满意度指标-偏差原因分析及改进措施 + */ + private String satisfactionDeviation; + + /** + * 项目决策得分 + */ + private BigDecimal decisionScore; + + /** + * 项目决策自评内容 + */ + private String decisionContent; + + /** + * 项目过程得分 + */ + private BigDecimal processScore; + + /** + * 项目过程自评内容 + */ + private String processContent; + + /** + * 项目产出得分 + */ + private BigDecimal outputScore; + + /** + * 项目产出自评内容 + */ + private String outputContent; + + /** + * 项目效益得分 + */ + private BigDecimal benefitScore; + + /** + * 项目效益自评内容 + */ + private String benefitContent; + + /** + * 自评总分 + */ + private BigDecimal totalScore; + + /** + * 自评等级(优秀/良好/合格/不合格) + */ + private String selfEvaluationLevel; + + /** + * 存在的主要问题 + */ + private String existingProblems; + + /** + * 改进措施和建议 + */ + private String improvementMeasures; + + /** + * 自评状态(draft-草稿 submitted-已提交 approved-已审核) + */ + private String evaluationStatus; + + /** + * 允许已提交后编辑(0-不允许 1-允许) + */ + private Integer allowEditAfterSubmit; + + /** + * 提交时间 + */ + private Date submitTime; + + /** + * 提交人ID + */ + private Long submitBy; + + /** + * 提交人姓名 + */ + private String submitByName; + + /** + * 审核时间 + */ + private Date approveTime; + + /** + * 审核人ID + */ + private Long approveBy; + + /** + * 审核人姓名 + */ + private String approveByName; + + /** + * 审核意见 + */ + private String approveOpinion; + + /** + * 是否锁定(0-未锁定 1-已锁定),锁定后不可修改 + */ + private Integer isLocked; + + /** + * 锁定时间 + */ + private Date lockTime; + + /** + * 锁定人ID + */ + private Long lockBy; + + /** + * 锁定人姓名 + */ + private String lockByName; + + /** + * 逻辑删除标志(0-正常 1-删除) + */ + @TableLogic + private Integer deleted; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwPerformanceMonitoringBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwPerformanceMonitoringBo.java new file mode 100644 index 00000000..d9012d19 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwPerformanceMonitoringBo.java @@ -0,0 +1,88 @@ +package org.dromara.cw.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.cw.domain.CwPerformanceMonitoring; + +/** + * 绩效运行监控业务对象 cw_performance_monitoring + * + * @author ruoyi + * @date 2025-11-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwPerformanceMonitoring.class, reverseConvertGenerate = false) +public class CwPerformanceMonitoringBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 绩效自评ID + */ + @NotNull(message = "绩效自评ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long evaluationId; + + /** + * 项目申报ID + */ + private Long projectApplicationId; + + /** + * 监控周期 + */ + @NotBlank(message = "监控周期不能为空", groups = { AddGroup.class, EditGroup.class }) + private String monitoringPeriod; + + /** + * 资金使用情况 + */ + @NotBlank(message = "资金使用情况不能为空", groups = { AddGroup.class, EditGroup.class }) + private String fundUsage; + + /** + * 绩效目标完成情况 + */ + @NotBlank(message = "绩效目标完成情况不能为空", groups = { AddGroup.class, EditGroup.class }) + private String performanceTargetCompletion; + + /** + * 绩效指标完成情况(JSON数组) + */ + private String indicators; + + /** + * 存在问题 + */ + private String existingProblems; + + /** + * 指标差异率分析和说明 + */ + private String indicatorVarianceAnalysis; + + /** + * 下一步工作建议和打算 + */ + private String nextStepSuggestions; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + private String fillByName; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectEvaluationIndicatorBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectEvaluationIndicatorBo.java new file mode 100644 index 00000000..859ac0fd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectEvaluationIndicatorBo.java @@ -0,0 +1,91 @@ +package org.dromara.cw.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.cw.domain.CwProjectEvaluationIndicator; + +import jakarta.validation.constraints.*; +import java.math.BigDecimal; + +/** + * 项目绩效自评指标明细业务对象 cw_project_evaluation_indicator + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwProjectEvaluationIndicator.class, reverseConvertGenerate = false) +public class CwProjectEvaluationIndicatorBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = {EditGroup.class}) + private Long id; + + /** + * 绩效自评ID + */ + @NotNull(message = "绩效自评ID不能为空", groups = {AddGroup.class}) + private Long evaluationId; + + /** + * 指标类别(decision-决策/process-过程/output-产出/benefit-效益) + */ + @NotBlank(message = "指标类别不能为空", groups = {AddGroup.class}) + private String indicatorCategory; + + /** + * 指标名称 + */ + @NotBlank(message = "指标名称不能为空", groups = {AddGroup.class}) + private String indicatorName; + + /** + * 指标类型(quantity-数量/quality-质量/time-时效/cost-成本) + */ + private String indicatorType; + + /** + * 目标值 + */ + private String targetValue; + + /** + * 实际完成值 + */ + private String actualValue; + + /** + * 完成率(%) + */ + @DecimalMin(value = "0", message = "完成率不能小于0") + @DecimalMax(value = "200", message = "完成率不能大于200") + private BigDecimal completionRate; + + /** + * 指标分值 + */ + private BigDecimal standardScore; + + /** + * 实际得分 + */ + private BigDecimal actualScore; + + /** + * 完成情况说明 + */ + private String completionDescription; + + /** + * 排序号 + */ + private Integer sortOrder; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectMonthlyMonitoringBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectMonthlyMonitoringBo.java new file mode 100644 index 00000000..3a46b71b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectMonthlyMonitoringBo.java @@ -0,0 +1,152 @@ +package org.dromara.cw.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.cw.domain.CwProjectMonthlyMonitoring; + +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目月度运行监控业务对象 cw_project_monthly_monitoring + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwProjectMonthlyMonitoring.class, reverseConvertGenerate = false) +public class CwProjectMonthlyMonitoringBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = {EditGroup.class}) + private Long id; + + /** + * 绩效自评ID + */ + @NotNull(message = "绩效自评ID不能为空", groups = {AddGroup.class}) + private Long evaluationId; + + /** + * 项目申报ID + */ + @NotNull(message = "项目申报ID不能为空", groups = {AddGroup.class}) + private Long projectApplicationId; + + /** + * 监控年度 + */ + @NotNull(message = "监控年度不能为空", groups = {AddGroup.class}) + private Integer monitoringYear; + + /** + * 监控月份(1-12) + */ + @NotNull(message = "监控月份不能为空", groups = {AddGroup.class}) + @Min(value = 1, message = "监控月份最小为1") + @Max(value = 12, message = "监控月份最大为12") + private Integer monitoringMonth; + + /** + * 计划进度(%) + */ + @DecimalMin(value = "0", message = "计划进度不能小于0") + @DecimalMax(value = "100", message = "计划进度不能大于100") + private BigDecimal plannedProgress; + + /** + * 实际进度(%) + */ + @DecimalMin(value = "0", message = "实际进度不能小于0") + @DecimalMax(value = "100", message = "实际进度不能大于100") + private BigDecimal actualProgress; + + /** + * 进度偏差(%) + */ + private BigDecimal progressVariance; + + /** + * 本月预算(万元) + */ + private BigDecimal monthlyBudget; + + /** + * 本月实际支出(万元) + */ + private BigDecimal monthlyExpenditure; + + /** + * 累计预算(万元) + */ + private BigDecimal cumulativeBudget; + + /** + * 累计实际支出(万元) + */ + private BigDecimal cumulativeExpenditure; + + /** + * 资金执行率(%) + */ + private BigDecimal fundExecutionRate; + + /** + * 本月完成的关键节点 + */ + private String keyMilestones; + + /** + * 节点完成情况(completed-已完成/delayed-延期/notStarted-未开始) + */ + private String milestoneCompletion; + + /** + * 本月遇到的问题 + */ + private String problemsEncountered; + + /** + * 风险描述 + */ + private String riskDescription; + + /** + * 应对措施 + */ + private String countermeasures; + + /** + * 下月工作计划 + */ + private String nextMonthPlan; + + /** + * 填报状态(not_filled-未填报/filled-已填报) + */ + private String fillStatus; + + /** + * 填报时间 + */ + private Date fillTime; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + private String fillByName; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectPerformanceSelfEvaluationBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectPerformanceSelfEvaluationBo.java new file mode 100644 index 00000000..b2228840 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectPerformanceSelfEvaluationBo.java @@ -0,0 +1,583 @@ +package org.dromara.cw.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.cw.domain.CwProjectPerformanceSelfEvaluation; + +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目绩效自评业务对象 cw_project_performance_self_evaluation + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwProjectPerformanceSelfEvaluation.class, reverseConvertGenerate = false) +public class CwProjectPerformanceSelfEvaluationBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = {EditGroup.class}) + private Long id; + + /** + * 项目申报ID + */ + @NotNull(message = "项目申报ID不能为空", groups = {AddGroup.class}) + private Long projectApplicationId; + + /** + * 申报编号 + */ + private String applicationNo; + + /** + * 项目名称 + */ + @NotBlank(message = "项目名称不能为空", groups = {AddGroup.class}) + private String projectName; + + /** + * 主管部门 + */ + private String supervisoryDept; + + /** + * 实施单位 + */ + private String implementUnit; + + /** + * 自评年度 + */ + @NotNull(message = "自评年度不能为空", groups = {AddGroup.class}) + private Integer evaluationYear; + + /** + * 年初预算数(万元) + */ + private BigDecimal initialBudget; + + /** + * 项目负责人 + */ + private String principal; + + /** + * 联系电话 + */ + private String contactPhone; + + /** + * 项目实施开始时间 + */ + private Date startDate; + + /** + * 项目实施结束时间 + */ + private Date endDate; + + /** + * 项目总投资(万元) + */ + private BigDecimal totalInvestment; + + /** + * 本年度预算(万元) + */ + private BigDecimal annualBudget; + + /** + * 本年度实际支出(万元) + */ + private BigDecimal actualExpenditure; + + /** + * 预算执行率(%) + */ + private BigDecimal budgetExecutionRate; + + /** + * 资金到位分值 + */ + private BigDecimal fundValue; + + /** + * 资金执行率(%) + */ + private BigDecimal fundExecutionRate; + + /** + * 资金到位得分 + */ + private BigDecimal fundScore; + + /** + * 当年财政拨款-年初预算数(万元) + */ + private BigDecimal currentYearBudgetInitial; + + /** + * 当年财政拨款-全年预算数(万元) + */ + private BigDecimal currentYearBudgetAnnual; + + /** + * 当年财政拨款-全年执行数(万元) + */ + private BigDecimal currentYearBudgetActual; + + /** + * 当年财政拨款-分值 + */ + private BigDecimal currentYearBudgetValue; + + /** + * 当年财政拨款-执行率(%) + */ + private BigDecimal currentYearBudgetExecutionRate; + + /** + * 当年财政拨款-得分 + */ + private BigDecimal currentYearBudgetScore; + + /** + * 上年结转资金-年初预算数(万元) + */ + private BigDecimal lastYearCarryoverInitial; + + /** + * 上年结转资金-全年预算数(万元) + */ + private BigDecimal lastYearCarryoverAnnual; + + /** + * 上年结转资金-全年执行数(万元) + */ + private BigDecimal lastYearCarryoverActual; + + /** + * 上年结转资金-分值 + */ + private BigDecimal lastYearCarryoverValue; + + /** + * 上年结转资金-执行率(%) + */ + private BigDecimal lastYearCarryoverExecutionRate; + + /** + * 上年结转资金-得分 + */ + private BigDecimal lastYearCarryoverScore; + + /** + * 其他资金-年初预算数(万元) + */ + private BigDecimal otherFundsInitial; + + /** + * 其他资金-全年预算数(万元) + */ + private BigDecimal otherFundsAnnual; + + /** + * 其他资金-全年执行数(万元) + */ + private BigDecimal otherFundsActual; + + /** + * 其他资金-分值 + */ + private BigDecimal otherFundsValue; + + /** + * 其他资金-执行率(%) + */ + private BigDecimal otherFundsExecutionRate; + + /** + * 其他资金-得分 + */ + private BigDecimal otherFundsScore; + + /** + * 年度绩效目标 + */ + private String performanceGoal; + + /** + * 目标完成情况说明 + */ + private String targetCompletion; + + /** + * 数量指标名称 + */ + private String quantityIndicator; + + /** + * 数量指标-指标值 + */ + private String quantityTarget; + + /** + * 数量指标-实际完成值 + */ + private String quantityActual; + + /** + * 数量指标-分值 + */ + private BigDecimal quantityValue; + + /** + * 数量指标-得分 + */ + private BigDecimal quantityScore; + + /** + * 数量指标-偏差原因分析及改进措施 + */ + private String quantityDeviation; + + /** + * 质量指标名称 + */ + private String qualityIndicator; + + /** + * 质量指标-指标值 + */ + private String qualityTarget; + + /** + * 质量指标-实际完成值 + */ + private String qualityActual; + + /** + * 质量指标-分值 + */ + private BigDecimal qualityValue; + + /** + * 质量指标-得分 + */ + private BigDecimal qualityScore; + + /** + * 质量指标-偏差原因分析及改进措施 + */ + private String qualityDeviation; + + /** + * 时效指标名称 + */ + private String timelinessIndicator; + + /** + * 时效指标-指标值 + */ + private String timelinessTarget; + + /** + * 时效指标-实际完成值 + */ + private String timelinessActual; + + /** + * 时效指标-分值 + */ + private BigDecimal timelinessValue; + + /** + * 时效指标-得分 + */ + private BigDecimal timelinessScore; + + /** + * 时效指标-偏差原因分析及改进措施 + */ + private String timelinessDeviation; + + /** + * 成本指标名称 + */ + private String costIndicator; + + /** + * 成本指标-指标值 + */ + private String costTarget; + + /** + * 成本指标-实际完成值 + */ + private String costActual; + + /** + * 成本指标-分值 + */ + private BigDecimal costValue; + + /** + * 成本指标-得分 + */ + private BigDecimal costScore; + + /** + * 成本指标-偏差原因分析及改进措施 + */ + private String costDeviation; + + /** + * 经济效益指标名称 + */ + private String economicIndicator; + + /** + * 经济效益指标-指标值 + */ + private String economicTarget; + + /** + * 经济效益指标-实际完成值 + */ + private String economicActual; + + /** + * 经济效益指标-分值 + */ + private BigDecimal economicValue; + + /** + * 经济效益指标-得分 + */ + private BigDecimal economicScore; + + /** + * 经济效益指标-偏差原因分析及改进措施 + */ + private String economicDeviation; + + /** + * 社会效益指标名称 + */ + private String socialIndicator; + + /** + * 社会效益指标-指标值 + */ + private String socialTarget; + + /** + * 社会效益指标-实际完成值 + */ + private String socialActual; + + /** + * 社会效益指标-分值 + */ + private BigDecimal socialValue; + + /** + * 社会效益指标-得分 + */ + private BigDecimal socialScore; + + /** + * 社会效益指标-偏差原因分析及改进措施 + */ + private String socialDeviation; + + /** + * 生态效益指标名称 + */ + private String ecologicalIndicator; + + /** + * 生态效益指标-指标值 + */ + private String ecologicalTarget; + + /** + * 生态效益指标-实际完成值 + */ + private String ecologicalActual; + + /** + * 生态效益指标-分值 + */ + private BigDecimal ecologicalValue; + + /** + * 生态效益指标-得分 + */ + private BigDecimal ecologicalScore; + + /** + * 生态效益指标-偏差原因分析及改进措施 + */ + private String ecologicalDeviation; + + /** + * 满意度指标名称 + */ + private String satisfactionIndicator; + + /** + * 满意度指标-指标值 + */ + private String satisfactionTarget; + + /** + * 满意度指标-实际完成值 + */ + private String satisfactionActual; + + /** + * 满意度指标-分值 + */ + private BigDecimal satisfactionValue; + + /** + * 满意度指标-得分 + */ + private BigDecimal satisfactionScore; + + /** + * 满意度指标-偏差原因分析及改进措施 + */ + private String satisfactionDeviation; + + /** + * 项目决策得分 + */ + @DecimalMin(value = "0", message = "项目决策得分不能小于0") + @DecimalMax(value = "20", message = "项目决策得分不能大于20") + private BigDecimal decisionScore; + + /** + * 项目决策自评内容 + */ + private String decisionContent; + + /** + * 项目过程得分 + */ + @DecimalMin(value = "0", message = "项目过程得分不能小于0") + @DecimalMax(value = "20", message = "项目过程得分不能大于20") + private BigDecimal processScore; + + /** + * 项目过程自评内容 + */ + private String processContent; + + /** + * 项目产出得分 + */ + @DecimalMin(value = "0", message = "项目产出得分不能小于0") + @DecimalMax(value = "30", message = "项目产出得分不能大于30") + private BigDecimal outputScore; + + /** + * 项目产出自评内容 + */ + private String outputContent; + + /** + * 项目效益得分 + */ + @DecimalMin(value = "0", message = "项目效益得分不能小于0") + @DecimalMax(value = "30", message = "项目效益得分不能大于30") + private BigDecimal benefitScore; + + /** + * 项目效益自评内容 + */ + private String benefitContent; + + /** + * 自评总分 + */ + private BigDecimal totalScore; + + /** + * 自评等级(优秀/良好/合格/不合格) + */ + private String selfEvaluationLevel; + + /** + * 存在的主要问题 + */ + private String existingProblems; + + /** + * 改进措施和建议 + */ + private String improvementMeasures; + + /** + * 自评状态(draft-草稿 submitted-已提交 approved-已审核) + */ + private String evaluationStatus; + + /** + * 允许已提交后编辑(0-不允许 1-允许) + */ + private Integer allowEditAfterSubmit; + + /** + * 提交时间 + */ + private Date submitTime; + + /** + * 提交人ID + */ + private Long submitBy; + + /** + * 提交人姓名 + */ + private String submitByName; + + /** + * 审核时间 + */ + private Date approveTime; + + /** + * 审核人ID + */ + private Long approveBy; + + /** + * 审核人姓名 + */ + private String approveByName; + + /** + * 审核意见 + */ + private String approveOpinion; + + /** + * 是否锁定(0-未锁定 1-已锁定),锁定后不可修改 + */ + private Integer isLocked; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwPerformanceMonitoringVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwPerformanceMonitoringVo.java new file mode 100644 index 00000000..2e18e521 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwPerformanceMonitoringVo.java @@ -0,0 +1,128 @@ +package org.dromara.cw.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.cw.domain.CwPerformanceMonitoring; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 绩效运行监控视图对象 cw_performance_monitoring + * + * @author ruoyi + * @date 2025-11-04 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwPerformanceMonitoring.class) +public class CwPerformanceMonitoringVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 绩效自评ID + */ + @ExcelProperty(value = "绩效自评ID") + private Long evaluationId; + + /** + * 项目申报ID + */ + @ExcelProperty(value = "项目申报ID") + private Long projectApplicationId; + + /** + * 监控周期 + */ + @ExcelProperty(value = "监控周期") + private String monitoringPeriod; + + /** + * 资金使用情况 + */ + @ExcelProperty(value = "资金使用情况") + private String fundUsage; + + /** + * 绩效目标完成情况 + */ + @ExcelProperty(value = "绩效目标完成情况") + private String performanceTargetCompletion; + + /** + * 绩效指标完成情况(JSON数组) + */ + @ExcelProperty(value = "绩效指标完成情况") + private String indicators; + + /** + * 存在问题 + */ + @ExcelProperty(value = "存在问题") + private String existingProblems; + + /** + * 指标差异率分析和说明 + */ + @ExcelProperty(value = "指标差异率分析和说明") + private String indicatorVarianceAnalysis; + + /** + * 下一步工作建议和打算 + */ + @ExcelProperty(value = "下一步工作建议和打算") + private String nextStepSuggestions; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + @ExcelProperty(value = "填报人") + private String fillByName; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建者 + */ + private Long createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新者 + */ + private Long updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectEvaluationIndicatorVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectEvaluationIndicatorVo.java new file mode 100644 index 00000000..f73e889a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectEvaluationIndicatorVo.java @@ -0,0 +1,118 @@ +package org.dromara.cw.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.cw.domain.CwProjectEvaluationIndicator; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目绩效自评指标明细视图对象 cw_project_evaluation_indicator + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwProjectEvaluationIndicator.class) +public class CwProjectEvaluationIndicatorVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 绩效自评ID + */ + @ExcelProperty(value = "绩效自评ID") + private Long evaluationId; + + /** + * 指标类别(decision-决策/process-过程/output-产出/benefit-效益) + */ + @ExcelProperty(value = "指标类别") + private String indicatorCategory; + + /** + * 指标名称 + */ + @ExcelProperty(value = "指标名称") + private String indicatorName; + + /** + * 指标类型(quantity-数量/quality-质量/time-时效/cost-成本) + */ + @ExcelProperty(value = "指标类型") + private String indicatorType; + + /** + * 目标值 + */ + @ExcelProperty(value = "目标值") + private String targetValue; + + /** + * 实际完成值 + */ + @ExcelProperty(value = "实际完成值") + private String actualValue; + + /** + * 完成率(%) + */ + @ExcelProperty(value = "完成率(%)") + private BigDecimal completionRate; + + /** + * 指标分值 + */ + @ExcelProperty(value = "指标分值") + private BigDecimal standardScore; + + /** + * 实际得分 + */ + @ExcelProperty(value = "实际得分") + private BigDecimal actualScore; + + /** + * 完成情况说明 + */ + @ExcelProperty(value = "完成情况说明") + private String completionDescription; + + /** + * 排序号 + */ + @ExcelProperty(value = "排序号") + private Integer sortOrder; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectMonthlyMonitoringVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectMonthlyMonitoringVo.java new file mode 100644 index 00000000..21be2b63 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectMonthlyMonitoringVo.java @@ -0,0 +1,183 @@ +package org.dromara.cw.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.cw.domain.CwProjectMonthlyMonitoring; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 项目月度运行监控视图对象 cw_project_monthly_monitoring + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwProjectMonthlyMonitoring.class) +public class CwProjectMonthlyMonitoringVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 绩效自评ID + */ + @ExcelProperty(value = "绩效自评ID") + private Long evaluationId; + + /** + * 项目申报ID + */ + @ExcelProperty(value = "项目申报ID") + private Long projectApplicationId; + + /** + * 监控年度 + */ + @ExcelProperty(value = "监控年度") + private Integer monitoringYear; + + /** + * 监控月份(1-12) + */ + @ExcelProperty(value = "监控月份") + private Integer monitoringMonth; + + /** + * 计划进度(%) + */ + @ExcelProperty(value = "计划进度(%)") + private BigDecimal plannedProgress; + + /** + * 实际进度(%) + */ + @ExcelProperty(value = "实际进度(%)") + private BigDecimal actualProgress; + + /** + * 进度偏差(%) + */ + @ExcelProperty(value = "进度偏差(%)") + private BigDecimal progressVariance; + + /** + * 本月预算(万元) + */ + @ExcelProperty(value = "本月预算(万元)") + private BigDecimal monthlyBudget; + + /** + * 本月实际支出(万元) + */ + @ExcelProperty(value = "本月实际支出(万元)") + private BigDecimal monthlyExpenditure; + + /** + * 累计预算(万元) + */ + @ExcelProperty(value = "累计预算(万元)") + private BigDecimal cumulativeBudget; + + /** + * 累计实际支出(万元) + */ + @ExcelProperty(value = "累计实际支出(万元)") + private BigDecimal cumulativeExpenditure; + + /** + * 资金执行率(%) + */ + @ExcelProperty(value = "资金执行率(%)") + private BigDecimal fundExecutionRate; + + /** + * 本月完成的关键节点 + */ + @ExcelProperty(value = "关键节点") + private String keyMilestones; + + /** + * 节点完成情况(completed-已完成/delayed-延期/notStarted-未开始) + */ + @ExcelProperty(value = "节点完成情况") + private String milestoneCompletion; + + /** + * 本月遇到的问题 + */ + @ExcelProperty(value = "遇到的问题") + private String problemsEncountered; + + /** + * 风险描述 + */ + @ExcelProperty(value = "风险描述") + private String riskDescription; + + /** + * 应对措施 + */ + @ExcelProperty(value = "应对措施") + private String countermeasures; + + /** + * 下月工作计划 + */ + @ExcelProperty(value = "下月工作计划") + private String nextMonthPlan; + + /** + * 填报状态(not_filled-未填报/filled-已填报) + */ + @ExcelProperty(value = "填报状态") + private String fillStatus; + + /** + * 填报时间 + */ + @ExcelProperty(value = "填报时间") + private Date fillTime; + + /** + * 填报人ID + */ + private Long fillBy; + + /** + * 填报人姓名 + */ + @ExcelProperty(value = "填报人姓名") + private String fillByName; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectPerformanceSelfEvaluationVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectPerformanceSelfEvaluationVo.java new file mode 100644 index 00000000..8930cfa6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectPerformanceSelfEvaluationVo.java @@ -0,0 +1,666 @@ +package org.dromara.cw.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.cw.domain.CwProjectPerformanceSelfEvaluation; +import org.dromara.cw.domain.vo.CwPerformanceMonitoringVo; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 项目绩效自评视图对象 cw_project_performance_self_evaluation + * + * @author ruoyi + * @date 2025-11-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwProjectPerformanceSelfEvaluation.class) +public class CwProjectPerformanceSelfEvaluationVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 项目申报ID + */ + @ExcelProperty(value = "项目申报ID") + private Long projectApplicationId; + + /** + * 申报编号 + */ + @ExcelProperty(value = "申报编号") + private String applicationNo; + + /** + * 项目名称 + */ + @ExcelProperty(value = "项目名称") + private String projectName; + + /** + * 主管部门 + */ + @ExcelProperty(value = "主管部门") + private String supervisoryDept; + + /** + * 实施单位 + */ + @ExcelProperty(value = "实施单位") + private String implementUnit; + + /** + * 自评年度 + */ + @ExcelProperty(value = "自评年度") + private Integer evaluationYear; + + /** + * 年初预算数(万元) + */ + @ExcelProperty(value = "年初预算数(万元)") + private BigDecimal initialBudget; + + /** + * 项目负责人 + */ + @ExcelProperty(value = "项目负责人") + private String principal; + + /** + * 联系电话 + */ + @ExcelProperty(value = "联系电话") + private String contactPhone; + + /** + * 项目实施开始时间 + */ + @ExcelProperty(value = "项目实施开始时间") + private Date startDate; + + /** + * 项目实施结束时间 + */ + @ExcelProperty(value = "项目实施结束时间") + private Date endDate; + + /** + * 项目总投资(万元) + */ + @ExcelProperty(value = "项目总投资(万元)") + private BigDecimal totalInvestment; + + /** + * 本年度预算(万元) + */ + @ExcelProperty(value = "本年度预算(万元)") + private BigDecimal annualBudget; + + /** + * 本年度实际支出(万元) + */ + @ExcelProperty(value = "本年度实际支出(万元)") + private BigDecimal actualExpenditure; + + /** + * 预算执行率(%) + */ + @ExcelProperty(value = "预算执行率(%)") + private BigDecimal budgetExecutionRate; + + /** + * 资金到位分值 + */ + private BigDecimal fundValue; + + /** + * 资金执行率(%) + */ + private BigDecimal fundExecutionRate; + + /** + * 资金到位得分 + */ + private BigDecimal fundScore; + + /** + * 当年财政拨款-年初预算数(万元) + */ + private BigDecimal currentYearBudgetInitial; + + /** + * 当年财政拨款-全年预算数(万元) + */ + private BigDecimal currentYearBudgetAnnual; + + /** + * 当年财政拨款-全年执行数(万元) + */ + private BigDecimal currentYearBudgetActual; + + /** + * 当年财政拨款-分值 + */ + private BigDecimal currentYearBudgetValue; + + /** + * 当年财政拨款-执行率(%) + */ + private BigDecimal currentYearBudgetExecutionRate; + + /** + * 当年财政拨款-得分 + */ + private BigDecimal currentYearBudgetScore; + + /** + * 上年结转资金-年初预算数(万元) + */ + private BigDecimal lastYearCarryoverInitial; + + /** + * 上年结转资金-全年预算数(万元) + */ + private BigDecimal lastYearCarryoverAnnual; + + /** + * 上年结转资金-全年执行数(万元) + */ + private BigDecimal lastYearCarryoverActual; + + /** + * 上年结转资金-分值 + */ + private BigDecimal lastYearCarryoverValue; + + /** + * 上年结转资金-执行率(%) + */ + private BigDecimal lastYearCarryoverExecutionRate; + + /** + * 上年结转资金-得分 + */ + private BigDecimal lastYearCarryoverScore; + + /** + * 其他资金-年初预算数(万元) + */ + private BigDecimal otherFundsInitial; + + /** + * 其他资金-全年预算数(万元) + */ + private BigDecimal otherFundsAnnual; + + /** + * 其他资金-全年执行数(万元) + */ + private BigDecimal otherFundsActual; + + /** + * 其他资金-分值 + */ + private BigDecimal otherFundsValue; + + /** + * 其他资金-执行率(%) + */ + private BigDecimal otherFundsExecutionRate; + + /** + * 其他资金-得分 + */ + private BigDecimal otherFundsScore; + + /** + * 年度绩效目标 + */ + @ExcelProperty(value = "年度绩效目标") + private String performanceGoal; + + /** + * 目标完成情况说明 + */ + @ExcelProperty(value = "目标完成情况说明") + private String targetCompletion; + + /** + * 数量指标名称 + */ + private String quantityIndicator; + + /** + * 数量指标-指标值 + */ + private String quantityTarget; + + /** + * 数量指标-实际完成值 + */ + private String quantityActual; + + /** + * 数量指标-分值 + */ + private BigDecimal quantityValue; + + /** + * 数量指标-得分 + */ + private BigDecimal quantityScore; + + /** + * 数量指标-偏差原因分析及改进措施 + */ + private String quantityDeviation; + + /** + * 质量指标名称 + */ + private String qualityIndicator; + + /** + * 质量指标-指标值 + */ + private String qualityTarget; + + /** + * 质量指标-实际完成值 + */ + private String qualityActual; + + /** + * 质量指标-分值 + */ + private BigDecimal qualityValue; + + /** + * 质量指标-得分 + */ + private BigDecimal qualityScore; + + /** + * 质量指标-偏差原因分析及改进措施 + */ + private String qualityDeviation; + + /** + * 时效指标名称 + */ + private String timelinessIndicator; + + /** + * 时效指标-指标值 + */ + private String timelinessTarget; + + /** + * 时效指标-实际完成值 + */ + private String timelinessActual; + + /** + * 时效指标-分值 + */ + private BigDecimal timelinessValue; + + /** + * 时效指标-得分 + */ + private BigDecimal timelinessScore; + + /** + * 时效指标-偏差原因分析及改进措施 + */ + private String timelinessDeviation; + + /** + * 成本指标名称 + */ + private String costIndicator; + + /** + * 成本指标-指标值 + */ + private String costTarget; + + /** + * 成本指标-实际完成值 + */ + private String costActual; + + /** + * 成本指标-分值 + */ + private BigDecimal costValue; + + /** + * 成本指标-得分 + */ + private BigDecimal costScore; + + /** + * 成本指标-偏差原因分析及改进措施 + */ + private String costDeviation; + + /** + * 经济效益指标名称 + */ + private String economicIndicator; + + /** + * 经济效益指标-指标值 + */ + private String economicTarget; + + /** + * 经济效益指标-实际完成值 + */ + private String economicActual; + + /** + * 经济效益指标-分值 + */ + private BigDecimal economicValue; + + /** + * 经济效益指标-得分 + */ + private BigDecimal economicScore; + + /** + * 经济效益指标-偏差原因分析及改进措施 + */ + private String economicDeviation; + + /** + * 社会效益指标名称 + */ + private String socialIndicator; + + /** + * 社会效益指标-指标值 + */ + private String socialTarget; + + /** + * 社会效益指标-实际完成值 + */ + private String socialActual; + + /** + * 社会效益指标-分值 + */ + private BigDecimal socialValue; + + /** + * 社会效益指标-得分 + */ + private BigDecimal socialScore; + + /** + * 社会效益指标-偏差原因分析及改进措施 + */ + private String socialDeviation; + + /** + * 生态效益指标名称 + */ + private String ecologicalIndicator; + + /** + * 生态效益指标-指标值 + */ + private String ecologicalTarget; + + /** + * 生态效益指标-实际完成值 + */ + private String ecologicalActual; + + /** + * 生态效益指标-分值 + */ + private BigDecimal ecologicalValue; + + /** + * 生态效益指标-得分 + */ + private BigDecimal ecologicalScore; + + /** + * 生态效益指标-偏差原因分析及改进措施 + */ + private String ecologicalDeviation; + + /** + * 满意度指标名称 + */ + private String satisfactionIndicator; + + /** + * 满意度指标-指标值 + */ + private String satisfactionTarget; + + /** + * 满意度指标-实际完成值 + */ + private String satisfactionActual; + + /** + * 满意度指标-分值 + */ + private BigDecimal satisfactionValue; + + /** + * 满意度指标-得分 + */ + private BigDecimal satisfactionScore; + + /** + * 满意度指标-偏差原因分析及改进措施 + */ + private String satisfactionDeviation; + + /** + * 项目决策得分 + */ + @ExcelProperty(value = "项目决策得分") + private BigDecimal decisionScore; + + /** + * 项目决策自评内容 + */ + @ExcelProperty(value = "项目决策自评内容") + private String decisionContent; + + /** + * 项目过程得分 + */ + @ExcelProperty(value = "项目过程得分") + private BigDecimal processScore; + + /** + * 项目过程自评内容 + */ + @ExcelProperty(value = "项目过程自评内容") + private String processContent; + + /** + * 项目产出得分 + */ + @ExcelProperty(value = "项目产出得分") + private BigDecimal outputScore; + + /** + * 项目产出自评内容 + */ + @ExcelProperty(value = "项目产出自评内容") + private String outputContent; + + /** + * 项目效益得分 + */ + @ExcelProperty(value = "项目效益得分") + private BigDecimal benefitScore; + + /** + * 项目效益自评内容 + */ + @ExcelProperty(value = "项目效益自评内容") + private String benefitContent; + + /** + * 自评总分 + */ + @ExcelProperty(value = "自评总分") + private BigDecimal totalScore; + + /** + * 自评等级(优秀/良好/合格/不合格) + */ + @ExcelProperty(value = "自评等级") + private String selfEvaluationLevel; + + /** + * 存在的主要问题 + */ + @ExcelProperty(value = "存在的主要问题") + private String existingProblems; + + /** + * 改进措施和建议 + */ + @ExcelProperty(value = "改进措施和建议") + private String improvementMeasures; + + /** + * 自评状态(draft-草稿 submitted-已提交 approved-已审核) + */ + @ExcelProperty(value = "自评状态") + private String evaluationStatus; + + /** + * 允许已提交后编辑(0-不允许 1-允许) + */ + @ExcelProperty(value = "允许已提交后编辑") + private Integer allowEditAfterSubmit; + + /** + * 提交时间 + */ + @ExcelProperty(value = "提交时间") + private Date submitTime; + + /** + * 提交人ID + */ + private Long submitBy; + + /** + * 提交人姓名 + */ + @ExcelProperty(value = "提交人姓名") + private String submitByName; + + /** + * 审核时间 + */ + @ExcelProperty(value = "审核时间") + private Date approveTime; + + /** + * 审核人ID + */ + private Long approveBy; + + /** + * 审核人姓名 + */ + @ExcelProperty(value = "审核人姓名") + private String approveByName; + + /** + * 审核意见 + */ + @ExcelProperty(value = "审核意见") + private String approveOpinion; + + /** + * 是否锁定(0-未锁定 1-已锁定),锁定后不可修改 + */ + @ExcelProperty(value = "锁定状态") + private Integer isLocked; + + /** + * 锁定时间 + */ + @ExcelProperty(value = "锁定时间") + private Date lockTime; + + /** + * 锁定人ID + */ + private Long lockBy; + + /** + * 锁定人姓名 + */ + @ExcelProperty(value = "锁定人") + private String lockByName; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建者 + */ + private Long createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新者 + */ + private Long updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "更新时间") + private Date updateTime; + + /** + * 运行监控数据列表(查看详情时返回) + */ + private List monitoringList; +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwPerformanceMonitoringMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwPerformanceMonitoringMapper.java new file mode 100644 index 00000000..991f3e2f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwPerformanceMonitoringMapper.java @@ -0,0 +1,16 @@ +package org.dromara.cw.mapper; + +import org.dromara.cw.domain.CwPerformanceMonitoring; +import org.dromara.cw.domain.vo.CwPerformanceMonitoringVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +/** + * 绩效运行监控Mapper接口 + * + * @author ruoyi + * @date 2025-11-04 + */ +public interface CwPerformanceMonitoringMapper extends BaseMapperPlus { + +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectEvaluationIndicatorMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectEvaluationIndicatorMapper.java new file mode 100644 index 00000000..285fa7c6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectEvaluationIndicatorMapper.java @@ -0,0 +1,38 @@ +package org.dromara.cw.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.cw.domain.CwProjectEvaluationIndicator; +import org.dromara.cw.domain.vo.CwProjectEvaluationIndicatorVo; + +import java.util.List; + +/** + * 项目绩效自评指标明细Mapper接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface CwProjectEvaluationIndicatorMapper extends BaseMapperPlus { + + /** + * 根据自评ID查询指标列表 + */ + List selectByEvaluationId(@Param("evaluationId") Long evaluationId); + + /** + * 根据自评ID和指标类别查询指标列表 + */ + List selectByEvaluationIdAndCategory(@Param("evaluationId") Long evaluationId, @Param("indicatorCategory") String indicatorCategory); + + /** + * 批量插入指标 + */ + int batchInsert(@Param("list") List list); + + /** + * 根据自评ID删除指标 + */ + int deleteByEvaluationId(@Param("evaluationId") Long evaluationId); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectMonthlyMonitoringMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectMonthlyMonitoringMapper.java new file mode 100644 index 00000000..0559356d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectMonthlyMonitoringMapper.java @@ -0,0 +1,54 @@ +package org.dromara.cw.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.cw.domain.CwProjectMonthlyMonitoring; +import org.dromara.cw.domain.vo.CwProjectMonthlyMonitoringVo; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 项目月度运行监控Mapper接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface CwProjectMonthlyMonitoringMapper extends BaseMapperPlus { + + /** + * 根据自评ID查询月度监控列表 + */ + List selectByEvaluationId(@Param("evaluationId") Long evaluationId); + + /** + * 根据自评ID和年度查询月度监控列表 + */ + List selectByEvaluationIdAndYear(@Param("evaluationId") Long evaluationId, @Param("monitoringYear") Integer monitoringYear); + + /** + * 根据自评ID、年度和月份查询月度监控 + */ + CwProjectMonthlyMonitoringVo selectByEvaluationIdAndMonth(@Param("evaluationId") Long evaluationId, @Param("monitoringYear") Integer monitoringYear, @Param("monitoringMonth") Integer monitoringMonth); + + /** + * 批量插入月度监控数据 + */ + int batchInsert(@Param("list") List list); + + /** + * 根据自评ID删除月度监控数据 + */ + int deleteByEvaluationId(@Param("evaluationId") Long evaluationId); + + /** + * 计算年度累计进度 + */ + BigDecimal calculateYearlyProgress(@Param("evaluationId") Long evaluationId, @Param("monitoringYear") Integer monitoringYear); + + /** + * 计算年度累计支出 + */ + BigDecimal calculateYearlyExpenditure(@Param("evaluationId") Long evaluationId, @Param("monitoringYear") Integer monitoringYear); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectPerformanceSelfEvaluationMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectPerformanceSelfEvaluationMapper.java new file mode 100644 index 00000000..454ae450 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectPerformanceSelfEvaluationMapper.java @@ -0,0 +1,55 @@ +package org.dromara.cw.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.cw.domain.CwProjectPerformanceSelfEvaluation; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 项目绩效自评Mapper接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface CwProjectPerformanceSelfEvaluationMapper extends BaseMapperPlus { + + /** + * 根据项目申报ID和年度查询自评 + */ + CwProjectPerformanceSelfEvaluationVo selectByProjectAndYear(@Param("projectApplicationId") Long projectApplicationId, @Param("evaluationYear") Integer evaluationYear); + + /** + * 查询我的自评列表 + */ + List selectMyEvaluations(@Param("userId") Long userId); + + /** + * 查询待审核的自评列表 + */ + List selectPendingApprovals(); + + /** + * 统计自评数量 + */ + Long countEvaluations(@Param("evaluationYear") Integer evaluationYear, @Param("evaluationStatus") String evaluationStatus); + + /** + * 计算平均分 + */ + BigDecimal calculateAverageScore(@Param("evaluationYear") Integer evaluationYear); + + /** + * 按部门统计自评情况 + */ + List> statisticsByDept(@Param("evaluationYear") Integer evaluationYear); + + /** + * 按等级统计自评数量 + */ + List> statisticsByLevel(@Param("evaluationYear") Integer evaluationYear); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwMonitoringManagementService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwMonitoringManagementService.java new file mode 100644 index 00000000..33373c3a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwMonitoringManagementService.java @@ -0,0 +1,56 @@ +package org.dromara.cw.service; + +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; + +import java.util.List; + +/** + * 运行监控管理Service接口 + * 财务管理员和学校领导使用,用于查看所有人提交的绩效自评,并进行锁定/解锁操作 + * + * @author ruoyi + * @date 2025-01-03 + */ +public interface ICwMonitoringManagementService { + + /** + * 查询绩效自评(运行监控管理) + * 财务管理员和学校领导可以看到所有人的数据 + */ + CwProjectPerformanceSelfEvaluationVo queryById(Long id); + + /** + * 查询绩效自评列表(运行监控管理) + * 财务管理员和学校领导可以看到所有人的数据 + */ + TableDataInfo queryPageList(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery); + + /** + * 查询绩效自评列表(运行监控管理) + */ + List queryList(CwProjectPerformanceSelfEvaluationBo bo); + + /** + * 锁定绩效自评(单个) + */ + Boolean lockEvaluation(Long id); + + /** + * 解锁绩效自评(单个) + */ + Boolean unlockEvaluation(Long id); + + /** + * 批量锁定绩效自评 + */ + Boolean lockBatch(List ids); + + /** + * 批量解锁绩效自评 + */ + Boolean unlockBatch(List ids); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwPerformanceMonitoringService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwPerformanceMonitoringService.java new file mode 100644 index 00000000..45a195f4 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwPerformanceMonitoringService.java @@ -0,0 +1,66 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.vo.CwPerformanceMonitoringVo; +import org.dromara.cw.domain.bo.CwPerformanceMonitoringBo; + +import java.util.Collection; +import java.util.List; + +/** + * 绩效运行监控Service接口 + * + * @author ruoyi + * @date 2025-11-04 + */ +public interface ICwPerformanceMonitoringService { + + /** + * 查询绩效运行监控 + * + * @param id 主键 + * @return 绩效运行监控 + */ + CwPerformanceMonitoringVo queryById(Long id); + + /** + * 根据自评ID查询运行监控列表 + * + * @param evaluationId 自评ID + * @return 运行监控列表 + */ + List queryByEvaluationId(Long evaluationId); + + /** + * 新增绩效运行监控 + * + * @param bo 绩效运行监控 + * @return 是否新增成功 + */ + Boolean insertByBo(CwPerformanceMonitoringBo bo); + + /** + * 修改绩效运行监控 + * + * @param bo 绩效运行监控 + * @return 是否修改成功 + */ + Boolean updateByBo(CwPerformanceMonitoringBo bo); + + /** + * 校验并批量删除绩效运行监控信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据自评ID删除所有运行监控数据 + * + * @param evaluationId 自评ID + * @return 是否删除成功 + */ + Boolean deleteByEvaluationId(Long evaluationId); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectEvaluationIndicatorService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectEvaluationIndicatorService.java new file mode 100644 index 00000000..ca430af5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectEvaluationIndicatorService.java @@ -0,0 +1,103 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.vo.CwProjectEvaluationIndicatorVo; +import org.dromara.cw.domain.bo.CwProjectEvaluationIndicatorBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 项目绩效自评指标明细Service接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface ICwProjectEvaluationIndicatorService { + + /** + * 查询项目绩效自评指标明细 + * + * @param id 主键 + * @return 项目绩效自评指标明细 + */ + CwProjectEvaluationIndicatorVo queryById(Long id); + + /** + * 查询项目绩效自评指标明细列表 + * + * @param bo 查询条件 + * @return 项目绩效自评指标明细列表 + */ + List queryList(CwProjectEvaluationIndicatorBo bo); + + /** + * 分页查询项目绩效自评指标明细列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 项目绩效自评指标明细分页列表 + */ + TableDataInfo queryPageList(CwProjectEvaluationIndicatorBo bo, PageQuery pageQuery); + + /** + * 根据自评ID查询指标明细列表 + * + * @param evaluationId 自评ID + * @return 指标明细列表 + */ + List queryByEvaluationId(Long evaluationId); + + /** + * 根据自评ID和指标类型查询指标明细列表 + * + * @param evaluationId 自评ID + * @param indicatorType 指标类型 + * @return 指标明细列表 + */ + List queryByEvaluationIdAndType(Long evaluationId, String indicatorType); + + /** + * 新增项目绩效自评指标明细 + * + * @param bo 项目绩效自评指标明细 + * @return 是否新增成功 + */ + Boolean insertByBo(CwProjectEvaluationIndicatorBo bo); + + /** + * 修改项目绩效自评指标明细 + * + * @param bo 项目绩效自评指标明细 + * @return 是否修改成功 + */ + Boolean updateByBo(CwProjectEvaluationIndicatorBo bo); + + /** + * 批量保存指标明细 + * + * @param evaluationId 自评ID + * @param boList 指标明细列表 + * @return 是否保存成功 + */ + Boolean batchSave(Long evaluationId, List boList); + + /** + * 校验并批量删除项目绩效自评指标明细信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据自评ID删除所有指标明细 + * + * @param evaluationId 自评ID + * @return 是否删除成功 + */ + Boolean deleteByEvaluationId(Long evaluationId); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectMonthlyMonitoringService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectMonthlyMonitoringService.java new file mode 100644 index 00000000..7d03da7b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectMonthlyMonitoringService.java @@ -0,0 +1,112 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.vo.CwProjectMonthlyMonitoringVo; +import org.dromara.cw.domain.bo.CwProjectMonthlyMonitoringBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 项目月度运行监控Service接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface ICwProjectMonthlyMonitoringService { + + /** + * 查询项目月度运行监控 + * + * @param id 主键 + * @return 项目月度运行监控 + */ + CwProjectMonthlyMonitoringVo queryById(Long id); + + /** + * 查询项目月度运行监控列表 + * + * @param bo 查询条件 + * @return 项目月度运行监控列表 + */ + List queryList(CwProjectMonthlyMonitoringBo bo); + + /** + * 分页查询项目月度运行监控列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 项目月度运行监控分页列表 + */ + TableDataInfo queryPageList(CwProjectMonthlyMonitoringBo bo, PageQuery pageQuery); + + /** + * 根据自评ID查询月度监控列表 + * + * @param evaluationId 自评ID + * @return 月度监控列表 + */ + List queryByEvaluationId(Long evaluationId); + + /** + * 查询某个自评的全年监控数据 + * + * @param evaluationId 自评ID + * @param year 年份 + * @return 月度数据Map,key为月份(1-12),value为监控数据 + */ + Map queryYearlyMonitoring(Long evaluationId, Integer year); + + /** + * 新增项目月度运行监控 + * + * @param bo 项目月度运行监控 + * @return 是否新增成功 + */ + Boolean insertByBo(CwProjectMonthlyMonitoringBo bo); + + /** + * 修改项目月度运行监控 + * + * @param bo 项目月度运行监控 + * @return 是否修改成功 + */ + Boolean updateByBo(CwProjectMonthlyMonitoringBo bo); + + /** + * 填报月度数据 + * + * @param bo 月度监控数据 + * @return 是否填报成功 + */ + Boolean fillMonthlyData(CwProjectMonthlyMonitoringBo bo); + + /** + * 批量保存月度监控数据 + * + * @param evaluationId 自评ID + * @param boList 月度监控数据列表 + * @return 是否保存成功 + */ + Boolean batchSave(Long evaluationId, List boList); + + /** + * 校验并批量删除项目月度运行监控信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据自评ID删除所有月度监控数据 + * + * @param evaluationId 自评ID + * @return 是否删除成功 + */ + Boolean deleteByEvaluationId(Long evaluationId); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectPerformanceSelfEvaluationService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectPerformanceSelfEvaluationService.java new file mode 100644 index 00000000..33d6e5a2 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectPerformanceSelfEvaluationService.java @@ -0,0 +1,106 @@ +package org.dromara.cw.service; + +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 项目绩效自评Service接口 + * + * @author ruoyi + * @date 2025-11-03 + */ +public interface ICwProjectPerformanceSelfEvaluationService { + + /** + * 查询项目绩效自评 + */ + CwProjectPerformanceSelfEvaluationVo queryById(Long id); + + /** + * 查询项目绩效自评列表 + */ + TableDataInfo queryPageList(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery); + + /** + * 查询项目绩效自评列表 + */ + List queryList(CwProjectPerformanceSelfEvaluationBo bo); + + /** + * 根据项目申报ID和年度查询自评 + */ + CwProjectPerformanceSelfEvaluationVo queryByProjectAndYear(Long projectApplicationId, Integer evaluationYear); + + /** + * 查询我的自评列表 + */ + List queryMyEvaluations(Long userId); + + /** + * 查询待审核的自评列表 + */ + List queryPendingApprovals(); + + /** + * 新增项目绩效自评 + */ + Boolean insertByBo(CwProjectPerformanceSelfEvaluationBo bo); + + /** + * 修改项目绩效自评 + */ + Boolean updateByBo(CwProjectPerformanceSelfEvaluationBo bo); + + /** + * 保存草稿 + */ + Boolean saveDraft(CwProjectPerformanceSelfEvaluationBo bo); + + /** + * 提交自评 + */ + Boolean submitEvaluation(Long id); + + /** + * 审核自评 + */ + Boolean approveEvaluation(Long id, String opinion, Boolean approved); + + /** + * 校验并批量删除项目绩效自评信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 统计自评数量 + */ + Long countEvaluations(Integer evaluationYear, String evaluationStatus); + + /** + * 计算平均分 + */ + BigDecimal calculateAverageScore(Integer evaluationYear); + + /** + * 按部门统计自评情况 + */ + List> statisticsByDept(Integer evaluationYear); + + /** + * 按等级统计自评数量 + */ + List> statisticsByLevel(Integer evaluationYear); + + /** + * 导出项目绩效自评 + */ + void export(CwProjectPerformanceSelfEvaluationBo bo); +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwMonitoringManagementServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwMonitoringManagementServiceImpl.java new file mode 100644 index 00000000..9b5531f9 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwMonitoringManagementServiceImpl.java @@ -0,0 +1,306 @@ +package org.dromara.cw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.cw.domain.CwProjectPerformanceSelfEvaluation; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; +import org.dromara.cw.mapper.CwProjectPerformanceSelfEvaluationMapper; +import org.dromara.cw.service.ICwMonitoringManagementService; +import org.dromara.cw.service.ICwPerformanceMonitoringService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; + +/** + * 运行监控管理Service业务层处理 + * 财务管理员和学校领导使用,用于查看所有人提交的绩效自评,并进行锁定/解锁操作 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class CwMonitoringManagementServiceImpl implements ICwMonitoringManagementService { + + private final CwProjectPerformanceSelfEvaluationMapper baseMapper; + private final ICwPerformanceMonitoringService performanceMonitoringService; + + /** + * 查询绩效自评(运行监控管理) + * 财务管理员和学校领导可以看到所有人的数据,不进行数据权限过滤 + */ + @Override + public CwProjectPerformanceSelfEvaluationVo queryById(Long id) { + CwProjectPerformanceSelfEvaluationVo vo = baseMapper.selectVoById(id); + if (vo != null) { + // 加载运行监控数据 + vo.setMonitoringList(performanceMonitoringService.queryByEvaluationId(id)); + } + return vo; + } + + /** + * 查询绩效自评列表(运行监控管理) + * 财务管理员和学校领导可以看到所有人的数据,不进行数据权限过滤 + */ + @Override + public TableDataInfo queryPageList(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询绩效自评列表(运行监控管理) + */ + @Override + public List queryList(CwProjectPerformanceSelfEvaluationBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + /** + * 构建查询条件 + * 注意:运行监控管理不进行数据权限过滤,财务管理员和学校领导可以看到所有人的数据 + */ + private LambdaQueryWrapper buildQueryWrapper(CwProjectPerformanceSelfEvaluationBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + // 🔧 修复:只有当projectApplicationId > 0时才作为查询条件 + lqw.eq(bo.getProjectApplicationId() != null && bo.getProjectApplicationId() > 0, + CwProjectPerformanceSelfEvaluation::getProjectApplicationId, bo.getProjectApplicationId()); + lqw.like(StringUtils.isNotBlank(bo.getApplicationNo()), CwProjectPerformanceSelfEvaluation::getApplicationNo, bo.getApplicationNo()); + lqw.like(StringUtils.isNotBlank(bo.getProjectName()), CwProjectPerformanceSelfEvaluation::getProjectName, bo.getProjectName()); + lqw.eq(bo.getEvaluationYear() != null, CwProjectPerformanceSelfEvaluation::getEvaluationYear, bo.getEvaluationYear()); + lqw.eq(StringUtils.isNotBlank(bo.getEvaluationStatus()), CwProjectPerformanceSelfEvaluation::getEvaluationStatus, bo.getEvaluationStatus()); + lqw.eq(StringUtils.isNotBlank(bo.getSelfEvaluationLevel()), CwProjectPerformanceSelfEvaluation::getSelfEvaluationLevel, bo.getSelfEvaluationLevel()); + lqw.eq(bo.getIsLocked() != null, CwProjectPerformanceSelfEvaluation::getIsLocked, bo.getIsLocked()); + + // 🔒 运行监控管理:不进行数据权限过滤,财务管理员和学校领导可以看到所有人的数据 + + lqw.orderByDesc(CwProjectPerformanceSelfEvaluation::getCreateTime); + return lqw; + } + + /** + * 锁定绩效自评(单个) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean lockEvaluation(Long id) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + log.error("无法获取登录用户信息,锁定失败"); + return false; + } + + Long userId = loginUser.getUserId(); + String userName = loginUser.getUsername(); + + // 检查记录是否存在 + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + log.error("绩效自评记录不存在,ID: {}", id); + return false; + } + + // 检查是否已经锁定 + if (evaluation.getIsLocked() != null && evaluation.getIsLocked() == 1) { + log.warn("绩效自评记录已锁定,ID: {}", id); + return false; + } + + // 执行锁定 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CwProjectPerformanceSelfEvaluation::getId, id) + .set(CwProjectPerformanceSelfEvaluation::getIsLocked, 1) + .set(CwProjectPerformanceSelfEvaluation::getLockTime, new Date()) + .set(CwProjectPerformanceSelfEvaluation::getLockBy, userId) + .set(CwProjectPerformanceSelfEvaluation::getLockByName, userName); + + int result = baseMapper.update(null, updateWrapper); + log.info("锁定绩效自评成功,ID: {}, 操作人: {}", id, userName); + return result > 0; + } catch (Exception e) { + log.error("锁定绩效自评异常,ID: {}", id, e); + throw new RuntimeException("锁定绩效自评失败: " + e.getMessage(), e); + } + } + + /** + * 解锁绩效自评(单个) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean unlockEvaluation(Long id) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + log.error("无法获取登录用户信息,解锁失败"); + return false; + } + + String userName = loginUser.getUsername(); + + // 检查记录是否存在 + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + log.error("绩效自评记录不存在,ID: {}", id); + return false; + } + + // 检查是否已经解锁 + if (evaluation.getIsLocked() == null || evaluation.getIsLocked() == 0) { + log.warn("绩效自评记录未锁定,ID: {}", id); + return false; + } + + // 执行解锁 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CwProjectPerformanceSelfEvaluation::getId, id) + .set(CwProjectPerformanceSelfEvaluation::getIsLocked, 0) + .set(CwProjectPerformanceSelfEvaluation::getLockTime, null) + .set(CwProjectPerformanceSelfEvaluation::getLockBy, null) + .set(CwProjectPerformanceSelfEvaluation::getLockByName, null); + + int result = baseMapper.update(null, updateWrapper); + log.info("解锁绩效自评成功,ID: {}, 操作人: {}", id, userName); + return result > 0; + } catch (Exception e) { + log.error("解锁绩效自评异常,ID: {}", id, e); + throw new RuntimeException("解锁绩效自评失败: " + e.getMessage(), e); + } + } + + /** + * 批量锁定绩效自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean lockBatch(List ids) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + log.error("无法获取登录用户信息,批量锁定失败"); + return false; + } + + Long userId = loginUser.getUserId(); + String userName = loginUser.getUsername(); + + // 批量锁定 + int successCount = 0; + for (Long id : ids) { + try { + // 检查记录是否存在 + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + log.warn("绩效自评记录不存在,跳过,ID: {}", id); + continue; + } + + // 检查是否已经锁定 + if (evaluation.getIsLocked() != null && evaluation.getIsLocked() == 1) { + log.warn("绩效自评记录已锁定,跳过,ID: {}", id); + continue; + } + + // 执行锁定 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CwProjectPerformanceSelfEvaluation::getId, id) + .set(CwProjectPerformanceSelfEvaluation::getIsLocked, 1) + .set(CwProjectPerformanceSelfEvaluation::getLockTime, new Date()) + .set(CwProjectPerformanceSelfEvaluation::getLockBy, userId) + .set(CwProjectPerformanceSelfEvaluation::getLockByName, userName); + + int result = baseMapper.update(null, updateWrapper); + if (result > 0) { + successCount++; + } + } catch (Exception e) { + log.error("锁定绩效自评失败,ID: {}", id, e); + } + } + + log.info("批量锁定绩效自评完成,总数: {}, 成功: {}, 操作人: {}", ids.size(), successCount, userName); + return successCount > 0; + } catch (Exception e) { + log.error("批量锁定绩效自评异常", e); + throw new RuntimeException("批量锁定绩效自评失败: " + e.getMessage(), e); + } + } + + /** + * 批量解锁绩效自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean unlockBatch(List ids) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + log.error("无法获取登录用户信息,批量解锁失败"); + return false; + } + + String userName = loginUser.getUsername(); + + // 批量解锁 + int successCount = 0; + for (Long id : ids) { + try { + // 检查记录是否存在 + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + log.warn("绩效自评记录不存在,跳过,ID: {}", id); + continue; + } + + // 检查是否已经解锁 + if (evaluation.getIsLocked() == null || evaluation.getIsLocked() == 0) { + log.warn("绩效自评记录未锁定,跳过,ID: {}", id); + continue; + } + + // 执行解锁 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CwProjectPerformanceSelfEvaluation::getId, id) + .set(CwProjectPerformanceSelfEvaluation::getIsLocked, 0) + .set(CwProjectPerformanceSelfEvaluation::getLockTime, null) + .set(CwProjectPerformanceSelfEvaluation::getLockBy, null) + .set(CwProjectPerformanceSelfEvaluation::getLockByName, null); + + int result = baseMapper.update(null, updateWrapper); + if (result > 0) { + successCount++; + } + } catch (Exception e) { + log.error("解锁绩效自评失败,ID: {}", id, e); + } + } + + log.info("批量解锁绩效自评完成,总数: {}, 成功: {}, 操作人: {}", ids.size(), successCount, userName); + return successCount > 0; + } catch (Exception e) { + log.error("批量解锁绩效自评异常", e); + throw new RuntimeException("批量解锁绩效自评失败: " + e.getMessage(), e); + } + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwPerformanceMonitoringServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwPerformanceMonitoringServiceImpl.java new file mode 100644 index 00000000..a1aca5f8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwPerformanceMonitoringServiceImpl.java @@ -0,0 +1,145 @@ +package org.dromara.cw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.cw.domain.CwPerformanceMonitoring; +import org.dromara.cw.domain.bo.CwPerformanceMonitoringBo; +import org.dromara.cw.domain.vo.CwPerformanceMonitoringVo; +import org.dromara.cw.mapper.CwPerformanceMonitoringMapper; +import org.dromara.cw.service.ICwPerformanceMonitoringService; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +/** + * 绩效运行监控Service业务层处理 + * + * @author ruoyi + * @date 2025-11-04 + */ +@RequiredArgsConstructor +@Service +public class CwPerformanceMonitoringServiceImpl implements ICwPerformanceMonitoringService { + + private final CwPerformanceMonitoringMapper baseMapper; + + /** + * 查询绩效运行监控 + */ + @Override + public CwPerformanceMonitoringVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 根据自评ID查询运行监控列表 + */ + @Override + public List queryByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwPerformanceMonitoring::getEvaluationId, evaluationId); + lqw.orderByDesc(CwPerformanceMonitoring::getCreateTime); + return baseMapper.selectVoList(lqw); + } + + /** + * 新增绩效运行监控 + */ + @Override + public Boolean insertByBo(CwPerformanceMonitoringBo bo) { + CwPerformanceMonitoring add = MapstructUtils.convert(bo, CwPerformanceMonitoring.class); + validEntityBeforeSave(add); + + // 设置填报人信息 + Long userId = getUserIdSafely(); + String username = LoginHelper.getUsername(); + String nickname = LoginHelper.getNickname(); + add.setFillBy(userId); + // 优先使用昵称,如果昵称为空则使用用户名 + add.setFillByName(StringUtils.isNotBlank(nickname) ? nickname : username); + + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 安全获取用户ID,处理可能的字符串格式(如 "sys_user:1") + */ + private Long getUserIdSafely() { + try { + Long userId = LoginHelper.getUserId(); + if (userId != null) { + return userId; + } + } catch (Exception e) { + // 如果getUserId()失败,尝试从getUserIdStr()解析 + } + + // 尝试从字符串格式解析 + try { + String userIdStr = LoginHelper.getUserIdStr(); + if (StringUtils.isNotBlank(userIdStr)) { + // 处理 "sys_user:1" 这种格式,提取数字部分 + if (userIdStr.contains(":")) { + String[] parts = userIdStr.split(":"); + if (parts.length > 1) { + return Long.parseLong(parts[1]); + } + } else { + return Long.parseLong(userIdStr); + } + } + } catch (Exception e) { + // 解析失败,返回null或抛出异常 + } + + return null; + } + + /** + * 修改绩效运行监控 + */ + @Override + public Boolean updateByBo(CwPerformanceMonitoringBo bo) { + CwPerformanceMonitoring update = MapstructUtils.convert(bo, CwPerformanceMonitoring.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwPerformanceMonitoring entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除绩效运行监控 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteByIds(ids) > 0; + } + + /** + * 根据自评ID删除所有运行监控数据 + */ + @Override + public Boolean deleteByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwPerformanceMonitoring::getEvaluationId, evaluationId); + return baseMapper.delete(lqw) > 0; + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectEvaluationIndicatorServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectEvaluationIndicatorServiceImpl.java new file mode 100644 index 00000000..2f52f40c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectEvaluationIndicatorServiceImpl.java @@ -0,0 +1,203 @@ +package org.dromara.cw.service.impl; + +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.cw.domain.bo.CwProjectEvaluationIndicatorBo; +import org.dromara.cw.domain.vo.CwProjectEvaluationIndicatorVo; +import org.dromara.cw.domain.CwProjectEvaluationIndicator; +import org.dromara.cw.mapper.CwProjectEvaluationIndicatorMapper; +import org.dromara.cw.service.ICwProjectEvaluationIndicatorService; + +import java.util.List; +import java.util.Collection; + +/** + * 项目绩效自评指标明细Service业务层处理 + * + * @author ruoyi + * @date 2025-11-03 + */ +@RequiredArgsConstructor +@Service +public class CwProjectEvaluationIndicatorServiceImpl implements ICwProjectEvaluationIndicatorService { + + private final CwProjectEvaluationIndicatorMapper baseMapper; + + /** + * 查询项目绩效自评指标明细 + * + * @param id 主键 + * @return 项目绩效自评指标明细 + */ + @Override + public CwProjectEvaluationIndicatorVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询项目绩效自评指标明细列表 + * + * @param bo 查询条件 + * @return 项目绩效自评指标明细列表 + */ + @Override + public List queryList(CwProjectEvaluationIndicatorBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + /** + * 分页查询项目绩效自评指标明细列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 项目绩效自评指标明细分页列表 + */ + @Override + public TableDataInfo queryPageList(CwProjectEvaluationIndicatorBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 根据自评ID查询指标明细列表 + * + * @param evaluationId 自评ID + * @return 指标明细列表 + */ + @Override + public List queryByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectEvaluationIndicator::getEvaluationId, evaluationId); + lqw.orderByAsc(CwProjectEvaluationIndicator::getIndicatorType); + lqw.orderByAsc(CwProjectEvaluationIndicator::getSortOrder); + return baseMapper.selectVoList(lqw); + } + + /** + * 根据自评ID和指标类型查询指标明细列表 + * + * @param evaluationId 自评ID + * @param indicatorType 指标类型 + * @return 指标明细列表 + */ + @Override + public List queryByEvaluationIdAndType(Long evaluationId, String indicatorType) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectEvaluationIndicator::getEvaluationId, evaluationId); + lqw.eq(StringUtils.isNotBlank(indicatorType), CwProjectEvaluationIndicator::getIndicatorType, indicatorType); + lqw.orderByAsc(CwProjectEvaluationIndicator::getSortOrder); + return baseMapper.selectVoList(lqw); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(CwProjectEvaluationIndicatorBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getEvaluationId() != null, CwProjectEvaluationIndicator::getEvaluationId, bo.getEvaluationId()); + lqw.eq(StringUtils.isNotBlank(bo.getIndicatorType()), CwProjectEvaluationIndicator::getIndicatorType, bo.getIndicatorType()); + lqw.like(StringUtils.isNotBlank(bo.getIndicatorName()), CwProjectEvaluationIndicator::getIndicatorName, bo.getIndicatorName()); + lqw.orderByAsc(CwProjectEvaluationIndicator::getIndicatorType); + lqw.orderByAsc(CwProjectEvaluationIndicator::getSortOrder); + return lqw; + } + + /** + * 新增项目绩效自评指标明细 + * + * @param bo 项目绩效自评指标明细 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(CwProjectEvaluationIndicatorBo bo) { + CwProjectEvaluationIndicator add = MapstructUtils.convert(bo, CwProjectEvaluationIndicator.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改项目绩效自评指标明细 + * + * @param bo 项目绩效自评指标明细 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(CwProjectEvaluationIndicatorBo bo) { + CwProjectEvaluationIndicator update = MapstructUtils.convert(bo, CwProjectEvaluationIndicator.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 批量保存指标明细 + * + * @param evaluationId 自评ID + * @param boList 指标明细列表 + * @return 是否保存成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean batchSave(Long evaluationId, List boList) { + // 先删除该自评下的所有指标 + deleteByEvaluationId(evaluationId); + + // 批量插入新的指标 + if (boList != null && !boList.isEmpty()) { + for (CwProjectEvaluationIndicatorBo bo : boList) { + bo.setEvaluationId(evaluationId); + insertByBo(bo); + } + } + return true; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwProjectEvaluationIndicator entity) { + // 可以在此添加业务校验逻辑 + } + + /** + * 批量删除项目绩效自评指标明细 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 可以在此添加删除前的校验逻辑 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据自评ID删除所有指标明细 + * + * @param evaluationId 自评ID + * @return 是否删除成功 + */ + @Override + public Boolean deleteByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectEvaluationIndicator::getEvaluationId, evaluationId); + return baseMapper.delete(lqw) >= 0; + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectMonthlyMonitoringServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectMonthlyMonitoringServiceImpl.java new file mode 100644 index 00000000..abdb7e7c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectMonthlyMonitoringServiceImpl.java @@ -0,0 +1,319 @@ +package org.dromara.cw.service.impl; + +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.cw.domain.bo.CwProjectMonthlyMonitoringBo; +import org.dromara.cw.domain.vo.CwProjectMonthlyMonitoringVo; +import org.dromara.cw.domain.CwProjectMonthlyMonitoring; +import org.dromara.cw.mapper.CwProjectMonthlyMonitoringMapper; +import org.dromara.cw.service.ICwProjectMonthlyMonitoringService; +import org.dromara.common.satoken.utils.LoginHelper; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 项目月度运行监控Service业务层处理 + * + * @author ruoyi + * @date 2025-11-03 + */ +@RequiredArgsConstructor +@Service +public class CwProjectMonthlyMonitoringServiceImpl implements ICwProjectMonthlyMonitoringService { + + private final CwProjectMonthlyMonitoringMapper baseMapper; + + /** + * 查询项目月度运行监控 + * + * @param id 主键 + * @return 项目月度运行监控 + */ + @Override + public CwProjectMonthlyMonitoringVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询项目月度运行监控列表 + * + * @param bo 查询条件 + * @return 项目月度运行监控列表 + */ + @Override + public List queryList(CwProjectMonthlyMonitoringBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + /** + * 分页查询项目月度运行监控列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 项目月度运行监控分页列表 + */ + @Override + public TableDataInfo queryPageList(CwProjectMonthlyMonitoringBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 根据自评ID查询月度监控列表 + * + * @param evaluationId 自评ID + * @return 月度监控列表 + */ + @Override + public List queryByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectMonthlyMonitoring::getEvaluationId, evaluationId); + lqw.orderByAsc(CwProjectMonthlyMonitoring::getMonitoringYear); + lqw.orderByAsc(CwProjectMonthlyMonitoring::getMonitoringMonth); + return baseMapper.selectVoList(lqw); + } + + /** + * 查询某个自评的全年监控数据 + * + * @param evaluationId 自评ID + * @param year 年份 + * @return 月度数据Map,key为月份(1-12),value为监控数据 + */ + @Override + public Map queryYearlyMonitoring(Long evaluationId, Integer year) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectMonthlyMonitoring::getEvaluationId, evaluationId); + lqw.eq(CwProjectMonthlyMonitoring::getMonitoringYear, year); + lqw.orderByAsc(CwProjectMonthlyMonitoring::getMonitoringMonth); + + List list = baseMapper.selectVoList(lqw); + return list.stream().collect( + Collectors.toMap(CwProjectMonthlyMonitoringVo::getMonitoringMonth, v -> v) + ); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(CwProjectMonthlyMonitoringBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getEvaluationId() != null, CwProjectMonthlyMonitoring::getEvaluationId, bo.getEvaluationId()); + lqw.eq(bo.getProjectApplicationId() != null, CwProjectMonthlyMonitoring::getProjectApplicationId, bo.getProjectApplicationId()); + lqw.eq(bo.getMonitoringYear() != null, CwProjectMonthlyMonitoring::getMonitoringYear, bo.getMonitoringYear()); + lqw.eq(bo.getMonitoringMonth() != null, CwProjectMonthlyMonitoring::getMonitoringMonth, bo.getMonitoringMonth()); + lqw.eq(StringUtils.isNotBlank(bo.getFillStatus()), CwProjectMonthlyMonitoring::getFillStatus, bo.getFillStatus()); + lqw.orderByDesc(CwProjectMonthlyMonitoring::getMonitoringYear); + lqw.orderByAsc(CwProjectMonthlyMonitoring::getMonitoringMonth); + return lqw; + } + + /** + * 新增项目月度运行监控 + * + * @param bo 项目月度运行监控 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(CwProjectMonthlyMonitoringBo bo) { + CwProjectMonthlyMonitoring add = MapstructUtils.convert(bo, CwProjectMonthlyMonitoring.class); + validEntityBeforeSave(add); + // 自动计算 + calculateFields(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改项目月度运行监控 + * + * @param bo 项目月度运行监控 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(CwProjectMonthlyMonitoringBo bo) { + CwProjectMonthlyMonitoring update = MapstructUtils.convert(bo, CwProjectMonthlyMonitoring.class); + validEntityBeforeSave(update); + // 自动计算 + calculateFields(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 填报月度数据 + * + * @param bo 月度监控数据 + * @return 是否填报成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean fillMonthlyData(CwProjectMonthlyMonitoringBo bo) { + // 检查是否已存在该月数据 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectMonthlyMonitoring::getEvaluationId, bo.getEvaluationId()); + lqw.eq(CwProjectMonthlyMonitoring::getMonitoringYear, bo.getMonitoringYear()); + lqw.eq(CwProjectMonthlyMonitoring::getMonitoringMonth, bo.getMonitoringMonth()); + + CwProjectMonthlyMonitoring existing = baseMapper.selectOne(lqw); + + if (existing != null) { + // 如果已存在,则更新 + bo.setId(existing.getId()); + return updateByBo(bo); + } else { + // 如果不存在,则新增 + bo.setFillStatus("filled"); + bo.setFillTime(new Date()); + bo.setFillBy(getUserIdSafely()); + return insertByBo(bo); + } + } + + /** + * 批量保存月度监控数据 + * + * @param evaluationId 自评ID + * @param boList 月度监控数据列表 + * @return 是否保存成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean batchSave(Long evaluationId, List boList) { + if (boList == null || boList.isEmpty()) { + return true; + } + + for (CwProjectMonthlyMonitoringBo bo : boList) { + bo.setEvaluationId(evaluationId); + fillMonthlyData(bo); + } + return true; + } + + /** + * 安全获取用户ID,处理可能的字符串格式(如 "sys_user:1") + */ + private Long getUserIdSafely() { + try { + Long userId = LoginHelper.getUserId(); + if (userId != null) { + return userId; + } + } catch (Exception e) { + // 如果getUserId()失败,尝试从getUserIdStr()解析 + } + + // 尝试从字符串格式解析 + try { + String userIdStr = LoginHelper.getUserIdStr(); + if (StringUtils.isNotBlank(userIdStr)) { + // 处理 "sys_user:1" 这种格式,提取数字部分 + if (userIdStr.contains(":")) { + String[] parts = userIdStr.split(":"); + if (parts.length > 1) { + return Long.parseLong(parts[1]); + } + } else { + return Long.parseLong(userIdStr); + } + } + } catch (Exception e) { + // 解析失败,返回null或抛出异常 + } + + return null; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwProjectMonthlyMonitoring entity) { + // 校验月份范围 + if (entity.getMonitoringMonth() != null) { + if (entity.getMonitoringMonth() < 1 || entity.getMonitoringMonth() > 12) { + throw new RuntimeException("月份必须在1-12之间"); + } + } + + // 校验进度范围 + if (entity.getPlannedProgress() != null) { + if (entity.getPlannedProgress().compareTo(BigDecimal.ZERO) < 0 + || entity.getPlannedProgress().compareTo(new BigDecimal("100")) > 0) { + throw new RuntimeException("计划进度必须在0-100之间"); + } + } + if (entity.getActualProgress() != null) { + if (entity.getActualProgress().compareTo(BigDecimal.ZERO) < 0 + || entity.getActualProgress().compareTo(new BigDecimal("100")) > 0) { + throw new RuntimeException("实际进度必须在0-100之间"); + } + } + } + + /** + * 自动计算字段 + */ + private void calculateFields(CwProjectMonthlyMonitoring entity) { + // 计算进度偏差 = 实际进度 - 计划进度 + if (entity.getPlannedProgress() != null && entity.getActualProgress() != null) { + BigDecimal variance = entity.getActualProgress().subtract(entity.getPlannedProgress()); + entity.setProgressVariance(variance); + } + + // 计算资金执行率 = (累计实际支出 / 累计预算) × 100 + if (entity.getCumulativeBudget() != null && entity.getCumulativeExpenditure() != null + && entity.getCumulativeBudget().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rate = entity.getCumulativeExpenditure() + .divide(entity.getCumulativeBudget(), 4, BigDecimal.ROUND_HALF_UP) + .multiply(new BigDecimal("100")); + entity.setFundExecutionRate(rate); + } + } + + /** + * 批量删除项目月度运行监控 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 可以在此添加删除前的校验逻辑 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据自评ID删除所有月度监控数据 + * + * @param evaluationId 自评ID + * @return 是否删除成功 + */ + @Override + public Boolean deleteByEvaluationId(Long evaluationId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(CwProjectMonthlyMonitoring::getEvaluationId, evaluationId); + return baseMapper.delete(lqw) >= 0; + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectPerformanceSelfEvaluationServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectPerformanceSelfEvaluationServiceImpl.java new file mode 100644 index 00000000..b7d33aa8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectPerformanceSelfEvaluationServiceImpl.java @@ -0,0 +1,581 @@ +package org.dromara.cw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.cw.domain.CwProjectPerformanceSelfEvaluation; +import org.dromara.cw.domain.bo.CwProjectPerformanceSelfEvaluationBo; +import org.dromara.cw.domain.vo.CwProjectPerformanceSelfEvaluationVo; +import org.dromara.cw.mapper.CwProjectPerformanceSelfEvaluationMapper; +import org.dromara.cw.service.ICwProjectPerformanceSelfEvaluationService; +import org.dromara.cw.service.ICwPerformanceMonitoringService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 项目绩效自评Service业务层处理 + * + * @author ruoyi + * @date 2025-11-03 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class CwProjectPerformanceSelfEvaluationServiceImpl implements ICwProjectPerformanceSelfEvaluationService { + + private final CwProjectPerformanceSelfEvaluationMapper baseMapper; + private final ICwPerformanceMonitoringService performanceMonitoringService; + + /** + * 查询项目绩效自评 + */ + @Override + public CwProjectPerformanceSelfEvaluationVo queryById(Long id) { + CwProjectPerformanceSelfEvaluationVo vo = baseMapper.selectVoById(id); + if (vo != null) { + // 🔒 数据权限验证:普通用户只能查看自己创建的数据 + if (!hasDataPermission(vo)) { + log.warn("用户尝试访问无权限的绩效自评数据,ID: {}", id); + throw new RuntimeException("无权访问该绩效自评数据"); + } + + // 查询并设置运行监控数据 + vo.setMonitoringList(performanceMonitoringService.queryByEvaluationId(id)); + } + return vo; + } + + /** + * 验证用户是否有查看指定绩效自评的权限 + * 只有超级管理员可以看到所有数据,其他用户(包括财务管理员)只能查看自己创建的数据 + * + * @param vo 绩效自评数据 + * @return 是否有权限 + */ + private boolean hasDataPermission(CwProjectPerformanceSelfEvaluationVo vo) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + log.warn("无法获取登录用户信息,拒绝数据访问"); + return false; + } + + Long currentUserId = loginUser.getUserId(); + + // 超级管理员拥有所有权限 + if (LoginHelper.isSuperAdmin(currentUserId)) { + return true; + } + + // 所有其他用户(包括财务管理员):只能查看自己创建的数据 + return currentUserId.equals(vo.getCreateBy()); + + } catch (Exception e) { + log.error("数据权限验证异常: {}", e.getMessage(), e); + return false; + } + } + + /** + * 查询项目绩效自评列表 + */ + @Override + public TableDataInfo queryPageList(CwProjectPerformanceSelfEvaluationBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询项目绩效自评列表 + */ + @Override + public List queryList(CwProjectPerformanceSelfEvaluationBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(CwProjectPerformanceSelfEvaluationBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + // 🔧 修复:只有当projectApplicationId > 0时才作为查询条件 + lqw.eq(bo.getProjectApplicationId() != null && bo.getProjectApplicationId() > 0, + CwProjectPerformanceSelfEvaluation::getProjectApplicationId, bo.getProjectApplicationId()); + lqw.like(StringUtils.isNotBlank(bo.getApplicationNo()), CwProjectPerformanceSelfEvaluation::getApplicationNo, bo.getApplicationNo()); + lqw.like(StringUtils.isNotBlank(bo.getProjectName()), CwProjectPerformanceSelfEvaluation::getProjectName, bo.getProjectName()); + lqw.eq(bo.getEvaluationYear() != null, CwProjectPerformanceSelfEvaluation::getEvaluationYear, bo.getEvaluationYear()); + lqw.eq(StringUtils.isNotBlank(bo.getEvaluationStatus()), CwProjectPerformanceSelfEvaluation::getEvaluationStatus, bo.getEvaluationStatus()); + lqw.eq(StringUtils.isNotBlank(bo.getSelfEvaluationLevel()), CwProjectPerformanceSelfEvaluation::getSelfEvaluationLevel, bo.getSelfEvaluationLevel()); + + // 🔒 数据权限过滤:普通用户只能看到自己提交的数据 + addDataPermissionFilter(lqw); + + lqw.orderByDesc(CwProjectPerformanceSelfEvaluation::getCreateTime); + return lqw; + } + + /** + * 添加数据权限过滤 + * 只有超级管理员可以看到所有数据,其他用户(包括财务管理员)只能看到自己创建的数据 + */ + private void addDataPermissionFilter(LambdaQueryWrapper lqw) { + try { + // 获取当前登录用户信息 + var loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + // 如果无法获取用户信息,默认只能看到自己的数据(通过设置一个不存在的用户ID) + lqw.eq(CwProjectPerformanceSelfEvaluation::getCreateBy, -1L); + log.warn("无法获取登录用户信息,应用最严格的数据权限过滤"); + return; + } + + Long userId = loginUser.getUserId(); + + // 超级管理员不过滤数据 + if (LoginHelper.isSuperAdmin(userId)) { + log.info("✅ [超级管理员权限] 用户ID: {}, 可以查看所有绩效自评数据", userId); + return; + } + + // 所有其他用户(包括财务管理员):只能看到自己创建的数据 + lqw.eq(CwProjectPerformanceSelfEvaluation::getCreateBy, userId); + log.info("🔒 [个人权限过滤] 用户ID: {}, 只能查看自己提交的绩效自评数据", userId); + + } catch (Exception e) { + log.error("❌ 数据权限过滤异常: {}", e.getMessage(), e); + // 异常情况下,应用最严格的数据权限过滤 + try { + Long userId = LoginHelper.getUserId(); + if (userId != null) { + lqw.eq(CwProjectPerformanceSelfEvaluation::getCreateBy, userId); + } else { + // 完全无法获取用户ID时,返回空结果 + lqw.eq(CwProjectPerformanceSelfEvaluation::getCreateBy, -1L); + } + } catch (Exception ex) { + log.error("❌ 获取当前用户ID失败: {}", ex.getMessage(), ex); + // 完全失败时返回空结果 + lqw.eq(CwProjectPerformanceSelfEvaluation::getCreateBy, -1L); + } + } + } + + /** + * 根据项目申报ID和年度查询自评 + */ + @Override + public CwProjectPerformanceSelfEvaluationVo queryByProjectAndYear(Long projectApplicationId, Integer evaluationYear) { + return baseMapper.selectByProjectAndYear(projectApplicationId, evaluationYear); + } + + /** + * 查询我的自评列表 + */ + @Override + public List queryMyEvaluations(Long userId) { + return baseMapper.selectMyEvaluations(userId); + } + + /** + * 查询待审核的自评列表 + */ + @Override + public List queryPendingApprovals() { + return baseMapper.selectPendingApprovals(); + } + + /** + * 新增项目绩效自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(CwProjectPerformanceSelfEvaluationBo bo) { + // 🔍 调试:打印接收到的Bo对象 + log.info("========== 新增绩效自评 - 接收到的Bo对象 =========="); + log.info("项目名称: {}", bo.getProjectName()); + log.info("主管部门: {}", bo.getSupervisoryDept()); + log.info("实施单位: {}", bo.getImplementUnit()); + log.info("绩效目标: {}", bo.getPerformanceGoal()); + log.info("目标完成情况: {}", bo.getTargetCompletion()); + log.info("数量指标: {}", bo.getQuantityIndicator()); + log.info("质量指标: {}", bo.getQualityIndicator()); + log.info("时效指标: {}", bo.getTimelinessIndicator()); + log.info("成本指标: {}", bo.getCostIndicator()); + log.info("经济指标: {}", bo.getEconomicIndicator()); + log.info("社会指标: {}", bo.getSocialIndicator()); + log.info("生态指标: {}", bo.getEcologicalIndicator()); + log.info("满意度指标: {}", bo.getSatisfactionIndicator()); + log.info("总分: {}", bo.getTotalScore()); + log.info("======================================================"); + + CwProjectPerformanceSelfEvaluation add = MapstructUtils.convert(bo, CwProjectPerformanceSelfEvaluation.class); + + // 🔍 调试:打印转换后的Entity对象 + log.info("========== 转换后的Entity对象 =========="); + log.info("主管部门: {}", add.getSupervisoryDept()); + log.info("实施单位: {}", add.getImplementUnit()); + log.info("数量指标: {}", add.getQuantityIndicator()); + log.info("质量指标: {}", add.getQualityIndicator()); + log.info("========================================="); + + validEntityBeforeSave(add); + + // 如果申报编号为空,设置默认值 + if (StringUtils.isBlank(add.getApplicationNo())) { + // 如果有项目申报ID且不为0,使用项目申报ID + if (add.getProjectApplicationId() != null && add.getProjectApplicationId() > 0) { + add.setApplicationNo("APP-" + add.getProjectApplicationId()); + } else { + // 否则使用时间戳生成唯一编号 + add.setApplicationNo("TEMP-" + System.currentTimeMillis()); + } + } + + // 设置默认状态为草稿 + if (StringUtils.isBlank(add.getEvaluationStatus())) { + add.setEvaluationStatus("draft"); + } + + // 计算预算执行率 + if (add.getAnnualBudget() != null && add.getActualExpenditure() != null + && add.getAnnualBudget().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rate = add.getActualExpenditure() + .divide(add.getAnnualBudget(), 4, BigDecimal.ROUND_HALF_UP) + .multiply(new BigDecimal("100")); + add.setBudgetExecutionRate(rate); + } + + // 🔧 新增:保存时自动计算总分和等级 + calculateTotalScoreAndLevel(add); + + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改项目绩效自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(CwProjectPerformanceSelfEvaluationBo bo) { + // 先查询原始记录,检查状态 + CwProjectPerformanceSelfEvaluation existing = baseMapper.selectById(bo.getId()); + if (existing == null) { + throw new RuntimeException("自评记录不存在"); + } + + // 🔒 检查是否已锁定:锁定状态的绩效自评不允许修改 + if (existing.getIsLocked() != null && existing.getIsLocked() == 1) { + throw new RuntimeException("该绩效自评已锁定,不允许修改。如需修改,请联系财务管理员或学校领导解锁"); + } + + // 🔧 已移除编辑限制:允许所有状态的自评都可以编辑 + // 原限制逻辑:检查是否允许修改:草稿状态 OR (已提交状态 AND 允许已提交后编辑) + // if (!"draft".equals(existing.getEvaluationStatus())) { + // // 如果不是草稿状态,检查是否允许已提交后编辑 + // if (existing.getAllowEditAfterSubmit() == null || existing.getAllowEditAfterSubmit() == 0) { + // throw new RuntimeException("该自评已提交,不允许修改。如需修改,请先设置允许已提交后编辑"); + // } + // } + + CwProjectPerformanceSelfEvaluation update = MapstructUtils.convert(bo, CwProjectPerformanceSelfEvaluation.class); + validEntityBeforeSave(update); + + // 计算预算执行率 + if (update.getAnnualBudget() != null && update.getActualExpenditure() != null + && update.getAnnualBudget().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal rate = update.getActualExpenditure() + .divide(update.getAnnualBudget(), 4, BigDecimal.ROUND_HALF_UP) + .multiply(new BigDecimal("100")); + update.setBudgetExecutionRate(rate); + } + + // 🔧 新增:更新时自动计算总分和等级 + calculateTotalScoreAndLevel(update); + + return baseMapper.updateById(update) > 0; + } + + /** + * 保存草稿 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean saveDraft(CwProjectPerformanceSelfEvaluationBo bo) { + bo.setEvaluationStatus("draft"); + if (bo.getId() == null) { + return insertByBo(bo); + } else { + return updateByBo(bo); + } + } + + /** + * 提交自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean submitEvaluation(Long id) { + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + throw new RuntimeException("自评记录不存在"); + } + + // 🔒 检查是否已锁定:锁定状态的绩效自评不允许提交 + if (evaluation.getIsLocked() != null && evaluation.getIsLocked() == 1) { + throw new RuntimeException("该绩效自评已锁定,不允许提交。如需提交,请联系财务管理员或学校领导解锁"); + } + + if (!"draft".equals(evaluation.getEvaluationStatus())) { + throw new RuntimeException("只能提交草稿状态的自评"); + } + + // 验证必填字段 + validateForSubmit(evaluation); + + // 🔧 修复:使用统一的计算方法(支持新的9个得分字段) + calculateTotalScoreAndLevel(evaluation); + + // 更新状态 + evaluation.setEvaluationStatus("submitted"); + evaluation.setSubmitTime(new Date()); + evaluation.setSubmitBy(getUserIdSafely()); + evaluation.setSubmitByName(LoginHelper.getUsername()); + + return baseMapper.updateById(evaluation) > 0; + } + + /** + * 审核自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean approveEvaluation(Long id, String opinion, Boolean approved) { + CwProjectPerformanceSelfEvaluation evaluation = baseMapper.selectById(id); + if (evaluation == null) { + throw new RuntimeException("自评记录不存在"); + } + + // 🔒 检查是否已锁定:锁定状态的绩效自评不允许审核 + if (evaluation.getIsLocked() != null && evaluation.getIsLocked() == 1) { + throw new RuntimeException("该绩效自评已锁定,不允许审核。如需审核,请联系财务管理员或学校领导解锁"); + } + + if (!"submitted".equals(evaluation.getEvaluationStatus())) { + throw new RuntimeException("只能审核已提交状态的自评"); + } + + // 更新审核信息 + evaluation.setEvaluationStatus("approved"); + evaluation.setApproveTime(new Date()); + evaluation.setApproveBy(getUserIdSafely()); + evaluation.setApproveByName(LoginHelper.getUsername()); + evaluation.setApproveOpinion(opinion); + + return baseMapper.updateById(evaluation) > 0; + } + + /** + * 批量删除项目绩效自评 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 校验是否可以删除(例如:已审核的不能删除) + List list = baseMapper.selectBatchIds(ids); + for (CwProjectPerformanceSelfEvaluation evaluation : list) { + if ("approved".equals(evaluation.getEvaluationStatus())) { + throw new RuntimeException("已审核的自评不能删除"); + } + } + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 统计自评数量 + */ + @Override + public Long countEvaluations(Integer evaluationYear, String evaluationStatus) { + return baseMapper.countEvaluations(evaluationYear, evaluationStatus); + } + + /** + * 计算平均分 + */ + @Override + public BigDecimal calculateAverageScore(Integer evaluationYear) { + return baseMapper.calculateAverageScore(evaluationYear); + } + + /** + * 按部门统计自评情况 + */ + @Override + public List> statisticsByDept(Integer evaluationYear) { + return baseMapper.statisticsByDept(evaluationYear); + } + + /** + * 按等级统计自评数量 + */ + @Override + public List> statisticsByLevel(Integer evaluationYear) { + return baseMapper.statisticsByLevel(evaluationYear); + } + + /** + * 导出项目绩效自评 + */ + @Override + public void export(CwProjectPerformanceSelfEvaluationBo bo) { + // TODO: 实现导出功能 + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwProjectPerformanceSelfEvaluation entity) { + // 校验项目名称不能为空 + if (StringUtils.isBlank(entity.getProjectName())) { + throw new RuntimeException("项目名称不能为空"); + } + + // 校验自评年度不能为空 + if (entity.getEvaluationYear() == null) { + throw new RuntimeException("自评年度不能为空"); + } + } + + /** + * 提交前验证必填字段 + */ + private void validateForSubmit(CwProjectPerformanceSelfEvaluation entity) { + if (StringUtils.isBlank(entity.getProjectName())) { + throw new RuntimeException("项目名称不能为空"); + } + if (entity.getEvaluationYear() == null) { + throw new RuntimeException("自评年度不能为空"); + } + if (entity.getTotalInvestment() == null) { + throw new RuntimeException("项目总投资不能为空"); + } + // ... 可以添加更多必填字段验证 + } + + /** + * 根据总分计算等级 + */ + private String calculateLevel(BigDecimal totalScore) { + if (totalScore.compareTo(new BigDecimal("90")) >= 0) { + return "优秀"; + } else if (totalScore.compareTo(new BigDecimal("80")) >= 0) { + return "良好"; + } else if (totalScore.compareTo(new BigDecimal("60")) >= 0) { + return "合格"; + } else { + return "不合格"; + } + } + + /** + * 计算总分和等级(保存时自动计算) + */ + private void calculateTotalScoreAndLevel(CwProjectPerformanceSelfEvaluation entity) { + // 🔧 修复:使用新的9个得分字段计算总分(而非旧的4个字段) + BigDecimal totalScore = BigDecimal.ZERO; + + // 资金到位得分 + if (entity.getFundScore() != null) { + totalScore = totalScore.add(entity.getFundScore()); + } + + // 产出指标得分(4项) + if (entity.getQuantityScore() != null) { + totalScore = totalScore.add(entity.getQuantityScore()); + } + if (entity.getQualityScore() != null) { + totalScore = totalScore.add(entity.getQualityScore()); + } + if (entity.getTimelinessScore() != null) { + totalScore = totalScore.add(entity.getTimelinessScore()); + } + if (entity.getCostScore() != null) { + totalScore = totalScore.add(entity.getCostScore()); + } + + // 效益指标得分(4项) + if (entity.getEconomicScore() != null) { + totalScore = totalScore.add(entity.getEconomicScore()); + } + if (entity.getSocialScore() != null) { + totalScore = totalScore.add(entity.getSocialScore()); + } + if (entity.getEcologicalScore() != null) { + totalScore = totalScore.add(entity.getEcologicalScore()); + } + if (entity.getSatisfactionScore() != null) { + totalScore = totalScore.add(entity.getSatisfactionScore()); + } + + // 只有当总分大于0时才设置 + if (totalScore.compareTo(BigDecimal.ZERO) > 0) { + entity.setTotalScore(totalScore); + // 根据总分自动判定等级 + String level = calculateLevel(totalScore); + entity.setSelfEvaluationLevel(level); + } + } + + /** + * 安全获取用户ID,处理可能的字符串格式(如 "sys_user:1") + */ + private Long getUserIdSafely() { + try { + Long userId = LoginHelper.getUserId(); + if (userId != null) { + return userId; + } + } catch (Exception e) { + // 如果getUserId()失败,尝试从getUserIdStr()解析 + } + + // 尝试从字符串格式解析 + try { + String userIdStr = LoginHelper.getUserIdStr(); + if (StringUtils.isNotBlank(userIdStr)) { + // 处理 "sys_user:1" 这种格式,提取数字部分 + if (userIdStr.contains(":")) { + String[] parts = userIdStr.split(":"); + if (parts.length > 1) { + return Long.parseLong(parts[1]); + } + } else { + return Long.parseLong(userIdStr); + } + } + } catch (Exception e) { + // 解析失败,返回null或抛出异常 + } + + return null; + } +} + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/check_evaluation_data.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/check_evaluation_data.sql new file mode 100644 index 00000000..4150e74e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/check_evaluation_data.sql @@ -0,0 +1,87 @@ +-- ============================================= +-- 检查绩效自评数据完整性 +-- 功能:查看指定记录的所有字段值 +-- 创建时间:2025-11-04 +-- ============================================= + +USE `hzdyjs_xg`; + +-- 查询最新的一条记录,显示所有字段 +SELECT + -- 基本信息 + id, + project_name, + supervisory_dept, + implement_unit, + evaluation_year, + + -- 资金信息 + initial_budget, + annual_budget, + actual_expenditure, + fund_score, + + -- 绩效目标 + performance_goal, + target_completion, + + -- 数量指标 + quantity_indicator, + quantity_target, + quantity_actual, + quantity_value, + quantity_score, + quantity_deviation, + + -- 质量指标 + quality_indicator, + quality_target, + quality_actual, + quality_value, + quality_score, + quality_deviation, + + -- 时效指标 + timeliness_indicator, + timeliness_target, + timeliness_actual, + timeliness_value, + timeliness_score, + timeliness_deviation, + + -- 成本指标 + cost_indicator, + cost_target, + cost_actual, + cost_value, + cost_score, + cost_deviation, + + -- 效益指标 + economic_indicator, + social_indicator, + ecological_indicator, + satisfaction_indicator, + + -- 总分和等级 + total_score, + self_evaluation_level, + + -- 状态 + evaluation_status, + create_time +FROM `cw_project_performance_self_evaluation` +WHERE deleted = 0 +ORDER BY create_time DESC +LIMIT 1; + +-- 统计各字段的NULL值数量 +SELECT + '项目名称' as field_name, COUNT(*) - COUNT(project_name) as null_count FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '主管部门', COUNT(*) - COUNT(supervisory_dept) FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '实施单位', COUNT(*) - COUNT(implement_unit) FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '绩效目标', COUNT(*) - COUNT(performance_goal) FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '目标完成情况', COUNT(*) - COUNT(target_completion) FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '数量指标名称', COUNT(*) - COUNT(quantity_indicator) FROM cw_project_performance_self_evaluation WHERE deleted = 0 +UNION ALL SELECT '质量指标名称', COUNT(*) - COUNT(quality_indicator) FROM cw_project_performance_self_evaluation WHERE deleted = 0; + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/dict_cw_evaluation_status.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/dict_cw_evaluation_status.sql new file mode 100644 index 00000000..0ebcd379 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/dict_cw_evaluation_status.sql @@ -0,0 +1,27 @@ +-- 项目绩效自评状态字典 +-- 删除旧数据(如果存在) +DELETE FROM sys_dict_data WHERE dict_type = 'cw_evaluation_status'; +DELETE FROM sys_dict_type WHERE dict_type = 'cw_evaluation_status'; + +-- 插入字典类型 +INSERT INTO sys_dict_type (dict_id, tenant_id, dict_name, dict_type, create_dept, create_by, create_time, update_by, update_time, remark) +VALUES ( + NEXT VALUE FOR sys_dict_type_seq, -- dict_id 使用序列生成 + '000000', -- tenant_id + '绩效自评状态', -- dict_name + 'cw_evaluation_status', -- dict_type + 103, -- create_dept + 1, -- create_by + NOW(), -- create_time + 1, -- update_by + NOW(), -- update_time + '项目绩效自评的状态' -- remark +); + +-- 插入字典数据项 +INSERT INTO sys_dict_data (dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, create_dept, create_by, create_time, update_by, update_time, remark) +VALUES + (NEXT VALUE FOR sys_dict_data_seq, '000000', 1, '草稿', 'draft', 'cw_evaluation_status', '', 'info', 'N', 103, 1, NOW(), 1, NOW(), '草稿状态'), + (NEXT VALUE FOR sys_dict_data_seq, '000000', 2, '已提交', 'submitted', 'cw_evaluation_status', '', 'primary', 'N', 103, 1, NOW(), 1, NOW(), '已提交待审核'), + (NEXT VALUE FOR sys_dict_data_seq, '000000', 3, '已审核', 'approved', 'cw_evaluation_status', '', 'success', 'N', 103, 1, NOW(), 1, NOW(), '已审核通过'); + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_application_no_field.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_application_no_field.sql new file mode 100644 index 00000000..488c8cb9 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_application_no_field.sql @@ -0,0 +1,22 @@ +-- ======================================== +-- 修复 application_no 字段问题 +-- 日期: 2025-01-03 +-- 描述: 允许 application_no 字段为空并设置默认值 +-- ======================================== + +-- 修改 application_no 字段:允许为空,设置默认空字符串 +ALTER TABLE cw_project_performance_self_evaluation +MODIFY COLUMN application_no varchar(50) NULL DEFAULT '' COMMENT '项目申报编号'; + +-- 更新现有的空值或无效值记录 +UPDATE cw_project_performance_self_evaluation +SET application_no = CONCAT('TEMP-', id) +WHERE application_no IS NULL + OR application_no = '' + OR application_no = 'APP-0'; + +-- 验证修改结果 +SELECT COUNT(*) as total_count, + SUM(CASE WHEN application_no IS NULL OR application_no = '' THEN 1 ELSE 0 END) as empty_count +FROM cw_project_performance_self_evaluation; + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_total_score.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_total_score.sql new file mode 100644 index 00000000..96518f0f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/fix_total_score.sql @@ -0,0 +1,66 @@ +-- ============================================= +-- 修复绩效自评总分计算脚本 +-- 功能:重新计算所有记录的total_score(使用新的9个得分字段) +-- 创建时间:2025-11-04 +-- ============================================= + +USE `hzdyjs_xg`; + +-- 更新所有记录的总分(使用新的9个得分字段) +UPDATE `cw_project_performance_self_evaluation` +SET + total_score = COALESCE(fund_score, 0) + + COALESCE(quantity_score, 0) + + COALESCE(quality_score, 0) + + COALESCE(timeliness_score, 0) + + COALESCE(cost_score, 0) + + COALESCE(economic_score, 0) + + COALESCE(social_score, 0) + + COALESCE(ecological_score, 0) + + COALESCE(satisfaction_score, 0), + + -- 根据总分自动判定等级 + self_evaluation_level = CASE + WHEN (COALESCE(fund_score, 0) + COALESCE(quantity_score, 0) + COALESCE(quality_score, 0) + + COALESCE(timeliness_score, 0) + COALESCE(cost_score, 0) + COALESCE(economic_score, 0) + + COALESCE(social_score, 0) + COALESCE(ecological_score, 0) + COALESCE(satisfaction_score, 0)) >= 90 THEN '优秀' + WHEN (COALESCE(fund_score, 0) + COALESCE(quantity_score, 0) + COALESCE(quality_score, 0) + + COALESCE(timeliness_score, 0) + COALESCE(cost_score, 0) + COALESCE(economic_score, 0) + + COALESCE(social_score, 0) + COALESCE(ecological_score, 0) + COALESCE(satisfaction_score, 0)) >= 80 THEN '良好' + WHEN (COALESCE(fund_score, 0) + COALESCE(quantity_score, 0) + COALESCE(quality_score, 0) + + COALESCE(timeliness_score, 0) + COALESCE(cost_score, 0) + COALESCE(economic_score, 0) + + COALESCE(social_score, 0) + COALESCE(ecological_score, 0) + COALESCE(satisfaction_score, 0)) >= 60 THEN '合格' + ELSE '不合格' + END +WHERE deleted = 0; + +-- 查看更新结果 +SELECT + id, + project_name, + fund_score, + quantity_score, + quality_score, + timeliness_score, + cost_score, + economic_score, + social_score, + ecological_score, + satisfaction_score, + total_score, + self_evaluation_level, + evaluation_status +FROM `cw_project_performance_self_evaluation` +WHERE deleted = 0 +ORDER BY create_time DESC +LIMIT 10; + +-- ============================================= +-- 说明 +-- ============================================= +-- 1. 本脚本重新计算所有记录的总分(基于新的9个得分字段) +-- 2. 同时更新自评等级(优秀≥90分,良好≥80分,合格≥60分) +-- 3. 只更新未删除的记录(deleted = 0) +-- 4. 执行后可以查看最近10条记录验证结果 +-- ============================================= + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/upgrade_performance_evaluation_table.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/upgrade_performance_evaluation_table.sql new file mode 100644 index 00000000..d768b538 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/upgrade_performance_evaluation_table.sql @@ -0,0 +1,237 @@ +-- ============================================= +-- 绩效自评表结构升级脚本 (方案一:完整版) +-- 功能:补全52个缺失字段,支持完整的杭州市绩效自评标准 +-- 创建时间:2025-11-03 +-- 执行前请备份数据库! +-- ============================================= + +USE `hzdyjs_xg`; + +-- ============================================= +-- 第一部分:基础信息字段(2个) +-- ============================================= +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `supervisory_dept` varchar(100) DEFAULT NULL COMMENT '主管部门' AFTER `project_name`, +ADD COLUMN `implement_unit` varchar(100) DEFAULT NULL COMMENT '实施单位' AFTER `supervisory_dept`; + +-- ============================================= +-- 第二部分:项目资金详细字段(18个) +-- ============================================= + +-- 2.1 年度资金总额(4个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `initial_budget` decimal(15,2) DEFAULT NULL COMMENT '年初预算数(万元)' AFTER `evaluation_year`, +ADD COLUMN `fund_value` decimal(5,2) DEFAULT NULL COMMENT '资金到位分值' AFTER `actual_expenditure`, +ADD COLUMN `fund_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '资金执行率(%)' AFTER `fund_value`, +ADD COLUMN `fund_score` decimal(5,2) DEFAULT NULL COMMENT '资金到位得分' AFTER `fund_execution_rate`; + +-- 2.2 当年财政拨款(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `current_year_budget_initial` decimal(15,2) DEFAULT NULL COMMENT '当年财政拨款-年初预算数(万元)' AFTER `fund_score`, +ADD COLUMN `current_year_budget_annual` decimal(15,2) DEFAULT NULL COMMENT '当年财政拨款-全年预算数(万元)' AFTER `current_year_budget_initial`, +ADD COLUMN `current_year_budget_actual` decimal(15,2) DEFAULT NULL COMMENT '当年财政拨款-全年执行数(万元)' AFTER `current_year_budget_annual`, +ADD COLUMN `current_year_budget_value` decimal(5,2) DEFAULT NULL COMMENT '当年财政拨款-分值' AFTER `current_year_budget_actual`, +ADD COLUMN `current_year_budget_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '当年财政拨款-执行率(%)' AFTER `current_year_budget_value`, +ADD COLUMN `current_year_budget_score` decimal(5,2) DEFAULT NULL COMMENT '当年财政拨款-得分' AFTER `current_year_budget_execution_rate`; + +-- 2.3 上年结转资金(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `last_year_carryover_initial` decimal(15,2) DEFAULT NULL COMMENT '上年结转资金-年初预算数(万元)' AFTER `current_year_budget_score`, +ADD COLUMN `last_year_carryover_annual` decimal(15,2) DEFAULT NULL COMMENT '上年结转资金-全年预算数(万元)' AFTER `last_year_carryover_initial`, +ADD COLUMN `last_year_carryover_actual` decimal(15,2) DEFAULT NULL COMMENT '上年结转资金-全年执行数(万元)' AFTER `last_year_carryover_annual`, +ADD COLUMN `last_year_carryover_value` decimal(5,2) DEFAULT NULL COMMENT '上年结转资金-分值' AFTER `last_year_carryover_actual`, +ADD COLUMN `last_year_carryover_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '上年结转资金-执行率(%)' AFTER `last_year_carryover_value`, +ADD COLUMN `last_year_carryover_score` decimal(5,2) DEFAULT NULL COMMENT '上年结转资金-得分' AFTER `last_year_carryover_execution_rate`; + +-- 2.4 其他资金(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `other_funds_initial` decimal(15,2) DEFAULT NULL COMMENT '其他资金-年初预算数(万元)' AFTER `last_year_carryover_score`, +ADD COLUMN `other_funds_annual` decimal(15,2) DEFAULT NULL COMMENT '其他资金-全年预算数(万元)' AFTER `other_funds_initial`, +ADD COLUMN `other_funds_actual` decimal(15,2) DEFAULT NULL COMMENT '其他资金-全年执行数(万元)' AFTER `other_funds_annual`, +ADD COLUMN `other_funds_value` decimal(5,2) DEFAULT NULL COMMENT '其他资金-分值' AFTER `other_funds_actual`, +ADD COLUMN `other_funds_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '其他资金-执行率(%)' AFTER `other_funds_value`, +ADD COLUMN `other_funds_score` decimal(5,2) DEFAULT NULL COMMENT '其他资金-得分' AFTER `other_funds_execution_rate`; + +-- ============================================= +-- 第三部分:产出指标字段(24个) +-- ============================================= + +-- 3.1 数量指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `quantity_indicator` varchar(200) DEFAULT NULL COMMENT '数量指标名称' AFTER `target_completion`, +ADD COLUMN `quantity_target` varchar(100) DEFAULT NULL COMMENT '数量指标-指标值' AFTER `quantity_indicator`, +ADD COLUMN `quantity_actual` varchar(100) DEFAULT NULL COMMENT '数量指标-实际完成值' AFTER `quantity_target`, +ADD COLUMN `quantity_value` decimal(5,2) DEFAULT NULL COMMENT '数量指标-分值' AFTER `quantity_actual`, +ADD COLUMN `quantity_score` decimal(5,2) DEFAULT NULL COMMENT '数量指标-得分' AFTER `quantity_value`, +ADD COLUMN `quantity_deviation` text DEFAULT NULL COMMENT '数量指标-偏差原因分析及改进措施' AFTER `quantity_score`; + +-- 3.2 质量指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `quality_indicator` varchar(200) DEFAULT NULL COMMENT '质量指标名称' AFTER `quantity_deviation`, +ADD COLUMN `quality_target` varchar(100) DEFAULT NULL COMMENT '质量指标-指标值' AFTER `quality_indicator`, +ADD COLUMN `quality_actual` varchar(100) DEFAULT NULL COMMENT '质量指标-实际完成值' AFTER `quality_target`, +ADD COLUMN `quality_value` decimal(5,2) DEFAULT NULL COMMENT '质量指标-分值' AFTER `quality_actual`, +ADD COLUMN `quality_score` decimal(5,2) DEFAULT NULL COMMENT '质量指标-得分' AFTER `quality_value`, +ADD COLUMN `quality_deviation` text DEFAULT NULL COMMENT '质量指标-偏差原因分析及改进措施' AFTER `quality_score`; + +-- 3.3 时效指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `timeliness_indicator` varchar(200) DEFAULT NULL COMMENT '时效指标名称' AFTER `quality_deviation`, +ADD COLUMN `timeliness_target` varchar(100) DEFAULT NULL COMMENT '时效指标-指标值' AFTER `timeliness_indicator`, +ADD COLUMN `timeliness_actual` varchar(100) DEFAULT NULL COMMENT '时效指标-实际完成值' AFTER `timeliness_target`, +ADD COLUMN `timeliness_value` decimal(5,2) DEFAULT NULL COMMENT '时效指标-分值' AFTER `timeliness_actual`, +ADD COLUMN `timeliness_score` decimal(5,2) DEFAULT NULL COMMENT '时效指标-得分' AFTER `timeliness_value`, +ADD COLUMN `timeliness_deviation` text DEFAULT NULL COMMENT '时效指标-偏差原因分析及改进措施' AFTER `timeliness_score`; + +-- 3.4 成本指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `cost_indicator` varchar(200) DEFAULT NULL COMMENT '成本指标名称' AFTER `timeliness_deviation`, +ADD COLUMN `cost_target` varchar(100) DEFAULT NULL COMMENT '成本指标-指标值' AFTER `cost_indicator`, +ADD COLUMN `cost_actual` varchar(100) DEFAULT NULL COMMENT '成本指标-实际完成值' AFTER `cost_target`, +ADD COLUMN `cost_value` decimal(5,2) DEFAULT NULL COMMENT '成本指标-分值' AFTER `cost_actual`, +ADD COLUMN `cost_score` decimal(5,2) DEFAULT NULL COMMENT '成本指标-得分' AFTER `cost_value`, +ADD COLUMN `cost_deviation` text DEFAULT NULL COMMENT '成本指标-偏差原因分析及改进措施' AFTER `cost_score`; + +-- ============================================= +-- 第四部分:效益指标字段(24个) +-- ============================================= + +-- 4.1 经济效益指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `economic_indicator` varchar(200) DEFAULT NULL COMMENT '经济效益指标名称' AFTER `cost_deviation`, +ADD COLUMN `economic_target` varchar(100) DEFAULT NULL COMMENT '经济效益指标-指标值' AFTER `economic_indicator`, +ADD COLUMN `economic_actual` varchar(100) DEFAULT NULL COMMENT '经济效益指标-实际完成值' AFTER `economic_target`, +ADD COLUMN `economic_value` decimal(5,2) DEFAULT NULL COMMENT '经济效益指标-分值' AFTER `economic_actual`, +ADD COLUMN `economic_score` decimal(5,2) DEFAULT NULL COMMENT '经济效益指标-得分' AFTER `economic_value`, +ADD COLUMN `economic_deviation` text DEFAULT NULL COMMENT '经济效益指标-偏差原因分析及改进措施' AFTER `economic_score`; + +-- 4.2 社会效益指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `social_indicator` varchar(200) DEFAULT NULL COMMENT '社会效益指标名称' AFTER `economic_deviation`, +ADD COLUMN `social_target` varchar(100) DEFAULT NULL COMMENT '社会效益指标-指标值' AFTER `social_indicator`, +ADD COLUMN `social_actual` varchar(100) DEFAULT NULL COMMENT '社会效益指标-实际完成值' AFTER `social_target`, +ADD COLUMN `social_value` decimal(5,2) DEFAULT NULL COMMENT '社会效益指标-分值' AFTER `social_actual`, +ADD COLUMN `social_score` decimal(5,2) DEFAULT NULL COMMENT '社会效益指标-得分' AFTER `social_value`, +ADD COLUMN `social_deviation` text DEFAULT NULL COMMENT '社会效益指标-偏差原因分析及改进措施' AFTER `social_score`; + +-- 4.3 生态效益指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `ecological_indicator` varchar(200) DEFAULT NULL COMMENT '生态效益指标名称' AFTER `social_deviation`, +ADD COLUMN `ecological_target` varchar(100) DEFAULT NULL COMMENT '生态效益指标-指标值' AFTER `ecological_indicator`, +ADD COLUMN `ecological_actual` varchar(100) DEFAULT NULL COMMENT '生态效益指标-实际完成值' AFTER `ecological_target`, +ADD COLUMN `ecological_value` decimal(5,2) DEFAULT NULL COMMENT '生态效益指标-分值' AFTER `ecological_actual`, +ADD COLUMN `ecological_score` decimal(5,2) DEFAULT NULL COMMENT '生态效益指标-得分' AFTER `ecological_value`, +ADD COLUMN `ecological_deviation` text DEFAULT NULL COMMENT '生态效益指标-偏差原因分析及改进措施' AFTER `ecological_score`; + +-- 4.4 满意度指标(6个字段) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `satisfaction_indicator` varchar(200) DEFAULT NULL COMMENT '满意度指标名称' AFTER `ecological_deviation`, +ADD COLUMN `satisfaction_target` varchar(100) DEFAULT NULL COMMENT '满意度指标-指标值' AFTER `satisfaction_indicator`, +ADD COLUMN `satisfaction_actual` varchar(100) DEFAULT NULL COMMENT '满意度指标-实际完成值' AFTER `satisfaction_target`, +ADD COLUMN `satisfaction_value` decimal(5,2) DEFAULT NULL COMMENT '满意度指标-分值' AFTER `satisfaction_actual`, +ADD COLUMN `satisfaction_score` decimal(5,2) DEFAULT NULL COMMENT '满意度指标-得分' AFTER `satisfaction_value`, +ADD COLUMN `satisfaction_deviation` text DEFAULT NULL COMMENT '满意度指标-偏差原因分析及改进措施' AFTER `satisfaction_score`; + +-- ============================================= +-- 升级完成验证 +-- ============================================= + +-- 查看表结构 +DESC `cw_project_performance_self_evaluation`; + +-- 统计字段数量(应该是89个字段:原37个 + 新增52个) +SELECT COUNT(*) AS total_columns +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = 'hzdyjs_xg' + AND TABLE_NAME = 'cw_project_performance_self_evaluation'; + +-- ============================================= +-- 回滚脚本(如果升级失败需要回滚) +-- ============================================= +/* +-- 删除所有新增字段 +ALTER TABLE `cw_project_performance_self_evaluation` +DROP COLUMN `supervisory_dept`, +DROP COLUMN `implement_unit`, +DROP COLUMN `initial_budget`, +DROP COLUMN `fund_value`, +DROP COLUMN `fund_execution_rate`, +DROP COLUMN `fund_score`, +DROP COLUMN `current_year_budget_initial`, +DROP COLUMN `current_year_budget_annual`, +DROP COLUMN `current_year_budget_actual`, +DROP COLUMN `current_year_budget_value`, +DROP COLUMN `current_year_budget_execution_rate`, +DROP COLUMN `current_year_budget_score`, +DROP COLUMN `last_year_carryover_initial`, +DROP COLUMN `last_year_carryover_annual`, +DROP COLUMN `last_year_carryover_actual`, +DROP COLUMN `last_year_carryover_value`, +DROP COLUMN `last_year_carryover_execution_rate`, +DROP COLUMN `last_year_carryover_score`, +DROP COLUMN `other_funds_initial`, +DROP COLUMN `other_funds_annual`, +DROP COLUMN `other_funds_actual`, +DROP COLUMN `other_funds_value`, +DROP COLUMN `other_funds_execution_rate`, +DROP COLUMN `other_funds_score`, +DROP COLUMN `quantity_indicator`, +DROP COLUMN `quantity_target`, +DROP COLUMN `quantity_actual`, +DROP COLUMN `quantity_value`, +DROP COLUMN `quantity_score`, +DROP COLUMN `quantity_deviation`, +DROP COLUMN `quality_indicator`, +DROP COLUMN `quality_target`, +DROP COLUMN `quality_actual`, +DROP COLUMN `quality_value`, +DROP COLUMN `quality_score`, +DROP COLUMN `quality_deviation`, +DROP COLUMN `timeliness_indicator`, +DROP COLUMN `timeliness_target`, +DROP COLUMN `timeliness_actual`, +DROP COLUMN `timeliness_value`, +DROP COLUMN `timeliness_score`, +DROP COLUMN `timeliness_deviation`, +DROP COLUMN `cost_indicator`, +DROP COLUMN `cost_target`, +DROP COLUMN `cost_actual`, +DROP COLUMN `cost_value`, +DROP COLUMN `cost_score`, +DROP COLUMN `cost_deviation`, +DROP COLUMN `economic_indicator`, +DROP COLUMN `economic_target`, +DROP COLUMN `economic_actual`, +DROP COLUMN `economic_value`, +DROP COLUMN `economic_score`, +DROP COLUMN `economic_deviation`, +DROP COLUMN `social_indicator`, +DROP COLUMN `social_target`, +DROP COLUMN `social_actual`, +DROP COLUMN `social_value`, +DROP COLUMN `social_score`, +DROP COLUMN `social_deviation`, +DROP COLUMN `ecological_indicator`, +DROP COLUMN `ecological_target`, +DROP COLUMN `ecological_actual`, +DROP COLUMN `ecological_value`, +DROP COLUMN `ecological_score`, +DROP COLUMN `ecological_deviation`, +DROP COLUMN `satisfaction_indicator`, +DROP COLUMN `satisfaction_target`, +DROP COLUMN `satisfaction_actual`, +DROP COLUMN `satisfaction_value`, +DROP COLUMN `satisfaction_score`, +DROP COLUMN `satisfaction_deviation`; +*/ + +-- ============================================= +-- 升级说明 +-- ============================================= +-- 1. 执行前请备份数据库 +-- 2. 本脚本增加了52个新字段,完整支持杭州市绩效自评表标准格式 +-- 3. 所有新字段均允许为NULL,不影响现有数据 +-- 4. 升级后前端表单所有字段都能正常保存和显示 +-- 5. 如需回滚,请执行上面注释中的回滚脚本 +-- ============================================= + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复唯一索引冲突.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复唯一索引冲突.sql new file mode 100644 index 00000000..abc20545 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复唯一索引冲突.sql @@ -0,0 +1,29 @@ +-- ============================================= +-- 修复唯一索引冲突问题 +-- 问题:原唯一索引导致同一项目同一年度无法创建多条自评记录 +-- 解决:删除旧索引,添加新的合理索引 +-- 创建时间:2025-11-03 +-- ============================================= + +USE `hzdyjs_xg`; + +-- 1. 删除原有的唯一索引 +ALTER TABLE `cw_project_performance_self_evaluation` +DROP INDEX `uk_application_evaluation`; + +-- 2. 添加普通索引(允许重复,但便于查询) +ALTER TABLE `cw_project_performance_self_evaluation` +ADD INDEX `idx_application_year` (`project_application_id`, `evaluation_year`, `tenant_id`) +COMMENT '项目-年度-租户联合索引'; + +-- 3. 验证索引 +SHOW INDEX FROM `cw_project_performance_self_evaluation`; + +-- ============================================= +-- 说明: +-- 1. 删除了唯一约束,允许同一项目同一年度创建多条自评记录 +-- 2. 保留普通索引以提升查询性能 +-- 3. 用户可以创建多个草稿,最终只提交一个 +-- 4. 如需控制重复提交,应在业务层面控制(如:状态为已提交时禁止再次提交) +-- ============================================= + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复菜单乱码数据.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复菜单乱码数据.sql new file mode 100644 index 00000000..769d05c1 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/修复菜单乱码数据.sql @@ -0,0 +1,257 @@ +-- ============================================= +-- 修复菜单乱码数据脚本 +-- 创建时间: 2025-01-03 +-- 说明: 修复因INSERT语句字段顺序错误导致的菜单数据乱码问题 +-- ============================================= + +-- 修复绩效自评管理菜单(menu_id = 4050) +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e7aea1e79086') USING utf8mb4), + `perms` = 'cw:evaluation:list', + `icon` = 'chart', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = CONVERT(UNHEX('e9a1b9e79baee7bba9e69588e887aae8af84e7aea1e79086e88f9ce58d95') USING utf8mb4) +WHERE `menu_id` = 4050; + +-- 修复绩效自评管理按钮权限 +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e69fa5e8afa2') USING utf8mb4), + `perms` = 'cw:evaluation:query', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405001; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e696b0e5a29e') USING utf8mb4), + `perms` = 'cw:evaluation:add', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405002; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e4bfaee694b9') USING utf8mb4), + `perms` = 'cw:evaluation:edit', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405003; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e588a0e999a4') USING utf8mb4), + `perms` = 'cw:evaluation:remove', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405004; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e68f90e4baa4') USING utf8mb4), + `perms` = 'cw:evaluation:submit', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405005; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e5aea1e6a0b8') USING utf8mb4), + `perms` = 'cw:evaluation:approve', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405006; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e7bba9e69588e887aae8af84e5afbce587ba') USING utf8mb4), + `perms` = 'cw:evaluation:export', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405007; + +-- 修复运行监控管理菜单(menu_id = 4052) +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e7aea1e79086') USING utf8mb4), + `perms` = 'cw:monitoringManagement:list', + `icon` = 'monitor', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = CONVERT(UNHEX('e9a1b9e79baee8bf90e8a18ce79b91e68ea7e7aea1e79086e88f9ce58d95') USING utf8mb4) +WHERE `menu_id` = 4052; + +-- 修复运行监控管理按钮权限 +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e69fa5e8afa2') USING utf8mb4), + `perms` = 'cw:monitoring:query', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405101; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e5a1abe68aa5') USING utf8mb4), + `perms` = 'cw:monitoring:fill', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405102; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e4bfaee694b9') USING utf8mb4), + `perms` = 'cw:monitoring:edit', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405103; + +-- 修复绩效运行监控按钮权限 +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e69fa5e79c8b') USING utf8mb4), + `perms` = 'cw:performanceMonitoring:query', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405008; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e696b0e5a29e') USING utf8mb4), + `perms` = 'cw:performanceMonitoring:add', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405009; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e7bc96e8be91') USING utf8mb4), + `perms` = 'cw:performanceMonitoring:edit', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405010; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e588a0e999a4') USING utf8mb4), + `perms` = 'cw:performanceMonitoring:remove', + `icon` = '#', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405011; + +-- 修复运行监控管理按钮权限(menu_id = 405201, 405202, 405203) +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e69fa5e8afa2') USING utf8mb4), + `perms` = 'cw:monitoringManagement:query', + `icon` = '', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405201; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e99481e5ae9a') USING utf8mb4), + `perms` = 'cw:monitoringManagement:lock', + `icon` = '', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405202; + +UPDATE `sys_menu` SET + `menu_name` = CONVERT(UNHEX('e8bf90e8a18ce79b91e68ea7e8a7a3e99481') USING utf8mb4), + `perms` = 'cw:monitoringManagement:unlock', + `icon` = '', + `create_dept` = NULL, + `create_by` = NULL, + `create_time` = NOW(), + `update_by` = NULL, + `update_time` = NULL, + `remark` = '' +WHERE `menu_id` = 405203; + +-- ============================================= +-- 验证修复结果 +-- ============================================= +-- 检查修复后的菜单数据 +SELECT + menu_id, + menu_name, + perms, + icon, + remark +FROM `sys_menu` +WHERE menu_id IN (4050, 405001, 405002, 405003, 405004, 405005, 405006, 405007, + 4051, 405101, 405102, 405103, + 405008, 405009, 405010, 405011, + 4052, 405201, 405202, 405203) +ORDER BY menu_id; + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/添加允许已提交后编辑字段.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/添加允许已提交后编辑字段.sql new file mode 100644 index 00000000..0fb6eb73 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/添加允许已提交后编辑字段.sql @@ -0,0 +1,15 @@ +-- ============================================= +-- 添加允许已提交后编辑字段 +-- 创建时间: 2025-01-03 +-- 说明: 允许已提交的项目在特定条件下可以修改 +-- ============================================= + +-- 添加允许已提交后编辑字段 +ALTER TABLE `cw_project_performance_self_evaluation` +ADD COLUMN `allow_edit_after_submit` tinyint(1) DEFAULT '0' COMMENT '允许已提交后编辑(0-不允许 1-允许)' +AFTER `evaluation_status`; + +-- 添加索引(可选,如果需要根据此字段查询) +-- ALTER TABLE `cw_project_performance_self_evaluation` +-- ADD INDEX `idx_allow_edit` (`allow_edit_after_submit`) USING BTREE; + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表建表脚本.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表建表脚本.sql new file mode 100644 index 00000000..2c10a0db --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表建表脚本.sql @@ -0,0 +1,63 @@ +-- ============================================= +-- 绩效运行监控表建表脚本 +-- 创建时间: 2025-11-04 +-- 说明: 基于PDF模板的绩效运行监控表 +-- ============================================= + +-- ============================================= +-- 表: 绩效运行监控表 +-- ============================================= +CREATE TABLE `cw_performance_monitoring` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenant_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '000000' COMMENT '租户ID', + + -- 关联信息 + `evaluation_id` bigint(20) NOT NULL COMMENT '绩效自评ID', + `project_application_id` bigint(20) DEFAULT NULL COMMENT '项目申报ID', + + -- 监控周期 + `monitoring_period` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '监控周期(例如:2024年度1-6月)', + + -- 项目概况 + `fund_usage` text COLLATE utf8mb4_unicode_ci COMMENT '资金使用情况', + `performance_target_completion` text COLLATE utf8mb4_unicode_ci COMMENT '绩效目标完成情况', + + -- 绩效指标完成情况(JSON格式存储) + `indicators` text COLLATE utf8mb4_unicode_ci COMMENT '绩效指标完成情况(JSON数组)', + + -- 绩效运行分析 + `existing_problems` text COLLATE utf8mb4_unicode_ci COMMENT '存在问题', + `indicator_variance_analysis` text COLLATE utf8mb4_unicode_ci COMMENT '指标差异率分析和说明', + `next_step_suggestions` text COLLATE utf8mb4_unicode_ci COMMENT '下一步工作建议和打算', + + -- 填报信息 + `fill_by` bigint(20) DEFAULT NULL COMMENT '填报人ID', + `fill_by_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '填报人姓名', + + -- 系统字段 + `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志', + + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_evaluation_id` (`evaluation_id`) USING BTREE, + KEY `idx_project_application` (`project_application_id`) USING BTREE, + KEY `idx_tenant` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='绩效运行监控表'; + +-- ============================================= +-- 菜单权限配置(如果需要独立菜单) +-- ============================================= +-- 注:这里只添加按钮权限,因为运行监控作为绩效自评的子功能 + +-- 绩效运行监控按钮权限 +INSERT INTO `sys_menu` VALUES +(405008, '运行监控查看', 4050, 8, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:query', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405009, '运行监控新增', 4050, 9, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:add', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405010, '运行监控编辑', 4050, 10, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:edit', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405011, '运行监控删除', 4050, 11, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:remove', '#', NULL, NULL, NOW(), NULL, NULL, ''); + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表权限脚本.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表权限脚本.sql new file mode 100644 index 00000000..19a62087 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/绩效运行监控表权限脚本.sql @@ -0,0 +1,35 @@ +-- ============================================= +-- 绩效运行监控菜单权限脚本 +-- 创建时间: 2025-11-04 +-- ============================================= + +-- 绩效运行监控按钮权限 +-- 注:这里只添加按钮权限,因为运行监控作为绩效自评的子功能 + +INSERT INTO `sys_menu` ( + `menu_id`, + `menu_name`, + `parent_id`, + `order_num`, + `path`, + `component`, + `query_param`, + `is_frame`, + `is_cache`, + `menu_type`, + `visible`, + `status`, + `perms`, + `icon`, + `create_dept`, + `create_by`, + `create_time`, + `update_by`, + `update_time`, + `remark` +) VALUES +(405008, '运行监控查看', 4050, 8, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:query', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405009, '运行监控新增', 4050, 9, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:add', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405010, '运行监控编辑', 4050, 10, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:edit', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405011, '运行监控删除', 4050, 11, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:performanceMonitoring:remove', '#', NULL, NULL, NOW(), NULL, NULL, ''); + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/运行监控管理-添加锁定字段.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/运行监控管理-添加锁定字段.sql new file mode 100644 index 00000000..648c3528 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/运行监控管理-添加锁定字段.sql @@ -0,0 +1,88 @@ +-- ============================================= +-- 运行监控管理功能 - 添加锁定字段 +-- 创建时间: 2025-01-03 +-- 说明: 为绩效自评表添加锁定字段,用于运行监控管理功能 +-- ============================================= + +-- 添加锁定字段(如果字段已存在则跳过) +-- 检查并添加 is_locked 字段 +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cw_project_performance_self_evaluation' AND COLUMN_NAME = 'is_locked'); +SET @sqlstmt := IF(@exist = 0, 'ALTER TABLE `cw_project_performance_self_evaluation` ADD COLUMN `is_locked` tinyint(1) DEFAULT ''0'' COMMENT ''是否锁定(0-未锁定 1-已锁定),锁定后不可修改'' AFTER `approve_opinion`', 'SELECT ''字段 is_locked 已存在,跳过添加'''); +PREPARE stmt FROM @sqlstmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 lock_time 字段 +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cw_project_performance_self_evaluation' AND COLUMN_NAME = 'lock_time'); +SET @sqlstmt := IF(@exist = 0, 'ALTER TABLE `cw_project_performance_self_evaluation` ADD COLUMN `lock_time` datetime DEFAULT NULL COMMENT ''锁定时间'' AFTER `is_locked`', 'SELECT ''字段 lock_time 已存在,跳过添加'''); +PREPARE stmt FROM @sqlstmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 lock_by 字段 +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cw_project_performance_self_evaluation' AND COLUMN_NAME = 'lock_by'); +SET @sqlstmt := IF(@exist = 0, 'ALTER TABLE `cw_project_performance_self_evaluation` ADD COLUMN `lock_by` bigint(20) DEFAULT NULL COMMENT ''锁定人ID'' AFTER `lock_time`', 'SELECT ''字段 lock_by 已存在,跳过添加'''); +PREPARE stmt FROM @sqlstmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 lock_by_name 字段 +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cw_project_performance_self_evaluation' AND COLUMN_NAME = 'lock_by_name'); +SET @sqlstmt := IF(@exist = 0, 'ALTER TABLE `cw_project_performance_self_evaluation` ADD COLUMN `lock_by_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''锁定人姓名'' AFTER `lock_by`', 'SELECT ''字段 lock_by_name 已存在,跳过添加'''); +PREPARE stmt FROM @sqlstmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 添加锁定状态索引(如果索引已存在则跳过) +SET @exist := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cw_project_performance_self_evaluation' AND INDEX_NAME = 'idx_is_locked'); +SET @sqlstmt := IF(@exist = 0, 'ALTER TABLE `cw_project_performance_self_evaluation` ADD KEY `idx_is_locked` (`is_locked`) USING BTREE', 'SELECT ''索引 idx_is_locked 已存在,跳过添加'''); +PREPARE stmt FROM @sqlstmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================= +-- 菜单权限配置 - 运行监控管理 +-- ============================================= + +-- 注意:运行监控管理作为绩效自评的同级菜单,父菜单ID为6000(与绩效自评4050同级) +-- 如果菜单ID 4052 已被使用,请修改为其他未使用的ID + +-- 二级菜单:运行监控管理(如果已存在则跳过,请根据实际情况调整菜单ID) +INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES +(4052, '运行监控管理', 6000, 8, 'monitoringManagement', 'cw/monitoringManagement/index', NULL, 1, 0, 'C', '0', '0', 'cw:monitoringManagement:list', 'monitor', NULL, NULL, NOW(), NULL, NULL, '项目运行监控管理菜单(财务管理员和学校领导使用)') +ON DUPLICATE KEY UPDATE menu_name = '运行监控管理', path = 'monitoringManagement', component = 'cw/monitoringManagement/index', perms = 'cw:monitoringManagement:list'; + +-- 运行监控管理按钮权限 +INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES +(405201, '运行监控查询', 4052, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoringManagement:query', '', NULL, NULL, NOW(), NULL, NULL, '') +ON DUPLICATE KEY UPDATE + menu_name = '运行监控查询', + perms = 'cw:monitoringManagement:query', + icon = '', + create_time = NOW(); + +INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES +(405202, '运行监控锁定', 4052, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoringManagement:lock', '', NULL, NULL, NOW(), NULL, NULL, '') +ON DUPLICATE KEY UPDATE + menu_name = '运行监控锁定', + perms = 'cw:monitoringManagement:lock', + icon = '', + create_time = NOW(); + +INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES +(405203, '运行监控解锁', 4052, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoringManagement:unlock', '', NULL, NULL, NOW(), NULL, NULL, '') +ON DUPLICATE KEY UPDATE + menu_name = '运行监控解锁', + perms = 'cw:monitoringManagement:unlock', + icon = '', + create_time = NOW(); + +-- ============================================= +-- 数据字典配置 - 锁定状态 +-- ============================================= + +-- 锁定状态字典(如果已存在则跳过) +-- INSERT INTO `sys_dict_type` VALUES (NULL, '000000', '绩效自评锁定状态', 'cw_evaluation_locked', '0', NULL, NOW(), NULL, NULL, '项目绩效自评锁定状态字典'); +-- INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 1, '未锁定', '0', 'cw_evaluation_locked', NULL, 'success', 'N', '0', NULL, NOW(), NULL, NULL, ''); +-- INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 2, '已锁定', '1', 'cw_evaluation_locked', NULL, 'danger', 'N', '0', NULL, NOW(), NULL, NULL, ''); + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/项目绩效自评模块建表脚本.sql b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/项目绩效自评模块建表脚本.sql new file mode 100644 index 00000000..3dc3c32c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/db/项目绩效自评模块建表脚本.sql @@ -0,0 +1,253 @@ +-- ============================================= +-- 项目绩效自评模块建表脚本 +-- 创建时间: 2025-11-03 +-- 说明: 项目支出绩效自评和运行监控管理 +-- 版本: v1.0 +-- ============================================= + +-- ============================================= +-- 表1: 项目绩效自评表 (主表) +-- ============================================= +CREATE TABLE `cw_project_performance_self_evaluation` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenant_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '000000' COMMENT '租户ID', + + -- 关联项目申报 + `project_application_id` bigint(20) NOT NULL COMMENT '项目申报ID(关联cw_project_application表)', + `application_no` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '申报编号', + `project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称', + + -- 基本信息 + `evaluation_year` int(11) NOT NULL COMMENT '自评年度', + `principal` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '项目负责人', + `contact_phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系电话', + `start_date` date DEFAULT NULL COMMENT '项目实施开始时间', + `end_date` date DEFAULT NULL COMMENT '项目实施结束时间', + + -- 资金情况 (单位:万元) + `total_investment` decimal(15,2) DEFAULT NULL COMMENT '项目总投资', + `annual_budget` decimal(15,2) DEFAULT NULL COMMENT '本年度预算', + `actual_expenditure` decimal(15,2) DEFAULT NULL COMMENT '本年度实际支出', + `budget_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '预算执行率(%)', + + -- 绩效目标 + `performance_goal` text COLLATE utf8mb4_unicode_ci COMMENT '年度绩效目标', + `target_completion` text COLLATE utf8mb4_unicode_ci COMMENT '目标完成情况说明', + + -- 绩效自评 (决策、过程、产出、效益四个维度) + `decision_score` decimal(5,2) DEFAULT NULL COMMENT '项目决策得分', + `decision_content` text COLLATE utf8mb4_unicode_ci COMMENT '项目决策自评内容', + + `process_score` decimal(5,2) DEFAULT NULL COMMENT '项目过程得分', + `process_content` text COLLATE utf8mb4_unicode_ci COMMENT '项目过程自评内容', + + `output_score` decimal(5,2) DEFAULT NULL COMMENT '项目产出得分', + `output_content` text COLLATE utf8mb4_unicode_ci COMMENT '项目产出自评内容', + + `benefit_score` decimal(5,2) DEFAULT NULL COMMENT '项目效益得分', + `benefit_content` text COLLATE utf8mb4_unicode_ci COMMENT '项目效益自评内容', + + `total_score` decimal(5,2) DEFAULT NULL COMMENT '自评总分', + `self_evaluation_level` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '自评等级(优秀/良好/合格/不合格)', + + -- 存在的问题和改进措施 + `existing_problems` text COLLATE utf8mb4_unicode_ci COMMENT '存在的主要问题', + `improvement_measures` text COLLATE utf8mb4_unicode_ci COMMENT '改进措施和建议', + + -- 状态管理 + `evaluation_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'draft' COMMENT '自评状态(draft-草稿 submitted-已提交 approved-已审核)', + `submit_time` datetime DEFAULT NULL COMMENT '提交时间', + `submit_by` bigint(20) DEFAULT NULL COMMENT '提交人ID', + `submit_by_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '提交人姓名', + + -- 审核信息 + `approve_time` datetime DEFAULT NULL COMMENT '审核时间', + `approve_by` bigint(20) DEFAULT NULL COMMENT '审核人ID', + `approve_by_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核人姓名', + `approve_opinion` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核意见', + + -- 系统字段 + `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0-正常 1-删除)', + + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_application_year` (`project_application_id`,`evaluation_year`,`tenant_id`) USING BTREE COMMENT '项目-年度-租户联合索引', + KEY `idx_application_no` (`application_no`) USING BTREE, + KEY `idx_evaluation_year` (`evaluation_year`) USING BTREE, + KEY `idx_status` (`evaluation_status`) USING BTREE, + KEY `idx_tenant` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目绩效自评表(允许同一项目同一年度创建多条自评)'; + + +-- ============================================= +-- 表2: 项目绩效自评指标明细表 +-- ============================================= +CREATE TABLE `cw_project_evaluation_indicator` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenant_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '000000' COMMENT '租户ID', + + -- 关联自评表 + `evaluation_id` bigint(20) NOT NULL COMMENT '绩效自评ID', + + -- 指标分类 + `indicator_category` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '指标类别(decision-决策/process-过程/output-产出/benefit-效益)', + `indicator_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '指标名称', + `indicator_type` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '指标类型(quantity-数量/quality-质量/time-时效/cost-成本)', + + -- 指标内容 + `target_value` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目标值', + `actual_value` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '实际完成值', + `completion_rate` decimal(5,2) DEFAULT NULL COMMENT '完成率(%)', + + -- 评分 + `standard_score` decimal(5,2) DEFAULT NULL COMMENT '指标分值', + `actual_score` decimal(5,2) DEFAULT NULL COMMENT '实际得分', + + -- 说明 + `completion_description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '完成情况说明', + `sort_order` int(11) DEFAULT '0' COMMENT '排序号', + + -- 系统字段 + `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志', + + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_evaluation_id` (`evaluation_id`) USING BTREE, + KEY `idx_category` (`indicator_category`) USING BTREE, + KEY `idx_tenant` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目绩效自评指标明细表'; + + +-- ============================================= +-- 表3: 项目月度运行监控表 +-- ============================================= +CREATE TABLE `cw_project_monthly_monitoring` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenant_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '000000' COMMENT '租户ID', + + -- 关联自评表 + `evaluation_id` bigint(20) NOT NULL COMMENT '绩效自评ID', + `project_application_id` bigint(20) NOT NULL COMMENT '项目申报ID', + + -- 月度信息 + `monitoring_year` int(11) NOT NULL COMMENT '监控年度', + `monitoring_month` int(11) NOT NULL COMMENT '监控月份(1-12)', + + -- 执行情况 + `planned_progress` decimal(5,2) DEFAULT NULL COMMENT '计划进度(%)', + `actual_progress` decimal(5,2) DEFAULT NULL COMMENT '实际进度(%)', + `progress_variance` decimal(5,2) DEFAULT NULL COMMENT '进度偏差(%) = 实际进度 - 计划进度', + + -- 资金情况 (单位:万元) + `monthly_budget` decimal(15,2) DEFAULT NULL COMMENT '本月预算', + `monthly_expenditure` decimal(15,2) DEFAULT NULL COMMENT '本月实际支出', + `cumulative_budget` decimal(15,2) DEFAULT NULL COMMENT '累计预算', + `cumulative_expenditure` decimal(15,2) DEFAULT NULL COMMENT '累计实际支出', + `fund_execution_rate` decimal(5,2) DEFAULT NULL COMMENT '资金执行率(%)', + + -- 关键节点 + `key_milestones` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '本月完成的关键节点', + `milestone_completion` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '节点完成情况(completed-已完成/delayed-延期/notStarted-未开始)', + + -- 问题与风险 + `problems_encountered` text COLLATE utf8mb4_unicode_ci COMMENT '本月遇到的问题', + `risk_description` text COLLATE utf8mb4_unicode_ci COMMENT '风险描述', + `countermeasures` text COLLATE utf8mb4_unicode_ci COMMENT '应对措施', + + -- 下月计划 + `next_month_plan` text COLLATE utf8mb4_unicode_ci COMMENT '下月工作计划', + + -- 填报信息 + `fill_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'not_filled' COMMENT '填报状态(not_filled-未填报/filled-已填报)', + `fill_time` datetime DEFAULT NULL COMMENT '填报时间', + `fill_by` bigint(20) DEFAULT NULL COMMENT '填报人ID', + `fill_by_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '填报人姓名', + + -- 系统字段 + `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志', + + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_evaluation_month` (`evaluation_id`,`monitoring_year`,`monitoring_month`,`tenant_id`,`deleted`) USING BTREE COMMENT '同一项目同一年同一月只能有一条记录', + KEY `idx_project_application` (`project_application_id`) USING BTREE, + KEY `idx_year_month` (`monitoring_year`,`monitoring_month`) USING BTREE, + KEY `idx_fill_status` (`fill_status`) USING BTREE, + KEY `idx_tenant` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目月度运行监控表'; + + +-- ============================================= +-- 菜单权限配置 +-- ============================================= + +-- 注意:请根据实际的项目申报管理菜单ID调整父菜单ID +-- 假设项目申报管理的菜单ID为 4040(请根据实际情况调整) + +-- 二级菜单:绩效自评管理 +INSERT INTO `sys_menu` VALUES +(4050, '绩效自评管理', 4040, 5, 'evaluation', 'cw/evaluation/index', NULL, 1, 0, 'C', '0', '0', 'cw:evaluation:list', 'chart', NULL, NULL, NOW(), NULL, NULL, '项目绩效自评管理菜单'); + +-- 绩效自评管理按钮权限 +INSERT INTO `sys_menu` VALUES +(405001, '绩效自评查询', 4050, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:query', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405002, '绩效自评新增', 4050, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:add', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405003, '绩效自评修改', 4050, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:edit', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405004, '绩效自评删除', 4050, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:remove', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405005, '绩效自评提交', 4050, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:submit', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405006, '绩效自评审核', 4050, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:approve', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405007, '绩效自评导出', 4050, 7, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:evaluation:export', '#', NULL, NULL, NOW(), NULL, NULL, ''); + +-- 二级菜单:运行监控管理 +INSERT INTO `sys_menu` VALUES +(4051, '运行监控管理', 4040, 6, 'monitoring', 'cw/monitoring/index', NULL, 1, 0, 'C', '0', '0', 'cw:monitoring:list', 'monitor', NULL, NULL, NOW(), NULL, NULL, '项目运行监控管理菜单'); + +-- 运行监控管理按钮权限 +INSERT INTO `sys_menu` VALUES +(405101, '运行监控查询', 4051, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoring:query', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405102, '运行监控填报', 4051, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoring:fill', '#', NULL, NULL, NOW(), NULL, NULL, ''), +(405103, '运行监控修改', 4051, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'cw:monitoring:edit', '#', NULL, NULL, NOW(), NULL, NULL, ''); + + +-- ============================================= +-- 数据字典配置 +-- ============================================= + +-- 自评状态 +INSERT INTO `sys_dict_type` VALUES (NULL, '000000', '绩效自评状态', 'cw_evaluation_status', '0', NULL, NOW(), NULL, NULL, '项目绩效自评状态字典'); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 1, '草稿', 'draft', 'cw_evaluation_status', NULL, 'info', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 2, '已提交', 'submitted', 'cw_evaluation_status', NULL, 'primary', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 3, '已审核', 'approved', 'cw_evaluation_status', NULL, 'success', 'N', '0', NULL, NOW(), NULL, NULL, ''); + +-- 自评等级 +INSERT INTO `sys_dict_type` VALUES (NULL, '000000', '绩效自评等级', 'cw_evaluation_level', '0', NULL, NOW(), NULL, NULL, '项目绩效自评等级字典'); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 1, '优秀', '优秀', 'cw_evaluation_level', NULL, 'success', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 2, '良好', '良好', 'cw_evaluation_level', NULL, 'primary', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 3, '合格', '合格', 'cw_evaluation_level', NULL, 'warning', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 4, '不合格', '不合格', 'cw_evaluation_level', NULL, 'danger', 'N', '0', NULL, NOW(), NULL, NULL, ''); + +-- 节点完成情况 +INSERT INTO `sys_dict_type` VALUES (NULL, '000000', '节点完成情况', 'cw_milestone_completion', '0', NULL, NOW(), NULL, NULL, '项目节点完成情况字典'); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 1, '已完成', 'completed', 'cw_milestone_completion', NULL, 'success', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 2, '延期', 'delayed', 'cw_milestone_completion', NULL, 'warning', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 3, '未开始', 'notStarted', 'cw_milestone_completion', NULL, 'info', 'N', '0', NULL, NOW(), NULL, NULL, ''); + +-- 月度填报状态 +INSERT INTO `sys_dict_type` VALUES (NULL, '000000', '月度填报状态', 'cw_monthly_fill_status', '0', NULL, NOW(), NULL, NULL, '月度监控填报状态字典'); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 1, '未填报', 'not_filled', 'cw_monthly_fill_status', NULL, 'info', 'N', '0', NULL, NOW(), NULL, NULL, ''); +INSERT INTO `sys_dict_data` VALUES (NULL, '000000', 2, '已填报', 'filled', 'cw_monthly_fill_status', NULL, 'success', 'N', '0', NULL, NOW(), NULL, NULL, ''); + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml index 43b4f9b6..7f254c75 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml @@ -275,7 +275,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND u.status = '0' AND u.del_flag = '0' AND r.status = '0' - ORDER BY u.user_name + ORDER BY u.user_name ASC, u.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml index fb8efb03..410732be 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml @@ -129,7 +129,7 @@ #{userId} - order by u.dept_id, u.user_name + order by u.user_name asc, u.dept_id diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwPerformanceMonitoringMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwPerformanceMonitoringMapper.xml new file mode 100644 index 00000000..6e2c080a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwPerformanceMonitoringMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectEvaluationIndicatorMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectEvaluationIndicatorMapper.xml new file mode 100644 index 00000000..1302c5ce --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectEvaluationIndicatorMapper.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO cw_project_evaluation_indicator + (id, tenant_id, evaluation_id, indicator_category, indicator_name, indicator_type, + target_value, actual_value, completion_rate, standard_score, actual_score, + completion_description, sort_order, remark, create_dept, create_by, create_time) + VALUES + + (#{item.id}, #{item.tenantId}, #{item.evaluationId}, #{item.indicatorCategory}, + #{item.indicatorName}, #{item.indicatorType}, #{item.targetValue}, #{item.actualValue}, + #{item.completionRate}, #{item.standardScore}, #{item.actualScore}, + #{item.completionDescription}, #{item.sortOrder}, #{item.remark}, + #{item.createDept}, #{item.createBy}, #{item.createTime}) + + + + + + UPDATE cw_project_evaluation_indicator + SET deleted = 1 + WHERE evaluation_id = #{evaluationId} + + + + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectMonthlyMonitoringMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectMonthlyMonitoringMapper.xml new file mode 100644 index 00000000..66964ed2 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectMonthlyMonitoringMapper.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO cw_project_monthly_monitoring + (id, tenant_id, evaluation_id, project_application_id, monitoring_year, monitoring_month, + planned_progress, actual_progress, progress_variance, monthly_budget, monthly_expenditure, + cumulative_budget, cumulative_expenditure, fund_execution_rate, key_milestones, + milestone_completion, problems_encountered, risk_description, countermeasures, + next_month_plan, fill_status, fill_time, fill_by, fill_by_name, + remark, create_dept, create_by, create_time) + VALUES + + (#{item.id}, #{item.tenantId}, #{item.evaluationId}, #{item.projectApplicationId}, + #{item.monitoringYear}, #{item.monitoringMonth}, #{item.plannedProgress}, + #{item.actualProgress}, #{item.progressVariance}, #{item.monthlyBudget}, + #{item.monthlyExpenditure}, #{item.cumulativeBudget}, #{item.cumulativeExpenditure}, + #{item.fundExecutionRate}, #{item.keyMilestones}, #{item.milestoneCompletion}, + #{item.problemsEncountered}, #{item.riskDescription}, #{item.countermeasures}, + #{item.nextMonthPlan}, #{item.fillStatus}, #{item.fillTime}, #{item.fillBy}, + #{item.fillByName}, #{item.remark}, #{item.createDept}, #{item.createBy}, + #{item.createTime}) + + + + + + UPDATE cw_project_monthly_monitoring + SET deleted = 1 + WHERE evaluation_id = #{evaluationId} + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectPerformanceSelfEvaluationMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectPerformanceSelfEvaluationMapper.xml new file mode 100644 index 00000000..6e255ac5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwProjectPerformanceSelfEvaluationMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatQuestionServiceImpl.java b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatQuestionServiceImpl.java index 92d211fb..d8963bc4 100644 --- a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatQuestionServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatQuestionServiceImpl.java @@ -75,6 +75,7 @@ public class HqSatQuestionServiceImpl implements IHqSatQuestionService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); // 处理排序参数 + boolean sortedByCreateTime = false; if (params != null && params.containsKey("orderByColumn") && params.containsKey("isAsc")) { String orderByColumn = (String) params.get("orderByColumn"); String isAsc = (String) params.get("isAsc"); @@ -85,6 +86,7 @@ public class HqSatQuestionServiceImpl implements IHqSatQuestionService { } else { lqw.orderByAsc(HqSatQuestion::getCreateTime); } + sortedByCreateTime = true; } else if ("updateTime".equals(orderByColumn)) { if ("desc".equals(isAsc)) { lqw.orderByDesc(HqSatQuestion::getUpdateTime); @@ -100,10 +102,15 @@ public class HqSatQuestionServiceImpl implements IHqSatQuestionService { } else { // 默认按创建时间倒序 lqw.orderByDesc(HqSatQuestion::getCreateTime); + sortedByCreateTime = true; } } else { // 默认按创建时间倒序 lqw.orderByDesc(HqSatQuestion::getCreateTime); + sortedByCreateTime = true; + } + if (!sortedByCreateTime) { + lqw.orderByDesc(HqSatQuestion::getCreateTime); } // 添加ID查询条件 diff --git a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatSurveyServiceImpl.java b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatSurveyServiceImpl.java index e1c020e5..92e86169 100644 --- a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatSurveyServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatSurveyServiceImpl.java @@ -1097,7 +1097,7 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService { } sql.append("GROUP BY u.user_id, rec.id "); - sql.append("ORDER BY CASE WHEN rec.submit_time IS NOT NULL THEN 0 ELSE 1 END, rec.submit_time DESC, u.user_id "); + sql.append("ORDER BY CASE WHEN rec.submit_time IS NOT NULL THEN 0 ELSE 1 END, rec.submit_time DESC, u.user_name ASC, u.user_id "); // 查询总数 StringBuilder countSql = new StringBuilder(); diff --git a/backend/ruoyi-modules/ruoyi-hq/src/main/resources/mapper/HqPmRepairOrderMapper.xml b/backend/ruoyi-modules/ruoyi-hq/src/main/resources/mapper/HqPmRepairOrderMapper.xml index 7952986e..32cc54a9 100644 --- a/backend/ruoyi-modules/ruoyi-hq/src/main/resources/mapper/HqPmRepairOrderMapper.xml +++ b/backend/ruoyi-modules/ruoyi-hq/src/main/resources/mapper/HqPmRepairOrderMapper.xml @@ -18,7 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHERE r.role_key = 'maintenance_worker' AND u.status = '0' AND u.del_flag = '0' - ORDER BY u.user_id + ORDER BY u.user_name ASC, u.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/service/impl/JcbtScholarshipApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/service/impl/JcbtScholarshipApplicationServiceImpl.java index fbf380e7..3fb9411d 100644 --- a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/service/impl/JcbtScholarshipApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/service/impl/JcbtScholarshipApplicationServiceImpl.java @@ -527,7 +527,7 @@ public class JcbtScholarshipApplicationServiceImpl implements IJcbtScholarshipAp WHERE cm.class_teacher_id = ? AND cm.deleted = 0 AND cm.class_status = '1' - ORDER BY cm.grade DESC, cm.class_name ASC + ORDER BY cm.grade DESC, cm.class_name ASC, cm.create_time DESC """; // 执行查询 @@ -713,7 +713,7 @@ public class JcbtScholarshipApplicationServiceImpl implements IJcbtScholarshipAp AND sm.deleted = 0 AND sm.student_status = '1' AND (cm.deleted IS NULL OR cm.deleted = 0) - ORDER BY sm.student_no ASC + ORDER BY sm.student_no DESC, sm.create_time DESC """; List> studentList = jdbcTemplate.queryForList(sql, classId); diff --git a/backend/ruoyi-modules/ruoyi-jcbt/src/main/resources/mapper/jcbt/JcbtPovertyApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-jcbt/src/main/resources/mapper/jcbt/JcbtPovertyApplicationMapper.xml index 65d576d3..4ec1a65f 100644 --- a/backend/ruoyi-modules/ruoyi-jcbt/src/main/resources/mapper/jcbt/JcbtPovertyApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-jcbt/src/main/resources/mapper/jcbt/JcbtPovertyApplicationMapper.xml @@ -246,7 +246,7 @@ and s.deleted = 0 and s.student_status = '1' and (c.deleted is null or c.deleted = 0) - order by s.student_no asc + order by s.student_no desc diff --git a/backend/ruoyi-modules/ruoyi-rs/pom.xml b/backend/ruoyi-modules/ruoyi-rs/pom.xml index d29f672e..2d6e79c6 100644 --- a/backend/ruoyi-modules/ruoyi-rs/pom.xml +++ b/backend/ruoyi-modules/ruoyi-rs/pom.xml @@ -116,6 +116,13 @@ itextpdf 5.5.13.3 + + + + com.itextpdf + itext-asian + 5.2.0 + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java new file mode 100644 index 00000000..e6ea829f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java @@ -0,0 +1,116 @@ +package org.dromara.rs.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.web.core.BaseController; +import org.dromara.rs.domain.bo.RsOverseasApplicationAttachmentBo; +import org.dromara.rs.domain.vo.RsOverseasApplicationAttachmentVo; +import org.dromara.rs.service.IRsOverseasApplicationAttachmentService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 出国申请附件Controller + * + * @author weizzzz + * @date 2025-10-10 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/rs/overseas/attachment") +public class RsOverseasApplicationAttachmentController extends BaseController { + + private final IRsOverseasApplicationAttachmentService attachmentService; + + /** + * 查询出国申请的附件列表 + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/list/{applicationId}") + public R> list( + @NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) { + List list = attachmentService.selectAttachmentList(applicationId); + return R.ok(list); + } + + /** + * 查询附件详情 + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/{attachmentId}") + public R getInfo( + @NotNull(message = "附件ID不能为空") @PathVariable Long attachmentId) { + RsOverseasApplicationAttachmentVo vo = attachmentService.selectAttachmentById(attachmentId); + return R.ok(vo); + } + + /** + * 上传附件 + * + * @param applicationId 申请ID + * @param title 附件标题 + * @param content 附件内容说明 + * @param file 文件 + */ + @SaCheckPermission("rs:overseas:edit") + @Log(title = "上传出国申请附件", businessType = BusinessType.INSERT) + @PostMapping("/upload") + public R upload( + @NotNull(message = "申请ID不能为空") @RequestParam Long applicationId, + @RequestParam String title, + @RequestParam(required = false) String content, + @RequestParam("file") MultipartFile file) { + try { + RsOverseasApplicationAttachmentVo vo = attachmentService.uploadAttachment( + applicationId, title, content, file); + return R.ok("上传成功", vo); + } catch (Exception e) { + log.error("上传附件失败", e); + return R.fail("上传失败:" + e.getMessage()); + } + } + + /** + * 更新附件信息(标题和内容) + */ + @SaCheckPermission("rs:overseas:edit") + @Log(title = "更新出国申请附件", businessType = BusinessType.UPDATE) + @PutMapping + public R update(@Validated @RequestBody RsOverseasApplicationAttachmentBo bo) { + boolean success = attachmentService.updateAttachment(bo); + return toAjax(success); + } + + /** + * 删除附件 + */ + @SaCheckPermission("rs:overseas:remove") + @Log(title = "删除出国申请附件", businessType = BusinessType.DELETE) + @DeleteMapping("/{attachmentId}") + public R remove(@NotNull(message = "附件ID不能为空") @PathVariable Long attachmentId) { + boolean success = attachmentService.deleteAttachment(attachmentId); + return toAjax(success); + } + + /** + * 批量删除附件 + */ + @SaCheckPermission("rs:overseas:remove") + @Log(title = "批量删除出国申请附件", businessType = BusinessType.DELETE) + @DeleteMapping("/batch") + public R removeBatch(@RequestBody Long[] attachmentIds) { + boolean success = attachmentService.deleteAttachments(attachmentIds); + return toAjax(success); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java index 7b101631..a02dff6d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java @@ -26,15 +26,28 @@ import org.dromara.common.core.validate.EditGroup; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.satoken.utils.LoginHelper; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import lombok.extern.slf4j.Slf4j; import org.dromara.rs.domain.bo.RsOverseasApplicationBo; +import org.dromara.rs.domain.bo.GenerateRegistrationFormBo; import org.dromara.rs.domain.vo.RsOverseasApplicationVo; import org.dromara.rs.domain.vo.RsOverseasApprovalVo; +import org.dromara.rs.domain.vo.RegistrationFormVO; import org.dromara.rs.domain.query.RsOverseasApplicationQuery; import org.dromara.rs.service.IRsOverseasApplicationService; import org.dromara.rs.service.IRsOverseasApprovalService; +import org.dromara.rs.service.IRegistrationFormService; import java.util.Date; import java.util.Map; import java.util.HashMap; +import org.springframework.web.multipart.MultipartFile; +import org.dromara.common.oss.core.OssClient; +import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.oss.entity.UploadResult; +import java.io.IOException; +import java.text.SimpleDateFormat; +import org.dromara.system.service.ISysOssService; +import org.dromara.system.domain.vo.SysOssVo; /** * 出国申请Controller @@ -42,6 +55,7 @@ import java.util.HashMap; * @author weizzzz * @date 2025-07-28 */ +@Slf4j @Validated @RestController @RequestMapping("/rs/overseas/application") @@ -53,6 +67,12 @@ public class RsOverseasApplicationController extends BaseController { @Autowired private IRsOverseasApprovalService overseasApprovalService; + @Autowired + private IRegistrationFormService registrationFormService; + + @Autowired + private ISysOssService ossService; + /** * 查询出国申请列表 */ @@ -76,47 +96,27 @@ public class RsOverseasApplicationController extends BaseController { } /** - * 查询待审批的申请列表(有审批权限的用户可以看到所有待审批申请) + * 查询当前用户的待审批申请列表(五级审批模式) */ - @SaCheckPermission("rs:overseas:approve") + @SaCheckPermission("rs:overseasapproval:list") @GetMapping("/pending/list") - public TableDataInfo pendingList(RsOverseasApplicationQuery query) { - // 不限制currentApprover,让有审批权限的用户看到所有待审批申请 - // String currentUser = LoginHelper.getUsername(); - // query.setCurrentApprover(currentUser); - query.setStatus("0"); // 只查询待审核状态 - query.setTenantId(LoginHelper.getTenantId()); // 设置租户ID - return applicationService.queryPageList(query); + public R> pendingList() { + List list = applicationService.queryPendingByCurrentUser(); + return R.ok(list); } - /** - * 临时测试API - 查询待审批的申请列表(调试用) - */ - @SaCheckPermission("rs:overseas:approve") - @GetMapping("/pending/debug") - public R pendingDebug(RsOverseasApplicationQuery query) { - // 获取当前用户信息 - String currentUser = LoginHelper.getUsername(); - Long userId = LoginHelper.getUserId(); - String tenantId = LoginHelper.getTenantId(); - - // 设置查询条件 - query.setStatus("0"); // 只查询待审核状态 - query.setTenantId(tenantId); // 设置租户ID - - // 执行查询 - TableDataInfo result = applicationService.queryPageList(query); - - // 返回调试信息 - Map debugInfo = new HashMap<>(); - debugInfo.put("currentUser", currentUser); - debugInfo.put("userId", userId); - debugInfo.put("tenantId", tenantId); - debugInfo.put("queryParams", query); - debugInfo.put("result", result); - - return R.ok(debugInfo); - } + // ======================================== + // 五级审批模式说明 + // + // 当前实现:自定义五级审批流程 + // 1. 部门负责人审批(根据申请人部门动态分配) + // 2. 人事处审批(角色:hr_dept_overseas_approver) + // 3. 纪委审批(角色:discipline_overseas_approver) + // 4. 学院分管领导审批(角色:college_leader_overseas_approver) + // 5. 学院党委书记审批(角色:party_secretary_overseas_approver) + // + // 审批权限控制:每个审批人只能看到分配给自己的待审批申请 + // ======================================== /** * 根据状态查询申请列表 @@ -139,40 +139,85 @@ public class RsOverseasApplicationController extends BaseController { ExcelUtil.exportExcel(list, "出国申请数据", RsOverseasApplicationVo.class, response); } + /** + * 下载登记备案表模板 + */ + @GetMapping("/downloadRegistrationTemplate") + public void downloadRegistrationTemplate(HttpServletResponse response) { + registrationFormService.downloadRegistrationTemplate(response); + } + /** * 获取出国申请详细信息 */ @SaCheckPermission("rs:overseas:query") - @GetMapping("/{applicationId}") + @GetMapping("/detail/{applicationId}") public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long applicationId) { return R.ok(applicationService.queryById(applicationId)); } + /** + * 获取申请人的部门负责人ID + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/dept-approver/{applicationId}") + public R getDepartmentApproverId(@PathVariable Long applicationId) { + RsOverseasApplicationVo application = applicationService.queryById(applicationId); + if (application == null) { + return R.fail("申请不存在"); + } + Long deptLeaderId = applicationService.getDepartmentApproverId(application.getApplicantId()); + if (deptLeaderId == null) { + return R.fail("未找到部门负责人,请联系管理员配置"); + } + return R.ok(deptLeaderId); + } + /** * 新增出国申请 */ @SaCheckPermission("rs:overseas:add") @Log(title = "出国申请", businessType = BusinessType.INSERT) @PostMapping - public R add(@Validated(AddGroup.class) @RequestBody RsOverseasApplicationBo bo) { + public R> add(@Validated(AddGroup.class) @RequestBody RsOverseasApplicationBo bo) { + // 🔍 调试日志:检查接收到的关键字段 + System.out.println("========== Controller 接收到的数据 =========="); + System.out.println("emergencyContact: " + bo.getEmergencyContact()); + System.out.println("destination: " + bo.getDestination()); + System.out.println("idNumber: " + bo.getIdNumber()); + // unitContact 字段已删除 + // System.out.println("unitContact: " + bo.getUnitContact()); + System.out.println("==============================================="); + // 自动设置申请人信息 bo.setApplicantId(LoginHelper.getUserId()); - bo.setApplicantName(LoginHelper.getUsername()); - + // ✅ 不覆盖用户填写的姓名,保留用户在表单中填写的申请证件姓名 + // 自动设置部门信息 bo.setDepartment(LoginHelper.getDeptName()); - + + // 🔴 关键修复:设置租户ID,否则列表查询时会因租户ID过滤而看不到数据 + bo.setTenantId(LoginHelper.getTenantId()); + // 自动计算出国天数 if (bo.getStartDate() != null && bo.getEndDate() != null) { long diffTime = Math.abs(bo.getEndDate().getTime() - bo.getStartDate().getTime()); int diffDays = (int) Math.ceil(diffTime / (1000.0 * 60 * 60 * 24)) + 1; bo.setDuration(diffDays); } - + // 状态由Service层控制,新增时为草稿状态 bo.setApplyTime(new Date()); - - return toAjax(applicationService.insertByBo(bo)); + + boolean result = applicationService.insertByBo(bo); + if (result) { + // 🔴 返回新创建的applicationId,供前端上传文件时使用 + Map data = new HashMap<>(); + data.put("applicationId", bo.getApplicationId()); + return R.ok("操作成功", data); + } else { + return R.fail("新增失败"); + } } /** @@ -192,6 +237,28 @@ public class RsOverseasApplicationController extends BaseController { @Log(title = "提交出国申请", businessType = BusinessType.UPDATE) @PutMapping("/submit/{applicationId}") public R submit(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) { + // 获取申请详情进行校验 + RsOverseasApplicationVo application = applicationService.queryById(applicationId); + if (application == null) { + return R.fail("申请不存在"); + } + + // 1. 校验出国登记前置条件(已禁用 - 允许直接提交申请) + // java.util.Map registrationValidation = applicationService + // .validateRegistrationPrerequisite(application.getApplicantId()); + // Boolean registrationValid = (Boolean) registrationValidation.get("isValid"); + // if (!registrationValid) { + // String message = (String) registrationValidation.get("message"); + // return R.fail(message); + // } + + // 2. 校验是否上传了盖章文件(可选 - 根据业务需要决定是否保留) + // if (application.getStampedFileUploadStatus() == null || + // !"1".equals(application.getStampedFileUploadStatus())) { + // return R.fail("请先下载登记备案附件填写盖章后上传,才能提交申请!"); + // } + + // 所有校验通过,执行提交操作 return toAjax(applicationService.submitApplication(applicationId)); } @@ -202,10 +269,31 @@ public class RsOverseasApplicationController extends BaseController { @Log(title = "重新提交出国申请", businessType = BusinessType.UPDATE) @PutMapping("/resubmit/{applicationId}") public R resubmit(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId, - @Validated @RequestBody RsOverseasApplicationBo bo) { + @Validated @RequestBody RsOverseasApplicationBo bo) { return toAjax(applicationService.resubmitApplication(applicationId, bo)); } + /** + * 校验出国申请前置条件 + * 用于前端在填写表单时实时提示用户 + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/validatePrerequisites") + public R> validatePrerequisites( + @RequestParam(required = false) String politicalStatus, + @RequestParam(required = false) Integer duration) { + + Long applicantId = LoginHelper.getUserId(); + java.util.Map result = new java.util.HashMap<>(); + + // 1. 校验出国登记前置条件 + java.util.Map registrationValidation = applicationService + .validateRegistrationPrerequisite(applicantId); + result.put("registrationValidation", registrationValidation); + + return R.ok(result); + } + /** * 取消出国申请 */ @@ -213,22 +301,36 @@ public class RsOverseasApplicationController extends BaseController { @Log(title = "取消出国申请", businessType = BusinessType.UPDATE) @PutMapping("/cancel/{applicationId}") public R cancel(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId, - @RequestParam(required = false) String reason) { + @RequestParam(required = false) String reason) { return toAjax(applicationService.cancelApplication(applicationId, reason)); } /** - * 审批出国申请 + * 审批出国申请(支持标准工作流模式) + * 🔴 支持五级审批权限:满足任一级别权限即可访问接口,具体级别在Service层验证 */ - @SaCheckPermission("rs:overseas:approve") + @SaCheckPermission(value = { + "rs:overseas:approve", // 通用审批权限(超级管理员/高级审批者) + "rs:overseas:approve1", // 第1级:部门负责人 + "rs:overseas:approve2", // 第2级:人事处 + "rs:overseas:approve3", // 第3级:纪委 + "rs:overseas:approve4", // 第4级:学院分管领导 + "rs:overseas:approve5" // 第5级:学院党委书记 + }, mode = SaMode.OR) // 🔴 关键:OR模式,满足任一权限即可通过 @Log(title = "审批出国申请", businessType = BusinessType.UPDATE) @PostMapping("/approve") public R approve(@RequestBody ApprovalRequest request) { Long approverId = LoginHelper.getUserId(); String approverName = LoginHelper.getUsername(); + + // 🔴 标准工作流模式:支持任务ID参数 + // 如果传入了taskId,将通过工作流引擎进行审批 + // 如果没有taskId,则使用原有的业务审批逻辑(向后兼容) + return toAjax(applicationService.approveApplication( - request.getApplicationId(), approverId, approverName, - request.getApprovalStatus(), request.getApprovalOpinion())); + request.getApplicationId(), approverId, approverName, + request.getApprovalStatus(), request.getApprovalOpinion(), + request.getApprovalSignature(), request.getTaskId())); } /** @@ -247,7 +349,8 @@ public class RsOverseasApplicationController extends BaseController { @SaCheckPermission("rs:overseas:query") @GetMapping("/approval/history/{applicationId}") public R> getApprovalHistory(@PathVariable("applicationId") Long applicationId) { - List approvalList = overseasApprovalService.selectApprovalHistoryByApplicationId(applicationId); + List approvalList = overseasApprovalService + .selectApprovalHistoryByApplicationId(applicationId); return R.ok(approvalList); } @@ -267,12 +370,14 @@ public class RsOverseasApplicationController extends BaseController { } /** - * 审批请求对象 + * 审批请求对象(支持标准工作流模式) */ public static class ApprovalRequest { private Long applicationId; private String approvalStatus; private String approvalOpinion; + private String approvalSignature; // 审批签字(base64图片) + private String taskId; // 🔴 新增:工作流任务ID,支持标准工作流模式 public Long getApplicationId() { return applicationId; @@ -297,5 +402,247 @@ public class RsOverseasApplicationController extends BaseController { public void setApprovalOpinion(String approvalOpinion) { this.approvalOpinion = approvalOpinion; } + + public String getApprovalSignature() { + return approvalSignature; + } + + public void setApprovalSignature(String approvalSignature) { + this.approvalSignature = approvalSignature; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } } -} \ No newline at end of file + + // ==================== PDF生成相关接口 ==================== + + /** + * 生成出国申请登记表PDF + */ + @SaCheckPermission("rs:overseas:generate") + @PostMapping("/generateRegistrationForm") + @Log(title = "生成出国申请登记表", businessType = BusinessType.OTHER) + public R generateRegistrationForm(@RequestBody @Validated GenerateRegistrationFormBo bo) { + RegistrationFormVO result = registrationFormService.generateRegistrationFormPdf(bo); + return R.ok(result); + } + + /** + * 下载生成的PDF文档 + */ + @SaCheckPermission("rs:overseas:download") + @GetMapping("/downloadPdf/{documentId}") + public void downloadPdf(@PathVariable String documentId, HttpServletResponse response) { + registrationFormService.downloadPdf(documentId, response); + } + + /** + * 预览生成的PDF文档 + */ + @SaCheckPermission("rs:overseas:preview") + @GetMapping("/previewPdf/{documentId}") + public void previewPdf(@PathVariable String documentId, HttpServletResponse response) { + registrationFormService.downloadPdf(documentId, response); + } + + // ==================== 动态审批人获取API ==================== + + /** + * 根据申请人ID获取部门审批人ID + * + * @param applicantId 申请人ID + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/getDepartmentApproverByApplicant/{applicantId}") + public R getDepartmentApproverByApplicant(@PathVariable Long applicantId) { + Long departmentApproverId = applicationService.getDepartmentApproverId(applicantId); + return R.ok(departmentApproverId); + } + + /** + * 根据申请人ID获取人事处审批人列表 + * + * @param applicantId 申请人ID + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/getHrApprovers/{applicantId}") + public R> getHrApprovers(@PathVariable Long applicantId) { + List hrApprovers = applicationService.getHrApprovers(applicantId); + return R.ok(hrApprovers); + } + + // ==================== 盖章文件上传接口 ==================== + + /** + * 上传登记备案表盖章文件 + */ + @SaCheckPermission("rs:overseas:add") + @PostMapping("/uploadStampedFile") + @Log(title = "上传盖章文件", businessType = BusinessType.OTHER) + public R> uploadStampedFile(@RequestParam("file") MultipartFile file, + @RequestParam("applicationId") Long applicationId) { + try { + // 验证文件格式 + String contentType = file.getContentType(); + if (!isValidFileType(contentType)) { + return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件"); + } + + // 验证文件大小(10MB) + long maxSize = 10 * 1024 * 1024; // 10MB + if (file.getSize() > maxSize) { + return R.fail("文件大小超过10MB限制"); + } + + // 使用系统OSS服务上传文件(自动保存到sys_oss表) + SysOssVo ossResult = ossService.upload(file); + + // 更新申请记录 - 存储OSS ID和文件路径 + applicationService.updateStampedFileInfo( + applicationId, + ossResult.getUrl(), // 文件URL + ossResult.getOriginalName(), // 原始文件名 + new Date(), // 上传时间 + "1", // 上传状态:已上传 + ossResult.getOssId() // OSS文件ID + ); + + // 返回结果 + Map result = new HashMap<>(); + result.put("filePath", ossResult.getUrl()); + result.put("fileName", ossResult.getOriginalName()); + result.put("uploadTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + result.put("ossId", ossResult.getOssId()); + + return R.ok("盖章文件上传成功", result); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("系统异常:" + e.getMessage()); + } + } + + /** + * 下载盖章文件(安全下载) + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/downloadStampedFile/{applicationId}") + public void downloadStampedFile(@PathVariable Long applicationId, HttpServletResponse response) { + try { + RsOverseasApplicationVo application = applicationService.queryById(applicationId); + if (application == null || application.getStampedFileOssId() == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}"); + return; + } + + // 使用系统OSS服务进行安全下载 + ossService.secureDownload(application.getStampedFileOssId(), response); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // ==================== 党籍保留附件上传接口 ==================== + + /** + * 上传党籍保留附件 + */ + @SaCheckPermission("rs:overseas:add") + @PostMapping("/uploadPartyRetentionFile") + @Log(title = "上传党籍保留附件", businessType = BusinessType.OTHER) + public R> uploadPartyRetentionFile(@RequestParam("file") MultipartFile file, + @RequestParam("applicationId") Long applicationId) { + try { + // 验证文件格式 + String contentType = file.getContentType(); + if (!isValidFileType(contentType)) { + return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件"); + } + + // 验证文件大小(10MB) + long maxSize = 10 * 1024 * 1024; // 10MB + if (file.getSize() > maxSize) { + return R.fail("文件大小超过10MB限制"); + } + + // 使用系统OSS服务上传文件(自动保存到sys_oss表) + SysOssVo ossResult = ossService.upload(file); + + // 更新申请记录 - 存储OSS ID和文件路径 + applicationService.updatePartyRetentionFileInfo( + applicationId, + ossResult.getUrl(), // 文件URL + ossResult.getOriginalName(), // 原始文件名 + new Date(), // 上传时间 + "1", // 上传状态:已上传 + ossResult.getOssId() // OSS文件ID + ); + + // 返回结果 + Map result = new HashMap<>(); + result.put("filePath", ossResult.getUrl()); + result.put("fileName", ossResult.getOriginalName()); + result.put("uploadTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + result.put("ossId", ossResult.getOssId()); + + return R.ok("党籍保留附件上传成功", result); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("系统异常:" + e.getMessage()); + } + } + + /** + * 下载党籍保留附件(安全下载) + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/downloadPartyRetentionFile/{applicationId}") + public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) { + try { + RsOverseasApplicationVo application = applicationService.queryById(applicationId); + if (application == null || application.getPartyRetentionFileOssId() == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}"); + return; + } + + // 使用系统OSS服务进行安全下载 + ossService.secureDownload(application.getPartyRetentionFileOssId(), response); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 验证文件类型 + */ + private boolean isValidFileType(String contentType) { + if (contentType == null) + return false; + return contentType.equals("application/pdf") || + contentType.equals("image/jpeg") || + contentType.equals("image/jpg") || + contentType.equals("image/png"); + } + + /** + * 获取文件扩展名 + */ + private String getFileExtension(String filename) { + if (filename == null || !filename.contains(".")) { + return ".pdf"; + } + return filename.substring(filename.lastIndexOf(".")); + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java index 8afe6ad8..87eb3ecd 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java @@ -25,11 +25,14 @@ import org.dromara.common.core.validate.EditGroup; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.satoken.utils.LoginHelper; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; import org.dromara.rs.domain.bo.RsOverseasApprovalBo; import org.dromara.rs.domain.vo.RsOverseasApprovalVo; import org.dromara.rs.domain.query.RsOverseasApprovalQuery; import org.dromara.rs.service.IRsOverseasApprovalService; import lombok.extern.slf4j.Slf4j; +import java.util.Map; +import java.util.HashMap; /** * 出国审批记录Controller @@ -75,6 +78,40 @@ public class RsOverseasApprovalController extends BaseController { return approvalService.queryCombinedPageList(query); } + /** + * 调试接口:查看当前用户的权限过滤情况 + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/debug/permission") + public R> debugPermission(RsOverseasApprovalQuery query) { + Map debugInfo = new HashMap<>(); + + // 当前用户信息 + debugInfo.put("currentUserId", LoginHelper.getUserId()); + debugInfo.put("currentUsername", LoginHelper.getUsername()); + debugInfo.put("currentDeptId", LoginHelper.getDeptId()); + debugInfo.put("isSuperAdmin", LoginHelper.isSuperAdmin()); + + // 角色信息 + debugInfo.put("hasRsApplicant", StpUtil.hasRole("rs-Applicant")); + debugInfo.put("hasRsAdmin", StpUtil.hasRole("rs-admin")); + debugInfo.put("hasApprove1Permission", StpUtil.hasPermission("rs:overseas:approve1")); + debugInfo.put("hasDeptHeadRole", StpUtil.hasRole("dept_head_role")); + debugInfo.put("hasOldApprovePermission", StpUtil.hasPermission("overseas:application:dept_head:approve")); + + // 查询参数(应用权限过滤前) + debugInfo.put("originalQuery", query.toString()); + + // 应用权限过滤 + TableDataInfo result = approvalService.queryCombinedPageList(query); + + // 查询参数(应用权限过滤后) + debugInfo.put("filteredQuery", query.toString()); + debugInfo.put("resultCount", result.getTotal()); + + return R.ok(debugInfo); + } + /** * 根据申请ID查询审批记录列表 */ @@ -153,4 +190,4 @@ public class RsOverseasApprovalController extends BaseController { return R.fail("修复失败:" + e.getMessage()); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java new file mode 100644 index 00000000..e91aac59 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java @@ -0,0 +1,129 @@ +package org.dromara.rs.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.web.core.BaseController; +import org.dromara.rs.domain.vo.RsOverseasApprovalMonitorVo; +import org.dromara.rs.domain.query.RsOverseasApprovalMonitorQuery; +import org.dromara.rs.service.IRsOverseasApprovalMonitorService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 出国申请审批监控Controller + *

+ * 功能说明: + * 1. 实时查看出国申请的审批流程(当前在哪个审批环节、审批人是谁) + * 2. 查看已完成审批的出国申请 + * + * @author Cursor AI + * @date 2025-10-21 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/rs/overseas/monitor") +public class RsOverseasApprovalMonitorController extends BaseController { + + private final IRsOverseasApprovalMonitorService monitorService; + + /** + * 查询待审批的申请列表(出国申请进度查询) + *

+ * 🔴 功能说明:查询所有待审批申请(不限制申请人) + * - 拥有权限的用户可以查看所有待审批申请 + * - 显示当前审批环节和审批人 + * - 可以查看审批历史记录 + * + * @param query 查询条件 + * @return 所有待审批申请列表 + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/pending/list") + public TableDataInfo pendingList(RsOverseasApprovalMonitorQuery query) { + log.info("查询所有待审批申请列表"); + // 只查询待审核状态(status = 0) + query.setStatus("0"); + return monitorService.queryPendingList(query); + } + + /** + * 查询已完成审批的申请列表 + *

+ * 🔴 功能说明:查询所有已完成审批申请(不限制申请人) + * - 拥有权限的用户可以查看所有已完成审批申请 + * - 显示最终审批结果和完成时间 + * - 可以查看所有审批环节的历史记录 + * + * @param query 查询条件 + * @return 所有已完成审批的申请列表 + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/completed/list") + public TableDataInfo completedList(RsOverseasApprovalMonitorQuery query) { + log.info("查询所有已完成审批的申请列表,查询条件:{}", query); + // 只查询已通过状态(status = 1) + query.setStatus("1"); + return monitorService.queryCompletedList(query); + } + + /** + * 查询申请详情(含审批流程信息) + *

+ * 获取申请的完整信息,包括: + * - 申请基本信息 + * - 当前审批状态 + * - 审批历史记录 + * + * @param applicationId 申请ID + * @return 申请详情 + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/detail/{applicationId}") + public R getDetail(@PathVariable @NotNull(message = "申请ID不能为空") Long applicationId) { + log.info("查询申请详情,申请ID:{}", applicationId); + RsOverseasApprovalMonitorVo detail = monitorService.queryDetailWithApprovalInfo(applicationId); + return R.ok(detail); + } + + /** + * 统计审批进度概况 + *

+ * 返回审批监控的统计数据: + * - 待审批总数 + * - 各审批级别的待审批数量 + * - 已完成审批总数 + * - 今日新增申请数 + * + * @return 统计数据 + */ + @SaCheckPermission("rs:overseas:list") + @GetMapping("/statistics") + public R statistics() { + log.info("查询审批进度统计"); + return R.ok(monitorService.getApprovalStatistics()); + } + + /** + * 下载出国申请审批表PDF + *

+ * 生成并下载"杭州第一技师学院登记报备人员因私事出国(境)审批表"PDF文件 + * + * @param applicationId 申请ID + * @param response HTTP响应对象 + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/downloadApprovalPdf/{applicationId}") + public void downloadApprovalPdf( + @PathVariable @NotNull(message = "申请ID不能为空") Long applicationId, + HttpServletResponse response) { + log.info("下载出国申请审批表PDF,申请ID:{}", applicationId); + monitorService.downloadApprovalPdf(applicationId, response); + } +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasItineraryChangeController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasItineraryChangeController.java new file mode 100644 index 00000000..dbcd2194 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasItineraryChangeController.java @@ -0,0 +1,187 @@ +package org.dromara.rs.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.validation.constraints.NotEmpty; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.common.web.core.BaseController; +import org.dromara.rs.domain.bo.RsOverseasItineraryChangeBo; +import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery; +import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo; +import org.dromara.rs.domain.vo.RsOverseasRecordVo; +import org.dromara.rs.service.IRsOverseasItineraryChangeService; +import org.dromara.rs.service.IRsOverseasRecordService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * 出国行程变更申请Controller + * + * @author weizzzz + * @date 2025-10-13 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/rs/itinerary/change") +public class RsOverseasItineraryChangeController extends BaseController { + + private final IRsOverseasItineraryChangeService changeService; + private final IRsOverseasRecordService recordService; + + /** + * 查询行程变更申请列表 + */ + @SaCheckPermission("rs:itineraryChange:list") + @GetMapping("/list") + public TableDataInfo list(RsOverseasItineraryChangeQuery query) { + query.setTenantId(LoginHelper.getTenantId()); + return changeService.queryPageList(query); + } + + /** + * 查询我的变更记录列表(只显示已通过的变更申请) + * 说明:变更记录Tab只显示已通过审批的变更申请,这些是真正生效的变更记录 + */ + @SaCheckPermission("rs:itineraryChange:list") + @GetMapping("/my/list") + public TableDataInfo myList(RsOverseasItineraryChangeQuery query) { + query.setTenantId(LoginHelper.getTenantId()); + return changeService.queryMyChangeList(query); + } + + /** + * 🔴 标准化改造:删除自定义待审批查询接口 + * + * 原因:出国行程变更工作流应使用标准工作流模式 + * 用户应通过工作流API pageByTaskWait() 获取待办任务 + * 而不是调用自定义的业务API + */ + @SaCheckPermission("rs:itineraryChange:approve") + @GetMapping("/pending/list") + public R pendingList(RsOverseasItineraryChangeQuery query) { + // 提示用户使用标准工作流API + return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务"); + } + + /** + * 查询行程变更申请详情 + */ + @SaCheckPermission("rs:itineraryChange:query") + @GetMapping("/{changeId}") + public R getInfo(@PathVariable Long changeId) { + return R.ok(changeService.queryById(changeId)); + } + + /** + * 获取可用的出国记录列表(用于选择原出国申请) + * 说明:选择的是出国记录选项卡中的记录 + */ + @SaCheckPermission("rs:itineraryChange:list") + @GetMapping("/available-records") + public R> getAvailableRecords() { + // 查询当前用户的所有出国记录 + org.dromara.rs.domain.query.RsOverseasRecordQuery query = new org.dromara.rs.domain.query.RsOverseasRecordQuery(); + query.setApplicantId(LoginHelper.getUserId()); + query.setTenantId(LoginHelper.getTenantId()); + + List records = recordService.queryList(query); + return R.ok(records); + } + + /** + * 根据出国记录ID查询记录详情(用于显示原申请信息) + */ + @SaCheckPermission("rs:itineraryChange:query") + @GetMapping("/record/{recordId}") + public R getRecordInfo(@PathVariable Long recordId) { + return R.ok(recordService.queryById(recordId)); + } + + /** + * 新增行程变更申请 + */ + @SaCheckPermission("rs:itineraryChange:add") + @Log(title = "行程变更申请", businessType = BusinessType.INSERT) + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody RsOverseasItineraryChangeBo bo) { + bo.setTenantId(LoginHelper.getTenantId()); + return toAjax(changeService.insertByBo(bo)); + } + + /** + * 修改行程变更申请(重新提交) + */ + @SaCheckPermission("rs:itineraryChange:edit") + @Log(title = "行程变更申请", businessType = BusinessType.UPDATE) + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody RsOverseasItineraryChangeBo bo) { + return toAjax(changeService.updateByBo(bo)); + } + + /** + * 删除行程变更申请 + */ + @SaCheckPermission("rs:itineraryChange:remove") + @Log(title = "行程变更申请", businessType = BusinessType.DELETE) + @DeleteMapping("/{changeIds}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] changeIds) { + return toAjax(changeService.deleteByIds(List.of(changeIds))); + } + + /** + * 撤回行程变更申请 + */ + @SaCheckPermission("rs:itineraryChange:cancel") + @Log(title = "行程变更申请", businessType = BusinessType.UPDATE) + @PutMapping("/cancel/{changeId}") + public R cancel(@PathVariable Long changeId) { + return toAjax(changeService.cancelChange(changeId)); + } + + /** + * 审批行程变更申请 + */ + @SaCheckPermission("rs:itineraryChange:approve") + @Log(title = "行程变更申请", businessType = BusinessType.UPDATE) + @PutMapping("/approve") + public R approve(@RequestBody Map params) { + Long changeId = Long.valueOf(params.get("changeId").toString()); + Boolean approved = Boolean.valueOf(params.get("approved").toString()); + String opinion = params.get("opinion") != null ? params.get("opinion").toString() : ""; + + return toAjax(changeService.approveChange(changeId, approved, opinion)); + } + + /** + * 导出行程变更申请列表 + */ + @SaCheckPermission("rs:itineraryChange:export") + @Log(title = "行程变更申请", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(RsOverseasItineraryChangeQuery query, HttpServletResponse response) { + query.setTenantId(LoginHelper.getTenantId()); + List list = changeService.queryList(query); + ExcelUtil.exportExcel(list, "行程变更申请", RsOverseasItineraryChangeVo.class, response); + } + + /** + * 查询某个出国记录的所有变更历史 + */ + @SaCheckPermission("rs:itineraryChange:query") + @GetMapping("/record/{recordId}/history") + public R> getChangeHistory(@PathVariable Long recordId) { + return R.ok(changeService.queryByRecordId(recordId)); + } +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java index f95d4cf0..3d414703 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java @@ -30,6 +30,12 @@ import org.dromara.rs.domain.bo.RsOverseasRecordBo; import org.dromara.rs.domain.vo.RsOverseasRecordVo; import org.dromara.rs.domain.query.RsOverseasRecordQuery; import org.dromara.rs.service.IRsOverseasRecordService; +import org.dromara.rs.service.IRegistrationFormService; +import org.dromara.rs.util.OverseasReceiptPdfUtil; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; /** * 出国记录Controller @@ -45,6 +51,9 @@ public class RsOverseasRecordController extends BaseController { @Autowired private IRsOverseasRecordService recordService; + @Autowired + private IRegistrationFormService registrationFormService; + /** * 查询出国记录列表 */ @@ -61,8 +70,17 @@ public class RsOverseasRecordController extends BaseController { @GetMapping("/my/list") public TableDataInfo myList(RsOverseasRecordQuery query) { Long userId = LoginHelper.getUserId(); + System.out.println("========== 出国记录Controller: 接收到查询请求 =========="); + System.out.println("当前用户ID: " + userId); + System.out.println("原始查询参数: " + query); query.setApplicantId(userId); - return recordService.queryMyRecordPageList(query); + System.out.println("设置申请人ID后: " + query); + + TableDataInfo result = recordService.queryMyRecordPageList(query); + System.out.println("Controller返回结果: total=" + result.getTotal() + ", rows=" + + (result.getRows() != null ? result.getRows().size() : 0)); + System.out.println("========== 出国记录Controller: 查询完成 =========="); + return result; } /** @@ -86,6 +104,17 @@ public class RsOverseasRecordController extends BaseController { ExcelUtil.exportExcel(list, "出国记录数据", RsOverseasRecordVo.class, response); } + /** + * 下载党籍保留申请PDF模板 + * 注意:此接口必须放在 /{recordId} 之前,避免路由冲突 + */ + @SaCheckPermission("rs:overseas:query") + @Log(title = "下载党籍保留申请模板", businessType = BusinessType.EXPORT) + @GetMapping("/downloadPartyRetention") + public void downloadPartyRetention(HttpServletResponse response) { + registrationFormService.downloadPartyMembershipTemplate(response); + } + /** * 获取出国记录详细信息 */ @@ -133,8 +162,8 @@ public class RsOverseasRecordController extends BaseController { @PutMapping("/complete") public R updateCompletion(@RequestBody CompletionRequest request) { return toAjax(recordService.updateCompletionStatus( - request.getRecordId(), request.getCompletionStatus(), - request.getAchievement(), request.getReportContent())); + request.getRecordId(), request.getCompletionStatus(), + request.getAchievement(), request.getReportContent())); } /** @@ -147,6 +176,53 @@ public class RsOverseasRecordController extends BaseController { return toAjax(recordService.deleteWithValidByIds(recordIds, true)); } + /** + * 下载因私出国(境)情况回执PDF + */ + @SaCheckPermission("rs:overseas:query") + @Log(title = "下载出国情况回执", businessType = BusinessType.EXPORT) + @GetMapping("/downloadReceipt/{recordId}") + public void downloadReceipt(@NotNull(message = "记录ID不能为空") @PathVariable Long recordId, + HttpServletResponse response) { + try { + // 查询出国记录详情 + RsOverseasRecordVo record = recordService.queryById(recordId); + if (record == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":404,\"msg\":\"出国记录不存在\"}"); + return; + } + + // 生成PDF + byte[] pdfBytes = OverseasReceiptPdfUtil.generateReceiptPdf(record); + + // 设置响应头 + String fileName = String.format("因私出国情况回执_%s_%s.pdf", + record.getApplicantName(), + new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())); + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"); + + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); + response.setContentLength(pdfBytes.length); + + // 输出PDF + response.getOutputStream().write(pdfBytes); + response.getOutputStream().flush(); + + } catch (Exception e) { + try { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":500,\"msg\":\"PDF生成失败: " + e.getMessage() + "\"}"); + } catch (IOException ioException) { + // 记录日志 + System.err.println("PDF生成失败: " + e.getMessage()); + } + } + } + /** * 初始化测试数据(临时接口,用于解决出国记录列表显示问题) */ @@ -156,7 +232,7 @@ public class RsOverseasRecordController extends BaseController { try { Long currentUserId = LoginHelper.getUserId(); String currentUserName = LoginHelper.getUsername(); - + // 创建一个测试出国记录 RsOverseasRecordBo testRecord = new RsOverseasRecordBo(); testRecord.setApplicationId(1L); @@ -173,9 +249,9 @@ public class RsOverseasRecordController extends BaseController { testRecord.setAchievement("参加国际智能控制学术会议,发表论文,与德国同行建立合作关系"); testRecord.setReportContent("此次德国学术交流收获丰富,重点学习了最新的智能控制理论和技术"); testRecord.setCertificatePath("/uploads/overseas/records/2025/08/conference_certificate.pdf"); - + recordService.insertByBo(testRecord); - + return R.ok("测试数据初始化成功,请刷新页面查看"); } catch (Exception e) { return R.fail("测试数据初始化失败: " + e.getMessage()); @@ -223,4 +299,4 @@ public class RsOverseasRecordController extends BaseController { this.reportContent = reportContent; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRegistrationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRegistrationController.java new file mode 100644 index 00000000..d73f0f0f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRegistrationController.java @@ -0,0 +1,206 @@ +package org.dromara.rs.controller; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.satoken.utils.LoginHelper; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.dromara.rs.domain.bo.RsOverseasRegistrationBo; +import org.dromara.rs.domain.vo.RsOverseasRegistrationVo; +import org.dromara.rs.domain.query.RsOverseasRegistrationQuery; +import org.dromara.rs.service.IRsOverseasRegistrationService; + +/** + * 出国登记Controller + * + * @author weizzzz + * @date 2025-09-30 + */ +@Validated +@RestController +@RequestMapping("/rs/overseas/registration") +public class RsOverseasRegistrationController extends BaseController { + + @Autowired + private IRsOverseasRegistrationService registrationService; + + /** + * 查询出国登记列表 + */ + @SaCheckPermission("rs:overseas:registration:list") + @GetMapping("/list") + public TableDataInfo list(RsOverseasRegistrationQuery query) { + return registrationService.queryPageList(query); + } + + /** + * 查询我的出国登记列表 + */ + @SaCheckPermission("rs:overseas:registration:list") + @GetMapping("/my/list") + public TableDataInfo myList(RsOverseasRegistrationQuery query) { + return registrationService.queryMyRegistrationList(query); + } + + /** + * 导出出国登记列表 + */ + @SaCheckPermission("rs:overseas:registration:export") + @Log(title = "出国登记", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(RsOverseasRegistrationQuery query, HttpServletResponse response) { + List list = registrationService.queryList(query); + ExcelUtil.exportExcel(list, "出国登记", RsOverseasRegistrationVo.class, response); + } + + /** + * 获取出国登记详细信息 + */ + @SaCheckPermission("rs:overseas:registration:query") + @GetMapping("/{registrationId}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId) { + return R.ok(registrationService.queryById(registrationId)); + } + + /** + * 根据申请ID获取出国登记信息 + */ + @SaCheckPermission("rs:overseas:registration:query") + @GetMapping("/application/{applicationId}") + public R getByApplicationId(@NotNull(message = "申请ID不能为空") + @PathVariable Long applicationId) { + return R.ok(registrationService.queryByApplicationId(applicationId)); + } + + /** + * 新增出国登记 + */ + @SaCheckPermission("rs:overseas:registration:add") + @Log(title = "出国登记", businessType = BusinessType.INSERT) + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) { + return toAjax(registrationService.insertByBo(bo)); + } + + /** + * 新增并提交出国登记 + */ + @SaCheckPermission("rs:overseas:registration:add") + @Log(title = "出国登记", businessType = BusinessType.INSERT) + @PostMapping("/addAndSubmit") + public R addAndSubmit(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) { + return toAjax(registrationService.addAndSubmit(bo)); + } + + /** + * 修改出国登记 + */ + @SaCheckPermission("rs:overseas:registration:edit") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody RsOverseasRegistrationBo bo) { + return toAjax(registrationService.updateByBo(bo)); + } + + /** + * 删除出国登记 + */ + @SaCheckPermission("rs:overseas:registration:remove") + @Log(title = "出国登记", businessType = BusinessType.DELETE) + @DeleteMapping("/{registrationIds}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] registrationIds) { + return toAjax(registrationService.deleteWithValidByIds(List.of(registrationIds), true)); + } + + /** + * 提交出国登记 + */ + @SaCheckPermission("rs:overseas:registration:submit") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping("/submit/{registrationId}") + public R submit(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId) { + return toAjax(registrationService.submitRegistration(registrationId)); + } + + /** + * 确认出国登记 + */ + @SaCheckPermission("rs:overseas:registration:confirm") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping("/confirm/{registrationId}") + public R confirm(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId) { + return toAjax(registrationService.confirmRegistration(registrationId)); + } + + /** + * 撤回出国登记 + */ + @SaCheckPermission("rs:overseas:registration:withdraw") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping("/withdraw/{registrationId}") + public R withdraw(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId) { + return toAjax(registrationService.withdrawRegistration(registrationId)); + } + + /** + * 基于出国申请创建登记记录 + */ + @SaCheckPermission("rs:overseas:registration:create") + @Log(title = "出国登记", businessType = BusinessType.INSERT) + @PostMapping("/create/{applicationId}") + public R createFromApplication(@NotNull(message = "申请ID不能为空") + @PathVariable Long applicationId) { + return toAjax(registrationService.createFromApplication(applicationId)); + } + + /** + * 批准出国登记 + */ + @SaCheckPermission("rs:registration:approve") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping("/approve/{registrationId}") + public R approve(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId, + @RequestBody(required = false) RsOverseasRegistrationBo bo) { + String approveNotes = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "同意"; + return toAjax(registrationService.approveRegistration(registrationId, approveNotes)); + } + + /** + * 拒绝出国登记 + */ + @SaCheckPermission("rs:registration:reject") + @Log(title = "出国登记", businessType = BusinessType.UPDATE) + @PutMapping("/reject/{registrationId}") + public R reject(@NotNull(message = "主键不能为空") + @PathVariable Long registrationId, + @RequestBody(required = false) RsOverseasRegistrationBo bo) { + String rejectReason = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "不同意"; + return toAjax(registrationService.rejectRegistration(registrationId, rejectReason)); + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java index ad5df660..5121a954 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java @@ -19,7 +19,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.stp.StpUtil; import jakarta.servlet.http.HttpServletResponse; @@ -28,6 +27,10 @@ import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.service.ISysUserService; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.rs.domain.vo.SysUserBasicVo; +import cn.hutool.core.collection.CollUtil; + +import java.util.ArrayList; /** * 培训申请控制器 @@ -38,12 +41,12 @@ import org.dromara.common.satoken.utils.LoginHelper; @RestController @RequestMapping("/rs/training/application") public class RsTrainingApplicationController extends BaseController { - + private static final Logger log = LoggerFactory.getLogger(RsTrainingApplicationController.class); - + @Autowired private IRsTrainingApplicationService trainingApplicationService; - + // 新增用户服务注入 @Autowired private ISysUserService sysUserService; @@ -87,7 +90,7 @@ public class RsTrainingApplicationController extends BaseController { // 获取用户ID,处理可能的字符串格式 Object loginId = StpUtil.getLoginId(); Long userId = null; - + if (loginId != null) { if (loginId instanceof Long) { userId = (Long) loginId; @@ -114,18 +117,18 @@ public class RsTrainingApplicationController extends BaseController { userId = ((Number) loginId).longValue(); } } - + if (userId != null) { log.info("获取当前登录用户信息: userId={}", userId); - + // 从系统用户表获取用户真实信息 SysUserVo userVo = sysUserService.selectUserById(userId); if (userVo != null) { String nickName = userVo.getNickName(); String userName = userVo.getUserName(); - + log.info("成功获取用户信息: userId={}, nickName={}, userName={}", userId, nickName, userName); - + // 使用用户名(根据前端要求) String displayName = StringUtils.isNotBlank(userName) ? userName : "用户" + userId; return new UserInfo(userId, displayName); @@ -138,7 +141,7 @@ public class RsTrainingApplicationController extends BaseController { } catch (Exception e) { log.error("获取用户信息异常: loginId={}, error={}", StpUtil.getLoginId(), e.getMessage(), e); } - + // 如果获取失败,尝试从Sa-Token获取基本信息 try { Object fallbackLoginId = StpUtil.getLoginId(); @@ -163,26 +166,31 @@ public class RsTrainingApplicationController extends BaseController { } catch (Exception e) { log.error("备用方案也失败: {}", e.getMessage(), e); } - + // 最后的备用方案 log.error("所有用户信息获取方案都失败,返回默认值"); return new UserInfo(1L, "系统用户"); } - + /** * 用户信息内部类 */ private static class UserInfo { private final Long userId; private final String userName; - + public UserInfo(Long userId, String userName) { this.userId = userId; this.userName = StringUtils.isNotBlank(userName) ? userName : "用户" + userId; } - - public Long getUserId() { return userId; } - public String getUserName() { return userName; } + + public Long getUserId() { + return userId; + } + + public String getUserName() { + return userName; + } } /** @@ -198,17 +206,17 @@ public class RsTrainingApplicationController extends BaseController { UserInfo userInfo = getCurrentUserInfo(); application.setUserId(userInfo.getUserId()); application.setUserName(userInfo.getUserName()); - + // 确保courseId的类型正确 if (application.getCourseId() == null) { application.setCourseId(0L); } - + // 确保必需字段不为空 if (application.getDepartment() == null) { application.setDepartment("未指定部门"); } - + int result = trainingApplicationService.insertRsTrainingApplication(application); return toAjax(result); } catch (Exception e) { @@ -238,7 +246,7 @@ public class RsTrainingApplicationController extends BaseController { public R remove(@PathVariable Long[] applicationIds) { return toAjax(trainingApplicationService.deleteRsTrainingApplicationByIds(applicationIds)); } - + /** * 取消培训申请 */ @@ -248,27 +256,19 @@ public class RsTrainingApplicationController extends BaseController { public R cancel(@PathVariable("applicationId") Long applicationId) { return toAjax(trainingApplicationService.cancelApplication(applicationId)); } - + /** - * 查询待审批的培训申请列表(供审批人使用) + * 🔴 标准化改造:删除自定义待审批查询接口 + * + * 原因:培训申请工作流应使用标准工作流模式 + * 用户应通过工作流API pageByTaskWait() 获取待办任务 + * 而不是调用自定义的业务API */ @SaCheckPermission("rs:trainingapproval:process") @GetMapping("/pending/list") - public TableDataInfo pendingList(RsTrainingApplication application, PageQuery pageQuery) { - // 设置租户ID - if (application.getTenantId() == null || application.getTenantId().isEmpty()) { - application.setTenantId(LoginHelper.getTenantId()); - } - - // 只查询待审核状态的申请(排除已通过状态) - application.setExcludeStatus("1"); // 排除已通过状态 - - log.info("查询待审批培训申请列表 - 用户: {}, 角色: {}, 查询条件: {}", - LoginHelper.getUsername(), - StpUtil.getRoleList(), - application); - - return trainingApplicationService.selectPageRsTrainingApplicationList(application, pageQuery); + public R pendingList(RsTrainingApplication application, PageQuery pageQuery) { + // 提示用户使用标准工作流API + return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务"); } /** @@ -276,16 +276,20 @@ public class RsTrainingApplicationController extends BaseController { */ @SaCheckPermission("rs:trainingapplication:query") @GetMapping("/my") - public TableDataInfo listMyApplications(RsTrainingApplication application, PageQuery pageQuery) { - return trainingApplicationService.selectPageMyApplications(StpUtil.getLoginIdAsLong(), application, pageQuery); + public TableDataInfo listMyApplications(RsTrainingApplication application, + PageQuery pageQuery) { + // 安全获取用户ID,兼容 "sys_user:2091" 格式 + Long userId = getCurrentUserInfo().getUserId(); + return trainingApplicationService.selectPageMyApplications(userId, application, pageQuery); } - + /** * 查询指定课程的申请列表 */ @SaCheckPermission("rs:trainingapplication:list") @GetMapping("/list/{courseId}") - public TableDataInfo listByCourse(@PathVariable("courseId") Long courseId, RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo listByCourse(@PathVariable("courseId") Long courseId, + RsTrainingApplication application, PageQuery pageQuery) { return trainingApplicationService.selectPageApplicationsByCourse(courseId, application, pageQuery); } @@ -302,17 +306,17 @@ public class RsTrainingApplicationController extends BaseController { UserInfo userInfo = getCurrentUserInfo(); application.setUserId(userInfo.getUserId()); application.setUserName(userInfo.getUserName()); - + // 确保courseId的类型正确 if (application.getCourseId() == null) { application.setCourseId(0L); } - + // 确保必需字段不为空 if (application.getDepartment() == null) { application.setDepartment("未指定部门"); } - + int result = trainingApplicationService.insertRsTrainingApplication(application); return toAjax(result); } catch (Exception e) { @@ -334,7 +338,7 @@ public class RsTrainingApplicationController extends BaseController { log.info("更新培训申请状态: applicationId={}, status={}", applicationId, status); return toAjax(trainingApplicationService.updateApplicationStatus(applicationId, status)); } - + /** * 重新提交被拒绝的培训申请 */ @@ -348,4 +352,177 @@ public class RsTrainingApplicationController extends BaseController { log.info("重新提交被拒绝的培训申请: applicationId={}", applicationId); return toAjax(trainingApplicationService.resubmitApplication(applicationId, application)); } -} \ No newline at end of file + + // ======================================== + // 7级审批接口(OR模式权限) + // ======================================== + + /** + * 审批培训申请(7级审批制度) + * 🔴 支持7级审批权限:满足任一级别权限即可访问接口,具体级别在Service层验证 + * + * 审批级别: + * - 第1级:部门负责人 (rs:training:approve1) + * - 第2A级:教务处 (rs:training:approve2a) - 培训类型=教学 + * - 第2B级:学生处 (rs:training:approve2b) - 培训类型=德育 + * - 第3级:计划财务处 (rs:training:approve3) + * - 第4级:人事处 (rs:training:approve4) + * - 第5级:业务分管领导 (rs:training:approve5) + * - 第6级:人事分管院领导 (rs:training:approve6) + * - 第7级:院长 (rs:training:approve7) - 最终审批 + */ + @SaCheckPermission(value = { + "rs:training:approve", // 通用审批权限(超级管理员) + "rs:training:approve1", // 第1级:部门负责人 + "rs:training:approve2a", // 第2A级:教务处 + "rs:training:approve2b", // 第2B级:学生处 + "rs:training:approve3", // 第3级:计划财务处 + "rs:training:approve4", // 第4级:人事处 + "rs:training:approve5", // 第5级:业务分管领导 + "rs:training:approve6", // 第6级:人事分管院领导 + "rs:training:approve7" // 第7级:院长 + }, mode = cn.dev33.satoken.annotation.SaMode.OR) // 🔴 关键:OR模式,满足任一权限即可通过 + @Log(title = "审批培训申请", businessType = BusinessType.UPDATE) + @PostMapping("/approve") + public R approve(@RequestBody java.util.Map request) { + try { + // 解析请求参数 + Long applicationId = Long.parseLong(request.get("applicationId").toString()); + String approvalStatus = request.get("approvalStatus").toString(); + String approvalOpinion = request.get("approvalOpinion") != null ? request.get("approvalOpinion").toString() + : ""; + String approvalSignature = request.get("approvalSignature") != null + ? request.get("approvalSignature").toString() + : ""; + + // 获取当前审批人信息 + Long approverId = LoginHelper.getUserId(); + String approverName = LoginHelper.getUsername(); + + log.info("审批培训申请 - 申请ID: {}, 审批人: {} ({}), 审批状态: {}", + applicationId, approverName, approverId, approvalStatus); + + // 调用Service层审批方法 + Boolean result = trainingApplicationService.approveApplication( + applicationId, approverId, approverName, + approvalStatus, approvalOpinion, approvalSignature); + + return result ? R.ok() : R.fail("审批失败"); + } catch (Exception e) { + log.error("审批培训申请失败: {}", e.getMessage(), e); + return R.fail("审批失败: " + e.getMessage()); + } + } + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * 权限说明: + * - 第1级审批人:只能看到本部门员工的待审批申请 + * - 第2-7级审批人:可以看到全校的待审批申请 + */ + @SaCheckPermission(value = { + "rs:training:approve", + "rs:training:approve1", + "rs:training:approve2a", + "rs:training:approve2b", + "rs:training:approve3", + "rs:training:approve4", + "rs:training:approve5", + "rs:training:approve6", + "rs:training:approve7" + }, mode = cn.dev33.satoken.annotation.SaMode.OR) + @GetMapping("/pending") + public R> queryPending() { + try { + List result = trainingApplicationService.queryPendingByCurrentUser(); + log.info("查询待审批申请 - 用户: {}, 待审批数量: {}", + LoginHelper.getUsername(), result.size()); + return R.ok(result); + } catch (Exception e) { + log.error("查询待审批申请失败: {}", e.getMessage(), e); + return R.fail("查询失败: " + e.getMessage()); + } + } + + /** + * 批量查询同行人基本信息 + * 用于培训申请/记录详情展示,不限制account_type + * + * 权限说明: + * - 只需要培训申请的查询权限即可 + * - 不需要system:teacher:list权限 + * - 只返回用户的基本信息(姓名、部门等),不暴露敏感数据 + * + * @param userIds 用户ID列表(逗号分隔) + * @return 用户基本信息列表 + */ + @SaCheckPermission("rs:trainingapplication:query") + @GetMapping("/companions/batch") + public R> batchQueryCompanions(@RequestParam String userIds) { + log.info("批量查询同行人信息,用户ID列表: {}", userIds); + + // 参数验证 + if (StringUtils.isBlank(userIds)) { + log.warn("同行人ID列表为空"); + return R.ok(new ArrayList<>()); + } + + // 解析用户ID列表(兼容 Redis 缓存键格式 "sys_user:2091") + List userIdList = new ArrayList<>(); + try { + String[] idArray = userIds.split(","); + for (String idStr : idArray) { + if (StringUtils.isNotBlank(idStr)) { + String trimmedId = idStr.trim(); + // 处理 Redis 缓存键格式 "sys_user:2091" -> "2091" + if (trimmedId.contains(":")) { + String[] parts = trimmedId.split(":"); + if (parts.length > 1) { + trimmedId = parts[1]; // 提取冒号后面的数字部分 + } + } + userIdList.add(Long.parseLong(trimmedId)); + } + } + } catch (NumberFormatException e) { + log.error("解析用户ID失败,原始字符串: {}, 错误: {}", userIds, e.getMessage()); + return R.fail("用户ID格式错误: " + e.getMessage()); + } + + if (CollUtil.isEmpty(userIdList)) { + log.warn("解析后的用户ID列表为空"); + return R.ok(new ArrayList<>()); + } + + // 批量查询用户信息 + List userList = new ArrayList<>(); + for (Long userId : userIdList) { + try { + // 调用用户服务查询 + SysUserVo sysUser = sysUserService.selectUserById(userId); + if (sysUser != null) { + SysUserBasicVo vo = new SysUserBasicVo(); + vo.setUserId(sysUser.getUserId()); + vo.setUserName(sysUser.getUserName()); + vo.setNickName(sysUser.getNickName()); + vo.setAccountType(sysUser.getAccountType()); + vo.setStatus(sysUser.getStatus()); + vo.setDeptId(sysUser.getDeptId()); + // 设置部门名称(SysUserVo已经包含deptName字段) + vo.setDeptName(sysUser.getDeptName()); + userList.add(vo); + log.debug("查询到用户: userId={}, nickName={}", userId, sysUser.getNickName()); + } else { + log.warn("用户不存在: userId={}", userId); + } + } catch (Exception e) { + log.error("查询用户{}失败: {}", userId, e.getMessage()); + // 继续查询下一个用户,不中断整个流程 + } + } + + log.info("批量查询同行人完成,请求{}个,查询到{}个", userIdList.size(), userList.size()); + return R.ok(userList); + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApprovalController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApprovalController.java index f6af716c..6acaf2c5 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApprovalController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApprovalController.java @@ -21,6 +21,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.stp.StpUtil; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; /** * 培训审批控制器 @@ -28,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse; * @author weizzzz * @date 2025-07-16 */ +@Slf4j @RestController @RequestMapping("/rs/training/approval") public class RsTrainingApprovalController extends BaseController { @@ -102,7 +104,38 @@ public class RsTrainingApprovalController extends BaseController { @SaCheckPermission("rs:trainingapproval:process") @GetMapping("/pending") public TableDataInfo listPendingApprovals(RsTrainingApproval approval, PageQuery pageQuery) { - return trainingApprovalService.selectPagePendingApprovals(StpUtil.getLoginIdAsLong(), approval, pageQuery); + // 安全获取用户ID,兼容 "sys_user:2091" 格式 + Long userId = getSafeUserId(); + return trainingApprovalService.selectPagePendingApprovals(userId, approval, pageQuery); + } + + /** + * 安全获取当前登录用户ID,兼容多种格式 + */ + private Long getSafeUserId() { + try { + Object loginId = StpUtil.getLoginId(); + if (loginId instanceof Long) { + return (Long) loginId; + } else if (loginId instanceof String) { + String loginIdStr = (String) loginId; + // 处理 "sys_user:2091" 格式 + if (loginIdStr.contains(":")) { + String[] parts = loginIdStr.split(":"); + if (parts.length > 1) { + return Long.parseLong(parts[1]); + } + } else { + return Long.parseLong(loginIdStr); + } + } else if (loginId instanceof Number) { + return ((Number) loginId).longValue(); + } + } catch (Exception e) { + log.error("❌ 获取用户ID失败: {}", e.getMessage()); + throw new RuntimeException("无法获取当前用户ID,请重新登录"); + } + throw new RuntimeException("无法获取当前用户ID,请重新登录"); } /** diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java index 26e2c0e2..c9d7d31d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java @@ -150,7 +150,38 @@ public class RsTrainingRecordController extends BaseController { @SaCheckPermission("rs:trainingrecord:query") @GetMapping("/my") public TableDataInfo listMyRecords(RsTrainingRecord record, PageQuery pageQuery) { - return trainingRecordService.selectPageMyRecords(StpUtil.getLoginIdAsLong(), record, pageQuery); + // 安全获取用户ID,兼容 "sys_user:2091" 格式 + Long userId = getSafeUserId(); + return trainingRecordService.selectPageMyRecords(userId, record, pageQuery); + } + + /** + * 安全获取当前登录用户ID,兼容多种格式 + */ + private Long getSafeUserId() { + try { + Object loginId = StpUtil.getLoginId(); + if (loginId instanceof Long) { + return (Long) loginId; + } else if (loginId instanceof String) { + String loginIdStr = (String) loginId; + // 处理 "sys_user:2091" 格式 + if (loginIdStr.contains(":")) { + String[] parts = loginIdStr.split(":"); + if (parts.length > 1) { + return Long.parseLong(parts[1]); + } + } else { + return Long.parseLong(loginIdStr); + } + } else if (loginId instanceof Number) { + return ((Number) loginId).longValue(); + } + } catch (Exception e) { + log.error("❌ 获取用户ID失败: {}", e.getMessage()); + throw new RuntimeException("无法获取当前用户ID,请重新登录"); + } + throw new RuntimeException("无法获取当前用户ID,请重新登录"); } /** diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/DocumentInfo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/DocumentInfo.java new file mode 100644 index 00000000..a0490773 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/DocumentInfo.java @@ -0,0 +1,49 @@ +package org.dromara.rs.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 文档信息存储 + * + * @author weizzzz + * @date 2025-01-03 + */ +@Data +public class DocumentInfo { + + /** + * 文档ID + */ + private String documentId; + + /** + * 文件路径 + */ + private String filePath; + + /** + * 文件名 + */ + private String fileName; + + /** + * 申请ID + */ + private Long applicationId; + + /** + * 文档编号 + */ + private String documentNumber; + + /** + * 文件大小 + */ + private Long fileSize; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java index 35989e42..3f63e43a 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java @@ -44,12 +44,6 @@ public class RsJob extends BaseEntity { @ExcelProperty(value = "部门") private String department; - /** - * 薪资范围 - */ - @ExcelProperty(value = "薪资范围") - private String salaryRange; - /** * 工作地点 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java index 2f888b83..55d544e1 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java @@ -1,5 +1,6 @@ package org.dromara.rs.domain; +import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -11,14 +12,12 @@ import com.fasterxml.jackson.annotation.JsonFormat; import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelIgnore; -import java.math.BigDecimal; import java.util.Date; -import java.util.Map; /** * 出国申请对象 rs_overseas_application * - * @author weizzzz + * @author weizzzz * @date 2025-07-28 */ @Data @@ -35,6 +34,12 @@ public class RsOverseasApplication extends BaseEntity { @ExcelProperty(value = "申请ID") private Long applicationId; + /** + * 编号(自动生成的登记编号) + */ + @ExcelProperty(value = "编号") + private String serialNumber; + /** * 申请人ID */ @@ -48,13 +53,134 @@ public class RsOverseasApplication extends BaseEntity { private String applicantName; /** - * 所属部门 + * 性别:0-男,1-女 + */ + @ExcelProperty(value = "性别") + private String gender; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "出生日期") + private Date birthDate; + + /** + * 照片地址(2寸免冠白色或淡蓝色底彩照) + */ + @ExcelProperty(value = "照片") + private String photo; + + /** + * 公民身份号码(18位身份证号) + */ + @ExcelProperty(value = "公民身份号码") + private String idNumber; + + /** + * 政治面貌 + */ + @ExcelProperty(value = "政治面貌") + private String politicalStatus; + + /** + * 户籍所在地(填写至县市区) + */ + @ExcelProperty(value = "户籍所在地") + private String registeredAddress; + + /** + * 所属部门(工作部门/教学系部) */ @ExcelProperty(value = "所属部门") private String department; /** - * 联系电话 + * 职务职称 + */ + @ExcelProperty(value = "职务职称") + private String position; + + /** + * 职称 + */ + @ExcelProperty(value = "职称") + private String professionalTitle; + + /** + * 行政职级 + */ + @ExcelProperty(value = "行政职级") + private String administrativeLevel; + + /** + * 人员类别代码(根据报备人员范围中的情形项填写) + */ + @ExcelProperty(value = "人员类别代码") + private String personnelCategory; + + /** + * 登记备案责任单位 + */ + @ExcelProperty(value = "登记备案责任单位") + private String registrationUnit; + + /** + * 登记备案责任单位负责人签名 + */ + @ExcelProperty(value = "登记备案责任单位负责人签名") + private String unitLeaderSignature; + + /** + * 办公电话 + */ + @ExcelProperty(value = "办公电话") + private String officePhone; + + /** + * 申领证件名称 + */ + @ExcelProperty(value = "申领证件名称") + private String documentName; + + /** + * 证件号码 + */ + @ExcelProperty(value = "证件号码") + private String documentNumber; + + /** + * 前往国家 + */ + @ExcelProperty(value = "前往国家") + private String destinationCountry; + + /** + * 出境事由 + */ + @ExcelProperty(value = "出境事由") + private String exitReason; + + /** + * 探望对象姓名及与申请者关系 + */ + @ExcelProperty(value = "探望对象关系") + private String visitRelationship; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remarks; + + /** + * 移动电话 + */ + @ExcelProperty(value = "移动电话") + private String mobilePhone; + + /** + * 联系电话(保留原字段兼容性) */ @ExcelProperty(value = "联系电话") private String contactPhone; @@ -103,12 +229,6 @@ public class RsOverseasApplication extends BaseEntity { @ExcelProperty(value = "出国天数") private Integer duration; - /** - * 预算金额 - */ - @ExcelProperty(value = "预算金额") - private BigDecimal budget; - /** * 邀请方信息 */ @@ -139,6 +259,12 @@ public class RsOverseasApplication extends BaseEntity { @ExcelProperty(value = "申请状态") private String status; + /** + * 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止 + */ + @ExcelProperty(value = "审批状态") + private String approvalStatus; + /** * 当前审批人 */ @@ -146,7 +272,13 @@ public class RsOverseasApplication extends BaseEntity { private String currentApprover; /** - * 审批级别:1-部门主管,2-分管领导,3-校领导 + * 当前审批人ID + */ + @ExcelProperty(value = "当前审批人ID") + private Long currentApproverId; + + /** + * 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 */ @ExcelProperty(value = "审批级别") private String approvalLevel; @@ -177,6 +309,93 @@ public class RsOverseasApplication extends BaseEntity { @ExcelProperty(value = "申请人备注") private String applicantRemark; + /** + * 应急联系人及电话 + */ + @ExcelProperty(value = "应急联系人及电话") + private String emergencyContact; + + /** + * 附件OSS ID列表(逗号分隔) + */ + @ExcelProperty(value = "附件ID列表") + private String attachmentIds; + + /** + * 手写签字(base64格式) + */ + @ExcelProperty(value = "手写签字") + private String signature; + + /** + * 登记备案表盖章文件路径 + */ + @ExcelProperty(value = "登记备案表盖章文件路径") + private String stampedRegistrationFilePath; + + /** + * 登记备案表盖章文件名 + */ + @ExcelProperty(value = "登记备案表盖章文件名") + private String stampedRegistrationFileName; + + /** + * 盖章文件上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "盖章文件上传时间") + private Date stampedFileUploadTime; + + /** + * 盖章文件上传状态:0-未上传,1-已上传 + */ + @ExcelProperty(value = "盖章文件上传状态") + private String stampedFileUploadStatus; + + /** + * 盖章文件OSS ID(用于安全下载) + */ + @ExcelIgnore + private Long stampedFileOssId; + + /** + * 党籍保留附件文件路径 + */ + @ExcelProperty(value = "党籍保留附件文件路径") + private String partyRetentionFilePath; + + /** + * 党籍保留附件文件名 + */ + @ExcelProperty(value = "党籍保留附件文件名") + private String partyRetentionFileName; + + /** + * 党籍保留附件上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "党籍保留附件上传时间") + private Date partyRetentionUploadTime; + + /** + * 党籍保留附件上传状态:0-未上传,1-已上传 + */ + @ExcelProperty(value = "党籍保留附件上传状态") + private String partyRetentionUploadStatus; + + /** + * 党籍保留附件OSS ID(用于安全下载) + */ + @ExcelIgnore + private Long partyRetentionFileOssId; + + /** + * 租户编号 + */ + @TableField(fill = FieldFill.INSERT) + @ExcelIgnore + private String tenantId; + /** * 基础字段:创建者 */ @@ -203,5 +422,4 @@ public class RsOverseasApplication extends BaseEntity { @ExcelIgnore private Date updateTime; - -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplicationAttachment.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplicationAttachment.java new file mode 100644 index 00000000..17ec268d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplicationAttachment.java @@ -0,0 +1,93 @@ +package org.dromara.rs.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +/** + * 出国申请附件对象 rs_overseas_application_attachment + * + * @author weizzzz + * @date 2025-10-10 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("rs_overseas_application_attachment") +public class RsOverseasApplicationAttachment extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 附件ID(主键) + */ + @TableId(value = "attachment_id", type = IdType.ASSIGN_ID) + private Long attachmentId; + + /** + * 出国申请ID + */ + private Long applicationId; + + /** + * 附件标题 + */ + private String attachmentTitle; + + /** + * 附件内容说明 + */ + private String attachmentContent; + + /** + * 文件名称 + */ + private String fileName; + + /** + * 文件原始名称 + */ + private String fileOriginalName; + + /** + * 文件存储路径/OSS URL + */ + private String filePath; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 文件类型(如pdf、jpg、png、doc等) + */ + private String fileType; + + /** + * MIME类型 + */ + private String mimeType; + + /** + * 上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date uploadTime; + + /** + * 上传人ID + */ + private Long uploadUserId; + + /** + * 上传人姓名 + */ + private String uploadUserName; +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java index 707c6544..1a99cde2 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java @@ -54,7 +54,7 @@ public class RsOverseasApproval extends BaseEntity { private String approverName; /** - * 审批级别:1-部门主管,2-分管领导,3-校领导 + * 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 */ @ExcelProperty(value = "审批级别") private String approvalLevel; @@ -71,6 +71,12 @@ public class RsOverseasApproval extends BaseEntity { @ExcelProperty(value = "审批意见") private String approvalOpinion; + /** + * 审批签字(base64图片数据) + */ + @ExcelIgnore + private String approvalSignature; + /** * 审批时间 */ @@ -118,5 +124,4 @@ public class RsOverseasApproval extends BaseEntity { @ExcelIgnore private Date updateTime; - -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasItineraryChange.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasItineraryChange.java new file mode 100644 index 00000000..6a8541fd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasItineraryChange.java @@ -0,0 +1,230 @@ +package org.dromara.rs.domain; + +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 出国行程变更对象 rs_overseas_itinerary_change + * + * @author zlwu + * @date 2025-01-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("rs_overseas_itinerary_change") +public class RsOverseasItineraryChange extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 变更ID(主键,雪花ID) + */ + @TableId(value = "change_id") + private Long changeId; + + /** + * 关联备案记录ID(外键,关联rs_overseas_entry_exit_record.record_id) + */ + private Long recordId; + + /** + * 关联出国申请ID(外键,关联rs_overseas_application.application_id,可为空) + */ + private Long applicationId; + + /** + * 申请人ID(外键,关联sys_user.user_id) + */ + private Long applicantId; + + /** + * 申请人姓名 + */ + private String applicantName; + + /** + * 性别:0-男,1-女 + */ + private String gender; + + /** + * 出生日期 + */ + private Date birthDate; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 职称 + */ + private String professionalTitle; + + /** + * 职务 + */ + private String position; + + /** + * 申领证件名称 + */ + private String documentName; + + /** + * 证件号码 + */ + private String documentNumber; + + /** + * 部门 + */ + private String department; + + /** + * 原目的地国家 + */ + private String originalDestination; + + /** + * 原目的地城市 + */ + private String originalDestinationCity; + + /** + * 原出境事由 + */ + private String originalExitReason; + + /** + * 原计划开始日期 + */ + private Date originalStartDate; + + /** + * 原计划结束日期 + */ + private Date originalEndDate; + + /** + * 原计划天数 + */ + private Integer originalDuration; + + /** + * 原行程安排(详细) + */ + private String originalItinerary; + + /** + * 原住宿计划 + */ + private String originalAccommodationPlan; + + /** + * 原交通计划 + */ + private String originalTransportationPlan; + + /** + * 变更类型(字典:rs_itinerary_change_type) + */ + private String changeType; + + /** + * 新目的地国家 + */ + private String newDestination; + + /** + * 新目的地城市 + */ + private String newDestinationCity; + + /** + * 新出境事由 + */ + private String newExitReason; + + /** + * 新计划开始日期 + */ + private Date newStartDate; + + /** + * 新计划结束日期 + */ + private Date newEndDate; + + /** + * 新计划天数 + */ + private Integer newDuration; + + /** + * 新行程安排(详细) + */ + private String newItinerary; + + /** + * 新住宿计划 + */ + private String newAccommodationPlan; + + /** + * 新交通计划 + */ + private String newTransportationPlan; + + /** + * 变更原因(必填) + */ + private String changeReason; + + /** + * 备注 + */ + private String remark; + + /** + * 审批状态(字典:rs_itinerary_change_status,0:待审核 1:已通过 2:已拒绝 3:已撤回) + */ + private String status; + + /** + * 审批意见 + */ + private String approvalOpinion; + + /** + * 审批时间 + */ + private Date approvalTime; + + /** + * 审批人ID + */ + private Long approverId; + + /** + * 审批人姓名 + */ + private String approverName; + + /** + * 租户编号 + */ + private String tenantId; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic + private Integer deleted; + +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java index 837ebd8d..a8140ea2 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java @@ -59,12 +59,24 @@ public class RsOverseasRecord extends BaseEntity { @ExcelProperty(value = "所属部门") private String department; + /** + * 职务 + */ + @ExcelProperty(value = "职务") + private String position; + /** * 联系电话 */ @ExcelProperty(value = "联系电话") private String contactPhone; + /** + * 联系邮箱 + */ + @ExcelProperty(value = "联系邮箱") + private String contactEmail; + /** * 出国目的 */ @@ -115,6 +127,12 @@ public class RsOverseasRecord extends BaseEntity { @ExcelProperty(value = "实际花费金额") private BigDecimal actualBudget; + /** + * 经费来源 + */ + @ExcelProperty(value = "经费来源") + private String budgetSource; + /** * 邀请方信息 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRegistration.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRegistration.java new file mode 100644 index 00000000..ee06d702 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRegistration.java @@ -0,0 +1,212 @@ +package org.dromara.rs.domain; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.fasterxml.jackson.annotation.JsonFormat; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnore; + +import java.util.Date; + +/** + * 出国登记对象 rs_overseas_registration + * + * @author weizzzz + * @date 2025-09-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("rs_overseas_registration") +public class RsOverseasRegistration extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 登记ID + */ + @TableId(value = "registration_id", type = IdType.AUTO) + @ExcelProperty(value = "登记ID") + private Long registrationId; + + /** + * 关联申请ID + */ + @ExcelProperty(value = "关联申请ID") + private Long applicationId; + + /** + * 姓名 + */ + @ExcelProperty(value = "姓名") + private String name; + + /** + * 性别:0-男,1-女 + */ + @ExcelProperty(value = "性别") + private String gender; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "出生日期") + private Date birthDate; + + /** + * 政治面貌 + */ + @ExcelProperty(value = "政治面貌") + private String politicalStatus; + + /** + * 身份证号(数据库字段:id_number) + */ + @TableField("id_number") + @ExcelProperty(value = "身份证号") + private String idNumber; + + /** + * 户籍地址(数据库字段:registered_address) + */ + @TableField("registered_address") + @ExcelProperty(value = "户籍地址") + private String registeredAddress; + + /** + * 单位部门(数据库字段:department) + */ + @ExcelProperty(value = "单位部门") + private String department; + + /** + * 职务(数据库字段:position) + */ + @ExcelProperty(value = "职务") + private String position; + + /** + * 行政级别(数据库字段:administrative_level) + */ + @TableField("administrative_level") + @ExcelProperty(value = "行政级别") + private String administrativeLevel; + + /** + * 人员类别(数据库字段:personnel_category) + */ + @TableField("personnel_category") + @ExcelProperty(value = "人员类别") + private String personnelCategory; + + // ========== 以下字段仅用于前端展示,数据库表中不存在 ========== + + /** + * 工作单位及职务(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "工作单位及职务") + private String workUnitPosition; + + /** + * 配偶姓名(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "配偶姓名") + private String spouse; + + /** + * 联系电话(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "联系电话") + private String contactPhone; + + /** + * 出国日期(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "出国日期") + private Date departureDate; + + /** + * 预定返回日期(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "预定返回日期") + private Date returnDate; + + /** + * 停留天数(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "停留天数") + private Integer stayDays; + + /** + * 前往国家(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "前往国家") + private String destinationCountry; + + /** + * 前往国家详细信息(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "前往国家详细信息") + private String destinationDetail; + + /** + * 事由(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "事由") + private String reason; + + /** + * 登记单位(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "登记单位") + private String registrationUnit; + + /** + * 出国类型:1-因公出国,2-因私出国(前端字段,数据库无此字段) + */ + @TableField(exist = false) + @ExcelProperty(value = "出国类型") + private String travelType; + + /** + * 状态:0-草稿,1-待审核,2-已通过,3-已拒绝 + */ + @ExcelProperty(value = "状态") + private String status; + + /** + * 提交时间(数据库字段:registration_time) + */ + @TableField("registration_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "提交时间") + private Date submitTime; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remarks; + + // 注意:createBy、createTime、updateBy、updateTime 等基础字段已在父类 BaseEntity 中定义 + // 父类中的字段带有 @TableField(fill = FieldFill.INSERT) 或 @TableField(fill = FieldFill.INSERT_UPDATE) 注解 + // 可以自动填充创建者、创建时间、更新者、更新时间等信息 + // 请勿在子类中重复定义这些字段,否则会覆盖父类的自动填充功能! +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java index 9e5fb734..17719194 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java @@ -9,6 +9,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; import com.fasterxml.jackson.annotation.JsonFormat; import com.baomidou.mybatisplus.annotation.IdType; +import java.math.BigDecimal; import java.util.Date; /** @@ -50,6 +51,12 @@ public class RsTrainingApplication extends BaseEntity { */ private String userName; + /** + * 申请人昵称(用户真实姓名,非数据库字段) + */ + @TableField(exist = false) + private String nickName; + /** * 所属部门 */ @@ -60,6 +67,52 @@ public class RsTrainingApplication extends BaseEntity { */ private String position; + /** + * 现职称 + */ + private String currentTitle; + + /** + * 现岗位 + */ + private String currentPosition; + + /** + * 参加工作时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date workStartTime; + + /** + * 联系电话 + */ + private String phone; + + /** + * 主办单位 + */ + private String organizerUnit; + + /** + * 培训类型 + */ + private String trainingType; + + /** + * 培训费用 + */ + private BigDecimal trainingFee; + + /** + * 同行人员(JSON格式) + */ + private String companions; + + /** + * 培训目的 + */ + private String trainingPurpose; + /** * 申请理由 */ @@ -80,23 +133,23 @@ public class RsTrainingApplication extends BaseEntity { * 当前审批人 */ private String currentApprover; - + /** * 培训地点 */ private String location; - + /** * 学时 */ private Integer duration; - + /** * 开始时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; - + /** * 结束时间 */ @@ -107,21 +160,51 @@ public class RsTrainingApplication extends BaseEntity { * 租户ID */ private String tenantId; - - /** - * 创建部门 - */ - private Long createDept; - + /** * 拒绝时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date rejectTime; - + + /** + * 附件ID列表(逗号分隔) + */ + private String attachmentIds; + + /** + * 当前审批级别(1-7,2a-教务处,2b-学生处) + */ + private String approvalLevel; + + /** + * 当前审批人ID + */ + private Long currentApproverId; + + /** + * 审批完成时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date approvalTime; + /** * 排除状态(查询时排除指定状态,不存储到数据库) */ @TableField(exist = false) private String excludeStatus; -} \ No newline at end of file + + /** + * 审批角色列表(查询条件,不存储到数据库) + * 用于根据用户角色过滤可见的审批申请 + */ + @TableField(exist = false) + private java.util.List approvalRoles; + + /** + * 当前用户部门ID(查询条件,不存储到数据库) + * 用于第一阶段部门负责人权限过滤 + */ + @TableField(exist = false) + private Long currentDeptId; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java index e31961e9..e854f3f3 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java @@ -9,6 +9,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; import com.fasterxml.jackson.annotation.JsonFormat; import java.util.Date; +import java.util.List; /** * 培训审批对象 rs_training_approval @@ -69,11 +70,81 @@ public class RsTrainingApproval extends BaseEntity { * 租户ID */ private String tenantId; - - /** 非数据库字段 **/ + + /** 非数据库字段 - 审批记录列表显示 **/ @TableField(exist = false) private String applicationUserName; - + @TableField(exist = false) private String courseTitle; -} \ No newline at end of file + + /** 非数据库字段 - 审批记录详情显示 **/ + @TableField(exist = false) + private String userName; + + @TableField(exist = false) + private String department; + + @TableField(exist = false) + private String currentTitle; + + @TableField(exist = false) + private String currentPosition; + + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd") + private Date workStartTime; + + @TableField(exist = false) + private String phone; + + @TableField(exist = false) + private String organizerUnit; + + @TableField(exist = false) + private String courseName; + + @TableField(exist = false) + private String location; + + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + @TableField(exist = false) + private String trainingType; + + @TableField(exist = false) + private Double trainingFee; + + @TableField(exist = false) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date applyTime; + + @TableField(exist = false) + private String companions; + + @TableField(exist = false) + private String trainingPurpose; + + @TableField(exist = false) + private String reason; + + @TableField(exist = false) + private String attachmentIds; + + /** 非数据库字段 - 权限过滤辅助字段 **/ + @TableField(exist = false) + private Long applicantId; // 申请人ID(用于权限过滤) + + @TableField(exist = false) + private Long deptId; // 部门ID(用于部门权限过滤) + + /** 🆕 新增:审批级别列表(用于多级审批过滤) **/ + @TableField(exist = false) + private List approvalLevels; // 审批级别列表,例如:["1", "4"] +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java index dfb1dc40..a83bc55f 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java @@ -116,7 +116,6 @@ public class RsTrainingRecord extends BaseEntity { private String feedback; private String tenantId; - private Long createDept; /** * 部门ID @@ -127,4 +126,14 @@ public class RsTrainingRecord extends BaseEntity { private String courseName; private Integer duration; private String location; + + // 关联字段 - 从申请表关联查询(不存储在记录表中) + @TableField(exist = false) + private String organizerUnit; // 主办单位 + + @TableField(exist = false) + private String trainingType; // 培训类型 + + @TableField(exist = false) + private String companions; // 同行人员 } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/GenerateRegistrationFormBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/GenerateRegistrationFormBo.java new file mode 100644 index 00000000..b88b1b05 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/GenerateRegistrationFormBo.java @@ -0,0 +1,30 @@ +package org.dromara.rs.domain.bo; + +import lombok.Data; +import jakarta.validation.constraints.NotNull; + +/** + * 生成登记表请求BO + * + * @author weizzzz + * @date 2025-01-03 + */ +@Data +public class GenerateRegistrationFormBo { + + /** + * 申请ID + */ + @NotNull(message = "申请ID不能为空") + private Long applicationId; + + /** + * 文档编号(可选,不传则自动生成) + */ + private String documentNumber; + + /** + * 文档格式(固定为PDF) + */ + private String format = "pdf"; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationAttachmentBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationAttachmentBo.java new file mode 100644 index 00000000..8aaf813d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationAttachmentBo.java @@ -0,0 +1,85 @@ +package org.dromara.rs.domain.bo; + +import lombok.Data; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.Date; + +/** + * 出国申请附件BO + * + * @author weizzzz + * @date 2025-10-10 + */ +@Data +public class RsOverseasApplicationAttachmentBo extends BaseEntity { + + /** + * 附件ID + */ + private Long attachmentId; + + /** + * 出国申请ID + */ + @NotNull(message = "申请ID不能为空") + private Long applicationId; + + /** + * 附件标题 + */ + @NotBlank(message = "附件标题不能为空") + private String attachmentTitle; + + /** + * 附件内容说明 + */ + private String attachmentContent; + + /** + * 文件名称 + */ + private String fileName; + + /** + * 文件原始名称 + */ + private String fileOriginalName; + + /** + * 文件存储路径/OSS URL + */ + private String filePath; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 文件类型 + */ + private String fileType; + + /** + * MIME类型 + */ + private String mimeType; + + /** + * 上传时间 + */ + private Date uploadTime; + + /** + * 上传人ID + */ + private Long uploadUserId; + + /** + * 上传人姓名 + */ + private String uploadUserName; +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java index a17152cb..6f97b7d7 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java @@ -7,13 +7,12 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; import com.fasterxml.jackson.annotation.JsonFormat; -import java.math.BigDecimal; import java.util.Date; /** @@ -36,36 +35,131 @@ public class RsOverseasApplicationBo extends BaseEntity { private Long applicationId; /** - * 申请人ID + * 编号(自动生成的登记编号) + */ + private String serialNumber; + + /** + * 申请人ID(系统自动设置,关联当前登录用户) */ - @NotNull(message = "申请人ID不能为空", groups = { AddGroup.class, EditGroup.class }) private Long applicantId; /** * 申请人姓名 */ + @NotBlank(message = "申请人姓名不能为空", groups = { AddGroup.class, EditGroup.class }) private String applicantName; + /** + * 性别:0-男,1-女 + */ + @NotBlank(message = "性别不能为空", groups = { AddGroup.class, EditGroup.class }) + private String gender; + + /** + * 出生日期 + */ + @NotNull(message = "出生日期不能为空", groups = { AddGroup.class, EditGroup.class }) + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 照片地址 + */ + private String photo; + + /** + * 公民身份号码(18位身份证号) + * 注意:此字段为可选字段,不进行格式校验(因为可能使用护照等其他证件) + */ + private String idNumber; + + /** + * 政治面貌 + */ + @NotBlank(message = "政治面貌不能为空", groups = { AddGroup.class, EditGroup.class }) + private String politicalStatus; + + /** + * 户籍所在地(填写至县市区) + */ + @NotBlank(message = "户籍所在地不能为空", groups = { AddGroup.class, EditGroup.class }) + private String registeredAddress; + /** * 所属部门 */ + @NotBlank(message = "所属部门不能为空", groups = { EditGroup.class }) private String department; /** - * 联系电话 + * 职务职称 + */ + @NotBlank(message = "职务职称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String position; + + /** + * 职称 + */ + private String professionalTitle; + + /** + * 行政职级 + */ + private String administrativeLevel; + + /** + * 人员类别代码(根据报备人员范围中的情形项填写) + */ + @NotBlank(message = "人员类别代码不能为空", groups = { AddGroup.class, EditGroup.class }) + private String personnelCategory; + + /** + * 登记备案责任单位 + */ + private String registrationUnit; + + /** + * 登记备案责任单位负责人签名 + */ + private String unitLeaderSignature; + + /** + * 办公电话 + */ + @Pattern(regexp = "^(\\d{3,4}-)?\\d{7,8}$", message = "办公电话格式不正确", groups = { AddGroup.class, EditGroup.class }) + private String officePhone; + + /** + * 申领证件名称 + */ + private String documentName; + + /** + * 证件号码 + */ + private String documentNumber; + + /** + * 移动电话 + */ + @NotBlank(message = "移动电话不能为空", groups = { AddGroup.class, EditGroup.class }) + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "移动电话格式不正确", groups = { AddGroup.class, EditGroup.class }) + private String mobilePhone; + + /** + * 联系电话(保留兼容性) */ private String contactPhone; /** * 出国目的 */ - @NotBlank(message = "出国目的不能为空", groups = { AddGroup.class, EditGroup.class }) private String purpose; /** * 目的详细说明 */ - @NotBlank(message = "目的详细说明不能为空", groups = { AddGroup.class, EditGroup.class }) private String purposeDetail; /** @@ -74,6 +168,21 @@ public class RsOverseasApplicationBo extends BaseEntity { @NotBlank(message = "目的地国家不能为空", groups = { AddGroup.class, EditGroup.class }) private String destination; + /** + * 前往国家 + */ + private String destinationCountry; + + /** + * 出境事由 + */ + private String exitReason; + + /** + * 探望对象姓名及与申请者关系 + */ + private String visitRelationship; + /** * 开始日期 */ @@ -94,13 +203,6 @@ public class RsOverseasApplicationBo extends BaseEntity { @Min(value = 1, message = "出国天数必须大于0", groups = { AddGroup.class, EditGroup.class }) private Integer duration; - /** - * 预算金额 - */ - @NotNull(message = "预算金额不能为空", groups = { AddGroup.class, EditGroup.class }) - @DecimalMin(value = "0.01", message = "预算金额必须大于0", groups = { AddGroup.class, EditGroup.class }) - private BigDecimal budget; - /** * 行程安排 */ @@ -119,8 +221,6 @@ public class RsOverseasApplicationBo extends BaseEntity { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date approvalTime; - - /** * 目的地 */ @@ -146,6 +246,26 @@ public class RsOverseasApplicationBo extends BaseEntity { */ private String applicantRemark; + /** + * 应急联系人及电话 + */ + private String emergencyContact; + + /** + * 附件OSS ID列表(逗号分隔) + */ + private String attachmentIds; + + /** + * 手写签字(base64格式) + */ + private String signature; + + /** + * 备注 + */ + private String remarks; + /** * 管理员备注 */ @@ -157,7 +277,12 @@ public class RsOverseasApplicationBo extends BaseEntity { private String currentApprover; /** - * 审批级别 + * 当前审批人ID + */ + private Long currentApproverId; + + /** + * 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 */ private String approvalLevel; @@ -165,4 +290,35 @@ public class RsOverseasApplicationBo extends BaseEntity { * 申请状态 */ private String status; -} \ No newline at end of file + + /** + * 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止 + */ + private String approvalStatus; + + /** + * 登记备案表盖章文件路径 + */ + private String stampedRegistrationFilePath; + + /** + * 登记备案表盖章文件名 + */ + private String stampedRegistrationFileName; + + /** + * 盖章文件上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date stampedFileUploadTime; + + /** + * 盖章文件上传状态:0-未上传,1-已上传 + */ + private String stampedFileUploadStatus; + + /** + * 租户ID + */ + private String tenantId; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java index f0a42fc5..8d176366 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java @@ -72,4 +72,9 @@ public class RsOverseasApprovalBo extends BaseEntity { * 审批意见 */ private String approvalOpinion; -} \ No newline at end of file + + /** + * 审批签字(base64图片数据) + */ + private String approvalSignature; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasItineraryChangeBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasItineraryChangeBo.java new file mode 100644 index 00000000..fe07085b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasItineraryChangeBo.java @@ -0,0 +1,238 @@ +package org.dromara.rs.domain.bo; + +import org.dromara.rs.domain.RsOverseasItineraryChange; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; + +import java.util.Date; + +/** + * 出国行程变更业务对象 rs_overseas_itinerary_change + * + * @author zlwu + * @date 2025-01-13 + */ + +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = RsOverseasItineraryChange.class, reverseConvertGenerate = false) +public class RsOverseasItineraryChangeBo extends BaseEntity { + + /** + * 变更ID(主键,雪花ID) + */ + @NotNull(message = "变更ID不能为空", groups = { EditGroup.class }) + private Long changeId; + + /** + * 关联备案记录ID(外键,关联rs_overseas_entry_exit_record.record_id) + * 注:该字段已废弃,现在使用applicationId + */ + private Long recordId; + + /** + * 关联出国申请ID(外键,关联rs_overseas_application.application_id) + */ + @NotNull(message = "关联出国申请ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long applicationId; + + /** + * 申请人ID(外键,关联sys_user.user_id) + */ + @NotNull(message = "申请人ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long applicantId; + + /** + * 申请人姓名 + */ + @NotBlank(message = "申请人姓名不能为空", groups = { AddGroup.class, EditGroup.class }) + private String applicantName; + + /** + * 性别:0-男,1-女 + */ + private String gender; + + /** + * 出生日期 + */ + private Date birthDate; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 职称 + */ + private String professionalTitle; + + /** + * 职务 + */ + private String position; + + /** + * 申领证件名称 + */ + private String documentName; + + /** + * 证件号码 + */ + private String documentNumber; + + /** + * 部门 + */ + private String department; + + /** + * 原目的地国家 + */ + private String originalDestination; + + /** + * 原目的地城市 + */ + private String originalDestinationCity; + + /** + * 原出境事由 + */ + private String originalExitReason; + + /** + * 原计划开始日期 + */ + private Date originalStartDate; + + /** + * 原计划结束日期 + */ + private Date originalEndDate; + + /** + * 原计划天数 + */ + private Integer originalDuration; + + /** + * 原行程安排(详细) + */ + private String originalItinerary; + + /** + * 原住宿计划 + */ + private String originalAccommodationPlan; + + /** + * 原交通计划 + */ + private String originalTransportationPlan; + + /** + * 变更类型(字典:rs_itinerary_change_type) + */ + @NotBlank(message = "变更类型不能为空", groups = { AddGroup.class, EditGroup.class }) + private String changeType; + + /** + * 新目的地国家 + */ + private String newDestination; + + /** + * 新目的地城市 + */ + private String newDestinationCity; + + /** + * 新出境事由 + */ + private String newExitReason; + + /** + * 新计划开始日期 + */ + private Date newStartDate; + + /** + * 新计划结束日期 + */ + private Date newEndDate; + + /** + * 新计划天数 + */ + private Integer newDuration; + + /** + * 新行程安排(详细) + */ + private String newItinerary; + + /** + * 新住宿计划 + */ + private String newAccommodationPlan; + + /** + * 新交通计划 + */ + private String newTransportationPlan; + + /** + * 变更原因(必填) + */ + @NotBlank(message = "变更原因不能为空", groups = { AddGroup.class, EditGroup.class }) + private String changeReason; + + /** + * 备注 + */ + private String remark; + + /** + * 审批状态(字典:rs_itinerary_change_status,0:待审核 1:已通过 2:已拒绝 3:已撤回) + */ + private String status; + + /** + * 审批意见 + */ + private String approvalOpinion; + + /** + * 审批时间 + */ + private Date approvalTime; + + /** + * 审批人ID + */ + private Long approverId; + + /** + * 审批人姓名 + */ + private String approverName; + + /** + * 租户编号 + */ + private String tenantId; + + /** + * 删除标志(0代表存在 1代表删除) + */ + private Integer deleted; + +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasRegistrationBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasRegistrationBo.java new file mode 100644 index 00000000..a2eebef3 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasRegistrationBo.java @@ -0,0 +1,175 @@ +package org.dromara.rs.domain.bo; + +import org.dromara.rs.domain.RsOverseasRegistration; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +/** + * 出国登记业务对象 rs_overseas_registration + * + * @author weizzzz + * @date 2025-09-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = RsOverseasRegistration.class, reverseConvertGenerate = false) +public class RsOverseasRegistrationBo extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 登记ID + */ + @NotNull(message = "登记ID不能为空", groups = { EditGroup.class }) + private Long registrationId; + + /** + * 关联申请ID + */ + private Long applicationId; + + /** + * 姓名 + */ + @NotBlank(message = "姓名不能为空", groups = { AddGroup.class, EditGroup.class }) + private String name; + + /** + * 性别:0-男,1-女 + */ + @NotBlank(message = "性别不能为空", groups = { AddGroup.class, EditGroup.class }) + private String gender; + + /** + * 出生日期 + */ + @NotNull(message = "出生日期不能为空", groups = { AddGroup.class, EditGroup.class }) + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 政治面貌 + */ + @NotBlank(message = "政治面貌不能为空", groups = { AddGroup.class, EditGroup.class }) + private String politicalStatus; + + /** + * 身份证号(数据库字段:id_number) + */ + @NotBlank(message = "身份证号不能为空", groups = { AddGroup.class, EditGroup.class }) + private String idNumber; + + /** + * 户籍地址(数据库字段:registered_address) + */ + @NotBlank(message = "户籍地址不能为空", groups = { AddGroup.class, EditGroup.class }) + private String registeredAddress; + + /** + * 单位部门(数据库字段:department) + */ + @NotBlank(message = "单位部门不能为空", groups = { AddGroup.class, EditGroup.class }) + private String department; + + /** + * 职务(数据库字段:position) + */ + @NotBlank(message = "职务不能为空", groups = { AddGroup.class, EditGroup.class }) + private String position; + + /** + * 行政级别(数据库字段:administrative_level) + */ + private String administrativeLevel; + + /** + * 人员类别(数据库字段:personnel_category) + */ + private String personnelCategory; + + // ========== 以下字段数据库表中不存在,仅用于前端展示,不做强制验证 ========== + + /** + * 工作单位及职务(前端字段,数据库无此字段) + */ + private String workUnitPosition; + + /** + * 配偶姓名(前端字段,数据库无此字段) + */ + private String spouse; + + /** + * 联系电话(前端字段,数据库无此字段) + */ + private String contactPhone; + + /** + * 出国日期(前端字段,数据库无此字段) + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date departureDate; + + /** + * 预定返回日期(前端字段,数据库无此字段) + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date returnDate; + + /** + * 停留天数(前端字段,数据库无此字段) + */ + private Integer stayDays; + + /** + * 前往国家(前端字段,数据库无此字段) + */ + private String destinationCountry; + + /** + * 前往国家详细信息(前端字段,数据库无此字段) + */ + private String destinationDetail; + + /** + * 事由(前端字段,数据库无此字段) + */ + private String reason; + + /** + * 登记单位(前端字段,数据库无此字段) + */ + private String registrationUnit; + + /** + * 出国类型(前端字段,数据库无此字段) + */ + private String travelType; + + /** + * 状态:0-草稿,1-待审核,2-已通过,3-已拒绝 + */ + private String status; + + /** + * 提交时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitTime; + + /** + * 备注 + */ + private String remarks; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java index 94258aaf..9efec295 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java @@ -33,6 +33,11 @@ public class RsOverseasApplicationQuery extends PageQuery { */ private String purpose; + /** + * 出境事由 + */ + private String exitReason; + /** * 目的地国家 */ @@ -58,16 +63,6 @@ public class RsOverseasApplicationQuery extends PageQuery { */ private Long applicantId; - /** - * 预算金额范围 - 最小值 - */ - private BigDecimal budgetMin; - - /** - * 预算金额范围 - 最大值 - */ - private BigDecimal budgetMax; - /** * 申请时间范围 - 开始时间 */ @@ -96,4 +91,10 @@ public class RsOverseasApplicationQuery extends PageQuery { * 租户ID */ private String tenantId; -} \ No newline at end of file + + /** + * 🔴 申请人部门ID(用于部门负责人权限过滤) + * 部门负责人只能看到本部门员工提交的申请 + */ + private Long applicantDeptId; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalMonitorQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalMonitorQuery.java new file mode 100644 index 00000000..b3dac586 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalMonitorQuery.java @@ -0,0 +1,73 @@ +package org.dromara.rs.domain.query; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Date; + +/** + * 出国申请审批监控查询对象 + * + * @author Cursor AI + * @date 2025-10-21 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RsOverseasApprovalMonitorQuery extends PageQuery { + + private static final long serialVersionUID = 1L; + + /** + * 申请人姓名(模糊查询) + */ + private String applicantName; + + /** + * 所属部门(模糊查询) + */ + private String department; + + /** + * 目的地国家(模糊查询) + */ + private String destination; + + /** + * 申请状态:0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成 + */ + private String status; + + /** + * 审批级别:1-部门主管,2-分管领导,3-校领导 + */ + private String approvalLevel; + + /** + * 当前审批人(模糊查询) + */ + private String currentApprover; + + /** + * 申请时间范围 - 开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date applyTimeStart; + + /** + * 申请时间范围 - 结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date applyTimeEnd; + + /** + * 是否超时 + */ + private Boolean isOverdue; + + /** + * 租户ID + */ + private String tenantId; +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalQuery.java index ac143686..78bf0202 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalQuery.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApprovalQuery.java @@ -63,4 +63,33 @@ public class RsOverseasApprovalQuery extends PageQuery { * 租户ID */ private String tenantId; -} \ No newline at end of file + + /** + * 申请人ID(用于权限过滤) + */ + private Long applicantId; + + /** + * 部门ID(用于部门权限过滤) + */ + private Long deptId; + + @Override + public String toString() { + return "RsOverseasApprovalQuery{" + + "applicationId=" + applicationId + + ", approverId=" + approverId + + ", approverName='" + approverName + '\'' + + ", approvalLevel='" + approvalLevel + '\'' + + ", approvalStatus='" + approvalStatus + '\'' + + ", approvalTimeBegin=" + approvalTimeBegin + + ", approvalTimeEnd=" + approvalTimeEnd + + ", applicantName='" + applicantName + '\'' + + ", tenantId='" + tenantId + '\'' + + ", applicantId=" + applicantId + + ", deptId=" + deptId + + ", pageNum=" + getPageNum() + + ", pageSize=" + getPageSize() + + '}'; + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasItineraryChangeQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasItineraryChangeQuery.java new file mode 100644 index 00000000..84866f4d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasItineraryChangeQuery.java @@ -0,0 +1,83 @@ +package org.dromara.rs.domain.query; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.io.Serial; +import java.util.Date; + +/** + * 出国行程变更申请查询对象 rs_overseas_itinerary_change + * + * @author weizzzz + * @date 2025-10-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RsOverseasItineraryChangeQuery extends PageQuery { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关联出国记录ID + */ + private Long recordId; + + /** + * 原出国申请ID + */ + private Long applicationId; + + /** + * 申请人ID + */ + private Long applicantId; + + /** + * 申请人姓名(模糊查询) + */ + private String applicantName; + + /** + * 部门(模糊查询) + */ + private String department; + + /** + * 变更状态 + */ + private String status; + + /** + * 变更类型 + */ + private String changeType; + + /** + * 创建时间开始 + */ + private Date createTimeStart; + + /** + * 创建时间结束 + */ + private Date createTimeEnd; + + /** + * 审批时间开始 + */ + private Date approvalTimeStart; + + /** + * 审批时间结束 + */ + private Date approvalTimeEnd; + + /** + * 租户ID + */ + private String tenantId; +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasRegistrationQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasRegistrationQuery.java new file mode 100644 index 00000000..7248f5b5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasRegistrationQuery.java @@ -0,0 +1,132 @@ +package org.dromara.rs.domain.query; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +/** + * 出国登记查询对象 rs_overseas_registration + * + * @author weizzzz + * @date 2025-09-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RsOverseasRegistrationQuery extends PageQuery { + + private static final long serialVersionUID = 1L; + + /** + * 登记ID + */ + private Long registrationId; + + /** + * 关联申请ID + */ + private Long applicationId; + + /** + * 姓名(模糊查询) + */ + private String name; + + /** + * 性别 + */ + private String gender; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 身份证号(模糊查询) + */ + private String idNumber; + + /** + * 工作部门(模糊查询) + */ + private String department; + + /** + * 职务职称(模糊查询) + */ + private String position; + + /** + * 行政级别 + */ + private String administrativeLevel; + + /** + * 人员类别代码 + */ + private String personnelCategory; + + /** + * 前往国家(模糊查询) + */ + private String destinationCountry; + + /** + * 登记单位(模糊查询) + */ + private String registrationUnit; + + /** + * 出国类型 + */ + private String travelType; + + /** + * 状态 + */ + private String status; + + /** + * 出国日期-开始 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date departureDateStart; + + /** + * 出国日期-结束 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date departureDateEnd; + + /** + * 登记时间-开始(前端传参) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date registrationTimeStart; + + /** + * 登记时间-结束(前端传参) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date registrationTimeEnd; + + /** + * 提交时间-开始 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitTimeStart; + + /** + * 提交时间-结束 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitTimeEnd; + + /** + * 创建者(用于查询我的登记) + */ + private Long createBy; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RegistrationFormVO.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RegistrationFormVO.java new file mode 100644 index 00000000..a170a881 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RegistrationFormVO.java @@ -0,0 +1,55 @@ +package org.dromara.rs.domain.vo; + +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * 登记表PDF生成响应VO + * + * @author weizzzz + * @date 2025-01-03 + */ +@Data +public class RegistrationFormVO { + + /** + * 文档唯一ID + */ + private String documentId; + + /** + * 文件名 + */ + private String fileName; + + /** + * 下载链接 + */ + private String downloadUrl; + + /** + * 预览链接 + */ + private String previewUrl; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 生成时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private String generateTime; + + /** + * 文档编号 + */ + private String documentNumber; + + /** + * 申请ID + */ + private Long applicationId; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsApplicationVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsApplicationVo.java index a63fcc5f..08a981e5 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsApplicationVo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsApplicationVo.java @@ -17,7 +17,7 @@ import java.util.List; public class RsApplicationVo extends RsApplication { private static final long serialVersionUID = 1L; - + /** * 创建者 */ @@ -37,64 +37,64 @@ public class RsApplicationVo extends RsApplication { * 更新时间 */ private Date updateTime; - + /** * 部门 */ private String department; - + /** * 应聘者手机(兼容旧代码,实际使用contactPhone) */ private String applicantPhone; - + /** * 应聘者邮箱(兼容旧代码,实际使用contactEmail) */ private String applicantEmail; - + /** * 教育经历列表 */ private List educationList; - + /** * 工作经历列表 */ private List experienceList; - + /** * 状态名称 */ private String statusName; - + /** * 获取应聘者手机号 */ public String getApplicantPhone() { - return this.getContactPhone(); + return this.applicantPhone != null ? this.applicantPhone : super.getContactPhone(); } - + /** * 设置应聘者手机号 */ public void setApplicantPhone(String applicantPhone) { - this.setContactPhone(applicantPhone); + super.setContactPhone(applicantPhone); this.applicantPhone = applicantPhone; } - + /** * 获取应聘者邮箱 */ public String getApplicantEmail() { - return this.getContactEmail(); + return this.applicantEmail != null ? this.applicantEmail : super.getContactEmail(); } - + /** * 设置应聘者邮箱 */ public void setApplicantEmail(String applicantEmail) { - this.setContactEmail(applicantEmail); + super.setContactEmail(applicantEmail); this.applicantEmail = applicantEmail; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationAttachmentVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationAttachmentVo.java new file mode 100644 index 00000000..433dce2d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationAttachmentVo.java @@ -0,0 +1,112 @@ +package org.dromara.rs.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import java.io.Serializable; +import java.util.Date; + +/** + * 出国申请附件VO + * + * @author weizzzz + * @date 2025-10-10 + */ +@Data +public class RsOverseasApplicationAttachmentVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 附件ID + */ + private Long attachmentId; + + /** + * 出国申请ID + */ + private Long applicationId; + + /** + * 附件标题 + */ + private String attachmentTitle; + + /** + * 附件内容说明 + */ + private String attachmentContent; + + /** + * 文件名称 + */ + private String fileName; + + /** + * 文件原始名称 + */ + private String fileOriginalName; + + /** + * 文件存储路径/OSS URL + */ + private String filePath; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 文件大小(格式化) + */ + private String fileSizeFormatted; + + /** + * 文件类型 + */ + private String fileType; + + /** + * MIME类型 + */ + private String mimeType; + + /** + * 上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date uploadTime; + + /** + * 上传人ID + */ + private Long uploadUserId; + + /** + * 上传人姓名 + */ + private String uploadUserName; + + /** + * 创建者 + */ + private Long createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + private Long updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java index d43c7f46..8e722a56 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import java.io.Serial; import java.io.Serializable; -import java.math.BigDecimal; import java.util.Date; /** @@ -28,6 +27,11 @@ public class RsOverseasApplicationVo implements Serializable { */ private Long applicationId; + /** + * 编号(自动生成的登记编号) + */ + private String serialNumber; + /** * 申请人ID */ @@ -38,11 +42,112 @@ public class RsOverseasApplicationVo implements Serializable { */ private String applicantName; + /** + * 性别 + */ + private String gender; + + /** + * 性别文本 + */ + private String genderText; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 照片地址 + */ + private String photo; + + /** + * 公民身份号码 + */ + private String idNumber; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 政治面貌文本 + */ + private String politicalStatusText; + + /** + * 户籍所在地 + */ + private String registeredAddress; + /** * 所属部门 */ private String department; + /** + * 职务职称 + */ + private String position; + + /** + * 职称 + */ + private String professionalTitle; + + /** + * 行政职级 + */ + private String administrativeLevel; + + /** + * 行政职级文本 + */ + private String administrativeLevelText; + + /** + * 人员类别代码 + */ + private String personnelCategory; + + /** + * 人员类别代码文本 + */ + private String personnelCategoryText; + + /** + * 登记备案责任单位 + */ + private String registrationUnit; + + /** + * 登记备案责任单位负责人签名 + */ + private String unitLeaderSignature; + + /** + * 办公电话 + */ + private String officePhone; + + /** + * 申领证件名称 + */ + private String documentName; + + /** + * 证件号码 + */ + private String documentNumber; + + /** + * 移动电话 + */ + private String mobilePhone; + /** * 联系电话 */ @@ -68,6 +173,21 @@ public class RsOverseasApplicationVo implements Serializable { */ private String destination; + /** + * 前往国家 + */ + private String destinationCountry; + + /** + * 出境事由 + */ + private String exitReason; + + /** + * 探望对象姓名及与申请者关系 + */ + private String visitRelationship; + /** * 目的地 */ @@ -90,11 +210,6 @@ public class RsOverseasApplicationVo implements Serializable { */ private Integer duration; - /** - * 预算金额 - */ - private BigDecimal budget; - /** * 邀请函信息 */ @@ -125,13 +240,33 @@ public class RsOverseasApplicationVo implements Serializable { */ private String statusText; + /** + * 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止 + */ + private String approvalStatus; + + /** + * 审批状态文本 + */ + private String approvalStatusText; + + /** + * 工作流任务ID(用于前端判断是否显示审批按钮) + */ + private Long taskId; + /** * 当前审批人 */ private String currentApprover; /** - * 审批级别 + * 当前审批人ID + */ + private Long currentApproverId; + + /** + * 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 */ private String approvalLevel; @@ -157,11 +292,83 @@ public class RsOverseasApplicationVo implements Serializable { */ private String applicantRemark; + /** + * 应急联系人及电话 + */ + private String emergencyContact; + + /** + * 附件OSS ID列表(逗号分隔) + */ + private String attachmentIds; + + /** + * 手写签字(base64格式) + */ + private String signature; + + /** + * 备注 + */ + private String remarks; + /** * 管理员备注 */ private String adminRemark; + /** + * 登记备案表盖章文件路径 + */ + private String stampedRegistrationFilePath; + + /** + * 登记备案表盖章文件名 + */ + private String stampedRegistrationFileName; + + /** + * 盖章文件上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date stampedFileUploadTime; + + /** + * 盖章文件上传状态:0-未上传,1-已上传 + */ + private String stampedFileUploadStatus; + + /** + * 盖章文件OSS ID(用于安全下载) + */ + private Long stampedFileOssId; + + /** + * 党籍保留附件文件路径 + */ + private String partyRetentionFilePath; + + /** + * 党籍保留附件文件名 + */ + private String partyRetentionFileName; + + /** + * 党籍保留附件上传时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date partyRetentionUploadTime; + + /** + * 党籍保留附件上传状态:0-未上传,1-已上传 + */ + private String partyRetentionUploadStatus; + + /** + * 党籍保留附件OSS ID(用于安全下载) + */ + private Long partyRetentionFileOssId; + /** * 租户编号 */ @@ -198,4 +405,4 @@ public class RsOverseasApplicationVo implements Serializable { * 获取经费来源文本 */ -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalMonitorVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalMonitorVo.java new file mode 100644 index 00000000..9a3e3219 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalMonitorVo.java @@ -0,0 +1,339 @@ +package org.dromara.rs.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 出国申请审批监控视图对象 + *

+ * 用于审批监控页面展示,包含申请信息和审批流程信息 + * + * @author Cursor AI + * @date 2025-10-21 + */ +@Data +public class RsOverseasApprovalMonitorVo implements Serializable { + + private static final long serialVersionUID = 1L; + + // ==================== 申请基本信息 ==================== + + /** + * 申请ID + */ + @ExcelProperty(value = "申请ID") + private Long applicationId; + + /** + * 出国记录ID(已完成的申请会有对应的记录ID) + */ + private Long recordId; + + /** + * 编号(自动生成的登记编号) + */ + @ExcelProperty(value = "编号") + private String serialNumber; + + /** + * 申请人ID + */ + private Long applicantId; + + /** + * 申请人姓名 + */ + @ExcelProperty(value = "申请人姓名") + private String applicantName; + + /** + * 性别:0-男,1-女 + */ + @ExcelProperty(value = "性别") + private String gender; + + /** + * 性别文本(用于前端显示) + */ + private String genderText; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "出生日期") + private Date birthDate; + + /** + * 政治面貌 + */ + @ExcelProperty(value = "政治面貌") + private String politicalStatus; + + /** + * 政治面貌文本(用于前端显示) + */ + private String politicalStatusText; + + /** + * 所属部门 + */ + @ExcelProperty(value = "所属部门") + private String department; + + /** + * 职称 + */ + @ExcelProperty(value = "职称") + private String professionalTitle; + + /** + * 职务职称 + */ + @ExcelProperty(value = "职务职称") + private String position; + + // ==================== 证件信息 ==================== + + /** + * 申领证件名称 + */ + @ExcelProperty(value = "申领证件名称") + private String documentName; + + /** + * 证件号码 + */ + @ExcelProperty(value = "证件号码") + private String documentNumber; + + // ==================== 出国信息 ==================== + + /** + * 出国目的 + */ + @ExcelProperty(value = "出国目的") + private String purpose; + + /** + * 目的地国家(兼容字段) + */ + @ExcelProperty(value = "目的地国家") + private String destination; + + /** + * 前往国家 + */ + @ExcelProperty(value = "前往国家") + private String destinationCountry; + + /** + * 目的地城市 + */ + @ExcelProperty(value = "目的地城市") + private String destinationCity; + + /** + * 出境事由 + */ + @ExcelProperty(value = "出境事由") + private String exitReason; + + /** + * 探望对象姓名及与申请者关系(仅探亲时) + */ + @ExcelProperty(value = "探望对象关系") + private String visitRelationship; + + /** + * 行程安排 + */ + @ExcelProperty(value = "行程安排") + private String itinerary; + + /** + * 开始日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "开始日期") + private Date startDate; + + /** + * 结束日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "结束日期") + private Date endDate; + + /** + * 出国天数 + */ + @ExcelProperty(value = "出国天数") + private Integer duration; + + // ==================== 联系信息 ==================== + + /** + * 移动电话 + */ + @ExcelProperty(value = "移动电话") + private String mobilePhone; + + /** + * 联系电话(兼容字段) + */ + @ExcelProperty(value = "联系电话") + private String contactPhone; + + /** + * 应急联系人及电话 + */ + @ExcelProperty(value = "应急联系人及电话") + private String emergencyContact; + + // ==================== 审批状态信息 ==================== + + /** + * 申请状态:0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成 + */ + @ExcelProperty(value = "申请状态") + private String status; + + /** + * 申请状态文本(用于前端显示) + */ + @ExcelProperty(value = "申请状态") + private String statusText; + + /** + * 当前审批人 + */ + @ExcelProperty(value = "当前审批人") + private String currentApprover; + + /** + * 当前审批人ID + */ + private Long currentApproverId; + + /** + * 审批级别:1-部门主管,2-分管领导,3-校领导 + */ + @ExcelProperty(value = "审批级别") + private String approvalLevel; + + /** + * 审批级别文本(用于前端显示) + */ + @ExcelProperty(value = "当前审批环节") + private String approvalLevelText; + + /** + * 申请时间(提交时间) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "申请时间") + private Date applyTime; + + /** + * 审批时间(最后一次审批时间) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "审批时间") + private Date approvalTime; + + // ==================== 审批进度信息 ==================== + + /** + * 审批进度:例如 "2/3"(已完成2级,共3级) + */ + @ExcelProperty(value = "审批进度") + private String approvalProgress; + + /** + * 已审批级别数 + */ + private Integer approvedLevels; + + /** + * 总审批级别数 + */ + private Integer totalLevels; + + /** + * 审批耗时(天) + */ + @ExcelProperty(value = "审批耗时(天)") + private Integer approvalDays; + + /** + * 是否超时(根据业务规则判断) + */ + private Boolean isOverdue; + + // ==================== 审批历史记录 ==================== + + /** + * 审批历史记录列表 + */ + private List approvalHistory; + + /** + * 最近一次审批意见 + */ + @ExcelProperty(value = "最近审批意见") + private String latestApprovalOpinion; + + /** + * 最近一次审批人 + */ + @ExcelProperty(value = "最近审批人") + private String latestApproverName; + + /** + * 最近一次审批时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ExcelProperty(value = "最近审批时间") + private Date latestApprovalTime; + + // ==================== 其他信息 ==================== + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remarks; + + /** + * 管理员备注 + */ + private String adminRemark; + + /** + * 申请人备注 + */ + private String applicantRemark; + + /** + * 租户编号 + */ + private String tenantId; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java index a9040758..486b8519 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java @@ -67,6 +67,11 @@ public class RsOverseasApprovalVo implements Serializable { */ private String approvalOpinion; + /** + * 审批签字(base64图片数据) + */ + private String approvalSignature; + /** * 审批时间 */ @@ -74,49 +79,49 @@ public class RsOverseasApprovalVo implements Serializable { private Date approvalTime; // ==================== 申请信息相关字段 ==================== - + /** * 申请人ID */ private Long applicantId; - + /** * 申请人姓名 */ private String applicantName; - + /** * 所属部门 */ private String department; - + /** * 出国目的 */ private String purpose; - + /** * 出国目的文本 */ private String purposeText; - + /** * 目的地 */ private String destination; - + /** * 出国开始日期 */ @JsonFormat(pattern = "yyyy-MM-dd") private Date startDate; - + /** * 出国结束日期 */ @JsonFormat(pattern = "yyyy-MM-dd") private Date endDate; - + /** * 出国天数 */ @@ -158,4 +163,4 @@ public class RsOverseasApprovalVo implements Serializable { */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasItineraryChangeVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasItineraryChangeVo.java new file mode 100644 index 00000000..98a0b809 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasItineraryChangeVo.java @@ -0,0 +1,271 @@ +package org.dromara.rs.domain.vo; + +import org.dromara.rs.domain.RsOverseasItineraryChange; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * 出国行程变更视图对象 rs_overseas_itinerary_change + * + * @author zlwu + * @date 2025-01-13 + */ +@Data +@AutoMapper(target = RsOverseasItineraryChange.class) +public class RsOverseasItineraryChangeVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 变更ID(主键,雪花ID) + */ + private Long changeId; + + /** + * 关联备案记录ID(外键,关联rs_overseas_entry_exit_record.record_id) + */ + private Long recordId; + + /** + * 关联出国申请ID(外键,关联rs_overseas_application.application_id,可为空) + */ + private Long applicationId; + + /** + * 申请人ID(外键,关联sys_user.user_id) + */ + private Long applicantId; + + /** + * 申请人姓名 + */ + private String applicantName; + + /** + * 性别:0-男,1-女 + */ + private String gender; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 职称 + */ + private String professionalTitle; + + /** + * 职务 + */ + private String position; + + /** + * 申领证件名称 + */ + private String documentName; + + /** + * 证件号码 + */ + private String documentNumber; + + /** + * 部门 + */ + private String department; + + /** + * 原目的地国家 + */ + private String originalDestination; + + /** + * 原目的地城市 + */ + private String originalDestinationCity; + + /** + * 原出境事由 + */ + private String originalExitReason; + + /** + * 原计划开始日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date originalStartDate; + + /** + * 原计划结束日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date originalEndDate; + + /** + * 原计划天数 + */ + private Integer originalDuration; + + /** + * 原行程安排(详细) + */ + private String originalItinerary; + + /** + * 原住宿计划 + */ + private String originalAccommodationPlan; + + /** + * 原交通计划 + */ + private String originalTransportationPlan; + + /** + * 变更类型(字典:rs_itinerary_change_type) + */ + private String changeType; + + /** + * 变更类型文本(字典翻译) + */ + private String changeTypeText; + + /** + * 新目的地国家 + */ + private String newDestination; + + /** + * 新目的地城市 + */ + private String newDestinationCity; + + /** + * 新出境事由 + */ + private String newExitReason; + + /** + * 新计划开始日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newStartDate; + + /** + * 新计划结束日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newEndDate; + + /** + * 新计划天数 + */ + private Integer newDuration; + + /** + * 新行程安排(详细) + */ + private String newItinerary; + + /** + * 新住宿计划 + */ + private String newAccommodationPlan; + + /** + * 新交通计划 + */ + private String newTransportationPlan; + + /** + * 变更原因(必填) + */ + private String changeReason; + + /** + * 备注 + */ + private String remark; + + /** + * 审批状态(字典:rs_itinerary_change_status,0:待审核 1:已通过 2:已拒绝 3:已撤回) + */ + private String status; + + /** + * 审批状态文本(字典翻译) + */ + private String statusText; + + /** + * 审批意见 + */ + private String approvalOpinion; + + /** + * 审批时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date approvalTime; + + /** + * 审批人ID + */ + private Long approverId; + + /** + * 审批人姓名 + */ + private String approverName; + + /** + * 租户编号 + */ + private String tenantId; + + /** + * 删除标志(0代表存在 1代表删除) + */ + private Integer deleted; + + /** + * 创建部门 + */ + private Long createDept; + + /** + * 创建者 + */ + private Long createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + private Long updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java index edf94076..dacd903b 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java @@ -48,11 +48,94 @@ public class RsOverseasRecordVo implements Serializable { */ private String department; + /** + * 职位 + */ + private String position; + + /** + * 职位文本(用于显示) + */ + private String positionText; + /** * 联系电话 */ private String contactPhone; + /** + * 联系邮箱 + */ + private String contactEmail; + + // ========== 以下字段来自关联的出国申请表 ========== + + /** + * 性别(来自申请表) + */ + private String gender; + + /** + * 性别文本(来自申请表) + */ + private String genderText; + + /** + * 出生日期(来自申请表) + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 政治面貌(来自申请表) + */ + private String politicalStatus; + + /** + * 政治面貌文本(来自申请表) + */ + private String politicalStatusText; + + /** + * 职称(来自申请表) + */ + private String professionalTitle; + + /** + * 申领证件名称(来自申请表) + */ + private String documentName; + + /** + * 证件号码(来自申请表) + */ + private String documentNumber; + + /** + * 移动电话(来自申请表) + */ + private String mobilePhone; + + /** + * 前往国家(来自申请表) + */ + private String destinationCountry; + + /** + * 出境事由(来自申请表) + */ + private String exitReason; + + /** + * 应急联系人及电话(来自申请表) + */ + private String emergencyContact; + + /** + * 备注(来自申请表) + */ + private String remarks; + /** * 出国目的 */ @@ -120,6 +203,16 @@ public class RsOverseasRecordVo implements Serializable { */ private String transportationPlan; + /** + * 申请状态(来自申请表) + */ + private String status; + + /** + * 申请状态文本(来自申请表) + */ + private String statusText; + /** * 完成状态 */ @@ -181,7 +274,7 @@ public class RsOverseasRecordVo implements Serializable { */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; - + /** * 获取出国目的文本 */ @@ -209,7 +302,7 @@ public class RsOverseasRecordVo implements Serializable { return this.purpose; } } - + /** * 数据来源标识 */ @@ -234,7 +327,7 @@ public class RsOverseasRecordVo implements Serializable { return "未知状态"; } } - + /** * 获取数据来源说明 */ @@ -251,4 +344,4 @@ public class RsOverseasRecordVo implements Serializable { return "出国记录"; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRegistrationVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRegistrationVo.java new file mode 100644 index 00000000..d99a2c4b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRegistrationVo.java @@ -0,0 +1,231 @@ +package org.dromara.rs.domain.vo; + +import org.dromara.rs.domain.RsOverseasRegistration; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 出国登记VO对象 rs_overseas_registration + * + * @author weizzzz + * @date 2025-09-30 + */ +@Data +@AutoMapper(target = RsOverseasRegistration.class) +public class RsOverseasRegistrationVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 登记ID + */ + private Long registrationId; + + /** + * 关联申请ID + */ + private Long applicationId; + + /** + * 姓名 + */ + private String name; + + /** + * 性别 + */ + private String gender; + + /** + * 性别文本 + */ + private String genderText; + + /** + * 身份证号 + */ + private String idNumber; + + /** + * 出生日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birthDate; + + /** + * 户籍所在地 + */ + private String registeredAddress; + + /** + * 工作部门 + */ + private String department; + + /** + * 职务职称 + */ + private String position; + + /** + * 行政职级 + */ + private String administrativeLevel; + + /** + * 行政职级文本 + */ + private String administrativeLevelText; + + /** + * 人员类别代码 + */ + private String personnelCategory; + + /** + * 人员类别文本 + */ + private String personnelCategoryText; + + /** + * 照片地址 + */ + private String photo; + + /** + * 政治面貌 + */ + private String politicalStatus; + + /** + * 政治面貌文本 + */ + private String politicalStatusText; + + /** + * 工作单位及职务 + */ + private String workUnitPosition; + + /** + * 配偶姓名 + */ + private String spouse; + + /** + * 联系电话 + */ + private String contactPhone; + + /** + * 出国日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date departureDate; + + /** + * 预定返回日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date returnDate; + + /** + * 停留天数 + */ + private Integer stayDays; + + /** + * 前往国家 + */ + private String destinationCountry; + + /** + * 前往国家详细信息 + */ + private String destinationDetail; + + /** + * 事由 + */ + private String reason; + + /** + * 登记单位 + */ + private String registrationUnit; + + /** + * 出国类型 + */ + private String travelType; + + /** + * 出国类型文本 + */ + private String travelTypeText; + + /** + * 状态 + */ + private String status; + + /** + * 状态文本 + */ + private String statusText; + + /** + * 提交时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submitTime; + + /** + * 登记时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date registrationTime; + + /** + * 备注 + */ + private String remarks; + + /** + * 租户编号 + */ + private String tenantId; + + /** + * 删除标志 + */ + private String deleted; + + /** + * 创建者(统一基础字段) + */ + private Long createBy; + + /** + * 创建时间(统一基础字段) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者(统一基础字段) + */ + private Long updateBy; + + /** + * 更新时间(统一基础字段) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/SysUserBasicVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/SysUserBasicVo.java new file mode 100644 index 00000000..9cc252ca --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/SysUserBasicVo.java @@ -0,0 +1,55 @@ +package org.dromara.rs.domain.vo; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户基本信息VO + * 用于培训申请/记录中的同行人信息展示 + * 不限制账号类型,只返回必要的基础信息 + * + * @author weizzzz + * @date 2025-11-06 + */ +@Data +public class SysUserBasicVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户账号 + */ + private String userName; + + /** + * 用户昵称(真实姓名) + */ + private String nickName; + + /** + * 账号类型(0-学生 1-教师 2-其他) + */ + private String accountType; + + /** + * 账号状态(0-正常 1-停用) + */ + private String status; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 部门名称 + */ + private String deptName; +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationAttachmentMapper.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationAttachmentMapper.java new file mode 100644 index 00000000..28526495 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationAttachmentMapper.java @@ -0,0 +1,35 @@ +package org.dromara.rs.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.dromara.rs.domain.RsOverseasApplicationAttachment; +import org.dromara.rs.domain.vo.RsOverseasApplicationAttachmentVo; + +import java.util.List; + +/** + * 出国申请附件Mapper接口 + * + * @author weizzzz + * @date 2025-10-10 + */ +@Mapper +public interface RsOverseasApplicationAttachmentMapper extends BaseMapper { + + /** + * 查询出国申请附件列表 + * + * @param applicationId 申请ID + * @return 附件列表 + */ + List selectAttachmentList(Long applicationId); + + /** + * 查询附件详情 + * + * @param attachmentId 附件ID + * @return 附件信息 + */ + RsOverseasApplicationAttachmentVo selectAttachmentById(Long attachmentId); +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationMapper.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationMapper.java index 093a9afa..d371ea95 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationMapper.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationMapper.java @@ -13,7 +13,7 @@ import org.dromara.rs.domain.query.RsOverseasApplicationQuery; * @date 2025-07-28 */ public interface RsOverseasApplicationMapper extends BaseMapperPlus { - + /** * 查询出国申请列表 * @@ -21,7 +21,7 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectOverseasApplicationList(RsOverseasApplicationQuery query); - + /** * 查询我的出国申请列表(排除已通过的申请) * @@ -29,7 +29,7 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectMyOverseasApplicationList(RsOverseasApplicationQuery query); - + /** * 查询出国申请详细信息 * @@ -37,7 +37,7 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectOverseasApplicationsByApplicantId(Long applicantId); - + /** * 根据状态查询出国申请列表 * @@ -53,7 +53,7 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectOverseasApplicationsByStatus(String status); - + /** * 根据当前审批人查询待审批申请列表 * @@ -61,7 +61,7 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectPendingApplicationsByApprover(String currentApprover); - + /** * 根据审批级别查询待审批申请列表 * @@ -69,4 +69,79 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus selectPendingApplicationsByLevel(String approvalLevel); -} \ No newline at end of file + + /** + * 查询申请人的部门ID + * + * @param applicantId 申请人ID + * @return 部门ID + */ + Long selectApplicantDeptId(Long applicantId); + + /** + * 根据部门ID查询部门负责人ID + * + * @param deptId 部门ID + * @return 部门负责人ID + */ + Long selectDeptHeadByDeptId(Long deptId); + + /** + * 查询上级部门负责人ID + * + * @param deptId 部门ID + * @return 上级部门负责人ID + */ + Long selectParentDeptHead(Long deptId); + + /** + * 根据用户ID查询用户姓名 + * + * @param userId 用户ID + * @return 用户姓名 + */ + String selectUserNameById(Long userId); + + /** + * 根据权限标识查询拥有该权限的用户ID列表 + * + * 查询逻辑: + * 1. 通过 sys_menu 查找权限对应的菜单ID + * 2. 通过 sys_role_menu 查找拥有该菜单的角色 + * 3. 通过 sys_user_role 查找拥有这些角色的用户 + * 4. 过滤掉已停用的用户 + * + * @param permissionCode 权限标识(如:rs:overseas:approve2) + * @return 用户ID列表 + */ + List selectUserIdsByPermission(String permissionCode); + + /** + * 根据用户ID查询其拥有的出国审批权限列表 + * + * 查询逻辑: + * 1. 通过 sys_user_role 查找用户的角色 + * 2. 通过 sys_role_menu 查找角色的菜单 + * 3. 通过 sys_menu 查找菜单的权限标识 + * 4. 过滤出出国审批相关的权限(rs:overseas:approve%) + * + * @param userId 用户ID + * @return 审批权限标识列表(如:["rs:overseas:approve2", "rs:overseas:approve3"]) + */ + List selectApprovalPermissionsByUserId(Long userId); + + /** + * 🔴 新增:查询待审批申请列表(支持部门过滤) + * + * 查询逻辑: + * 1. 关联 sys_user 表获取申请人的部门信息 + * 2. 按审批级别过滤(approvalLevel IN (...) ) + * 3. 按状态过滤(status = '0' 待审核) + * 4. 按部门过滤(deptId != null 时,u.dept_id = #{deptId}) + * + * @param approvalLevels 审批级别列表,例如:["1", "2"] + * @param deptId 部门ID(部门负责人需要),为null则不过滤部门 + * @return 待审批申请列表 + */ + List selectPendingApplications(List approvalLevels, Long deptId); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasItineraryChangeMapper.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasItineraryChangeMapper.java new file mode 100644 index 00000000..56bf72bd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasItineraryChangeMapper.java @@ -0,0 +1,42 @@ +package org.dromara.rs.mapper; + +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.rs.domain.RsOverseasItineraryChange; +import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo; +import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery; + +import java.util.List; + +/** + * 出国行程变更申请Mapper接口 + * + * @author weizzzz + * @date 2025-10-13 + */ +public interface RsOverseasItineraryChangeMapper extends BaseMapperPlus { + + /** + * 查询出国行程变更申请列表 + * + * @param query 查询条件 + * @return 出国行程变更申请集合 + */ + List selectChangeList(RsOverseasItineraryChangeQuery query); + + /** + * 查询出国行程变更申请详情 + * + * @param changeId 变更ID + * @return 出国行程变更申请 + */ + RsOverseasItineraryChangeVo selectChangeById(Long changeId); + + /** + * 根据出国记录ID查询变更记录列表 + * + * @param recordId 出国记录ID + * @return 变更记录列表 + */ + List selectChangeListByRecordId(Long recordId); +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasRegistrationMapper.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasRegistrationMapper.java new file mode 100644 index 00000000..c927563b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasRegistrationMapper.java @@ -0,0 +1,73 @@ +package org.dromara.rs.mapper; + +import java.util.List; +import java.util.Map; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.rs.domain.RsOverseasRegistration; +import org.dromara.rs.domain.vo.RsOverseasRegistrationVo; +import org.dromara.rs.domain.query.RsOverseasRegistrationQuery; + +/** + * 出国登记Mapper接口 + * + * @author weizzzz + * @date 2025-09-30 + */ +public interface RsOverseasRegistrationMapper extends BaseMapperPlus { + + /** + * 查询出国登记列表 + * + * @param query 查询条件 + * @return 出国登记集合 + */ + List selectRegistrationList(RsOverseasRegistrationQuery query); + + /** + * 查询我的出国登记列表 + * + * @param query 查询条件 + * @return 出国登记集合 + */ + List selectMyRegistrationList(RsOverseasRegistrationQuery query); + + /** + * 查询出国登记详细信息 + * + * @param registrationId 出国登记主键 + * @return 出国登记 + */ + RsOverseasRegistrationVo selectRegistrationById(Long registrationId); + + /** + * 根据申请ID查询出国登记信息 + * + * @param applicationId 申请ID + * @return 出国登记 + */ + RsOverseasRegistrationVo selectRegistrationByApplicationId(Long applicationId); + + /** + * 根据状态查询出国登记列表 + * + * @param status 状态 + * @return 出国登记集合 + */ + List selectRegistrationsByStatus(String status); + + /** + * 根据姓名查询出国登记列表 + * + * @param name 姓名 + * @return 出国登记集合 + */ + List selectRegistrationsByName(String name); + + /** + * 统计各状态的登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> selectStatusStatistics(RsOverseasRegistrationQuery query); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsTrainingApprovalMapper.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsTrainingApprovalMapper.java index 6a73e593..7a069c58 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsTrainingApprovalMapper.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsTrainingApprovalMapper.java @@ -20,4 +20,12 @@ public interface RsTrainingApprovalMapper extends BaseMapperPlus selectRsTrainingApprovalWithDetailsList(RsTrainingApproval approval); + /** + * 查询带详细信息的审批记录数量(关联申请人和课程信息) + * + * @param approval 查询条件 + * @return 记录数量 + */ + long selectRsTrainingApprovalWithDetailsCount(RsTrainingApproval approval); + } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRegistrationFormService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRegistrationFormService.java new file mode 100644 index 00000000..377d65cf --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRegistrationFormService.java @@ -0,0 +1,52 @@ +package org.dromara.rs.service; + +import org.dromara.rs.domain.bo.GenerateRegistrationFormBo; +import org.dromara.rs.domain.vo.RegistrationFormVO; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 登记表PDF生成服务接口 + * + * @author weizzzz + * @date 2025-01-03 + */ +public interface IRegistrationFormService { + + /** + * 生成出国登记表PDF + * + * @param bo 生成请求参数 + * @return 生成结果 + */ + RegistrationFormVO generateRegistrationFormPdf(GenerateRegistrationFormBo bo); + + /** + * 下载生成的PDF文档 + * + * @param documentId 文档ID + * @param response HTTP响应 + */ + void downloadPdf(String documentId, HttpServletResponse response); + + /** + * 获取PDF预览链接 + * + * @param documentId 文档ID + * @return 预览链接 + */ + String getPreviewUrl(String documentId); + + /** + * 下载登记备案表模板 + * + * @param response HTTP响应 + */ + void downloadRegistrationTemplate(HttpServletResponse response); + + /** + * 下载保留党籍申请模板 + * + * @param response HTTP响应 + */ + void downloadPartyMembershipTemplate(HttpServletResponse response); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationAttachmentService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationAttachmentService.java new file mode 100644 index 00000000..8a5fa648 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationAttachmentService.java @@ -0,0 +1,70 @@ +package org.dromara.rs.service; + +import org.dromara.rs.domain.bo.RsOverseasApplicationAttachmentBo; +import org.dromara.rs.domain.vo.RsOverseasApplicationAttachmentVo; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +/** + * 出国申请附件Service接口 + * + * @author weizzzz + * @date 2025-10-10 + */ +public interface IRsOverseasApplicationAttachmentService { + + /** + * 查询出国申请的附件列表 + * + * @param applicationId 申请ID + * @return 附件列表 + */ + List selectAttachmentList(Long applicationId); + + /** + * 查询附件详情 + * + * @param attachmentId 附件ID + * @return 附件信息 + */ + RsOverseasApplicationAttachmentVo selectAttachmentById(Long attachmentId); + + /** + * 上传附件 + * + * @param applicationId 申请ID + * @param title 附件标题 + * @param content 附件内容说明 + * @param file 文件 + * @return 附件信息 + * @throws IOException IO异常 + */ + RsOverseasApplicationAttachmentVo uploadAttachment(Long applicationId, String title, String content, MultipartFile file) throws IOException; + + /** + * 更新附件信息(标题和内容) + * + * @param bo 附件业务对象 + * @return 是否成功 + */ + boolean updateAttachment(RsOverseasApplicationAttachmentBo bo); + + /** + * 删除附件 + * + * @param attachmentId 附件ID + * @return 是否成功 + */ + boolean deleteAttachment(Long attachmentId); + + /** + * 批量删除附件 + * + * @param attachmentIds 附件ID数组 + * @return 是否成功 + */ + boolean deleteAttachments(Long[] attachmentIds); +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java index c541aabd..cbdc7557 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java @@ -17,145 +17,222 @@ import java.util.Collection; */ public interface IRsOverseasApplicationService { - /** - * 查询出国申请分页列表 - * - * @param query 查询条件 - * @return 出国申请分页集合 - */ - TableDataInfo queryPageList(RsOverseasApplicationQuery query); + /** + * 查询出国申请分页列表 + * + * @param query 查询条件 + * @return 出国申请分页集合 + */ + TableDataInfo queryPageList(RsOverseasApplicationQuery query); - /** - * 查询我的出国申请分页列表(不包括已通过的申请) - * - * @param query 查询条件 - * @return 出国申请分页集合 - */ - TableDataInfo queryMyApplicationList(RsOverseasApplicationQuery query); + /** + * 查询我的出国申请分页列表(不包括已通过的申请) + * + * @param query 查询条件 + * @return 出国申请分页集合 + */ + TableDataInfo queryMyApplicationList(RsOverseasApplicationQuery query); - /** - * 查询出国申请列表 - * - * @param query 查询条件 - * @return 出国申请集合 - */ - List queryList(RsOverseasApplicationQuery query); + /** + * 查询出国申请列表 + * + * @param query 查询条件 + * @return 出国申请集合 + */ + List queryList(RsOverseasApplicationQuery query); - /** - * 查询出国申请详细信息 - * - * @param applicationId 出国申请主键 - * @return 出国申请 - */ - RsOverseasApplicationVo queryById(Long applicationId); + /** + * 查询当前用户的待审批申请列表 + * + * @return 待审批申请集合 + */ + List queryPendingByCurrentUser(); - /** - * 根据申请人ID查询出国申请列表 - * - * @param applicantId 申请人ID - * @return 出国申请集合 - */ - List queryByApplicantId(Long applicantId); + /** + * 查询出国申请详细信息 + * + * @param applicationId 出国申请主键 + * @return 出国申请 + */ + RsOverseasApplicationVo queryById(Long applicationId); - /** - * 根据状态查询出国申请列表 - * - * @param status 申请状态 - * @return 出国申请集合 - */ - List queryByStatus(String status); + /** + * 根据申请人ID查询出国申请列表 + * + * @param applicantId 申请人ID + * @return 出国申请集合 + */ + List queryByApplicantId(Long applicantId); - /** - * 根据当前审批人查询待审批申请列表 - * - * @param currentApprover 当前审批人 - * @return 出国申请集合 - */ - List queryPendingByApprover(String currentApprover); + /** + * 根据状态查询出国申请列表 + * + * @param status 申请状态 + * @return 出国申请集合 + */ + List queryByStatus(String status); - /** - * 根据审批级别查询待审批申请列表 - * - * @param approvalLevel 审批级别 - * @return 出国申请集合 - */ - List queryPendingByLevel(String approvalLevel); + /** + * 根据当前审批人查询待审批申请列表 + * + * @param currentApprover 当前审批人 + * @return 出国申请集合 + */ + List queryPendingByApprover(String currentApprover); - /** - * 新增出国申请 - * - * @param bo 出国申请业务对象 - * @return 是否成功 - */ - Boolean insertByBo(RsOverseasApplicationBo bo); + /** + * 根据审批级别查询待审批申请列表 + * + * @param approvalLevel 审批级别 + * @return 出国申请集合 + */ + List queryPendingByLevel(String approvalLevel); - /** - * 修改出国申请 - * - * @param bo 出国申请业务对象 - * @return 是否成功 - */ - Boolean updateByBo(RsOverseasApplicationBo bo); + /** + * 新增出国申请 + * + * @param bo 出国申请业务对象 + * @return 是否成功 + */ + Boolean insertByBo(RsOverseasApplicationBo bo); - /** - * 提交出国申请(更新状态为待审核) - * - * @param applicationId 申请ID - * @return 是否成功 - */ - Boolean submitApplication(Long applicationId); + /** + * 修改出国申请 + * + * @param bo 出国申请业务对象 + * @return 是否成功 + */ + Boolean updateByBo(RsOverseasApplicationBo bo); - /** - * 取消出国申请 - * - * @param applicationId 申请ID - * @param reason 取消原因 - * @return 是否成功 - */ - Boolean cancelApplication(Long applicationId, String reason); + /** + * 提交出国申请(更新状态为待审核) + * + * @param applicationId 申请ID + * @return 是否成功 + */ + Boolean submitApplication(Long applicationId); - /** - * 审批出国申请 - * - * @param applicationId 申请ID - * @param approverId 审批人ID - * @param approverName 审批人姓名 - * @param approvalStatus 审批结果:1-通过,2-拒绝 - * @param approvalOpinion 审批意见 - * @return 是否成功 - */ - Boolean approveApplication(Long applicationId, Long approverId, String approverName, - String approvalStatus, String approvalOpinion); + /** + * 取消出国申请 + * + * @param applicationId 申请ID + * @param reason 取消原因 + * @return 是否成功 + */ + Boolean cancelApplication(Long applicationId, String reason); - /** - * 校验删除数据 - * - * @param ids 主键集合 - * @param isValid 是否有效性校验 - */ - void validatedByIds(List ids, Boolean isValid); + /** + * 审批出国申请 + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批结果:1-通过,2-拒绝 + * @param approvalOpinion 审批意见 + * @return 是否成功 + */ + Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature); - /** - * 批量删除出国申请 - * - * @param ids 主键集合 - * @return 是否成功 - */ - Boolean deleteWithValidByIds(List ids, Boolean isValid); + /** + * 审批出国申请(支持标准工作流模式) + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批结果:1-通过,2-拒绝 + * @param approvalOpinion 审批意见 + * @param approvalSignature 审批签字(base64图片) + * @param taskId 工作流任务ID(可选,用于标准工作流模式) + * @return 是否成功 + */ + Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature, String taskId); - /** - * 修复已审批通过但未创建记录的申请 - * - * @return 修复的记录数量 - */ - int fixApprovedApplicationsWithoutRecords(); + /** + * 校验删除数据 + * + * @param ids 主键集合 + * @param isValid 是否有效性校验 + */ + void validatedByIds(List ids, Boolean isValid); - /** - * 重新提交已被拒绝的申请 - * - * @param applicationId 申请ID - * @param applicationData 申请更新数据 - * @return 是否成功 - */ - Boolean resubmitApplication(Long applicationId, RsOverseasApplicationBo applicationData); -} \ No newline at end of file + /** + * 批量删除出国申请 + * + * @param ids 主键集合 + * @return 是否成功 + */ + Boolean deleteWithValidByIds(List ids, Boolean isValid); + + /** + * 修复已审批通过但未创建记录的申请 + * + * @return 修复的记录数量 + */ + int fixApprovedApplicationsWithoutRecords(); + + /** + * 重新提交已被拒绝的申请 + * + * @param applicationId 申请ID + * @param applicationData 申请更新数据 + * @return 是否成功 + */ + Boolean resubmitApplication(Long applicationId, RsOverseasApplicationBo applicationData); + + /** + * 更新盖章文件信息 + * + * @param applicationId 申请ID + * @param filePath 文件路径 + * @param fileName 文件名 + * @param uploadTime 上传时间 + * @param uploadStatus 上传状态 + * @param ossId OSS文件ID + * @return 是否成功 + */ + Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId); + + /** + * 更新党籍保留附件信息 + * + * @param applicationId 申请ID + * @param filePath 文件路径 + * @param fileName 文件名 + * @param uploadTime 上传时间 + * @param uploadStatus 上传状态 + * @param ossId OSS文件ID + * @return 是否成功 + */ + Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId); + + /** + * 校验出国登记前置条件 + * 检查近3个月内是否有已通过的出国登记记录 + * + * @param applicantId 申请人ID + * @return 校验结果Map,包含 isValid(是否通过校验)和 message(提示信息) + */ + java.util.Map validateRegistrationPrerequisite(Long applicantId); + + // ==================== 动态审批人获取方法 ==================== + + /** + * 根据申请人ID获取部门审批人ID + * + * @param applicantId 申请人ID + * @return 部门审批人ID + */ + Long getDepartmentApproverId(Long applicantId); + + /** + * 根据申请人ID获取人事处审批人列表 + * + * @param applicantId 申请人ID + * @return 人事处审批人ID列表 + */ + List getHrApprovers(Long applicantId); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java new file mode 100644 index 00000000..8659fe33 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java @@ -0,0 +1,56 @@ +package org.dromara.rs.service; + +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.rs.domain.vo.RsOverseasApprovalMonitorVo; +import org.dromara.rs.domain.query.RsOverseasApprovalMonitorQuery; + +import java.util.Map; + +/** + * 出国申请审批监控Service接口 + * + * @author Cursor AI + * @date 2025-10-21 + */ +public interface IRsOverseasApprovalMonitorService { + + /** + * 查询审批中的申请列表(待审核状态) + * + * @param query 查询条件 + * @return 审批中的申请列表 + */ + TableDataInfo queryPendingList(RsOverseasApprovalMonitorQuery query); + + /** + * 查询已完成审批的申请列表(已通过状态) + * + * @param query 查询条件 + * @return 已完成审批的申请列表 + */ + TableDataInfo queryCompletedList(RsOverseasApprovalMonitorQuery query); + + /** + * 查询申请详情(含审批流程信息) + * + * @param applicationId 申请ID + * @return 申请详情 + */ + RsOverseasApprovalMonitorVo queryDetailWithApprovalInfo(Long applicationId); + + /** + * 获取审批进度统计 + * + * @return 统计数据 + */ + Map getApprovalStatistics(); + + /** + * 下载出国申请审批表PDF + * + * @param applicationId 申请ID + * @param response HTTP响应对象 + */ + void downloadApprovalPdf(Long applicationId, HttpServletResponse response); +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasItineraryChangeService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasItineraryChangeService.java new file mode 100644 index 00000000..a85131ea --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasItineraryChangeService.java @@ -0,0 +1,108 @@ +package org.dromara.rs.service; + +import java.util.List; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.rs.domain.bo.RsOverseasItineraryChangeBo; +import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo; +import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery; + +/** + * 出国行程变更申请Service接口 + * + * @author weizzzz + * @date 2025-10-13 + */ +public interface IRsOverseasItineraryChangeService { + + /** + * 查询出国行程变更申请分页列表 + * + * @param query 查询条件 + * @return 分页结果 + */ + TableDataInfo queryPageList(RsOverseasItineraryChangeQuery query); + + /** + * 查询出国行程变更申请列表 + * + * @param query 查询条件 + * @return 列表结果 + */ + List queryList(RsOverseasItineraryChangeQuery query); + + /** + * 查询出国行程变更申请详情 + * + * @param changeId 变更ID + * @return 详情数据 + */ + RsOverseasItineraryChangeVo queryById(Long changeId); + + /** + * 查询我的变更记录列表(当前登录用户的已通过变更申请) + * 说明:变更记录Tab只显示已通过审批的变更申请,这些是真正生效的变更记录 + * + * @param query 查询条件 + * @return 分页结果 + */ + TableDataInfo queryMyChangeList(RsOverseasItineraryChangeQuery query); + + /** + * 查询待审批的变更申请列表 + * + * @param query 查询条件 + * @return 分页结果 + */ + TableDataInfo queryPendingList(RsOverseasItineraryChangeQuery query); + + /** + * 新增出国行程变更申请 + * + * @param bo 业务对象 + * @return 是否成功 + */ + Boolean insertByBo(RsOverseasItineraryChangeBo bo); + + /** + * 修改出国行程变更申请(重新提交) + * + * @param bo 业务对象 + * @return 是否成功 + */ + Boolean updateByBo(RsOverseasItineraryChangeBo bo); + + /** + * 删除出国行程变更申请 + * + * @param changeIds 变更ID列表 + * @return 是否成功 + */ + Boolean deleteByIds(List changeIds); + + /** + * 撤回变更申请 + * + * @param changeId 变更ID + * @return 是否成功 + */ + Boolean cancelChange(Long changeId); + + /** + * 审批变更申请 + * + * @param changeId 变更ID + * @param approved 是否通过 + * @param opinion 审批意见 + * @return 是否成功 + */ + Boolean approveChange(Long changeId, Boolean approved, String opinion); + + /** + * 根据出国记录ID查询变更记录列表 + * + * @param recordId 出国记录ID + * @return 变更记录列表 + */ + List queryByRecordId(Long recordId); +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasRegistrationService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasRegistrationService.java new file mode 100644 index 00000000..9e8ecf6e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasRegistrationService.java @@ -0,0 +1,377 @@ +package org.dromara.rs.service; + +import java.util.List; +import java.util.Collection; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.rs.domain.bo.RsOverseasRegistrationBo; +import org.dromara.rs.domain.vo.RsOverseasRegistrationVo; +import org.dromara.rs.domain.query.RsOverseasRegistrationQuery; + +/** + * 出国登记Service接口 + * + * @author weizzzz + * @date 2025-09-30 + */ +public interface IRsOverseasRegistrationService { + + /** + * 查询出国登记分页列表 + * + * @param query 查询条件 + * @return 出国登记分页集合 + */ + TableDataInfo queryPageList(RsOverseasRegistrationQuery query); + + /** + * 查询我的出国登记分页列表 + * + * @param query 查询条件 + * @return 出国登记分页集合 + */ + TableDataInfo queryMyRegistrationList(RsOverseasRegistrationQuery query); + + /** + * 查询出国登记列表 + * + * @param query 查询条件 + * @return 出国登记集合 + */ + List queryList(RsOverseasRegistrationQuery query); + + /** + * 查询出国登记详细信息 + * + * @param registrationId 出国登记主键 + * @return 出国登记 + */ + RsOverseasRegistrationVo queryById(Long registrationId); + + /** + * 根据申请ID查询出国登记信息 + * + * @param applicationId 申请ID + * @return 出国登记 + */ + RsOverseasRegistrationVo queryByApplicationId(Long applicationId); + + /** + * 根据状态查询出国登记列表 + * + * @param status 状态 + * @return 出国登记集合 + */ + List queryByStatus(String status); + + /** + * 根据姓名查询出国登记列表 + * + * @param name 姓名 + * @return 出国登记集合 + */ + List queryByName(String name); + + /** + * 新增出国登记 + * + * @param bo 出国登记业务对象 + * @return 是否成功 + */ + Boolean insertByBo(RsOverseasRegistrationBo bo); + + /** + * 新增并提交出国登记 + * + * @param bo 出国登记业务对象 + * @return 是否成功 + */ + Boolean addAndSubmit(RsOverseasRegistrationBo bo); + + /** + * 修改出国登记 + * + * @param bo 出国登记业务对象 + * @return 是否成功 + */ + Boolean updateByBo(RsOverseasRegistrationBo bo); + + /** + * 校验并批量删除出国登记信息 + * + * @param ids 主键集合 + * @param isValid 是否校验,true-删除前校验,false-不校验 + * @return 是否成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 提交出国登记 + * + * @param registrationId 登记ID + * @return 是否成功 + */ + Boolean submitRegistration(Long registrationId); + + /** + * 确认出国登记 + * + * @param registrationId 登记ID + * @return 是否成功 + */ + Boolean confirmRegistration(Long registrationId); + + /** + * 撤回出国登记 + * + * @param registrationId 登记ID + * @return 是否成功 + */ + Boolean withdrawRegistration(Long registrationId); + + /** + * 基于出国申请创建登记记录 + * + * @param applicationId 申请ID + * @return 是否成功 + */ + Boolean createFromApplication(Long applicationId); + + /** + * 统计各状态的登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> getStatusStatistics(RsOverseasRegistrationQuery query); + + /** + * 批量提交出国登记 + * + * @param registrationIds 登记ID集合 + * @return 是否成功 + */ + Boolean batchSubmitRegistrations(List registrationIds); + + /** + * 批量确认出国登记 + * + * @param registrationIds 登记ID集合 + * @return 是否成功 + */ + Boolean batchConfirmRegistrations(List registrationIds); + + /** + * 获取可用的登记选项(字典数据) + * + * @return 选项集合 + */ + java.util.Map>> getRegistrationOptions(); + + /** + * 批量导出出国登记数据 + * + * @param query 查询条件 + * @param response 响应对象 + */ + void exportRegistrations(RsOverseasRegistrationQuery query, jakarta.servlet.http.HttpServletResponse response); + + /** + * 按部门统计出国登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> statisticsByDepartment(RsOverseasRegistrationQuery query); + + /** + * 按目的地国家统计出国登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> statisticsByCountry(RsOverseasRegistrationQuery query); + + /** + * 按出国类型统计出国登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> statisticsByTravelType(RsOverseasRegistrationQuery query); + + /** + * 按人员类别统计出国登记数量 + * + * @param query 查询条件 + * @return 统计结果 + */ + List> statisticsByPersonnelCategory(RsOverseasRegistrationQuery query); + + /** + * 检查是否存在重复的出国登记(同一人同一时间段) + * + * @param registration 登记信息 + * @return 是否重复 + */ + Boolean checkDuplicateRegistration(RsOverseasRegistrationVo registration); + + /** + * 获取即将到期的出国登记列表 + * + * @param days 天数阈值 + * @return 登记列表 + */ + List getExpiringRegistrations(Integer days); + + /** + * 获取已过期的出国登记列表 + * + * @return 登记列表 + */ + List getExpiredRegistrations(); + + /** + * 按时间段统计出国登记趋势 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 统计结果 + */ + List> getTrendStatistics(java.util.Date startDate, java.util.Date endDate); + + /** + * 获取出国登记年度报告 + * + * @param year 年份 + * @return 报告数据 + */ + java.util.Map getAnnualReport(Integer year); + + /** + * 更新登记状态 + * + * @param registrationId 登记ID + * @param status 新状态 + * @return 是否成功 + */ + Boolean updateStatus(Long registrationId, String status); + + /** + * 获取我的出国登记统计 + * + * @return 统计数据 + */ + java.util.Map getMyStatistics(); + + /** + * 获取趋势数据 + * + * @return 趋势数据 + */ + java.util.Map getTrendData(); + + /** + * 获取状态选项列表 + * + * @return 状态选项 + */ + List> getStatusOptions(); + + /** + * 获取出国类型选项列表 + * + * @return 出国类型选项 + */ + List> getTravelTypeOptions(); + + /** + * 获取完成状态选项列表 + * + * @return 完成状态选项 + */ + List> getCompletionStatusOptions(); + + /** + * 生成出国证明文件 + * + * @param registrationId 登记ID + * @param response 响应对象 + * @throws Exception 异常 + */ + void generateCertificate(Long registrationId, jakarta.servlet.http.HttpServletResponse response) throws Exception; + + /** + * 查询历史记录分页列表 + * + * @param query 查询条件 + * @return 历史记录分页集合 + */ + TableDataInfo queryHistoryPageList(RsOverseasRegistrationQuery query); + + /** + * 查询历史记录列表 + * + * @param query 查询条件 + * @return 历史记录集合 + */ + List queryHistoryList(RsOverseasRegistrationQuery query); + + /** + * 查询历史记录详细信息 + * + * @param historyId 历史记录ID + * @return 历史记录 + */ + RsOverseasRegistrationVo queryHistoryById(Long historyId); + + /** + * 新增历史记录 + * + * @param bo 历史记录业务对象 + * @return 是否成功 + */ + Boolean insertHistoryByBo(RsOverseasRegistrationBo bo); + + /** + * 修改历史记录 + * + * @param bo 历史记录业务对象 + * @return 是否成功 + */ + Boolean updateHistoryByBo(RsOverseasRegistrationBo bo); + + /** + * 删除历史记录 + * + * @param historyIds 历史记录ID集合 + * @return 是否成功 + */ + Boolean deleteHistoryWithValidByIds(List historyIds); + + /** + * 完成登记 + * + * @param registrationId 登记ID + * @param data 完成数据 + * @return 是否成功 + */ + Boolean completeRegistration(Long registrationId, java.util.Map data); + + /** + * 批准出国登记 + * + * @param registrationId 登记ID + * @param approveNotes 审批意见 + * @return 是否成功 + */ + Boolean approveRegistration(Long registrationId, String approveNotes); + + /** + * 拒绝出国登记 + * + * @param registrationId 登记ID + * @param rejectReason 拒绝原因 + * @return 是否成功 + */ + Boolean rejectRegistration(Long registrationId, String rejectReason); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java index 9c03a329..1ea417b0 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java @@ -17,10 +17,11 @@ public interface IRsTrainingApplicationService { * 查询培训申请列表 * * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, PageQuery pageQuery); + TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, + PageQuery pageQuery); /** * 查询培训申请列表 @@ -33,22 +34,24 @@ public interface IRsTrainingApplicationService { /** * 查询指定课程的培训申请列表 * - * @param courseId 课程ID + * @param courseId 课程ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageApplicationsByCourse(Long courseId, RsTrainingApplication application, PageQuery pageQuery); - + TableDataInfo selectPageApplicationsByCourse(Long courseId, + RsTrainingApplication application, PageQuery pageQuery); + /** * 查询我的培训申请列表 * - * @param userId 用户ID + * @param userId 用户ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, PageQuery pageQuery); + TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, + PageQuery pageQuery); /** * 查询培训申请详细信息 @@ -89,7 +92,7 @@ public interface IRsTrainingApplicationService { * @return 结果 */ int deleteRsTrainingApplicationById(Long applicationId); - + /** * 取消培训申请 * @@ -97,22 +100,43 @@ public interface IRsTrainingApplicationService { * @return 结果 */ int cancelApplication(Long applicationId); - + /** * 更新申请状态 * * @param applicationId 培训申请ID - * @param status 状态 + * @param status 状态 * @return 结果 */ int updateApplicationStatus(Long applicationId, String status); - + /** * 重新提交已被拒绝的申请 * * @param applicationId 培训申请ID - * @param application 培训申请更新数据 + * @param application 培训申请更新数据 * @return 结果 */ int resubmitApplication(Long applicationId, RsTrainingApplication application); -} \ No newline at end of file + + /** + * 审批培训申请(7级审批) + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批状态(1-通过,2-拒绝) + * @param approvalOpinion 审批意见 + * @param approvalSignature 审批签字(base64格式) + * @return 结果 + */ + Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature); + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * @return 待审批申请列表 + */ + List queryPendingByCurrentUser(); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RegistrationFormServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RegistrationFormServiceImpl.java new file mode 100644 index 00000000..7666ec80 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RegistrationFormServiceImpl.java @@ -0,0 +1,609 @@ +package org.dromara.rs.service.impl; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.*; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.rs.domain.vo.RsOverseasApplicationVo; +import org.dromara.rs.domain.DocumentInfo; +import org.dromara.rs.domain.bo.GenerateRegistrationFormBo; +import org.dromara.rs.domain.vo.RegistrationFormVO; +import org.dromara.rs.service.IRegistrationFormService; +import org.dromara.rs.service.IRsOverseasApplicationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +/** + * 登记表PDF生成服务实现类 + * + * @author weizzzz + * @date 2025-01-03 + */ +@Slf4j +@Service +public class RegistrationFormServiceImpl implements IRegistrationFormService { + + @Autowired + private IRsOverseasApplicationService applicationService; + + @Value("${ruoyi.profile:./uploadPath}") + private String uploadPath; + + private static final String REGISTRATION_FORMS_DIR = "registration-forms"; + private static final String PDF_EXTENSION = ".pdf"; + + // 简化实现:内存存储文档信息(实际项目中应存储到数据库) + private static final Map DOCUMENT_CACHE = new ConcurrentHashMap<>(); + + @Override + public RegistrationFormVO generateRegistrationFormPdf(GenerateRegistrationFormBo bo) { + try { + // 1. 查询申请数据 + RsOverseasApplicationVo application = applicationService.queryById(bo.getApplicationId()); + if (application == null) { + throw new ServiceException("申请记录不存在"); + } + + // 2. 生成文档编号 + String documentNumber = bo.getDocumentNumber(); + if (documentNumber == null || documentNumber.trim().isEmpty()) { + documentNumber = generateDocumentNumber(); + } + + // 3. 创建存储目录 + String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + Path storageDir = Paths.get(uploadPath, REGISTRATION_FORMS_DIR, dateDir); + Files.createDirectories(storageDir); + + // 4. 生成文件名和路径 + String documentId = UUID.randomUUID().toString(); + String fileName = String.format("registration_form_%d_%s%s", + application.getApplicationId(), + System.currentTimeMillis(), + PDF_EXTENSION); + Path filePath = storageDir.resolve(fileName); + + // 5. 生成PDF文档 + generatePdfDocument(application, documentNumber, filePath); + + // 6. 构建返回结果 + RegistrationFormVO result = new RegistrationFormVO(); + result.setDocumentId(documentId); + result.setFileName(fileName); + result.setDownloadUrl("/rs/overseas/application/downloadPdf/" + documentId); + result.setPreviewUrl("/rs/overseas/application/previewPdf/" + documentId); + result.setFileSize(Files.size(filePath)); + result.setGenerateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + result.setDocumentNumber(documentNumber); + result.setApplicationId(application.getApplicationId()); + + // 7. 存储文档信息(这里简化处理,实际项目中可能需要存储到数据库) + storeDocumentInfo(documentId, filePath.toString(), result); + + log.info("成功生成登记表PDF,申请ID: {}, 文档ID: {}", bo.getApplicationId(), documentId); + return result; + + } catch (Exception e) { + log.error("生成登记表PDF失败,申请ID: {}", bo.getApplicationId(), e); + throw new ServiceException("生成PDF文档失败: " + e.getMessage()); + } + } + + /** + * 生成PDF文档 - 严格按照登记表图片格式 + */ + private void generatePdfDocument(RsOverseasApplicationVo application, String documentNumber, Path filePath) + throws DocumentException, IOException { + + Document document = new Document(PageSize.A4, 25, 25, 25, 25); + PdfWriter writer = PdfWriter.getInstance(document, Files.newOutputStream(filePath)); + document.open(); + + try { + // 创建中文字体 + BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + Font titleFont = new Font(baseFont, 16, Font.BOLD); + Font normalFont = new Font(baseFont, 12, Font.NORMAL); + Font smallFont = new Font(baseFont, 10, Font.NORMAL); + + // 1. 标题 + Paragraph title = new Paragraph("登记报备人员因私事出国(境)登记表", titleFont); + title.setAlignment(Element.ALIGN_CENTER); + title.setSpacingAfter(10); + document.add(title); + + // 2. 编号 + Paragraph number = new Paragraph("编号:" + documentNumber, normalFont); + number.setAlignment(Element.ALIGN_RIGHT); + number.setSpacingAfter(15); + document.add(number); + + // 3. 创建主表格 - 严格按照图片结构 + PdfPTable mainTable = new PdfPTable(7); // 7列表格 + mainTable.setWidthPercentage(100); + mainTable.setWidths(new float[]{8, 15, 8, 10, 10, 15, 20}); // 列宽比例 + + // 第一行:姓名、性别、出生日期、照片 + addTableCell(mainTable, "姓名", normalFont, true); + addTableCell(mainTable, application.getApplicantName(), normalFont, false); + addTableCell(mainTable, "性别", normalFont, true); + addTableCell(mainTable, application.getGender(), normalFont, false); + addTableCell(mainTable, "出生日期", normalFont, true); + addTableCell(mainTable, formatBirthDate(application.getBirthDate()), normalFont, false); + + // 照片区域 - 跨7行 + PdfPCell photoCell = new PdfPCell(new Phrase("照片\n(2寸免冠白\n色或淡蓝色\n底彩照\n32MM×26MM)", normalFont)); + photoCell.setRowspan(7); + photoCell.setHorizontalAlignment(Element.ALIGN_CENTER); + photoCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + photoCell.setMinimumHeight(120f); + mainTable.addCell(photoCell); + + // 第二行:公民身份号码、政治面貌 + addTableCell(mainTable, "公民身份号码", normalFont, true); + PdfPCell idCell = new PdfPCell(new Phrase("(请填写18位公民身份\n号码)\n" + + (application.getIdNumber() != null ? application.getIdNumber() : ""), normalFont)); + idCell.setColspan(2); + mainTable.addCell(idCell); + addTableCell(mainTable, "政治\n面貌", normalFont, true); + PdfPCell politicalCell = new PdfPCell(new Phrase(application.getPoliticalStatus(), normalFont)); + politicalCell.setColspan(2); + mainTable.addCell(politicalCell); + + // 第三行:户籍所在地 + addTableCell(mainTable, "户籍所在地", normalFont, true); + PdfPCell addressCell = new PdfPCell(new Phrase("(请填写至县(市、区))\n" + + (application.getRegisteredAddress() != null ? application.getRegisteredAddress() : ""), normalFont)); + addressCell.setColspan(5); + mainTable.addCell(addressCell); + + // 第四行:工作部门 + addTableCell(mainTable, "工作部门\n(教学系部)", normalFont, true); + PdfPCell deptCell = new PdfPCell(new Phrase("(请填写全称)\n" + + (application.getDepartment() != null ? application.getDepartment() : ""), normalFont)); + deptCell.setColspan(5); + mainTable.addCell(deptCell); + + // 第五行:职务职称、行政职级、人员类别代码 + addTableCell(mainTable, "职务职称", normalFont, true); + addTableCell(mainTable, application.getPosition(), normalFont, false); + addTableCell(mainTable, "行政\n职级", normalFont, true); + addTableCell(mainTable, application.getAdministrativeLevel(), normalFont, false); + addTableCell(mainTable, "人员类\n别代码", normalFont, true); + addTableCell(mainTable, application.getPersonnelCategory(), normalFont, false); + + // 第六行:登记备案责任单位(第一部分) + PdfPCell unitCell1 = new PdfPCell(new Phrase("登记备案\n责任单位", normalFont)); + unitCell1.setRowspan(2); + unitCell1.setVerticalAlignment(Element.ALIGN_MIDDLE); + mainTable.addCell(unitCell1); + + PdfPCell stampCell = new PdfPCell(new Phrase("盖 章", normalFont)); + stampCell.setColspan(2); + stampCell.setHorizontalAlignment(Element.ALIGN_CENTER); + stampCell.setMinimumHeight(40f); + mainTable.addCell(stampCell); + + PdfPCell leaderCell = new PdfPCell(new Phrase("登记备案责任单位\n负责人签名:\n" + + (application.getUnitLeaderSignature() != null ? application.getUnitLeaderSignature() : ""), normalFont)); + leaderCell.setColspan(3); + mainTable.addCell(leaderCell); + + // 第七行:联系电话 + addTableCell(mainTable, "联系电话", normalFont, true); + addTableCell(mainTable, "办公电话:\n" + + (application.getOfficePhone() != null ? application.getOfficePhone() : ""), normalFont, false); + addTableCell(mainTable, "移动电话:\n" + + (application.getMobilePhone() != null ? application.getMobilePhone() : ""), normalFont, false); + // 填充剩余列 + addTableCell(mainTable, "", normalFont, false); + addTableCell(mainTable, "", normalFont, false); + + document.add(mainTable); + + // 4. 填表说明 + Paragraph note = new Paragraph("填表说明:人员类别代码根据报备人员属于报备人员范围中的情形项,填写所属项的序号即可。", smallFont); + note.setSpacingBefore(20); + document.add(note); + + } finally { + document.close(); + } + } + + /** + * 添加表格单元格 + */ + private void addTableCell(PdfPTable table, String content, Font font, boolean isHeader) { + PdfPCell cell = new PdfPCell(new Phrase(content != null ? content : "", font)); + if (isHeader) { + cell.setBackgroundColor(BaseColor.LIGHT_GRAY); + } + cell.setHorizontalAlignment(Element.ALIGN_CENTER); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setMinimumHeight(25f); + table.addCell(cell); + } + + /** + * 格式化出生日期 + */ + private String formatBirthDate(Date birthDate) { + if (birthDate == null) { + return "年 月 日"; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日"); + return sdf.format(birthDate); + } + + /** + * 生成文档编号 + */ + private String generateDocumentNumber() { + return "CG" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + } + + /** + * 存储文档信息(简化实现) + */ + private void storeDocumentInfo(String documentId, String filePath, RegistrationFormVO result) { + DocumentInfo docInfo = new DocumentInfo(); + docInfo.setDocumentId(documentId); + docInfo.setFilePath(filePath); + docInfo.setFileName(result.getFileName()); + docInfo.setApplicationId(result.getApplicationId()); + docInfo.setDocumentNumber(result.getDocumentNumber()); + docInfo.setFileSize(result.getFileSize()); + docInfo.setCreateTime(LocalDateTime.now()); + + DOCUMENT_CACHE.put(documentId, docInfo); + log.info("存储文档信息: documentId={}, filePath={}", documentId, filePath); + } + + @Override + public void downloadPdf(String documentId, HttpServletResponse response) { + try { + // 从缓存中获取文档信息 + DocumentInfo docInfo = DOCUMENT_CACHE.get(documentId); + if (docInfo == null) { + throw new ServiceException("文档不存在或已过期"); + } + + // 检查文件是否存在 + Path filePath = Paths.get(docInfo.getFilePath()); + if (!Files.exists(filePath)) { + throw new ServiceException("文件不存在"); + } + + // 设置响应头 + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", + "attachment; filename=\"" + docInfo.getFileName() + "\""); + response.setHeader("Content-Length", String.valueOf(docInfo.getFileSize())); + + // 输出文件内容 + try (InputStream inputStream = Files.newInputStream(filePath); + OutputStream outputStream = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } + + log.info("成功下载PDF文档: documentId={}, fileName={}", documentId, docInfo.getFileName()); + + } catch (Exception e) { + log.error("下载PDF文档失败: documentId={}", documentId, e); + throw new ServiceException("下载文档失败: " + e.getMessage()); + } + } + + @Override + public String getPreviewUrl(String documentId) { + return "/rs/overseas/application/previewPdf/" + documentId; + } + + @Override + public void downloadRegistrationTemplate(HttpServletResponse response) { + log.info("开始生成登记备案表模板PDF"); + + try { + // 生成登记备案表模板PDF + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Document document = new Document(PageSize.A4, 50, 50, 50, 50); + PdfWriter writer = PdfWriter.getInstance(document, baos); + + document.open(); + + // 创建支持中文的字体 + BaseFont baseFont; + try { + // 使用iText内置的中文字体支持 - 需要itext-asian依赖 + baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + log.info("✅ 成功加载中文字体: STSong-Light"); + } catch (Exception e) { + log.warn("⚠️ 无法加载STSong-Light字体: {}", e.getMessage()); + try { + // 备选方案:使用宋体标准版 + baseFont = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + log.info("✅ 成功加载中文字体: STSongStd-Light"); + } catch (Exception e2) { + log.error("❌ 无法加载任何中文字体,PDF将显示空白"); + log.error("💡 解决方案: 确保已添加itext-asian依赖并重启服务"); + // 抛出异常,不生成空白PDF + throw new ServiceException("无法加载中文字体,请检查itext-asian依赖配置"); + } + } + + Font titleFont = new Font(baseFont, 16, Font.BOLD); + Font headerFont = new Font(baseFont, 12, Font.BOLD); + Font normalFont = new Font(baseFont, 10, Font.NORMAL); + Font smallFont = new Font(baseFont, 8, Font.NORMAL); + + // 标题 - 修改为纯中文版本 + Paragraph title = new Paragraph("登记报备人员因私事出国(境)登记表", titleFont); + title.setAlignment(Element.ALIGN_CENTER); + title.setSpacingAfter(30f); + document.add(title); + + // 编号 + Paragraph codeP = new Paragraph("编号:_______________", normalFont); + codeP.setAlignment(Element.ALIGN_RIGHT); + codeP.setSpacingAfter(15f); + document.add(codeP); + + // 创建表格 + PdfPTable table = new PdfPTable(4); + table.setWidthPercentage(100); + table.setWidths(new float[]{2f, 3f, 2f, 3f}); + + // 基本信息部分 + addFormCell(table, "姓名", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "性别", headerFont); + addFormCell(table, "", normalFont); + + addFormCell(table, "出生日期", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "身份证号", headerFont); + addFormCell(table, "", normalFont); + + addFormCell(table, "政治面貌", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "户籍", headerFont); + addFormCell(table, "", normalFont); + + addFormCell(table, "工作部门", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "职务职称", headerFont); + addFormCell(table, "", normalFont); + + addFormCell(table, "行政职级", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "人员类别", headerFont); + addFormCell(table, "", normalFont); + + // 责任单位信息 + PdfPCell responsibleUnitCell = new PdfPCell(new Phrase("登记备案责任单位", headerFont)); + responsibleUnitCell.setColspan(2); + responsibleUnitCell.setMinimumHeight(40f); + responsibleUnitCell.setPadding(8f); + responsibleUnitCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + table.addCell(responsibleUnitCell); + + PdfPCell sealCell = new PdfPCell(new Phrase("盖章", headerFont)); + sealCell.setColspan(2); + sealCell.setMinimumHeight(40f); + sealCell.setPadding(8f); + sealCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + table.addCell(sealCell); + + // 联系信息 + addFormCell(table, "联系人", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "联系电话", headerFont); + addFormCell(table, "", normalFont); + + addFormCell(table, "负责人签名", headerFont); + addFormCell(table, "", normalFont); + addFormCell(table, "日期", headerFont); + addFormCell(table, "", normalFont); + + document.add(table); + + // 照片区域 + Paragraph photoNote = new Paragraph("照片粘贴处(2寸彩色证件照,白色或浅蓝色背景,32MM×26MM)", smallFont); + photoNote.setAlignment(Element.ALIGN_RIGHT); + photoNote.setSpacingBefore(15f); + photoNote.setSpacingAfter(15f); + document.add(photoNote); + + // 说明 + Paragraph instructions = new Paragraph("填表说明:人员类别代码根据报备人员属于报备人员范围中的情况,填写所属项的序号即可。", smallFont); + instructions.setSpacingBefore(10f); + document.add(instructions); + + document.close(); + + // 获取PDF字节数组 + byte[] pdfBytes = baos.toByteArray(); + + if (pdfBytes.length == 0) { + throw new ServiceException("生成的PDF文件为空"); + } + + log.info("PDF文件生成成功,大小: {} bytes", pdfBytes.length); + + // 设置响应头 + response.setContentType("application/pdf"); + String fileName = "登记报备人员因私事出国(境)登记表.pdf"; + String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); + response.setContentLength(pdfBytes.length); + + // 输出PDF + response.getOutputStream().write(pdfBytes); + response.getOutputStream().flush(); + + log.info("登记备案表模板下载成功,文件大小: {} bytes", pdfBytes.length); + + } catch (Exception e) { + log.error("下载登记备案表模板失败", e); + + try { + if (!response.isCommitted()) { + response.reset(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":500,\"msg\":\"下载模板失败: " + e.getMessage() + "\"}"); + } + } catch (IOException ioEx) { + log.error("发送错误响应失败", ioEx); + } + + throw new ServiceException("下载模板失败: " + e.getMessage()); + } + } + + @Override + public void downloadPartyMembershipTemplate(HttpServletResponse response) { + log.info("开始生成保留党籍申请模板PDF"); + + try { + // 生成保留党籍申请模板PDF + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Document document = new Document(PageSize.A4, 50, 50, 50, 50); + PdfWriter writer = PdfWriter.getInstance(document, baos); + + document.open(); + + // 创建支持中文的字体 + BaseFont baseFont; + try { + // 使用iText内置的中文字体支持 - 需要itext-asian依赖 + baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + log.info("✅ 成功加载中文字体: STSong-Light"); + } catch (Exception e) { + log.warn("⚠️ 无法加载STSong-Light字体: {}", e.getMessage()); + try { + // 备选方案:使用宋体标准版 + baseFont = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + log.info("✅ 成功加载中文字体: STSongStd-Light"); + } catch (Exception e2) { + log.error("❌ 无法加载任何中文字体,PDF将显示空白"); + log.error("💡 解决方案: 确保已添加itext-asian依赖并重启服务"); + // 抛出异常,不生成空白PDF + throw new ServiceException("无法加载中文字体,请检查itext-asian依赖配置"); + } + } + + Font titleFont = new Font(baseFont, 16, Font.BOLD); + Font normalFont = new Font(baseFont, 12, Font.NORMAL); + Font smallFont = new Font(baseFont, 10, Font.NORMAL); + + // 标题 + Paragraph title = new Paragraph("关于因私赴 ______ 探亲保留党籍的申请", titleFont); + title.setAlignment(Element.ALIGN_CENTER); + title.setSpacingAfter(40f); + document.add(title); + + // 收件人 + Paragraph recipient = new Paragraph("________________________ 支部:", normalFont); + recipient.setSpacingAfter(30f); + document.add(recipient); + + // 正文段落1 + String paragraph1 = " 本人 ______ (身份证号),现任 ______ 。计划于 ______ 年 ______ 月 ______ 日至 ______ 月 ______ 日,借配偶 ______ ,因私赴 ______ 探望在 ______ 的工作(留学)的孙女 ______ ,在外停留 ______ 天(含离、抵我国国境当日)。现申请保留 ______ 年 ______ 月 ______ 日至 ______ 年 ______ 月 ______ 日期间的党籍。"; + Paragraph content1 = new Paragraph(paragraph1, normalFont); + content1.setSpacingAfter(30f); + content1.setAlignment(Element.ALIGN_JUSTIFIED); + content1.setIndentationLeft(0f); + document.add(content1); + + // 正文段落2 + Paragraph content2 = new Paragraph(" 特此申请,望予批准。", normalFont); + content2.setSpacingAfter(80f); + content2.setAlignment(Element.ALIGN_JUSTIFIED); + document.add(content2); + + // 申请人署名区域 + Paragraph applicant = new Paragraph("申请人:", normalFont); + applicant.setAlignment(Element.ALIGN_RIGHT); + applicant.setSpacingAfter(30f); + document.add(applicant); + + // 日期 + Paragraph date = new Paragraph("______ 年 ______ 月 ______ 日", normalFont); + date.setAlignment(Element.ALIGN_RIGHT); + document.add(date); + + document.close(); + + // 获取PDF字节数组 + byte[] pdfBytes = baos.toByteArray(); + + if (pdfBytes.length == 0) { + throw new ServiceException("生成的PDF文件为空"); + } + + log.info("保留党籍申请PDF文件生成成功,大小: {} bytes", pdfBytes.length); + + // 设置响应头 + response.setContentType("application/pdf"); + String fileName = "关于因私赴XXX探亲保留党籍的申请.pdf"; + String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); + response.setContentLength(pdfBytes.length); + + // 输出PDF + response.getOutputStream().write(pdfBytes); + response.getOutputStream().flush(); + + log.info("保留党籍申请模板下载成功,文件大小: {} bytes", pdfBytes.length); + + } catch (Exception e) { + log.error("下载保留党籍申请模板失败", e); + + try { + if (!response.isCommitted()) { + response.reset(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":500,\"msg\":\"下载模板失败: " + e.getMessage() + "\"}"); + } + } catch (IOException ioEx) { + log.error("发送错误响应失败", ioEx); + } + + throw new ServiceException("下载模板失败: " + e.getMessage()); + } + } + + private void addFormCell(PdfPTable table, String text, Font font) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setHorizontalAlignment(Element.ALIGN_LEFT); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setPadding(8f); + cell.setMinimumHeight(30f); + cell.setBorder(Rectangle.BOX); + cell.setBorderWidth(0.5f); + table.addCell(cell); + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java new file mode 100644 index 00000000..b5813b53 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java @@ -0,0 +1,212 @@ +package org.dromara.rs.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.oss.core.OssClient; +import org.dromara.common.oss.entity.UploadResult; +import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.rs.domain.RsOverseasApplicationAttachment; +import org.dromara.rs.domain.bo.RsOverseasApplicationAttachmentBo; +import org.dromara.rs.domain.vo.RsOverseasApplicationAttachmentVo; +import org.dromara.rs.mapper.RsOverseasApplicationAttachmentMapper; +import org.dromara.rs.service.IRsOverseasApplicationAttachmentService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * 出国申请附件Service实现 + * + * @author weizzzz + * @date 2025-10-10 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class RsOverseasApplicationAttachmentServiceImpl implements IRsOverseasApplicationAttachmentService { + + private final RsOverseasApplicationAttachmentMapper attachmentMapper; + + /** + * 查询出国申请的附件列表 + */ + @Override + public List selectAttachmentList(Long applicationId) { + List list = attachmentMapper.selectAttachmentList(applicationId); + // 格式化文件大小 + list.forEach(vo -> vo.setFileSizeFormatted(formatFileSize(vo.getFileSize()))); + return list; + } + + /** + * 查询附件详情 + */ + @Override + public RsOverseasApplicationAttachmentVo selectAttachmentById(Long attachmentId) { + RsOverseasApplicationAttachmentVo vo = attachmentMapper.selectAttachmentById(attachmentId); + if (vo != null) { + vo.setFileSizeFormatted(formatFileSize(vo.getFileSize())); + } + return vo; + } + + /** + * 上传附件 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public RsOverseasApplicationAttachmentVo uploadAttachment(Long applicationId, String title, String content, + MultipartFile file) throws IOException { + if (file == null || file.isEmpty()) { + throw new ServiceException("上传文件不能为空"); + } + + if (StringUtils.isBlank(title)) { + throw new ServiceException("附件标题不能为空"); + } + + // 验证文件大小(限制50MB) + long maxSize = 50 * 1024 * 1024; // 50MB + if (file.getSize() > maxSize) { + throw new ServiceException("文件大小超过50MB限制"); + } + + // 获取文件信息 + String originalFilename = file.getOriginalFilename(); + String fileExtension = getFileExtension(originalFilename); + String contentType = file.getContentType(); + + // 生成文件名 + String fileName = "overseas_attachment_" + applicationId + "_" + System.currentTimeMillis() + + "." + fileExtension; + + // 上传到OSS + OssClient ossClient = OssFactory.instance(); + String filePath = "overseas/attachments/" + fileName; + UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath, + file.getSize(), contentType); + + // 保存附件信息到数据库 + RsOverseasApplicationAttachment attachment = new RsOverseasApplicationAttachment(); + attachment.setApplicationId(applicationId); + attachment.setAttachmentTitle(title); + attachment.setAttachmentContent(content); + attachment.setFileName(fileName); + attachment.setFileOriginalName(originalFilename); + attachment.setFilePath(uploadResult.getUrl()); + attachment.setFileSize(file.getSize()); + attachment.setFileType(fileExtension); + attachment.setMimeType(contentType); + attachment.setUploadTime(new Date()); + + // 设置上传人信息 + try { + Long userId = LoginHelper.getUserId(); + String username = LoginHelper.getUsername(); + attachment.setUploadUserId(userId); + attachment.setUploadUserName(username); + } catch (Exception e) { + log.warn("获取用户信息失败: {}", e.getMessage()); + } + + attachmentMapper.insert(attachment); + + // 返回VO + RsOverseasApplicationAttachmentVo vo = BeanUtil.copyProperties(attachment, RsOverseasApplicationAttachmentVo.class); + vo.setFileSizeFormatted(formatFileSize(file.getSize())); + return vo; + } + + /** + * 更新附件信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateAttachment(RsOverseasApplicationAttachmentBo bo) { + RsOverseasApplicationAttachment attachment = BeanUtil.copyProperties(bo, RsOverseasApplicationAttachment.class); + return attachmentMapper.updateById(attachment) > 0; + } + + /** + * 删除附件 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteAttachment(Long attachmentId) { + // 查询附件信息 + RsOverseasApplicationAttachment attachment = attachmentMapper.selectById(attachmentId); + if (attachment == null) { + throw new ServiceException("附件不存在"); + } + + // 删除OSS文件(可选,根据业务需求决定是否物理删除) + try { + OssClient ossClient = OssFactory.instance(); + // ossClient.delete(attachment.getFilePath()); + } catch (Exception e) { + log.warn("删除OSS文件失败: {}", e.getMessage()); + } + + // 逻辑删除数据库记录 + return attachmentMapper.deleteById(attachmentId) > 0; + } + + /** + * 批量删除附件 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteAttachments(Long[] attachmentIds) { + if (attachmentIds == null || attachmentIds.length == 0) { + return false; + } + return attachmentMapper.deleteBatchIds(Arrays.asList(attachmentIds)) > 0; + } + + /** + * 获取文件扩展名 + */ + private String getFileExtension(String filename) { + if (StrUtil.isBlank(filename)) { + return ""; + } + int lastDotIndex = filename.lastIndexOf("."); + if (lastDotIndex > 0 && lastDotIndex < filename.length() - 1) { + return filename.substring(lastDotIndex + 1).toLowerCase(); + } + return ""; + } + + /** + * 格式化文件大小 + */ + private String formatFileSize(Long size) { + if (size == null || size == 0) { + return "0 B"; + } + + DecimalFormat df = new DecimalFormat("#.##"); + if (size < 1024) { + return size + " B"; + } else if (size < 1024 * 1024) { + return df.format(size / 1024.0) + " KB"; + } else if (size < 1024 * 1024 * 1024) { + return df.format(size / (1024.0 * 1024.0)) + " MB"; + } else { + return df.format(size / (1024.0 * 1024.0 * 1024.0)) + " GB"; + } + } +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java index 9289895d..f9fa4656 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java @@ -1,23 +1,35 @@ package org.dromara.rs.service.impl; import java.util.Arrays; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Calendar; +import java.util.stream.Collectors; -import org.dromara.common.core.utils.DateUtils; +// 基础功能导入 import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.satoken.utils.LoginHelper; +import cn.dev33.satoken.stp.StpUtil; import org.springframework.beans.factory.annotation.Autowired; +// EventListener 导入已移除 import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.dromara.rs.mapper.RsOverseasApplicationMapper; import org.dromara.rs.mapper.RsOverseasApprovalMapper; +import org.dromara.rs.mapper.RsOverseasRegistrationMapper; import org.dromara.rs.domain.RsOverseasApplication; import org.dromara.rs.domain.RsOverseasApproval; +import org.dromara.rs.domain.RsOverseasRegistration; import org.dromara.rs.domain.bo.RsOverseasApplicationBo; import org.dromara.rs.domain.vo.RsOverseasApplicationVo; import org.dromara.rs.domain.query.RsOverseasApplicationQuery; @@ -26,6 +38,16 @@ import org.dromara.rs.service.IRsOverseasRecordService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.dromara.rs.domain.vo.RsOverseasRecordVo; +import org.dromara.system.domain.vo.SysUserVo; +import org.dromara.system.domain.vo.SysDeptVo; +import org.dromara.system.domain.vo.SysRoleVo; +import org.dromara.system.domain.bo.SysDeptBo; +import org.dromara.system.domain.bo.SysUserBo; +import org.dromara.system.domain.bo.SysRoleBo; +import org.dromara.system.domain.SysUser; +import org.dromara.system.domain.SysDept; +import org.dromara.system.domain.SysRole; +import org.dromara.common.mybatis.core.page.PageQuery; /** * 出国申请Service业务层处理 @@ -34,7 +56,8 @@ import org.dromara.rs.domain.vo.RsOverseasRecordVo; * @date 2025-07-28 */ @Service -public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationService { +public class RsOverseasApplicationServiceImpl extends ServiceImpl + implements IRsOverseasApplicationService { private static final Logger log = LoggerFactory.getLogger(RsOverseasApplicationServiceImpl.class); @@ -47,32 +70,84 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS @Autowired private IRsOverseasRecordService overseasRecordService; + @Autowired + private RsOverseasRegistrationMapper registrationMapper; + + @Autowired + private org.dromara.system.service.ISysUserService userService; + + @Autowired + private org.dromara.system.service.ISysDeptService deptService; + + @Autowired + private org.dromara.system.service.ISysRoleService roleService; + /** * 查询出国申请分页列表 */ @Override public TableDataInfo queryPageList(RsOverseasApplicationQuery query) { + // 🔴 新增:多级审批权限过滤逻辑 + Long currentUserId = LoginHelper.getUserId(); + + // 1. 判断是否是rs-admin管理员(管理员可以看所有数据) + boolean isAdmin = cn.dev33.satoken.stp.StpUtil.hasRole("rs-admin"); + + if (!isAdmin) { + // 2. 判断是否有高级别审批权限(人事处及以上:approve2/3/4/5) + boolean hasHigherApprovalPermission = cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve2") || // 人事处 + cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve3") || // 纪委 + cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导 + cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记 + + if (hasHigherApprovalPermission) { + // 🔴 人事处及以上:可以看到所有申请(不按部门过滤) + log.info("✅ [高级审批权限] 用户ID: {}, 可以查看所有申请", currentUserId); + } else { + // 3. 判断是否是部门负责人(只有 rs:overseas:approve1 权限) + boolean isDeptHead = cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve1"); + + if (isDeptHead) { + // 🔴 纯部门负责人:只能看到本部门员工提交的申请 + SysUserVo currentUser = userService.selectUserById(currentUserId); + if (currentUser != null && currentUser.getDeptId() != null) { + query.setApplicantDeptId(currentUser.getDeptId()); + log.info("🔒 [部门权限过滤] 用户: {}, 部门ID: {}, 只能查看本部门申请", + currentUser.getUserName(), currentUser.getDeptId()); + } + } else { + // 4. 普通用户:只能看到自己提交的申请 + if (query.getApplicantId() == null) { + query.setApplicantId(currentUserId); + log.info("🔒 [个人权限过滤] 用户ID: {}, 只能查看自己的申请", currentUserId); + } + } + } + } else { + log.info("✅ [管理员权限] 用户ID: {}, 可以查看所有申请", currentUserId); + } + // 使用自定义查询方法,包含状态文本等扩展字段 List list = baseMapper.selectOverseasApplicationList(query); - + // 手动分页处理 int pageNum = query.getPageNum(); int pageSize = query.getPageSize(); int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, list.size()); - + List pageList; if (start > list.size()) { pageList = Arrays.asList(); } else { pageList = list.subList(start, end); } - + // 构建分页对象 Page page = new Page<>(pageNum, pageSize); page.setRecords(pageList); page.setTotal(list.size()); - + return TableDataInfo.build(page); } @@ -85,25 +160,25 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS // 显示用户的申请记录,排除已通过的申请(已通过的申请在出国记录中显示) // 查询条件:草稿、待审核、已拒绝、已取消、已完成等状态(不包括已通过状态) List list = baseMapper.selectMyOverseasApplicationList(query); - + // 手动分页处理 int pageNum = query.getPageNum(); int pageSize = query.getPageSize(); int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, list.size()); - + List pageList; if (start > list.size()) { pageList = Arrays.asList(); } else { pageList = list.subList(start, end); } - + // 构建分页对象 Page page = new Page<>(pageNum, pageSize); page.setRecords(pageList); page.setTotal(list.size()); - + return TableDataInfo.build(page); } @@ -155,6 +230,95 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS return baseMapper.selectPendingApplicationsByLevel(approvalLevel); } + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * 🆕 核心改进: + * 1. 不再依赖 current_approver_id 字段(该字段仅作为冗余展示用) + * 2. 动态查询用户的审批权限,转换为审批级别 + * 3. 根据审批级别和状态查询待审批申请 + * 4. 🔴 部门负责人只能看到本部门的申请 + * 5. 支持人员岗位变动:权限调整后立即生效 + * + * 查询逻辑: + * - 用户有 rs:overseas:approve1 权限(部门负责人)→ 查询本部门员工的、approval_level='1' 且 + * status='0' 的申请 + * - 用户有 rs:overseas:approve2 权限(人事处)→ 查询全校的、approval_level='2' 且 status='0' 的申请 + * - 用户有多个审批权限 → 返回所有对应级别的待审批申请(无部门限制) + */ + @Override + public List queryPendingByCurrentUser() { + Long currentUserId = LoginHelper.getUserId(); + String currentUserName = LoginHelper.getUsername(); + + log.info("🔍 查询用户待审批申请 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName); + + // 1. 获取当前用户的审批级别列表(基于权限动态查询) + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels == null || approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何出国审批权限,返回空列表", currentUserName); + return new ArrayList<>(); + } + + log.info("✅ 用户 {} 拥有审批级别: {}", currentUserName, approvalLevels); + + // 2. 🔴 新逻辑:对每个级别单独查询并合并(方案1) + List result = new ArrayList<>(); + + // 遍历每个审批级别 + for (String level : approvalLevels) { + Long deptIdForFilter = null; + + // 🔴 关键:只对 level=1 进行部门过滤 + if ("1".equals(level)) { + // 获取当前用户的部门ID + SysUserVo currentUser = userService.selectUserById(currentUserId); + if (currentUser != null && currentUser.getDeptId() != null) { + deptIdForFilter = currentUser.getDeptId(); + log.info("🔒 [部门权限过滤] 级别1,用户: {}, 部门ID: {}, 只查看本部门待审批", + currentUserName, deptIdForFilter); + } else { + log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName); + } + } else { + // level=2, 3, 4, 5 不进行部门过滤 + log.info("✅ [全校权限] 级别{}, 用户 {}, 查看全校待审批", + level, currentUserName); + } + + // 3. 查询该级别的待审批申请(单个级别) + List levelApplications = baseMapper.selectPendingApplications( + Arrays.asList(level), // 🔴 关键:只查询单个级别 + deptIdForFilter // level=1时有值,其他为null + ); + + log.info("📋 级别{} 查询到 {} 条待审批申请", level, levelApplications.size()); + + // 4. 转换为VO对象并添加到结果列表 + result.addAll(levelApplications.stream() + .map(this::convertToVo) + .collect(Collectors.toList())); + } + + log.info("📋 用户 {} 总共查询到 {} 条待审批申请", currentUserName, result.size()); + + // 5. 返回合并后的结果 + return result; + } + + /** + * 转换实体为VO对象 + * 🔴 修复:使用Mapper查询完整的VO对象,包含字典翻译等扩展信息 + */ + private RsOverseasApplicationVo convertToVo(RsOverseasApplication entity) { + if (entity == null) { + return null; + } + // 🔴 使用Mapper的详细查询方法,获取完整的VO对象(包含字典翻译) + return baseMapper.selectOverseasApplicationById(entity.getApplicationId()); + } + /** * 新增出国申请(直接进入待审核状态) */ @@ -166,10 +330,23 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS add.setApplyTime(new Date()); // 直接设置为待审核状态(适应数据库DEFAULT '0'设计) add.setStatus("0"); - // 设置初始审批级别为部门主管 + // 设置初始审批级别为部门负责人 add.setApprovalLevel("1"); - // 设置当前审批环节描述 - add.setCurrentApprover("待部门主管审批"); + + // 动态分配第1级审批人(部门负责人) + try { + Long deptApproverId = findDeptApproverByApplicant(add.getApplicantId()); + if (deptApproverId != null) { + add.setCurrentApproverId(deptApproverId); + add.setCurrentApprover("待部门负责人审批"); + } else { + add.setCurrentApprover("待指定部门负责人"); + log.warn("未找到申请人 {} 对应的部门负责人", add.getApplicantId()); + } + } catch (Exception e) { + log.error("分配部门负责人失败,申请人ID: {}, 错误: {}", add.getApplicantId(), e.getMessage()); + add.setCurrentApprover("待指定部门负责人"); + } // 自动计算出国天数 if (add.getStartDate() != null && add.getEndDate() != null) { long diffInMillies = add.getEndDate().getTime() - add.getStartDate().getTime(); @@ -198,11 +375,151 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS update.setDuration(days); } validEntityBeforeSave(update); - return baseMapper.updateById(update) > 0; + + // 使用LambdaUpdateWrapper安全更新,只更新非null字段 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(RsOverseasApplication::getApplicationId, update.getApplicationId()); + + // ==================== 基本信息 ==================== + if (update.getApplicantId() != null) + updateWrapper.set(RsOverseasApplication::getApplicantId, update.getApplicantId()); + if (update.getApplicantName() != null) + updateWrapper.set(RsOverseasApplication::getApplicantName, update.getApplicantName()); + if (update.getGender() != null) + updateWrapper.set(RsOverseasApplication::getGender, update.getGender()); + if (update.getBirthDate() != null) + updateWrapper.set(RsOverseasApplication::getBirthDate, update.getBirthDate()); + if (update.getPhoto() != null) + updateWrapper.set(RsOverseasApplication::getPhoto, update.getPhoto()); + if (update.getIdNumber() != null) + updateWrapper.set(RsOverseasApplication::getIdNumber, update.getIdNumber()); + if (update.getPoliticalStatus() != null) + updateWrapper.set(RsOverseasApplication::getPoliticalStatus, update.getPoliticalStatus()); // 政治面貌 ⬅️ 关键修复 + if (update.getRegisteredAddress() != null) + updateWrapper.set(RsOverseasApplication::getRegisteredAddress, update.getRegisteredAddress()); + + // ==================== 工作信息 ==================== + if (update.getDepartment() != null) + updateWrapper.set(RsOverseasApplication::getDepartment, update.getDepartment()); + if (update.getPosition() != null) + updateWrapper.set(RsOverseasApplication::getPosition, update.getPosition()); // 职务 + if (update.getProfessionalTitle() != null) + updateWrapper.set(RsOverseasApplication::getProfessionalTitle, update.getProfessionalTitle()); // 职称 + if (update.getAdministrativeLevel() != null) + updateWrapper.set(RsOverseasApplication::getAdministrativeLevel, update.getAdministrativeLevel()); + if (update.getPersonnelCategory() != null) + updateWrapper.set(RsOverseasApplication::getPersonnelCategory, update.getPersonnelCategory()); + + // ==================== 单位和联系信息 ==================== + if (update.getRegistrationUnit() != null) + updateWrapper.set(RsOverseasApplication::getRegistrationUnit, update.getRegistrationUnit()); + if (update.getUnitLeaderSignature() != null) + updateWrapper.set(RsOverseasApplication::getUnitLeaderSignature, update.getUnitLeaderSignature()); + // unitContact 字段已删除 + // if (update.getUnitContact() != null) + // updateWrapper.set(RsOverseasApplication::getUnitContact, + // update.getUnitContact()); + if (update.getOfficePhone() != null) + updateWrapper.set(RsOverseasApplication::getOfficePhone, update.getOfficePhone()); + if (update.getMobilePhone() != null) + updateWrapper.set(RsOverseasApplication::getMobilePhone, update.getMobilePhone()); + if (update.getContactPhone() != null) + updateWrapper.set(RsOverseasApplication::getContactPhone, update.getContactPhone()); + if (update.getEmergencyContact() != null) + updateWrapper.set(RsOverseasApplication::getEmergencyContact, update.getEmergencyContact()); // 应急联系人 ⬅️ + // 关键修复 + + // ==================== 出国目的和行程 ==================== + if (update.getPurpose() != null) + updateWrapper.set(RsOverseasApplication::getPurpose, update.getPurpose()); + if (update.getPurposeDetail() != null) + updateWrapper.set(RsOverseasApplication::getPurposeDetail, update.getPurposeDetail()); + if (update.getDestination() != null) + updateWrapper.set(RsOverseasApplication::getDestination, update.getDestination()); + if (update.getDestinationCountry() != null) + updateWrapper.set(RsOverseasApplication::getDestinationCountry, update.getDestinationCountry()); + if (update.getDestinationCity() != null) + updateWrapper.set(RsOverseasApplication::getDestinationCity, update.getDestinationCity()); + if (update.getStartDate() != null) + updateWrapper.set(RsOverseasApplication::getStartDate, update.getStartDate()); + if (update.getEndDate() != null) + updateWrapper.set(RsOverseasApplication::getEndDate, update.getEndDate()); + if (update.getDuration() != null) + updateWrapper.set(RsOverseasApplication::getDuration, update.getDuration()); + // budget 字段已删除 + // if (update.getBudget() != null) + // updateWrapper.set(RsOverseasApplication::getBudget, update.getBudget()); + + // ==================== 证件和文档 ==================== + if (update.getInvitation() != null) + updateWrapper.set(RsOverseasApplication::getInvitation, update.getInvitation()); + if (update.getDocumentName() != null) + updateWrapper.set(RsOverseasApplication::getDocumentName, update.getDocumentName()); + if (update.getDocumentNumber() != null) + updateWrapper.set(RsOverseasApplication::getDocumentNumber, update.getDocumentNumber()); + if (update.getItinerary() != null) + updateWrapper.set(RsOverseasApplication::getItinerary, update.getItinerary()); + if (update.getAccommodationPlan() != null) + updateWrapper.set(RsOverseasApplication::getAccommodationPlan, update.getAccommodationPlan()); + if (update.getTransportationPlan() != null) + updateWrapper.set(RsOverseasApplication::getTransportationPlan, update.getTransportationPlan()); + + // ==================== 出境事由和备注 ==================== + if (update.getExitReason() != null) + updateWrapper.set(RsOverseasApplication::getExitReason, update.getExitReason()); // 出境事由 ⬅️ 关键字段 + if (update.getVisitRelationship() != null) + updateWrapper.set(RsOverseasApplication::getVisitRelationship, update.getVisitRelationship()); + if (update.getRemarks() != null) + updateWrapper.set(RsOverseasApplication::getRemarks, update.getRemarks()); + if (update.getApplicantRemark() != null) + updateWrapper.set(RsOverseasApplication::getApplicantRemark, update.getApplicantRemark()); + if (update.getAdminRemark() != null) + updateWrapper.set(RsOverseasApplication::getAdminRemark, update.getAdminRemark()); // 管理员备注 ⬅️ 补充遗漏字段 + if (update.getSignature() != null) + updateWrapper.set(RsOverseasApplication::getSignature, update.getSignature()); // 手写签名 ⬅️ 补充遗漏字段 + + // ==================== 盖章文件相关字段 ==================== + if (update.getStampedRegistrationFilePath() != null) + updateWrapper.set(RsOverseasApplication::getStampedRegistrationFilePath, + update.getStampedRegistrationFilePath()); + if (update.getStampedRegistrationFileName() != null) + updateWrapper.set(RsOverseasApplication::getStampedRegistrationFileName, + update.getStampedRegistrationFileName()); + if (update.getStampedFileUploadTime() != null) + updateWrapper.set(RsOverseasApplication::getStampedFileUploadTime, update.getStampedFileUploadTime()); + if (update.getStampedFileUploadStatus() != null) + updateWrapper.set(RsOverseasApplication::getStampedFileUploadStatus, update.getStampedFileUploadStatus()); + if (update.getStampedFileOssId() != null) + updateWrapper.set(RsOverseasApplication::getStampedFileOssId, update.getStampedFileOssId()); + + // ==================== 党籍保留附件字段 ==================== + if (update.getPartyRetentionFilePath() != null) + updateWrapper.set(RsOverseasApplication::getPartyRetentionFilePath, update.getPartyRetentionFilePath()); + if (update.getPartyRetentionFileName() != null) + updateWrapper.set(RsOverseasApplication::getPartyRetentionFileName, update.getPartyRetentionFileName()); + if (update.getPartyRetentionUploadTime() != null) + updateWrapper.set(RsOverseasApplication::getPartyRetentionUploadTime, update.getPartyRetentionUploadTime()); + if (update.getPartyRetentionUploadStatus() != null) + updateWrapper.set(RsOverseasApplication::getPartyRetentionUploadStatus, + update.getPartyRetentionUploadStatus()); + if (update.getPartyRetentionFileOssId() != null) + updateWrapper.set(RsOverseasApplication::getPartyRetentionFileOssId, update.getPartyRetentionFileOssId()); + + // ==================== 附件IDs ==================== + if (update.getAttachmentIds() != null) + updateWrapper.set(RsOverseasApplication::getAttachmentIds, update.getAttachmentIds()); + + return baseMapper.update(null, updateWrapper) > 0; } /** - * 提交出国申请(更新状态为待审核) + * 提交出国申请(动态审批人方案) + * + * 🆕 核心改进: + * 1. 不再设置固定的 current_approver_id(该字段改为冗余展示字段) + * 2. 仅设置 approval_level='1',审批人根据权限动态查询 + * 3. current_approver 设置为提示文本(如"待一级审批") + * 4. 支持人员岗位变动:权限调整后立即生效 */ @Override @Transactional @@ -211,22 +528,27 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS if (application == null) { throw new RuntimeException("申请不存在"); } - // 只有草稿状态(status为null)的申请才能通过此方法提交 - if (application.getStatus() != null && !"".equals(application.getStatus())) { - throw new RuntimeException("只有草稿状态的申请才能提交"); + + // 只有草稿状态或待审核状态的申请才能提交 + if ("1".equals(application.getStatus()) || "4".equals(application.getStatus())) { + throw new RuntimeException("已通过或已完成的申请不能重新提交"); } - - // 设置状态为待审核 + + // 🆕 设置状态为待审核 application.setStatus("0"); application.setApplyTime(new Date()); - - // 设置初始审批级别为1(单级审批制度) + + // 🆕 设置初始审批级别为1(不再设置固定审批人ID) application.setApprovalLevel("1"); - - // 设置当前审批环节描述(单级审批制度) - application.setCurrentApprover("待审批"); - - return baseMapper.updateById(application) > 0; + application.setCurrentApproverId(null); // 清空固定审批人ID + application.setCurrentApprover("待一级审批"); // 提示文本 + + boolean updated = baseMapper.updateById(application) > 0; + + log.info("✅ [出国申请提交] 提交成功 - applicationId: {}, 申请人: {}, 审批级别: 1", + applicationId, application.getApplicantName()); + + return updated; } /** @@ -242,10 +564,10 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS if ("4".equals(application.getStatus())) { throw new RuntimeException("已完成的申请不能取消"); } - + application.setStatus("3"); application.setApplicantRemark(reason); - + return baseMapper.updateById(application) > 0; } @@ -254,8 +576,8 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS */ @Override @Transactional - public Boolean approveApplication(Long applicationId, Long approverId, String approverName, - String approvalStatus, String approvalOpinion) { + public Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature) { RsOverseasApplication application = baseMapper.selectById(applicationId); if (application == null) { throw new RuntimeException("申请不存在"); @@ -264,6 +586,15 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS throw new RuntimeException("只有待审核状态的申请才能审批"); } + // 🔴 新增:验证审批人权限级别 + String currentApprovalLevel = application.getApprovalLevel(); + if (!validateApproverLevel(approverId, currentApprovalLevel)) { + throw new ServiceException(String.format( + "您没有第%s级审批权限,当前申请需要第%s级审批人审批", + currentApprovalLevel, currentApprovalLevel)); + } + log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", approverName, currentApprovalLevel); + // 创建审批记录 RsOverseasApproval approval = new RsOverseasApproval(); approval.setApplicationId(applicationId); @@ -272,8 +603,16 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS approval.setApprovalLevel(application.getApprovalLevel()); approval.setApprovalStatus(approvalStatus); approval.setApprovalOpinion(approvalOpinion); + approval.setApprovalSignature(approvalSignature); // 🔴 保存审批签字 approval.setApprovalTime(new Date()); - + + // 日志记录签字数据 + if (approvalSignature != null && !approvalSignature.trim().isEmpty()) { + log.info("✅ 保存审批签字数据,长度: {} 字符", approvalSignature.length()); + } else { + log.info("ℹ️ 未提供审批签字"); + } + // 手动设置多租户和删除标志字段 if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { approval.setTenantId(LoginHelper.getTenantId()); @@ -281,22 +620,26 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS if (approval.getDeleted() == null || approval.getDeleted().isEmpty()) { approval.setDeleted("0"); } - + approvalMapper.insert(approval); - // 更新申请状态 + // 🆕 更新申请状态(动态审批人方案) if ("1".equals(approvalStatus)) { // 审批通过 String nextLevel = getNextApprovalLevel(application.getApprovalLevel()); if (nextLevel != null) { - // 进入下一级审批 + // 🆕 进入下一级审批(不再设置固定审批人ID) application.setApprovalLevel(nextLevel); - application.setCurrentApprover("待指定"); + application.setCurrentApproverId(null); // 不再设置固定审批人ID + application.setCurrentApprover(getApprovalLevelDescription(nextLevel)); // 如"待二级审批" + + log.info("✅ 审批流转到第{}级,申请ID: {}, 审批人根据权限动态查询", + nextLevel, applicationId); } else { // 所有级别审批完成 - 先尝试创建出国记录,再更新申请状态 - log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}", - applicationId, application.getStatus(), application.getApplicantName()); - + log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}", + applicationId, application.getStatus(), application.getApplicantName()); + try { // 如果审批通过且为最终级别,先创建出国记录 log.info("调用创建出国记录服务,申请ID: {}", applicationId); @@ -306,7 +649,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS throw new RuntimeException("创建出国记录失败,请联系管理员"); } log.info("审批通过,成功创建出国记录,申请ID: {}", applicationId); - + // 记录创建成功后,才更新申请状态为通过 log.info("准备更新申请状态为通过,申请ID: {}", applicationId); application.setStatus("1"); @@ -325,17 +668,55 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS // 更新申请状态到数据库 boolean updateResult = baseMapper.updateById(application) > 0; - + return updateResult; } + /** + * 审批出国申请(支持标准工作流模式) + * 🔴 标准化改造:新增重载方法支持工作流任务ID + */ + @Override + @Transactional + public Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature, String taskId) { + + // 🔴 标准化改造:如果提供了taskId,说明是标准工作流模式 + if (taskId != null && !taskId.trim().isEmpty()) { + log.info("🔄 [标准工作流审批] 申请ID: {}, 任务ID: {}, 审批人: {}, 审批状态: {}", + applicationId, taskId, approverName, approvalStatus); + + // TODO: 这里应该调用工作流引擎的API来完成审批 + // 例如:workflowService.completeTask(taskId, approvalStatus, approvalOpinion); + + // 目前先调用原有的审批逻辑(向后兼容) + log.info("⚠️ [兼容模式] 暂时使用原有审批逻辑,等待集成工作流引擎API"); + } + + // 调用原有的审批方法(增加签字参数) + return approveApplication(applicationId, approverId, approverName, approvalStatus, approvalOpinion, + approvalSignature); + } + /** * 获取下一级审批级别 - * 修改为单级审批制度:审批通过后直接完成,不进入下一级 + * 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 */ private String getNextApprovalLevel(String currentLevel) { - // 单级审批:直接返回null表示审批完成 - return null; + switch (currentLevel) { + case "1": + return "2"; // 部门负责人 → 人事处 + case "2": + return "3"; // 人事处 → 纪委 + case "3": + return "4"; // 纪委 → 学院分管领导 + case "4": + return "5"; // 学院分管领导 → 学院党委书记 + case "5": + return null; // 学院党委书记 → 审批完成 + default: + return null; + } } /** @@ -347,24 +728,25 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS @Transactional public int fixApprovedApplicationsWithoutRecords() { log.info("开始修复已审批通过但未创建记录的申请"); - + // 查询所有审批通过的申请 List approvedApplications = baseMapper.selectList( - new LambdaQueryWrapper() - .eq(RsOverseasApplication::getStatus, "1") // 已通过 + new LambdaQueryWrapper() + .eq(RsOverseasApplication::getStatus, "1") // 已通过 ); - + int fixedCount = 0; - + for (RsOverseasApplication application : approvedApplications) { try { // 检查是否已存在记录 - RsOverseasRecordVo existingRecord = overseasRecordService.queryByApplicationId(application.getApplicationId()); + RsOverseasRecordVo existingRecord = overseasRecordService + .queryByApplicationId(application.getApplicationId()); if (existingRecord == null) { // 不存在记录,尝试创建 - log.info("为已通过申请创建记录,申请ID: {}, 申请人: {}", - application.getApplicationId(), application.getApplicantName()); - + log.info("为已通过申请创建记录,申请ID: {}, 申请人: {}", + application.getApplicationId(), application.getApplicantName()); + boolean created = overseasRecordService.createRecordFromApplication(application.getApplicationId()); if (created) { fixedCount++; @@ -377,7 +759,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS log.error("修复申请记录失败,申请ID: {}, 错误: {}", application.getApplicationId(), e.getMessage(), e); } } - + log.info("修复完成,共修复 {} 条记录", fixedCount); return fixedCount; } @@ -385,7 +767,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS /** * 重新提交已被拒绝的申请 * - * @param applicationId 申请ID + * @param applicationId 申请ID * @param applicationData 申请更新数据 * @return 是否成功 */ @@ -397,14 +779,14 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS if (existingApp == null) { throw new RuntimeException("申请不存在"); } - + // 只有状态为已拒绝的申请才能重新提交 if (!"2".equals(existingApp.getStatus())) { throw new RuntimeException("只有已拒绝状态的申请才能重新提交"); } - + log.info("【重新提交申请】原申请状态: {}, 申请ID: {}", existingApp.getStatus(), applicationId); - + // 更新申请数据 if (applicationData.getDestination() != null) { existingApp.setDestination(applicationData.getDestination()); @@ -424,33 +806,35 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS if (applicationData.getEndDate() != null) { existingApp.setEndDate(applicationData.getEndDate()); } - if (applicationData.getBudget() != null) { - existingApp.setBudget(applicationData.getBudget()); - } + // budget 字段已删除 + // if (applicationData.getBudget() != null) { + // existingApp.setBudget(applicationData.getBudget()); + // } if (applicationData.getItinerary() != null) { existingApp.setItinerary(applicationData.getItinerary()); } if (applicationData.getApplicantRemark() != null) { existingApp.setApplicantRemark(applicationData.getApplicantRemark()); } - + // 自动计算出国天数 if (existingApp.getStartDate() != null && existingApp.getEndDate() != null) { long diffInMillies = existingApp.getEndDate().getTime() - existingApp.getStartDate().getTime(); int days = (int) (diffInMillies / (1000 * 60 * 60 * 24)) + 1; existingApp.setDuration(days); } - - // 重置为待审核状态 + + // 🆕 重置为待审核状态(动态审批人方案) existingApp.setStatus("0"); existingApp.setApplyTime(new Date()); // 更新申请时间 - - // 设置初始审批级别为1(单级审批制度) + + // 🆕 设置初始审批级别为1(不再设置固定审批人ID) existingApp.setApprovalLevel("1"); - existingApp.setCurrentApprover("待审批"); - - log.info("【重新提交申请】更新为待审核状态,申请ID: {}", applicationId); - + existingApp.setCurrentApproverId(null); // 清空固定审批人ID + existingApp.setCurrentApprover("待一级审批"); // 提示文本 + + log.info("【重新提交申请】更新为待审核状态,申请ID: {}, 审批级别: 1", applicationId); + return baseMapper.updateById(existingApp) > 0; } @@ -464,10 +848,8 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS throw new RuntimeException("开始日期不能晚于结束日期"); } } - - if (entity.getBudget() != null && entity.getBudget().doubleValue() <= 0) { - throw new RuntimeException("预算金额必须大于0"); - } + + // 预算金额校验已删除(业务不需要预算金额字段) } /** @@ -475,16 +857,24 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS */ private LambdaQueryWrapper buildQueryWrapper(RsOverseasApplicationQuery query) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.like(StringUtils.isNotBlank(query.getApplicantName()), RsOverseasApplication::getApplicantName, query.getApplicantName()); - lqw.like(StringUtils.isNotBlank(query.getDepartment()), RsOverseasApplication::getDepartment, query.getDepartment()); + lqw.like(StringUtils.isNotBlank(query.getApplicantName()), RsOverseasApplication::getApplicantName, + query.getApplicantName()); + lqw.like(StringUtils.isNotBlank(query.getDepartment()), RsOverseasApplication::getDepartment, + query.getDepartment()); lqw.eq(StringUtils.isNotBlank(query.getPurpose()), RsOverseasApplication::getPurpose, query.getPurpose()); - lqw.like(StringUtils.isNotBlank(query.getDestination()), RsOverseasApplication::getDestination, query.getDestination()); + lqw.like(StringUtils.isNotBlank(query.getDestination()), RsOverseasApplication::getDestination, + query.getDestination()); lqw.eq(StringUtils.isNotBlank(query.getStatus()), RsOverseasApplication::getStatus, query.getStatus()); - lqw.eq(StringUtils.isNotBlank(query.getApprovalLevel()), RsOverseasApplication::getApprovalLevel, query.getApprovalLevel()); - lqw.like(StringUtils.isNotBlank(query.getCurrentApprover()), RsOverseasApplication::getCurrentApprover, query.getCurrentApprover()); + lqw.eq(StringUtils.isNotBlank(query.getApprovalLevel()), RsOverseasApplication::getApprovalLevel, + query.getApprovalLevel()); + lqw.like(StringUtils.isNotBlank(query.getCurrentApprover()), RsOverseasApplication::getCurrentApprover, + query.getCurrentApprover()); lqw.eq(query.getApplicantId() != null, RsOverseasApplication::getApplicantId, query.getApplicantId()); - lqw.ge(query.getBudgetMin() != null, RsOverseasApplication::getBudget, query.getBudgetMin()); - lqw.le(query.getBudgetMax() != null, RsOverseasApplication::getBudget, query.getBudgetMax()); + // budget 字段已删除,移除相关查询条件 + // lqw.ge(query.getBudgetMin() != null, RsOverseasApplication::getBudget, + // query.getBudgetMin()); + // lqw.le(query.getBudgetMax() != null, RsOverseasApplication::getBudget, + // query.getBudgetMax()); lqw.ge(query.getApplyTimeBegin() != null, RsOverseasApplication::getApplyTime, query.getApplyTimeBegin()); lqw.le(query.getApplyTimeEnd() != null, RsOverseasApplication::getApplyTime, query.getApplyTimeEnd()); lqw.ge(query.getStartDateBegin() != null, RsOverseasApplication::getStartDate, query.getStartDateBegin()); @@ -498,23 +888,74 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS private RsOverseasApplication buildEntityFromBo(RsOverseasApplicationBo bo) { RsOverseasApplication entity = new RsOverseasApplication(); entity.setApplicationId(bo.getApplicationId()); + + // ==================== 基本信息 ==================== entity.setApplicantId(bo.getApplicantId()); entity.setApplicantName(bo.getApplicantName()); + entity.setGender(bo.getGender()); // 性别 + entity.setBirthDate(bo.getBirthDate()); // 出生日期 + entity.setPhoto(bo.getPhoto()); // 照片 + entity.setIdNumber(bo.getIdNumber()); // 公民身份号码 + entity.setPoliticalStatus(bo.getPoliticalStatus()); // 政治面貌 ⬅️ 关键修复 + entity.setRegisteredAddress(bo.getRegisteredAddress()); // 户籍所在地 + + // ==================== 工作信息 ==================== entity.setDepartment(bo.getDepartment()); - entity.setContactPhone(bo.getContactPhone()); + entity.setPosition(bo.getPosition()); // 职务 + entity.setProfessionalTitle(bo.getProfessionalTitle()); // 职称 + entity.setAdministrativeLevel(bo.getAdministrativeLevel()); // 行政职级 + entity.setPersonnelCategory(bo.getPersonnelCategory()); // 人员类别代码 + + // ==================== 单位和联系信息 ==================== + entity.setRegistrationUnit(bo.getRegistrationUnit()); // 登记备案责任单位 + entity.setUnitLeaderSignature(bo.getUnitLeaderSignature()); // 单位负责人签名 + // unitContact 字段已删除 + // entity.setUnitContact(bo.getUnitContact()); // 单位联系人 + entity.setOfficePhone(bo.getOfficePhone()); // 办公电话 + entity.setMobilePhone(bo.getMobilePhone()); // 移动电话 + entity.setContactPhone(bo.getContactPhone()); // 联系电话(兼容字段) + entity.setEmergencyContact(bo.getEmergencyContact()); // 应急联系人及电话 ⬅️ 关键修复 + + // ==================== 出国目的和行程 ==================== entity.setPurpose(bo.getPurpose()); entity.setPurposeDetail(bo.getPurposeDetail()); entity.setDestination(bo.getDestination()); + entity.setDestinationCountry(bo.getDestinationCountry()); // 前往国家 entity.setDestinationCity(bo.getDestinationCity()); entity.setStartDate(bo.getStartDate()); entity.setEndDate(bo.getEndDate()); entity.setDuration(bo.getDuration()); - entity.setBudget(bo.getBudget()); + // budget 字段已删除 + // entity.setBudget(bo.getBudget()); + + // ==================== 证件和文档 ==================== entity.setInvitation(bo.getInvitation()); + entity.setDocumentName(bo.getDocumentName()); // 申领证件名称 + entity.setDocumentNumber(bo.getDocumentNumber()); // 证件号码 entity.setItinerary(bo.getItinerary()); entity.setAccommodationPlan(bo.getAccommodationPlan()); entity.setTransportationPlan(bo.getTransportationPlan()); - entity.setApplicantRemark(bo.getApplicantRemark()); + + // ==================== 出境事由和备注 ==================== + entity.setExitReason(bo.getExitReason()); // 出境事由 ⬅️ 关键字段 + entity.setVisitRelationship(bo.getVisitRelationship()); // 探望对象关系 + entity.setRemarks(bo.getRemarks()); // 备注 + entity.setApplicantRemark(bo.getApplicantRemark()); // 申请人备注 + entity.setAdminRemark(bo.getAdminRemark()); // 管理员备注 ⬅️ 补充遗漏字段 + entity.setSignature(bo.getSignature()); // 手写签名 ⬅️ 补充遗漏字段 + + // ==================== 盖章文件相关字段 ==================== + entity.setStampedRegistrationFilePath(bo.getStampedRegistrationFilePath()); + entity.setStampedRegistrationFileName(bo.getStampedRegistrationFileName()); + entity.setStampedFileUploadTime(bo.getStampedFileUploadTime()); + entity.setStampedFileUploadStatus(bo.getStampedFileUploadStatus()); + + // ==================== 租户信息 ==================== + // 🔴 关键修复:设置租户ID(如果Bo中有值则使用Bo的值,否则依赖MyBatis-Plus自动填充) + if (bo.getTenantId() != null) { + entity.setTenantId(bo.getTenantId()); + } + return entity; } @@ -541,6 +982,445 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS @Transactional public Boolean deleteWithValidByIds(List ids, Boolean isValid) { validatedByIds(ids, isValid); - return baseMapper.deleteBatchIds(ids) > 0; + return baseMapper.deleteByIds(ids) > 0; } -} \ No newline at end of file + + /** + * 更新盖章文件信息 + */ + @Override + @Transactional + public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId) { + return baseMapper.update(null, + new LambdaUpdateWrapper() + .eq(RsOverseasApplication::getApplicationId, applicationId) + .set(RsOverseasApplication::getStampedRegistrationFilePath, filePath) + .set(RsOverseasApplication::getStampedRegistrationFileName, fileName) + .set(RsOverseasApplication::getStampedFileUploadTime, uploadTime) + .set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus) + .set(RsOverseasApplication::getStampedFileOssId, ossId)) > 0; + } + + /** + * 更新党籍保留附件信息 + */ + @Override + @Transactional + public Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId) { + return baseMapper.update(null, + new LambdaUpdateWrapper() + .eq(RsOverseasApplication::getApplicationId, applicationId) + .set(RsOverseasApplication::getPartyRetentionFilePath, filePath) + .set(RsOverseasApplication::getPartyRetentionFileName, fileName) + .set(RsOverseasApplication::getPartyRetentionUploadTime, uploadTime) + .set(RsOverseasApplication::getPartyRetentionUploadStatus, uploadStatus) + .set(RsOverseasApplication::getPartyRetentionFileOssId, ossId)) > 0; + } + + /** + * 校验出国登记前置条件 + * 检查当前自然年内是否有已通过的出国登记记录 + */ + @Override + public Map validateRegistrationPrerequisite(Long applicantId) { + Map result = new HashMap<>(); + + // 计算当前自然年的开始日期(1月1日 00:00:00) + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.MONTH, Calendar.JANUARY); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date currentYearStart = calendar.getTime(); + + int currentYear = calendar.get(Calendar.YEAR); + + // 查询当前自然年内的出国登记记录(使用createBy字段) + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(RsOverseasRegistration::getCreateBy, applicantId) + .ge(RsOverseasRegistration::getCreateTime, currentYearStart) + .orderByDesc(RsOverseasRegistration::getCreateTime) + .last("LIMIT 1"); + + RsOverseasRegistration latestRegistration = registrationMapper.selectOne(queryWrapper); + + if (latestRegistration == null) { + // 当前自然年内没有登记记录 + result.put("isValid", false); + result.put("message", "请先提交出国登记申请,审核通过后再提交出国申请"); + log.warn("用户{}在{}年内没有出国登记记录", applicantId, currentYear); + return result; + } + + String status = latestRegistration.getStatus(); + if ("2".equals(status)) { + // 状态为已通过(审批通过)- 出国登记状态2表示已通过 + result.put("isValid", true); + result.put("message", "出国登记已通过审核"); + result.put("registrationId", latestRegistration.getRegistrationId()); + log.info("用户{}的出国登记已通过审核,registrationId: {}", applicantId, latestRegistration.getRegistrationId()); + } else if ("1".equals(status)) { + // 状态为待审核 + result.put("isValid", false); + result.put("message", "出国登记审核中,请审核通过后再提交出国申请"); + log.warn("用户{}的出国登记还在审核中,status: {}", applicantId, status); + } else if ("3".equals(status)) { + // 状态为已拒绝 + result.put("isValid", false); + result.put("message", "出国登记已被拒绝,请重新提交出国登记并审核通过后再提交出国申请"); + log.warn("用户{}的出国登记已被拒绝,status: {}", applicantId, status); + } else { + // 其他状态(草稿等) + result.put("isValid", false); + result.put("message", "出国登记状态异常,请先提交出国登记申请"); + log.warn("用户{}的出国登记状态异常,status: {}", applicantId, status); + } + + return result; + } + + // ==================== 动态审批人获取方法实现 ==================== + + /** + * 根据申请人ID获取部门审批人ID + * + * @param applicantId 申请人ID + * @return 部门审批人ID + */ + @Override + public Long getDepartmentApproverId(Long applicantId) { + try { + // 1. 根据申请人ID获取用户信息 + SysUserVo user = userService.selectUserById(applicantId); + if (user == null) { + log.warn("用户不存在,applicantId: {}", applicantId); + return null; + } + + // 2. 获取用户所在部门ID + Long deptId = user.getDeptId(); + if (deptId == null) { + log.warn("用户没有部门信息,applicantId: {}", applicantId); + return null; + } + + // 3. 根据部门ID获取部门负责人 + SysDeptVo dept = deptService.selectDeptById(deptId); + if (dept == null) { + log.warn("部门不存在,deptId: {}", deptId); + return null; + } + + // 4. 获取部门负责人ID(假设部门表中有leader字段存储负责人ID) + Long leaderId = dept.getLeader(); + if (leaderId == null) { + log.warn("部门没有负责人,deptId: {}", deptId); + // 如果没有设置负责人,可以返回默认的部门审批人或者抛出异常 + return null; + } + + log.info("获取到部门审批人,applicantId: {}, deptId: {}, leaderId: {}", applicantId, deptId, leaderId); + return leaderId; + + } catch (Exception e) { + log.error("获取部门审批人失败,applicantId: {}", applicantId, e); + return null; + } + } + + /** + * 根据申请人ID获取人事处审批人列表 + * + * @param applicantId 申请人ID + * @return 人事处审批人ID列表 + */ + @Override + public List getHrApprovers(Long applicantId) { + try { + // 1. 查询人事处部门(假设人事处的部门名称包含"人事"或者有特定的部门编码) + List hrDepts = deptService.selectDeptList(new SysDeptBo()); + List hrApprovers = new ArrayList<>(); + + for (SysDeptVo dept : hrDepts) { + // 根据部门名称判断是否为人事处 + if (dept.getDeptName() != null && + (dept.getDeptName().contains("人事") || + dept.getDeptName().contains("人力资源"))) { + + // 获取该部门的负责人作为审批人 + if (dept.getLeader() != null) { + hrApprovers.add(dept.getLeader()); + } + } + } + + // 如果没有找到人事处审批人,返回默认的人事处审批人列表 + if (hrApprovers.isEmpty()) { + log.warn("没有找到人事处审批人,applicantId: {}", applicantId); + // 可以配置默认的人事处审批人ID列表 + // hrApprovers.add(defaultHrApproverId); + } + + log.info("获取到人事处审批人列表,applicantId: {}, hrApprovers: {}", applicantId, hrApprovers); + return hrApprovers; + + } catch (Exception e) { + log.error("获取人事处审批人失败,applicantId: {}", applicantId, e); + return new ArrayList<>(); + } + } + + /** + * 根据申请人查找对应的部门负责人审批人ID + * + * @param applicantId 申请人ID + * @return 部门负责人审批人ID + */ + private Long findDeptApproverByApplicant(Long applicantId) { + try { + // 1. 获取申请人信息 + SysUserVo applicant = userService.selectUserById(applicantId); + if (applicant == null || applicant.getDeptId() == null) { + log.warn("申请人不存在或未分配部门,申请人ID: {}", applicantId); + return null; + } + + Long deptId = applicant.getDeptId(); + log.info("查找部门负责人,申请人: {}, 部门ID: {}", applicant.getUserName(), deptId); + + // 2. 查找该部门对应的部门负责人审批人 + return findDeptApproverByDeptId(deptId); + + } catch (Exception e) { + log.error("查找部门负责人失败,申请人ID: {}", applicantId, e); + return null; + } + } + + /** + * 根据部门ID查找对应的部门负责人审批人 + * + * @param deptId 部门ID + * @return 部门负责人审批人ID + */ + private Long findDeptApproverByDeptId(Long deptId) { + try { + // 1. 获取"部门负责人审批(出国申请)"角色 + SysRoleBo roleQuery = new SysRoleBo(); + roleQuery.setRoleKey("dept_head_overseas_approver"); + List roles = roleService.selectRoleList(roleQuery); + if (roles.isEmpty()) { + log.error("未找到部门负责人审批角色: dept_head_overseas_approver"); + return null; + } + SysRoleVo deptApproverRole = roles.get(0); + + // 2. 获取该角色下的所有用户 + SysUserBo userQuery = new SysUserBo(); + userQuery.setRoleId(deptApproverRole.getRoleId()); + PageQuery pageQuery = new PageQuery(); + pageQuery.setPageSize(1000); // 设置较大的页面大小以获取所有用户 + List allocatedUsers = userService.selectAllocatedList(userQuery, pageQuery).getRows(); + if (allocatedUsers.isEmpty()) { + log.warn("部门负责人审批角色下没有用户"); + return null; + } + + // 3. 遍历审批人,找到与申请人同部门的审批人 + for (SysUserVo approver : allocatedUsers) { + if (approver.getDeptId() != null && approver.getDeptId().equals(deptId)) { + log.info("找到部门负责人审批人: {} ({}), 部门ID: {}", + approver.getUserName(), approver.getNickName(), deptId); + return approver.getUserId(); + } + } + + // 4. 如果没找到同部门审批人,尝试查找父部门负责人 + log.info("当前部门 {} 没有找到负责人,尝试查找父部门负责人", deptId); + return findParentDeptApprover(deptId, allocatedUsers); + + } catch (Exception e) { + log.error("查找部门负责人失败,部门ID: {}", deptId, e); + return null; + } + } + + /** + * 查找父部门的负责人审批人 + * + * @param deptId 当前部门ID + * @param approvers 所有部门负责人审批人列表 + * @return 父部门负责人审批人ID + */ + private Long findParentDeptApprover(Long deptId, List approvers) { + try { + // 获取当前部门信息 + SysDeptVo currentDept = deptService.selectDeptById(deptId); + if (currentDept == null || currentDept.getParentId() == null || currentDept.getParentId() == 0) { + log.warn("部门 {} 没有父部门", deptId); + return null; + } + + Long parentDeptId = currentDept.getParentId(); + log.info("查找父部门负责人,父部门ID: {}", parentDeptId); + + // 在审批人列表中查找父部门的负责人 + for (SysUserVo approver : approvers) { + if (approver.getDeptId() != null && approver.getDeptId().equals(parentDeptId)) { + log.info("找到父部门负责人审批人: {} ({}), 父部门ID: {}", + approver.getUserName(), approver.getNickName(), parentDeptId); + return approver.getUserId(); + } + } + + // 递归查找更上级的父部门 + return findParentDeptApprover(parentDeptId, approvers); + + } catch (Exception e) { + log.error("查找父部门负责人失败,部门ID: {}", deptId, e); + return null; + } + } + + /** + * 根据审批级别获取下一级审批人ID + * + * @param approvalLevel 审批级别 + * @return 下一级审批人ID + */ + // 🗑️ getNextApprover() 方法已删除 + // 原因:采用动态审批人方案后,不再需要预先指定固定的审批人ID + // 新方案:待审批列表通过 getApprovalLevelsByUser() 动态查询 + + /** + * 根据审批级别获取审批环节描述 + * + * @param approvalLevel 审批级别 + * @return 审批环节描述 + */ + private String getApprovalLevelDescription(String approvalLevel) { + switch (approvalLevel) { + case "1": + return "待部门负责人审批"; + case "2": + return "待人事处审批"; + case "3": + return "待纪委审批"; + case "4": + return "待学院分管领导审批"; + case "5": + return "待学院党委书记审批"; + default: + return "待审批"; + } + } + + /** + * 🆕 根据当前登录用户的审批权限,获取其可审批的级别列表 + * + * 权限到级别的映射: + * - rs:overseas:approve1 → 审批级别 "1" (部门负责人) + * - rs:overseas:approve2 → 审批级别 "2" (人事处) + * - rs:overseas:approve3 → 审批级别 "3" (纪委) + * - rs:overseas:approve4 → 审批级别 "4" (学院分管领导) + * - rs:overseas:approve5 → 审批级别 "5" (学院党委书记) + * + * @param userId 用户ID + * @return 可审批的级别列表,例如:["2", "3"] + */ + private List getApprovalLevelsByUser(Long userId) { + try { + List approvalLevels = new ArrayList<>(); + + // 🔴 修复:直接使用Sa-Token API检查权限,避免数据库查询问题 + // 检查5个审批级别的权限(1-5级) + String[] levels = { "1", "2", "3", "4", "5" }; + for (String level : levels) { + String permission = "rs:overseas:approve" + level; + if (StpUtil.hasPermission(permission)) { + approvalLevels.add(level); + log.info("✅ 用户 {} 拥有第{}级审批权限: {}", userId, level, permission); + } + } + + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:overseas:approve")) { + // 通用审批权限可以审批所有级别 + log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId); + approvalLevels.clear(); + approvalLevels.addAll(Arrays.asList(levels)); + } + + if (approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何出国审批权限", userId); + } else { + log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels); + } + + return approvalLevels; + + } catch (Exception e) { + log.error("❌ 获取用户 {} 的审批级别失败", userId, e); + return new ArrayList<>(); + } + } + + /** + * 🆕 根据审批级别获取对应的权限标识 + * + * @param approvalLevel 审批级别(2-5) + * @return 权限标识,例如:"rs:overseas:approve2" + */ + private String getPermissionByLevel(String approvalLevel) { + if (approvalLevel == null || approvalLevel.isEmpty()) { + return null; + } + return "rs:overseas:approve" + approvalLevel; + } + + // 工作流监听器已移至独立的 OverseasApplicationWorkflowListener 类中 + + // 状态更新方法已移至独立的 OverseasApplicationWorkflowListener 类中 + + // 删除事件监听器已移至独立的 OverseasApplicationWorkflowListener 类中 + + // 任务创建监听器已移至独立的 OverseasApplicationWorkflowListener 类中 + + // 删除了复杂的审批记录创建方法 - 使用标准工作流管理审批记录 + + // 删除了复杂的部门负责人查找方法 - 工作流引擎会自动处理审批人分配 + + /** + * 🔴 新增方法:验证审批人是否有指定级别的审批权限 + * + * @param approverId 审批人ID + * @param approvalLevel 需要的审批级别(1-5) + * @return true-有权限,false-无权限 + */ + private boolean validateApproverLevel(Long approverId, String approvalLevel) { + // 检查通用审批权限(超级管理员/高级审批者) + if (StpUtil.hasPermission("rs:overseas:approve")) { + log.info("用户 {} 拥有通用审批权限 rs:overseas:approve", approverId); + return true; + } + + // 检查分级审批权限 + String requiredPermission = "rs:overseas:approve" + approvalLevel; + boolean hasPermission = StpUtil.hasPermission(requiredPermission); + + if (hasPermission) { + log.info("用户 {} 拥有第{}级审批权限 {}", + approverId, approvalLevel, requiredPermission); + } else { + log.warn("用户 {} 缺少第{}级审批权限 {},权限验证失败", + approverId, approvalLevel, requiredPermission); + } + + return hasPermission; + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java new file mode 100644 index 00000000..352e2069 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java @@ -0,0 +1,1100 @@ +package org.dromara.rs.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.itextpdf.text.Chunk; +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Element; +import com.itextpdf.text.Font; +import com.itextpdf.text.Image; +import com.itextpdf.text.PageSize; +import com.itextpdf.text.Paragraph; +import com.itextpdf.text.Phrase; +import com.itextpdf.text.pdf.BaseFont; +import com.itextpdf.text.pdf.PdfPCell; +import com.itextpdf.text.pdf.PdfPTable; +import com.itextpdf.text.pdf.PdfWriter; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.rs.domain.RsOverseasApplication; +import org.dromara.rs.domain.RsOverseasApproval; +import org.dromara.rs.domain.vo.RsOverseasApprovalMonitorVo; +import org.dromara.rs.domain.vo.RsOverseasApprovalVo; +import org.dromara.rs.domain.vo.RsOverseasRecordVo; +import org.dromara.rs.domain.query.RsOverseasApprovalMonitorQuery; +import org.dromara.rs.mapper.RsOverseasApplicationMapper; +import org.dromara.rs.mapper.RsOverseasApprovalMapper; +import org.dromara.rs.mapper.RsOverseasRecordMapper; +import org.dromara.rs.service.IRsOverseasApprovalMonitorService; +import org.dromara.rs.service.IRsOverseasApprovalService; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 出国申请审批监控Service实现 + * + * @author Cursor AI + * @date 2025-10-21 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApprovalMonitorService { + + private final RsOverseasApplicationMapper applicationMapper; + private final RsOverseasRecordMapper recordMapper; + private final RsOverseasApprovalMapper approvalMapper; + private final IRsOverseasApprovalService approvalService; + + /** + * 查询审批中的申请列表(待审核状态) + * 🔴 查询所有待审批申请(不限制申请人) + */ + @Override + public TableDataInfo queryPendingList(RsOverseasApprovalMonitorQuery query) { + log.info("查询审批中的申请列表,查询条件:{}", query); + + // 构建查询条件 + QueryWrapper wrapper = buildQueryWrapper(query); + wrapper.eq("status", "0"); // 只查询待审核状态 + // 🔴 不再限制申请人,拥有权限的用户可以看到所有数据 + + // 分页查询 + Page page = applicationMapper.selectPage( + new Page<>(query.getPageNum(), query.getPageSize()), + wrapper); + + // 转换为VO并填充审批信息 + List voList = page.getRecords().stream() + .map(this::convertToMonitorVo) + .collect(Collectors.toList()); + + // 构建分页结果 + Page resultPage = new Page<>(query.getPageNum(), query.getPageSize()); + resultPage.setRecords(voList); + resultPage.setTotal(page.getTotal()); + + log.info("✅ 查询到 {} 条待审批申请", voList.size()); + return TableDataInfo.build(resultPage); + } + + /** + * 查询已完成审批的申请列表(已通过状态) + * 🔴 查询所有已完成审批申请(不限制申请人) + */ + @Override + public TableDataInfo queryCompletedList(RsOverseasApprovalMonitorQuery query) { + log.info("查询已完成审批的申请列表,查询条件:{}", query); + + // 构建查询条件 + QueryWrapper wrapper = buildQueryWrapper(query); + wrapper.eq("status", "1"); // 只查询已通过状态 + // 🔴 不再限制申请人,拥有权限的用户可以看到所有数据 + + // 分页查询 + Page page = applicationMapper.selectPage( + new Page<>(query.getPageNum(), query.getPageSize()), + wrapper); + + // 转换为VO并填充审批信息 + List voList = page.getRecords().stream() + .map(this::convertToMonitorVo) + .collect(Collectors.toList()); + + // 构建分页结果 + Page resultPage = new Page<>(query.getPageNum(), query.getPageSize()); + resultPage.setRecords(voList); + resultPage.setTotal(page.getTotal()); + + log.info("✅ 查询到 {} 条已完成审批申请", voList.size()); + return TableDataInfo.build(resultPage); + } + + /** + * 查询申请详情(含审批流程信息) + */ + @Override + public RsOverseasApprovalMonitorVo queryDetailWithApprovalInfo(Long applicationId) { + log.info("查询申请详情,申请ID:{}", applicationId); + + // 查询申请基本信息 + RsOverseasApplication application = applicationMapper.selectById(applicationId); + if (application == null) { + log.warn("未找到申请信息,申请ID:{}", applicationId); + return null; + } + + // 转换为VO并填充审批信息 + RsOverseasApprovalMonitorVo vo = convertToMonitorVo(application); + + // 查询审批历史记录 + List approvalHistory = approvalService.queryByApplicationId(applicationId); + vo.setApprovalHistory(approvalHistory); + + return vo; + } + + /** + * 获取审批进度统计 + */ + @Override + public Map getApprovalStatistics() { + log.info("获取审批进度统计"); + + Map statistics = new HashMap<>(); + String tenantId = LoginHelper.getTenantId(); + + // 1. 待审批总数 + Long pendingCount = applicationMapper.selectCount( + Wrappers.query() + .eq("status", "0") + .eq("tenant_id", tenantId)); + statistics.put("pendingCount", pendingCount); + + // 2. 各审批级别的待审批数量(五级审批制度) + Map pendingByLevel = new HashMap<>(); + pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门负责人 + pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 人事处 + pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 纪委 + pendingByLevel.put("level4", countByStatusAndLevel("0", "4", tenantId)); // 学院分管领导 + pendingByLevel.put("level5", countByStatusAndLevel("0", "5", tenantId)); // 学院党委书记 + statistics.put("pendingByLevel", pendingByLevel); + + // 3. 已完成审批总数 + Long completedCount = applicationMapper.selectCount( + Wrappers.query() + .eq("status", "1") + .eq("tenant_id", tenantId)); + statistics.put("completedCount", completedCount); + + // 4. 今日新增申请数 + LocalDate today = LocalDate.now(); + Date startOfToday = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant()); + Long todayCount = applicationMapper.selectCount( + Wrappers.query() + .ge("apply_time", startOfToday) + .eq("tenant_id", tenantId)); + statistics.put("todayCount", todayCount); + + // 5. 已拒绝总数 + Long rejectedCount = applicationMapper.selectCount( + Wrappers.query() + .eq("status", "2") + .eq("tenant_id", tenantId)); + statistics.put("rejectedCount", rejectedCount); + + log.info("审批进度统计结果:{}", statistics); + return statistics; + } + + // ==================== 私有辅助方法 ==================== + + /** + * 构建查询条件 + */ + private QueryWrapper buildQueryWrapper(RsOverseasApprovalMonitorQuery query) { + QueryWrapper wrapper = Wrappers.query(); + + // 申请人姓名(模糊查询) + if (StringUtils.isNotBlank(query.getApplicantName())) { + wrapper.like("applicant_name", query.getApplicantName()); + } + + // 所属部门(模糊查询) + if (StringUtils.isNotBlank(query.getDepartment())) { + wrapper.like("department", query.getDepartment()); + } + + // 目的地国家(模糊查询) + if (StringUtils.isNotBlank(query.getDestination())) { + wrapper.like("destination", query.getDestination()); + } + + // 审批级别 + if (StringUtils.isNotBlank(query.getApprovalLevel())) { + wrapper.eq("approval_level", query.getApprovalLevel()); + } + + // 当前审批人(模糊查询) + if (StringUtils.isNotBlank(query.getCurrentApprover())) { + wrapper.like("current_approver", query.getCurrentApprover()); + } + + // 申请时间范围 + if (query.getApplyTimeStart() != null) { + wrapper.ge("apply_time", query.getApplyTimeStart()); + } + if (query.getApplyTimeEnd() != null) { + wrapper.le("apply_time", query.getApplyTimeEnd()); + } + + // 租户ID + String tenantId = StringUtils.isNotBlank(query.getTenantId()) + ? query.getTenantId() + : LoginHelper.getTenantId(); + wrapper.eq("tenant_id", tenantId); + + // 按申请时间倒序排列 + wrapper.orderByDesc("apply_time"); + + return wrapper; + } + + /** + * 将Application转换为MonitorVo + */ + private RsOverseasApprovalMonitorVo convertToMonitorVo(RsOverseasApplication application) { + RsOverseasApprovalMonitorVo vo = new RsOverseasApprovalMonitorVo(); + + // 基本信息 + vo.setApplicationId(application.getApplicationId()); + + // 查询关联的出国记录ID(如果已完成审批) + String status = application.getStatus(); + if ("1".equals(status) || "4".equals(status)) { + RsOverseasRecordVo record = recordMapper + .selectOverseasRecordByApplicationId(application.getApplicationId()); + if (record != null && record.getRecordId() != null) { + vo.setRecordId(record.getRecordId()); + } + } + + vo.setSerialNumber(application.getSerialNumber()); + vo.setApplicantId(application.getApplicantId()); + vo.setApplicantName(application.getApplicantName()); + vo.setGender(application.getGender()); + vo.setGenderText(translateGender(application.getGender())); + vo.setBirthDate(application.getBirthDate()); + vo.setPoliticalStatus(application.getPoliticalStatus()); + vo.setPoliticalStatusText(translatePoliticalStatus(application.getPoliticalStatus())); + vo.setDepartment(application.getDepartment()); + vo.setProfessionalTitle(application.getProfessionalTitle()); + vo.setPosition(application.getPosition()); + + // 证件信息 + vo.setDocumentName(application.getDocumentName()); + vo.setDocumentNumber(application.getDocumentNumber()); + + // 出国信息 + vo.setPurpose(application.getPurpose()); + vo.setDestination(application.getDestination()); + vo.setDestinationCountry(application.getDestinationCountry()); + vo.setDestinationCity(application.getDestinationCity()); + vo.setExitReason(application.getExitReason()); + vo.setVisitRelationship(application.getVisitRelationship()); + vo.setItinerary(application.getItinerary()); + vo.setStartDate(application.getStartDate()); + vo.setEndDate(application.getEndDate()); + vo.setDuration(application.getDuration()); + + // 联系信息 + vo.setMobilePhone(application.getMobilePhone()); + vo.setContactPhone(application.getContactPhone()); + vo.setEmergencyContact(application.getEmergencyContact()); + + // 审批状态信息 + vo.setStatus(application.getStatus()); + vo.setStatusText(getStatusText(application.getStatus())); + + // 根据申请状态设置当前审批人和审批级别 + if ("1".equals(application.getStatus()) || "2".equals(application.getStatus())) { + // 审批已完成(通过或拒绝) + vo.setCurrentApprover("审批已完成"); + vo.setApprovalLevel(application.getApprovalLevel()); + vo.setApprovalLevelText("审批已完成"); + } else { + // 待审核状态,显示当前审批信息 + vo.setCurrentApprover(application.getCurrentApprover()); + vo.setApprovalLevel(application.getApprovalLevel()); + vo.setApprovalLevelText(getApprovalLevelText(application.getApprovalLevel())); + } + + vo.setApplyTime(application.getApplyTime()); + vo.setApprovalTime(application.getApprovalTime()); + + // 审批进度信息 + fillApprovalProgress(vo, application); + + // 审批耗时 + if (application.getApplyTime() != null) { + LocalDate applyDate = application.getApplyTime().toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate now = LocalDate.now(); + vo.setApprovalDays((int) ChronoUnit.DAYS.between(applyDate, now)); + + // 判断是否超时(超过5天视为超时,可根据业务规则调整) + vo.setIsOverdue(vo.getApprovalDays() > 5 && "0".equals(application.getStatus())); + } + + // 其他信息 + vo.setRemarks(application.getRemarks()); + vo.setAdminRemark(application.getAdminRemark()); + vo.setApplicantRemark(application.getApplicantRemark()); + vo.setTenantId(application.getTenantId()); + vo.setCreateTime(application.getCreateTime()); + vo.setUpdateTime(application.getUpdateTime()); + + return vo; + } + + /** + * 填充审批进度信息 + */ + private void fillApprovalProgress(RsOverseasApprovalMonitorVo vo, RsOverseasApplication application) { + // 查询该申请的所有审批记录 + List approvals = approvalMapper.selectList( + Wrappers.query() + .eq("application_id", application.getApplicationId()) + .orderByAsc("approval_level") + .orderByDesc("approval_time")); + + if (approvals != null && !approvals.isEmpty()) { + // 已审批级别数(状态为1-通过的记录) + long approvedCount = approvals.stream() + .filter(a -> "1".equals(a.getApprovalStatus())) + .map(RsOverseasApproval::getApprovalLevel) + .distinct() + .count(); + vo.setApprovedLevels((int) approvedCount); + + // 总审批级别数(五级审批制度:部门负责人、人事处、纪委、学院分管领导、学院党委书记) + vo.setTotalLevels(5); + + // 审批进度文本 + vo.setApprovalProgress(approvedCount + "/" + vo.getTotalLevels()); + + // 最近一次审批信息 + RsOverseasApproval latestApproval = approvals.get(0); + vo.setLatestApproverName(latestApproval.getApproverName()); + vo.setLatestApprovalOpinion(latestApproval.getApprovalOpinion()); + vo.setLatestApprovalTime(latestApproval.getApprovalTime()); + } else { + // 未开始审批 + vo.setApprovedLevels(0); + vo.setTotalLevels(5); + vo.setApprovalProgress("0/5"); + } + } + + /** + * 获取状态文本 + */ + private String getStatusText(String status) { + if (status == null) { + return "未知"; + } + switch (status) { + case "0": + return "待审核"; + case "1": + return "已通过"; + case "2": + return "已拒绝"; + case "3": + return "已取消"; + case "4": + return "已完成"; + default: + return "未知"; + } + } + + /** + * 获取审批级别文本 + * 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记 + */ + private String getApprovalLevelText(String level) { + if (level == null) { + return "未开始"; + } + switch (level) { + case "1": + return "部门负责人审批"; + case "2": + return "人事处审批"; + case "3": + return "纪委审批"; + case "4": + return "学院分管领导审批"; + case "5": + return "学院党委书记审批"; + default: + return "未知"; + } + } + + /** + * 按状态和审批级别统计数量 + */ + private Long countByStatusAndLevel(String status, String level, String tenantId) { + return applicationMapper.selectCount( + Wrappers.query() + .eq("status", status) + .eq("approval_level", level) + .eq("tenant_id", tenantId)); + } + + /** + * 翻译性别代码为文本 + * + * @param gender 性别代码:0-男,1-女 + * @return 性别文本 + */ + private String translateGender(String gender) { + if (gender == null) { + return ""; + } + switch (gender) { + case "0": + return "男"; + case "1": + return "女"; + default: + return ""; + } + } + + /** + * 翻译政治面貌代码为文本 + * + * @param politicalStatus 政治面貌代码 + * @return 政治面貌文本 + */ + private String translatePoliticalStatus(String politicalStatus) { + if (politicalStatus == null || politicalStatus.isEmpty()) { + return ""; + } + + // 支持多种编码格式的政治面貌翻译 + switch (politicalStatus) { + // 两位数字格式(01-13) + case "01": + case "1": + case "ccp_member": + return "中共党员"; + case "02": + case "2": + case "ccp_probationary": + return "中共预备党员"; + case "03": + case "3": + case "league_member": + return "共青团员"; + case "04": + case "4": + case "revolutionary": + return "民革党员"; + case "05": + case "5": + case "democratic_league": + return "民盟盟员"; + case "06": + case "6": + case "democratic_construction": + return "民建会员"; + case "07": + case "7": + case "democratic_progress": + return "民进会员"; + case "08": + case "8": + case "peasants_workers": + return "农工党党员"; + case "09": + case "9": + case "zhi_gong": + return "致公党党员"; + case "10": + case "jiusan": + return "九三学社社员"; + case "11": + case "taiwan_league": + return "台盟盟员"; + case "12": + case "non_partisan": + return "无党派人士"; + case "13": + case "masses": + return "群众"; + default: + // 如果是未知编码,返回原值 + return politicalStatus; + } + } + + /** + * 下载出国申请审批表PDF + */ + @Override + public void downloadApprovalPdf(Long applicationId, HttpServletResponse response) { + log.info("开始生成出国申请审批表PDF,申请ID:{}", applicationId); + + try { + // 查询申请信息 + RsOverseasApplication application = applicationMapper.selectById(applicationId); + if (application == null) { + throw new ServiceException("未找到申请信息"); + } + + // 查询审批历史 + List approvalHistory = approvalService.queryByApplicationId(applicationId); + + // 调试日志:输出审批历史数据 + log.info("📋 查询到审批历史记录数量: {}", approvalHistory != null ? approvalHistory.size() : 0); + if (approvalHistory != null && !approvalHistory.isEmpty()) { + for (RsOverseasApprovalVo approval : approvalHistory) { + log.info(" - 级别{}: 审批人={}, 意见={}, 签字数据长度={}", + approval.getApprovalLevel(), + approval.getApproverName(), + approval.getApprovalOpinion(), + approval.getApprovalSignature() != null ? approval.getApprovalSignature().length() : 0); + } + } + + // 设置响应头 + String fileName = "出国申请审批表_" + application.getApplicantName() + "_" + + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".pdf"; + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", + "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + + // 生成PDF + generateApprovalPdf(application, approvalHistory, response.getOutputStream()); + + log.info("✅ 出国申请审批表PDF生成成功"); + } catch (Exception e) { + log.error("❌ 生成出国申请审批表PDF失败:{}", e.getMessage(), e); + throw new ServiceException("生成PDF失败:" + e.getMessage()); + } + } + + /** + * 生成审批表PDF文档 + */ + private void generateApprovalPdf(RsOverseasApplication application, + List approvalHistory, OutputStream outputStream) + throws DocumentException, IOException { + + Document document = new Document(PageSize.A4, 40, 40, 40, 40); + PdfWriter.getInstance(document, outputStream); + document.open(); + + try { + // 创建中文字体 + BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + Font titleFont = new Font(baseFont, 16, Font.BOLD); + Font headerFont = new Font(baseFont, 14, Font.BOLD); + Font normalFont = new Font(baseFont, 10, Font.NORMAL); + Font smallFont = new Font(baseFont, 9, Font.NORMAL); + + // 1. 标题 + Paragraph title1 = new Paragraph("杭州第一技师学院", titleFont); + title1.setAlignment(Element.ALIGN_CENTER); + title1.setSpacingAfter(5); + document.add(title1); + + Paragraph title2 = new Paragraph("登记报备人员因私事出国(境)审批表", headerFont); + title2.setAlignment(Element.ALIGN_CENTER); + title2.setSpacingAfter(15); + document.add(title2); + + // 2. 个人信息表格(优化列宽,使垂直线对齐) + PdfPTable infoTable = new PdfPTable(12); // 12列 + infoTable.setWidthPercentage(100); + // 列宽:使关键分界线对齐 + infoTable.setWidths(new float[] { 6f, 9f, 5f, 7f, 8f, 9f, 8f, 9f, 6f, 9f, 8f, 10f }); + + // 第一行:姓名、性别、出生年月、政治面貌 + addCell(infoTable, "姓名", normalFont, true, 1); // 列1: 6f + addCell(infoTable, application.getApplicantName(), normalFont, false, 2); // 列2-3: 9f+5f=14f + addCell(infoTable, "性别", normalFont, true, 1); // 列4: 7f + addCell(infoTable, translateGender(application.getGender()), normalFont, false, 1); // 列5: 8f + addCell(infoTable, "出生年月", normalFont, true, 1); // 列6: 9f + addCell(infoTable, formatBirthDate(application.getBirthDate()), normalFont, false, 2); // 列7-8: 8f+9f=17f + addCell(infoTable, "政治面貌", normalFont, true, 1); // 列9: 6f + addCell(infoTable, translatePoliticalStatus(application.getPoliticalStatus()), normalFont, false, 3); // 列10-12: + // 9f+8f+10f=27f + + // 第二行:现工作部门、职称、职务 + addCell(infoTable, "现工作部门\n(教学系)", normalFont, true, 3); // 列1-3: 6f+9f+5f=20f + addCell(infoTable, application.getDepartment(), normalFont, false, 2); // 列4-5: 7f+8f=15f + addCell(infoTable, "职称", normalFont, true, 1); // 列6: 9f + addCell(infoTable, application.getProfessionalTitle(), normalFont, false, 2); // 列7-8: 8f+9f=17f + addCell(infoTable, "职务", normalFont, true, 1); // 列9: 6f + addCell(infoTable, application.getPosition(), normalFont, false, 3); // 列10-12: 9f+8f+10f=27f + + // 第三行:申领证件名称、证件号码 + addCell(infoTable, "申领证件名称", normalFont, true, 1); // 列1: 6f + addCell(infoTable, application.getDocumentName(), normalFont, false, 2); // 列2-3: 9f+5f=14f + addCell(infoTable, "证件号码", normalFont, true, 2); // 列4-5: 7f+8f=15f + addCell(infoTable, application.getDocumentNumber(), normalFont, false, 7); // 列6-12: + // 9f+8f+9f+6f+9f+8f+10f=59f + + // 第四行:前往国家或地区(按照附件2标准格式) + addCell(infoTable, "前往国家\n或地区", normalFont, true, 1); // 列1: 6f + + PdfPCell destinationCell = new PdfPCell(); + destinationCell.setColspan(11); // 列2-12: 占据剩余11列 + destinationCell.setMinimumHeight(60); + destinationCell.setVerticalAlignment(Element.ALIGN_TOP); + destinationCell.setPadding(0); + + // 创建嵌套表格:5列布局(国家名称 | 出境事由 | 旅游/探亲 | 探望对象关系标签 | 探望对象关系内容) + // 列宽精确对齐外层表格的垂直线位置 + PdfPTable innerTable = new PdfPTable(5); + innerTable.setWidthPercentage(100); + // 对齐到外层列3|列5|列8|列10|列12的位置: 14f, 15f, 26f, 15f, 18f (总计88f) + innerTable.setWidths(new float[] { 14f, 15f, 26f, 15f, 18f }); + + // 第一列:国家名称(合并3行) + String destination = StringUtils.isNotBlank(application.getDestinationCountry()) + ? application.getDestinationCountry() + : ""; + PdfPCell countryCell = new PdfPCell(new Phrase(destination, smallFont)); + countryCell.setRowspan(3); + countryCell.setMinimumHeight(60); + countryCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + countryCell.setHorizontalAlignment(Element.ALIGN_CENTER); + countryCell.setPadding(3); + innerTable.addCell(countryCell); + + // 第一行:出境事由(合并2行)| 旅游选项(占据右侧3列) + // 出境事由标签(合并2行) + PdfPCell reasonLabelCell = new PdfPCell(new Phrase("出境事由", smallFont)); + reasonLabelCell.setRowspan(2); + reasonLabelCell.setMinimumHeight(40); + reasonLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + reasonLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + innerTable.addCell(reasonLabelCell); + + // 旅游选项框(占据3列) + String travelMark = "旅游".equals(application.getExitReason()) ? "√" : ""; + PdfPCell travelCell = new PdfPCell(new Phrase("旅游( " + travelMark + " )", smallFont)); + travelCell.setColspan(3); + travelCell.setMinimumHeight(20); + travelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + travelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + innerTable.addCell(travelCell); + + // 第二行:探亲选项 | 探望对象关系标签 | 探望对象关系内容 + // 探亲选项框 + String visitMark = "探亲".equals(application.getExitReason()) ? "√" : ""; + PdfPCell visitCell = new PdfPCell(new Phrase("探亲( " + visitMark + " )", smallFont)); + visitCell.setMinimumHeight(20); + visitCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + visitCell.setHorizontalAlignment(Element.ALIGN_CENTER); + innerTable.addCell(visitCell); + + // 探望对象关系标签(独立单元格) + PdfPCell relationLabelCell = new PdfPCell(new Phrase("探望对象姓名及\n与申请者关系", smallFont)); + relationLabelCell.setMinimumHeight(20); + relationLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + relationLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + relationLabelCell.setPadding(3); + innerTable.addCell(relationLabelCell); + + // 探望对象关系内容(独立单元格) + String relationship = StringUtils.isNotBlank(application.getVisitRelationship()) + ? application.getVisitRelationship() + : ""; + PdfPCell relationContentCell = new PdfPCell(new Phrase(relationship, smallFont)); + relationContentCell.setMinimumHeight(20); + relationContentCell.setVerticalAlignment(Element.ALIGN_TOP); + relationContentCell.setPadding(3); + innerTable.addCell(relationContentCell); + + // 第三行:备注 | 备注内容(国家名称已合并,从第2列开始,备注内容占据3列) + PdfPCell remarkLabelCell = new PdfPCell(new Phrase("备注", smallFont)); + remarkLabelCell.setMinimumHeight(20); + remarkLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + remarkLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + remarkLabelCell.setPadding(3); + innerTable.addCell(remarkLabelCell); + + String remarks = StringUtils.isNotBlank(application.getRemarks()) + ? application.getRemarks() + : ""; + PdfPCell remarkValueCell = new PdfPCell(new Phrase(remarks, smallFont)); + remarkValueCell.setColspan(3); // 占据第3、4、5列 + remarkValueCell.setMinimumHeight(20); + remarkValueCell.setVerticalAlignment(Element.ALIGN_TOP); + remarkValueCell.setPadding(3); + innerTable.addCell(remarkValueCell); + + destinationCell.addElement(innerTable); + infoTable.addCell(destinationCell); + + // 第五行:出国时间 + addCell(infoTable, "出过(境)时间", normalFont, true, 2); // 列1-2 + PdfPCell timeCell = new PdfPCell(); + timeCell.setColspan(10); // 列3-12: 占据剩余10列 + timeCell.setMinimumHeight(30); + timeCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + + String timeText = String.format("拟于 %s 至 %s 出国(境),为期 %d 天。", + formatDateChinese(application.getStartDate()), + formatDateChinese(application.getEndDate()), + application.getDuration() != null ? application.getDuration() : 0); + Paragraph timePara = new Paragraph(timeText, smallFont); + timeCell.addElement(timePara); + infoTable.addCell(timeCell); + + // 第六行:承诺声明(修复断层问题,使用单一段落) + PdfPCell promiseCell = new PdfPCell(); + promiseCell.setColspan(12); // 占据全部12列 + promiseCell.setMinimumHeight(70); + promiseCell.setVerticalAlignment(Element.ALIGN_TOP); + promiseCell.setPadding(5); + + // 使用单一段落避免断层,通过Chunk控制对齐 + Paragraph promisePara = new Paragraph(); + promisePara.setSpacingAfter(0); + promisePara.setSpacingBefore(0); + + // 承诺内容 - 居中部分 + Chunk promiseText1 = new Chunk("本人承诺,在出国(境)期间遵守当地法律法规和中国的法律法规,结束后如期回国并到学校", smallFont); + Chunk promiseText2 = new Chunk("\n报到,如逾期不归,同意作自动离职处理。\n", smallFont); + promisePara.add(promiseText1); + promisePara.add(promiseText2); + promisePara.setAlignment(Element.ALIGN_CENTER); + promiseCell.addElement(promisePara); + + // 联系信息和签字 - 左对齐部分 + Paragraph contactPara = new Paragraph(); + contactPara.setSpacingAfter(0); + contactPara.setSpacingBefore(2); + contactPara.setAlignment(Element.ALIGN_LEFT); + contactPara.setIndentationLeft(10); + contactPara.add(new Chunk("手写签字:", smallFont)); + + // 如果有签字,插入签字图片 + if (StringUtils.isNotBlank(application.getSignature())) { + try { + Image signatureImage = getImageFromBase64(application.getSignature()); + if (signatureImage != null) { + signatureImage.scaleToFit(60, 20); + contactPara.add(new Chunk(signatureImage, 0, -5)); + } + } catch (Exception e) { + log.warn("插入申请人签字图片失败: {}", e.getMessage()); + } + } + + contactPara.add(new Chunk(" 联系电话:", smallFont)); + if (StringUtils.isNotBlank(application.getMobilePhone())) { + contactPara.add(new Chunk(application.getMobilePhone(), smallFont)); + } else { + contactPara.add(new Chunk("13333333333", smallFont)); + } + contactPara.add(new Chunk(" 应急联系人及联系电话:", smallFont)); + if (StringUtils.isNotBlank(application.getEmergencyContact())) { + contactPara.add(new Chunk(application.getEmergencyContact(), smallFont)); + } else { + contactPara.add(new Chunk("13333333333", smallFont)); + } + promiseCell.addElement(contactPara); + + // 日期 - 靠右对齐部分(使用申请日期) + Paragraph datePara = new Paragraph(); + datePara.setSpacingAfter(0); + datePara.setSpacingBefore(2); + datePara.setAlignment(Element.ALIGN_RIGHT); + datePara.setIndentationRight(20); + // 使用申请时间作为申请人签名日期(优先使用applyTime,其次使用createTime) + Date applicationDateTime = application.getApplyTime() != null + ? application.getApplyTime() + : application.getCreateTime(); + String applicationDate = applicationDateTime != null + ? formatDateChinese(applicationDateTime) + : "年 月 日"; + datePara.add(new Chunk(applicationDate, smallFont)); + promiseCell.addElement(datePara); + + infoTable.addCell(promiseCell); + + document.add(infoTable); + + // 3. 审批意见表格(紧贴个人信息表格,无间隔) + PdfPTable approvalTable = new PdfPTable(2); + approvalTable.setWidthPercentage(100); + approvalTable.setWidths(new float[] { 20f, 80f }); + approvalTable.setSpacingBefore(0); // 设置为0,与上方表格紧密连接 + + // 部门(教学系)意见 + addApprovalRowWithSignature(approvalTable, "部门\n(教学\n系)\n意见", + approvalHistory, "1", normalFont, smallFont); + + // 人事(组织)处审核意见 + addApprovalRowWithSignature(approvalTable, "人事\n(组\n织)处\n审核\n意见", + approvalHistory, "2", normalFont, smallFont); + + // 纪委意见 + addApprovalRowWithSignature(approvalTable, "纪委\n意见", + approvalHistory, "3", normalFont, smallFont); + + // 学院分管领导意见(独立行) + addApprovalRowWithLeaderSignature(approvalTable, "学院分\n管领导\n意见", + approvalHistory, "4", normalFont, smallFont); + + // 学院党委书记意见(独立行) + addApprovalRowWithSignature(approvalTable, "学院党\n委书记\n意见", + approvalHistory, "5", normalFont, smallFont); + + document.add(approvalTable); + + // 4. 备注说明(移除空段落,直接连接) + Paragraph note = new Paragraph(); + note.setSpacingBefore(5); // 只留很小的间距 + note.add(new Chunk("备注:1. 因私出境时间,除特殊原因外,原则上应为假期;\n", smallFont)); + note.add(new Chunk(" 2. 新申办证件名,本表证件号码一栏不填。", smallFont)); + document.add(note); + + } finally { + document.close(); + } + } + + /** + * 添加表格单元格 + */ + private void addCell(PdfPTable table, String text, Font font, boolean isHeader, int colspan) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setColspan(colspan); + cell.setMinimumHeight(25); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setHorizontalAlignment(Element.ALIGN_CENTER); + // 移除灰色背景,所有单元格统一白色背景 + // if (isHeader) { + // cell.setBackgroundColor(BaseColor.LIGHT_GRAY); + // } + table.addCell(cell); + } + + /** + * 添加审批意见行(带签字显示) + */ + private void addApprovalRowWithSignature(PdfPTable table, String label, + List approvalHistory, String level, Font labelFont, Font contentFont) { + PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont)); + labelCell.setMinimumHeight(50); + labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + labelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + table.addCell(labelCell); + + PdfPCell opinionCell = new PdfPCell(); + opinionCell.setMinimumHeight(50); + opinionCell.setVerticalAlignment(Element.ALIGN_TOP); + opinionCell.setPadding(5); + + Paragraph para = new Paragraph(); + String opinion = getApprovalOpinion(approvalHistory, level); + para.add(new Chunk(opinion, contentFont)); + para.add(new Chunk("\n\n负责人签字:", contentFont)); + + // 添加签字图片 + String signature = getApprovalSignature(approvalHistory, level); + if (StringUtils.isNotBlank(signature)) { + try { + Image signImage = getImageFromBase64(signature); + if (signImage != null) { + signImage.scaleToFit(60, 20); + para.add(new Chunk(signImage, 0, -5)); + } + } catch (Exception e) { + log.warn("插入审批人签字图片失败: {}", e.getMessage()); + } + } + + // 日期靠右(使用审批日期) + opinionCell.addElement(para); + Paragraph datePara = new Paragraph(); + datePara.setAlignment(Element.ALIGN_RIGHT); + datePara.setIndentationRight(10); + // 获取审批日期 + Date approvalDate = getApprovalDate(approvalHistory, level); + String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日"; + datePara.add(new Chunk(dateText, contentFont)); + opinionCell.addElement(datePara); + + table.addCell(opinionCell); + } + + /** + * 添加学院分管领导审批行(使用"领导签字"并包含"学院"文字) + */ + private void addApprovalRowWithLeaderSignature(PdfPTable table, String label, + List approvalHistory, String level, Font labelFont, Font contentFont) { + PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont)); + labelCell.setMinimumHeight(50); + labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + labelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + table.addCell(labelCell); + + PdfPCell opinionCell = new PdfPCell(); + opinionCell.setMinimumHeight(50); + opinionCell.setVerticalAlignment(Element.ALIGN_TOP); + opinionCell.setPadding(5); + + Paragraph para = new Paragraph(); + String opinion = getApprovalOpinion(approvalHistory, level); + para.add(new Chunk(opinion, contentFont)); + para.add(new Chunk("\n\n领导签字:", contentFont)); + + // 添加签字图片 + String signature = getApprovalSignature(approvalHistory, level); + if (StringUtils.isNotBlank(signature)) { + try { + Image signImage = getImageFromBase64(signature); + if (signImage != null) { + signImage.scaleToFit(60, 20); + para.add(new Chunk(signImage, 0, -5)); + } + } catch (Exception e) { + log.warn("插入分管领导签字图片失败: {}", e.getMessage()); + } + } + + // 日期靠右(使用审批日期) + opinionCell.addElement(para); + Paragraph datePara = new Paragraph(); + datePara.setAlignment(Element.ALIGN_RIGHT); + datePara.setIndentationRight(10); + // 获取审批日期 + Date approvalDate = getApprovalDate(approvalHistory, level); + String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日"; + datePara.add(new Chunk(dateText, contentFont)); + opinionCell.addElement(datePara); + + table.addCell(opinionCell); + } + + /** + * 获取指定级别的审批意见 + */ + private String getApprovalOpinion(List approvalHistory, String level) { + if (approvalHistory == null || approvalHistory.isEmpty()) { + return ""; + } + + return approvalHistory.stream() + .filter(a -> level.equals(a.getApprovalLevel())) + .findFirst() + .map(a -> { + String opinion = StringUtils.isNotBlank(a.getApprovalOpinion()) + ? a.getApprovalOpinion() + : ("1".equals(a.getApprovalStatus()) ? "同意" : ""); + return opinion; + }) + .orElse(""); + } + + /** + * 格式化日期 + */ + private String formatDate(Date date) { + if (date == null) { + return ""; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(date); + } + + /** + * 格式化日期为中文格式(yyyy 年 MM 月 dd 日) + */ + private String formatDateChinese(Date date) { + if (date == null) { + return ""; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日"); + return sdf.format(date); + } + + /** + * 格式化出生日期(只显示年月) + */ + private String formatBirthDate(Date date) { + if (date == null) { + return ""; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月"); + return sdf.format(date); + } + + /** + * 获取指定级别的审批签字 + */ + private String getApprovalSignature(List approvalHistory, String level) { + if (approvalHistory == null || approvalHistory.isEmpty()) { + return null; + } + + return approvalHistory.stream() + .filter(a -> level.equals(a.getApprovalLevel())) + .findFirst() + .map(RsOverseasApprovalVo::getApprovalSignature) + .orElse(null); + } + + /** + * 获取指定级别的审批日期 + */ + private Date getApprovalDate(List approvalHistory, String level) { + if (approvalHistory == null || approvalHistory.isEmpty()) { + return null; + } + + return approvalHistory.stream() + .filter(a -> level.equals(a.getApprovalLevel())) + .findFirst() + .map(RsOverseasApprovalVo::getApprovalTime) + .orElse(null); + } + + /** + * 从Base64字符串创建Image对象 + */ + private Image getImageFromBase64(String base64String) { + if (StringUtils.isBlank(base64String)) { + return null; + } + + try { + // 处理data URL格式:data:image/png;base64,xxxxx + String base64Data = base64String; + if (base64String.contains(",")) { + base64Data = base64String.substring(base64String.indexOf(",") + 1); + } + + // Base64解码 + byte[] imageBytes = java.util.Base64.getDecoder().decode(base64Data); + + // 创建Image对象 + Image image = Image.getInstance(imageBytes); + return image; + } catch (Exception e) { + log.error("从Base64创建图片失败: {}", e.getMessage()); + return null; + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalServiceImpl.java index 78a04600..11cbc923 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalServiceImpl.java @@ -24,7 +24,6 @@ import org.dromara.rs.domain.query.RsOverseasApprovalQuery; import org.dromara.rs.service.IRsOverseasApprovalService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.dromara.common.mybatis.core.page.PageQuery; /** * 出国审批记录Service业务层处理 @@ -45,24 +44,40 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService */ @Override public TableDataInfo queryPageList(RsOverseasApprovalQuery query) { + log.info("==================== 查询审批列表 ===================="); + log.info("原始查询参数: {}", query); + + // 🔧 应用数据权限过滤 + applyDataPermissionFilter(query); + + log.info("权限过滤后查询参数: {}", query); + log.info(" - applicationId: {}", query.getApplicationId()); + log.info(" - approverId: {}", query.getApproverId()); + log.info(" - applicantId: {}", query.getApplicantId()); + log.info(" - deptId: {}", query.getDeptId()); + // 使用自定义SQL查询,包含申请信息的联合查询 List list = baseMapper.selectOverseasApprovalList(query); - + + log.info("查询结果数量: {}", list.size()); + // 手动分页处理 int pageNum = query.getPageNum(); int pageSize = query.getPageSize(); int total = list.size(); int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, total); - - List pageList = total > 0 && start < total ? - list.subList(start, end) : new ArrayList<>(); - + + List pageList = total > 0 && start < total ? list.subList(start, end) : new ArrayList<>(); + // 构建分页对象 Page page = new Page<>(pageNum, pageSize); page.setRecords(pageList); page.setTotal(total); - + + log.info("分页结果: 总数={}, 当前页={}, 每页={}, 返回={}", total, pageNum, pageSize, pageList.size()); + log.info("==================== 查询完成 ===================="); + return TableDataInfo.build(page); } @@ -126,18 +141,18 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService private void validEntityBeforeSave(RsOverseasApproval entity) { // 校验审批级别 if (entity.getApprovalLevel() != null) { - if (!"1".equals(entity.getApprovalLevel()) && - !"2".equals(entity.getApprovalLevel()) && - !"3".equals(entity.getApprovalLevel())) { + if (!"1".equals(entity.getApprovalLevel()) && + !"2".equals(entity.getApprovalLevel()) && + !"3".equals(entity.getApprovalLevel())) { throw new RuntimeException("审批级别只能是1(部门主管)、2(分管领导)、3(校领导)"); } } - + // 校验审批状态 if (entity.getApprovalStatus() != null) { - if (!"0".equals(entity.getApprovalStatus()) && - !"1".equals(entity.getApprovalStatus()) && - !"2".equals(entity.getApprovalStatus())) { + if (!"0".equals(entity.getApprovalStatus()) && + !"1".equals(entity.getApprovalStatus()) && + !"2".equals(entity.getApprovalStatus())) { throw new RuntimeException("审批状态只能是0(待审核)、1(通过)、2(拒绝)"); } } @@ -148,35 +163,45 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService */ private LambdaQueryWrapper buildQueryWrapper(RsOverseasApprovalQuery query) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - + // ===== 业务逻辑层权限过滤 ===== // 根据用户角色进行数据权限过滤 if (!LoginHelper.isSuperAdmin()) { // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); - + Long currentDeptId = LoginHelper.getDeptId(); + // 检查用户角色并应用相应的数据过滤 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到与自己相关的审批记录 - lqw.exists("SELECT 1 FROM rs_overseas_application app WHERE app.application_id = rs_overseas_approval.application_id AND app.applicant_id = " + currentUserId); + lqw.exists( + "SELECT 1 FROM rs_overseas_application app WHERE app.application_id = rs_overseas_approval.application_id AND app.applicant_id = " + + currentUserId); } else if (StpUtil.hasRole("rs-admin")) { // 人事管理员可以看到所有审批记录 // 不设置额外过滤条件,让管理员能看到所有审批 + } else if (isDeptApprover(currentUserId)) { + // 部门负责人只能看到本部门的审批记录 + lqw.exists( + "SELECT 1 FROM rs_overseas_application app INNER JOIN sys_user u ON app.applicant_id = u.user_id WHERE app.application_id = rs_overseas_approval.application_id AND u.dept_id = " + + currentDeptId); } else { // 默认情况:只能看到自己创建或处理的审批 lqw.and(wrapper -> wrapper.eq(RsOverseasApproval::getCreateBy, currentUserId) - .or().eq(RsOverseasApproval::getApproverId, currentUserId)); + .or().eq(RsOverseasApproval::getApproverId, currentUserId)); } } - + // 添加租户过滤 lqw.eq(RsOverseasApproval::getTenantId, LoginHelper.getTenantId()); - + lqw.eq(query.getApplicationId() != null, RsOverseasApproval::getApplicationId, query.getApplicationId()); lqw.eq(query.getApproverId() != null, RsOverseasApproval::getApproverId, query.getApproverId()); - lqw.like(StringUtils.isNotBlank(query.getApproverName()), RsOverseasApproval::getApproverName, query.getApproverName()); - lqw.eq(StringUtils.isNotBlank(query.getApprovalLevel()), RsOverseasApproval::getApprovalLevel, query.getApprovalLevel()); - + lqw.like(StringUtils.isNotBlank(query.getApproverName()), RsOverseasApproval::getApproverName, + query.getApproverName()); + lqw.eq(StringUtils.isNotBlank(query.getApprovalLevel()), RsOverseasApproval::getApprovalLevel, + query.getApprovalLevel()); + // 如果指定了审批状态,使用指定的状态;对于管理员,显示所有状态 if (StringUtils.isNotBlank(query.getApprovalStatus())) { lqw.eq(RsOverseasApproval::getApprovalStatus, query.getApprovalStatus()); @@ -185,12 +210,102 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService lqw.in(RsOverseasApproval::getApprovalStatus, "1", "2"); } // 管理员和超级管理员可以看到所有状态的审批记录 - + lqw.ge(query.getApprovalTimeBegin() != null, RsOverseasApproval::getApprovalTime, query.getApprovalTimeBegin()); lqw.le(query.getApprovalTimeEnd() != null, RsOverseasApproval::getApprovalTime, query.getApprovalTimeEnd()); return lqw; } + /** + * 应用数据权限过滤 + */ + private void applyDataPermissionFilter(RsOverseasApprovalQuery query) { + // 安全获取用户ID,兼容 "sys_user:2091" 格式 + Long currentUserId = null; + try { + Object loginId = StpUtil.getLoginId(); + if (loginId instanceof Long) { + currentUserId = (Long) loginId; + } else if (loginId instanceof String) { + String loginIdStr = (String) loginId; + // 处理 "sys_user:2091" 格式 + if (loginIdStr.contains(":")) { + String[] parts = loginIdStr.split(":"); + if (parts.length > 1) { + currentUserId = Long.parseLong(parts[1]); + } + } else { + currentUserId = Long.parseLong(loginIdStr); + } + } else if (loginId instanceof Number) { + currentUserId = ((Number) loginId).longValue(); + } + } catch (Exception e) { + log.error("❌ 获取用户ID失败: {}", e.getMessage()); + throw new RuntimeException("无法获取当前用户ID,请重新登录"); + } + + String currentUsername = LoginHelper.getUsername(); + Long currentDeptId = LoginHelper.getDeptId(); + boolean isSuperAdmin = LoginHelper.isSuperAdmin(); + + log.info("==================== 开始应用数据权限过滤 ===================="); + log.info("当前用户: {}, ID: {}, 部门ID: {}, 超级管理员: {}", + currentUsername, currentUserId, currentDeptId, isSuperAdmin); + + // 超级管理员不需要过滤 + if (isSuperAdmin) { + log.info("→ 权限结果:超级管理员,跳过所有过滤"); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 申请人只能看到自己的申请 + boolean hasApplicantRole = StpUtil.hasRole("rs-Applicant"); + log.info("✓ 角色检查 [rs-Applicant]: {}", hasApplicantRole); + if (hasApplicantRole) { + query.setApplicantId(currentUserId); + log.info("→ 权限结果:申请人角色,设置申请人过滤 applicantId={}", currentUserId); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 人事管理员可以看到所有申请 + boolean hasAdminRole = StpUtil.hasRole("rs-admin"); + log.info("✓ 角色检查 [rs-admin]: {}", hasAdminRole); + if (hasAdminRole) { + log.info("→ 权限结果:管理员角色,不设置过滤(可查看所有数据)"); + log.info("==================== 权限过滤结束 ===================="); + return; // 全局权限 + } + + // 🔧 检查部门负责人权限(但不能与其他审批权限冲突) + boolean isDeptApprover = isDeptApprover(currentUserId); + log.info("✓ 部门负责人检查结果: {}", isDeptApprover); + + // 检查是否有其他审批权限(人事处、纪委等) + boolean hasOtherApprovePermission = StpUtil.hasPermission("rs:overseas:approve2") || // 人事处 + StpUtil.hasPermission("rs:overseas:approve3") || // 纪委 + StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导 + StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记 + log.info("✓ 其他审批权限检查: {}", hasOtherApprovePermission); + + if (isDeptApprover && !hasOtherApprovePermission) { + // 纯部门负责人:只能看到本部门且分配给自己的申请 + query.setDeptId(currentDeptId); + query.setApproverId(currentUserId); + log.info("→ 权限结果:部门负责人,设置部门过滤 deptId={} 和审批人过滤 approverId={}", + currentDeptId, currentUserId); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 其他审批人(包括有其他审批权限的用户)只能看到分配给自己的审批 + query.setApproverId(currentUserId); + log.info("→ 权限结果:普通审批人,设置审批人过滤 approverId={}", currentUserId); + log.info("==================== 权限过滤结束 ===================="); + } + /** * 从BO构建Entity */ @@ -269,12 +384,12 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService */ private List createTestApprovalData() { List approvals = new ArrayList<>(); - + // 创建5条标准测试审批记录,对应申请ID 1-5 - Long[] applicationIds = {1L, 2L, 3L, 4L, 5L}; - String[] approverNames = {"管理员", "张主任", "李领导", "王校长", "管理员"}; - String[] approvalLevels = {"1", "1", "2", "3", "1"}; - String[] approvalStatuses = {"1", "1", "1", "1", "1"}; // 全部通过 + Long[] applicationIds = { 1L, 2L, 3L, 4L, 5L }; + String[] approverNames = { "管理员", "张主任", "李领导", "王校长", "管理员" }; + String[] approvalLevels = { "1", "1", "2", "3", "1" }; + String[] approvalStatuses = { "1", "1", "1", "1", "1" }; // 全部通过 String[] opinions = { "申请材料齐全,出国目的明确,同意申请", "培训项目对教学有益,同意参加", @@ -282,7 +397,7 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService "研究交流项目重要,全力支持", "培训有助于专业发展,批准参加" }; - + for (int i = 0; i < applicationIds.length; i++) { RsOverseasApproval approval = new RsOverseasApproval(); approval.setApplicationId(applicationIds[i]); @@ -291,15 +406,17 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService approval.setApprovalLevel(approvalLevels[i]); approval.setApprovalStatus(approvalStatuses[i]); approval.setApprovalOpinion(opinions[i]); - approval.setApprovalTime(Date.from(LocalDateTime.now().minusDays(i + 1).atZone(ZoneId.systemDefault()).toInstant())); + approval.setApprovalTime( + Date.from(LocalDateTime.now().minusDays(i + 1).atZone(ZoneId.systemDefault()).toInstant())); approval.setTenantId(LoginHelper.getTenantId()); approval.setDeleted("0"); approval.setCreateBy(1L); - approval.setCreateTime(Date.from(LocalDateTime.now().minusDays(i + 1).atZone(ZoneId.systemDefault()).toInstant())); - + approval.setCreateTime( + Date.from(LocalDateTime.now().minusDays(i + 1).atZone(ZoneId.systemDefault()).toInstant())); + approvals.add(approval); } - + return approvals; } @@ -312,36 +429,39 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService lqw.eq(RsOverseasApproval::getApplicationId, applicationId); lqw.orderByAsc(RsOverseasApproval::getApprovalLevel); lqw.orderByDesc(RsOverseasApproval::getApprovalTime); // 按审批时间降序,最新的在前面 - + return baseMapper.selectVoList(lqw); } - + /** * 联合查询分页列表:待审批申请 + 审批记录 */ @Override public TableDataInfo queryCombinedPageList(RsOverseasApprovalQuery query) { + // 添加部门权限过滤 + applyDeptPermissionFilter(query); + List list = baseMapper.selectCombinedApprovalList(query); - + // 手动分页处理 int total = list.size(); int pageNum = query.getPageNum() != null ? query.getPageNum() : 1; int pageSize = query.getPageSize() != null ? query.getPageSize() : 10; int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, total); - + if (start >= total) { return TableDataInfo.build(new ArrayList<>()); } - + List pageList = list.subList(start, end); - + // 创建一个简单的分页结果 TableDataInfo result = TableDataInfo.build(pageList); result.setTotal((long) total); return result; } - + /** * 联合查询列表:待审批申请 + 审批记录 */ @@ -349,4 +469,100 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService public List queryCombinedList(RsOverseasApprovalQuery query) { return baseMapper.selectCombinedApprovalList(query); } -} \ No newline at end of file + + /** + * 判断当前用户是否为部门审批人 + * 检查用户是否具有部门负责人审批权限 + */ + private boolean isDeptApprover(Long userId) { + String currentUsername = LoginHelper.getUsername(); + + log.info("========== 开始检查部门审批人权限 =========="); + log.info("用户名: {}, 用户ID: {}", currentUsername, userId); + + // 检查用户是否具有部门负责人审批权限 + // 方法1:检查是否有第1级审批权限 + boolean hasApprove1 = StpUtil.hasPermission("rs:overseas:approve1"); + log.info("✓ 权限检查 [rs:overseas:approve1]: {}", hasApprove1); + if (hasApprove1) { + log.info("========== 检查结果:是部门审批人(通过rs:overseas:approve1权限) =========="); + return true; + } + + // 方法2:检查是否有部门负责人角色(如果有定义的话) + boolean hasDeptHeadRole = StpUtil.hasRole("dept_head_role"); + log.info("✓ 角色检查 [dept_head_role]: {}", hasDeptHeadRole); + if (hasDeptHeadRole) { + log.info("========== 检查结果:是部门审批人(通过dept_head_role角色) =========="); + return true; + } + + // 方法3:检查是否有旧版权限标识符(兼容性) + boolean hasOldPermission = StpUtil.hasPermission("overseas:application:dept_head:approve"); + log.info("✓ 权限检查 [overseas:application:dept_head:approve]: {}", hasOldPermission); + if (hasOldPermission) { + log.info("========== 检查结果:是部门审批人(通过旧版权限) =========="); + return true; + } + + log.info("========== 检查结果:不是部门审批人 =========="); + return false; + } + + /** + * 应用部门权限过滤 + * 根据当前用户的角色和部门,设置查询条件 + */ + private void applyDeptPermissionFilter(RsOverseasApprovalQuery query) { + Long currentUserId = LoginHelper.getUserId(); + String currentUsername = LoginHelper.getUsername(); + Long currentDeptId = LoginHelper.getDeptId(); + boolean isSuperAdmin = LoginHelper.isSuperAdmin(); + + log.info("权限过滤开始 - 用户: {}, ID: {}, 部门ID: {}, 是否超级管理员: {}", + currentUsername, currentUserId, currentDeptId, isSuperAdmin); + + // 超级管理员不需要过滤 + if (isSuperAdmin) { + log.info("用户 {} 是超级管理员,跳过权限过滤", currentUsername); + return; + } + + // 申请人只能看到自己的申请 + if (StpUtil.hasRole("rs-Applicant")) { + log.info("用户 {} 具有rs-Applicant角色,设置申请人过滤: {}", currentUsername, currentUserId); + query.setApplicantId(currentUserId); + return; + } + + // 人事管理员可以看到所有申请 + if (StpUtil.hasRole("rs-admin")) { + log.info("用户 {} 具有rs-admin角色,不设置过滤条件", currentUsername); + return; + } + + // 🔧 检查部门负责人权限(但不能与其他审批权限冲突) + boolean isDeptApprover = isDeptApprover(currentUserId); + log.info("用户 {} 部门负责人检查结果: {}", currentUsername, isDeptApprover); + + // 检查是否有其他审批权限(人事处、纪委等) + boolean hasOtherApprovePermission = StpUtil.hasPermission("rs:overseas:approve2") || // 人事处 + StpUtil.hasPermission("rs:overseas:approve3") || // 纪委 + StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导 + StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记 + log.info("用户 {} 其他审批权限检查: {}", currentUsername, hasOtherApprovePermission); + + if (isDeptApprover && !hasOtherApprovePermission) { + // 纯部门负责人:只能看到本部门且分配给自己的申请 + log.info("用户 {} 是纯部门负责人,设置部门过滤: {} 和审批人过滤: {}", + currentUsername, currentDeptId, currentUserId); + query.setDeptId(currentDeptId); // 只显示本部门员工的申请 + query.setApproverId(currentUserId); // 只显示当前审批人是自己的申请(待审批) + return; + } + + // 其他审批人(包括有其他审批权限的用户)只能看到分配给自己的审批 + log.info("用户 {} 是普通审批人,设置审批人过滤: {}", currentUsername, currentUserId); + query.setApproverId(currentUserId); + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasItineraryChangeServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasItineraryChangeServiceImpl.java new file mode 100644 index 00000000..6bef51cc --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasItineraryChangeServiceImpl.java @@ -0,0 +1,265 @@ +package org.dromara.rs.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.rs.domain.RsOverseasItineraryChange; +import org.dromara.rs.domain.bo.RsOverseasItineraryChangeBo; +import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery; +import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo; +import org.dromara.rs.mapper.RsOverseasItineraryChangeMapper; +import org.dromara.rs.service.IRsOverseasItineraryChangeService; +import org.dromara.rs.service.IRsOverseasApplicationService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * 出国行程变更申请Service业务层处理 + * + * @author weizzzz + * @date 2025-10-13 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItineraryChangeService { + + private final RsOverseasItineraryChangeMapper changeMapper; + private final IRsOverseasApplicationService applicationService; + + /** + * 查询出国行程变更申请分页列表 + */ + @Override + public TableDataInfo queryPageList(RsOverseasItineraryChangeQuery query) { + List list = changeMapper.selectChangeList(query); + return TableDataInfo.build(list); + } + + /** + * 查询出国行程变更申请列表 + */ + @Override + public List queryList(RsOverseasItineraryChangeQuery query) { + return changeMapper.selectChangeList(query); + } + + /** + * 查询出国行程变更申请详情 + */ + @Override + public RsOverseasItineraryChangeVo queryById(Long changeId) { + return changeMapper.selectChangeById(changeId); + } + + /** + * 查询我的变更记录列表(当前登录用户的已通过变更申请) + * 说明:变更记录Tab只显示已通过审批的变更申请,这些是真正生效的变更记录 + */ + @Override + public TableDataInfo queryMyChangeList(RsOverseasItineraryChangeQuery query) { + Long userId = LoginHelper.getUserId(); + query.setApplicantId(userId); + query.setStatus("1"); // 只查询已通过的变更申请 + return queryPageList(query); + } + + /** + * 查询待审批的变更申请列表 + */ + @Override + public TableDataInfo queryPendingList(RsOverseasItineraryChangeQuery query) { + query.setStatus("0"); // 待审核状态 + return queryPageList(query); + } + + /** + * 新增出国行程变更申请 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(RsOverseasItineraryChangeBo bo) { + // 查询出国申请信息 + org.dromara.rs.domain.vo.RsOverseasApplicationVo application = applicationService.queryById(bo.getApplicationId()); + if (application == null) { + throw new ServiceException("出国申请不存在"); + } + + // 检查申请状态是否为已通过 + if (!"1".equals(application.getStatus())) { + throw new ServiceException("只能为已通过审批的出国申请提交变更申请"); + } + + // 检查是否有未完成的变更申请 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(RsOverseasItineraryChange::getApplicationId, bo.getApplicationId()) + .in(RsOverseasItineraryChange::getStatus, Arrays.asList("0")) // 待审核 + .eq(RsOverseasItineraryChange::getDeleted, 0); + Long count = changeMapper.selectCount(wrapper); + if (count > 0) { + throw new ServiceException("该出国申请已有待审核的变更申请,请先处理"); + } + + // 转换BO到Entity + RsOverseasItineraryChange entity = BeanUtil.toBean(bo, RsOverseasItineraryChange.class); + + // 设置申请人信息 + Long userId = LoginHelper.getUserId(); + entity.setApplicantId(userId); + entity.setApplicantName(LoginHelper.getUsername()); + entity.setApplicationId(bo.getApplicationId()); + + // 从出国申请中复制原始信息 + entity.setOriginalDestination(application.getDestination()); + entity.setOriginalDestinationCity(application.getDestinationCity()); + entity.setOriginalExitReason(application.getExitReason() != null ? application.getExitReason() : application.getPurposeDetail()); + entity.setOriginalStartDate(application.getStartDate()); + entity.setOriginalEndDate(application.getEndDate()); + entity.setOriginalDuration(application.getDuration()); + entity.setOriginalItinerary(application.getItinerary()); + entity.setOriginalAccommodationPlan(application.getAccommodationPlan()); + entity.setOriginalTransportationPlan(application.getTransportationPlan()); + + // 设置初始状态为待审核 + entity.setStatus("0"); + entity.setTenantId(LoginHelper.getTenantId()); + + return changeMapper.insert(entity) > 0; + } + + /** + * 修改出国行程变更申请(重新提交) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(RsOverseasItineraryChangeBo bo) { + // 查询原变更记录 + RsOverseasItineraryChange original = changeMapper.selectById(bo.getChangeId()); + if (original == null) { + throw new ServiceException("变更申请不存在"); + } + + // 只有待审核或已拒绝的申请可以修改 + if (!"0".equals(original.getStatus()) && !"2".equals(original.getStatus())) { + throw new ServiceException("只有待审核或已拒绝的变更申请可以修改"); + } + + // 检查权限:只能修改自己的申请 + Long userId = LoginHelper.getUserId(); + if (!userId.equals(original.getApplicantId())) { + throw new ServiceException("只能修改自己的变更申请"); + } + + // 转换BO到Entity + RsOverseasItineraryChange entity = BeanUtil.toBean(bo, RsOverseasItineraryChange.class); + + // 重新提交时,状态改为待审核 + entity.setStatus("0"); + entity.setApprovalOpinion(null); + entity.setApprovalTime(null); + entity.setApproverId(null); + entity.setApproverName(null); + + return changeMapper.updateById(entity) > 0; + } + + /** + * 删除出国行程变更申请 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteByIds(List changeIds) { + Long userId = LoginHelper.getUserId(); + + for (Long changeId : changeIds) { + RsOverseasItineraryChange change = changeMapper.selectById(changeId); + if (change == null) { + continue; + } + + // 检查权限:只能删除自己的申请 + if (!userId.equals(change.getApplicantId())) { + throw new ServiceException("只能删除自己的变更申请"); + } + + // 只有待审核或已拒绝的申请可以删除 + if (!"0".equals(change.getStatus()) && !"2".equals(change.getStatus())) { + throw new ServiceException("只有待审核或已拒绝的变更申请可以删除"); + } + } + + return changeMapper.deleteBatchIds(changeIds) > 0; + } + + /** + * 撤回变更申请 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean cancelChange(Long changeId) { + RsOverseasItineraryChange change = changeMapper.selectById(changeId); + if (change == null) { + throw new ServiceException("变更申请不存在"); + } + + // 检查权限 + Long userId = LoginHelper.getUserId(); + if (!userId.equals(change.getApplicantId())) { + throw new ServiceException("只能撤回自己的变更申请"); + } + + // 只有待审核的申请可以撤回 + if (!"0".equals(change.getStatus())) { + throw new ServiceException("只有待审核的变更申请可以撤回"); + } + + // 更新状态为已撤回 + change.setStatus("3"); + return changeMapper.updateById(change) > 0; + } + + /** + * 审批变更申请 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean approveChange(Long changeId, Boolean approved, String opinion) { + RsOverseasItineraryChange change = changeMapper.selectById(changeId); + if (change == null) { + throw new ServiceException("变更申请不存在"); + } + + // 只有待审核的申请可以审批 + if (!"0".equals(change.getStatus())) { + throw new ServiceException("只有待审核的变更申请可以审批"); + } + + // 更新审批信息 + Long userId = LoginHelper.getUserId(); + change.setStatus(approved ? "1" : "2"); // 1=已通过,2=已拒绝 + change.setApprovalOpinion(opinion); + change.setApprovalTime(new Date()); + change.setApproverId(userId); + change.setApproverName(LoginHelper.getUsername()); + + return changeMapper.updateById(change) > 0; + } + + /** + * 根据出国记录ID查询变更记录列表 + */ + @Override + public List queryByRecordId(Long recordId) { + return changeMapper.selectChangeListByRecordId(recordId); + } +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java index aaaeab70..8383a4a9 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java @@ -87,20 +87,36 @@ public class RsOverseasRecordServiceImpl implements IRsOverseasRecordService { */ @Override public TableDataInfo queryMyRecordPageList(RsOverseasRecordQuery query) { + log.info("============= 出国记录列表查询开始 ============="); + log.info("查询参数: {}", query); + log.info("申请人ID: {}", query.getApplicantId()); + log.info("分页参数: pageNum={}, pageSize={}", query.getPageNum(), query.getPageSize()); + // 使用联合查询方法,包含已通过审批的申请数据 List list = baseMapper.selectMyOverseasRecordList(query); + log.info("查询结果数量: {}", list != null ? list.size() : 0); + if (list != null && !list.isEmpty()) { + log.info("第一条记录: {}", list.get(0)); + } else { + log.warn("⚠️ 查询结果为空!请检查数据库中是否有数据"); + } + // 手动分页处理 int pageNum = query.getPageNum(); int pageSize = query.getPageSize(); int start = (pageNum - 1) * pageSize; int end = Math.min(start + pageSize, list.size()); + log.info("分页计算: start={}, end={}, totalSize={}", start, end, list.size()); + List pageList; if (start > list.size()) { pageList = Arrays.asList(); + log.warn("⚠️ 起始位置超出数据范围,返回空列表"); } else { pageList = list.subList(start, end); + log.info("分页后数据量: {}", pageList.size()); } // 构建分页对象 @@ -108,7 +124,11 @@ public class RsOverseasRecordServiceImpl implements IRsOverseasRecordService { page.setRecords(pageList); page.setTotal(list.size()); - return TableDataInfo.build(page); + TableDataInfo result = TableDataInfo.build(page); + log.info("返回结果: total={}, rows={}", result.getTotal(), result.getRows() != null ? result.getRows().size() : 0); + log.info("============= 出国记录列表查询结束 ============="); + + return result; } /** @@ -178,7 +198,8 @@ public class RsOverseasRecordServiceImpl implements IRsOverseasRecordService { record.setActualStartDate(application.getStartDate()); record.setActualEndDate(application.getEndDate()); record.setActualDuration(application.getDuration()); - record.setActualBudget(application.getBudget()); + // 预算字段已删除,不再设置 + // record.setActualBudget(application.getBudget()); record.setInvitation(application.getInvitation()); record.setItinerary(application.getItinerary()); record.setAccommodationPlan(application.getAccommodationPlan()); diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRegistrationServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRegistrationServiceImpl.java new file mode 100644 index 00000000..0f7de511 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRegistrationServiceImpl.java @@ -0,0 +1,844 @@ +package org.dromara.rs.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Collection; + +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dromara.rs.mapper.RsOverseasRegistrationMapper; +import org.dromara.rs.mapper.RsOverseasApplicationMapper; +import org.dromara.rs.domain.RsOverseasRegistration; +import org.dromara.rs.domain.bo.RsOverseasRegistrationBo; +import org.dromara.rs.domain.vo.RsOverseasRegistrationVo; +import org.dromara.rs.domain.vo.RsOverseasApplicationVo; +import org.dromara.rs.domain.query.RsOverseasRegistrationQuery; +import org.dromara.rs.service.IRsOverseasRegistrationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 出国登记Service业务层处理 + * + * @author weizzzz + * @date 2025-09-30 + */ +@Service +public class RsOverseasRegistrationServiceImpl implements IRsOverseasRegistrationService { + + private static final Logger log = LoggerFactory.getLogger(RsOverseasRegistrationServiceImpl.class); + + @Autowired + private RsOverseasRegistrationMapper baseMapper; + + @Autowired + private RsOverseasApplicationMapper applicationMapper; + + /** + * 查询出国登记分页列表 + */ + @Override + public TableDataInfo queryPageList(RsOverseasRegistrationQuery query) { + log.info("============= 出国登记列表查询开始 ============="); + log.info("查询参数: pageNum={}, pageSize={}, name={}, status={}", + query.getPageNum(), query.getPageSize(), query.getName(), query.getStatus()); + + List list = baseMapper.selectRegistrationList(query); + + log.info("查询结果: 总记录数={}", list != null ? list.size() : 0); + if (list != null && !list.isEmpty()) { + log.info("第一条记录: registrationId={}, name={}, status={}, tenantId={}", + list.get(0).getRegistrationId(), + list.get(0).getName(), + list.get(0).getStatus(), + list.get(0).getTenantId()); + } + + // 手动分页处理 + int pageNum = query.getPageNum(); + int pageSize = query.getPageSize(); + int start = (pageNum - 1) * pageSize; + int end = Math.min(start + pageSize, list.size()); + + List pageList; + if (start > list.size()) { + pageList = Arrays.asList(); + } else { + pageList = list.subList(start, end); + } + + log.info("分页结果: start={}, end={}, pageListSize={}", start, end, pageList.size()); + + Page page = new Page<>(pageNum, pageSize); + page.setRecords(pageList); + page.setTotal(list.size()); + + TableDataInfo result = TableDataInfo.build(page); + log.info("最终返回: total={}, rows.size={}", result.getTotal(), result.getRows() != null ? result.getRows().size() : 0); + log.info("============= 出国登记列表查询结束 ============="); + + return result; + } + + /** + * 查询我的出国登记分页列表 + */ + @Override + public TableDataInfo queryMyRegistrationList(RsOverseasRegistrationQuery query) { + // 设置当前用户ID + query.setCreateBy(LoginHelper.getUserId()); + + List list = baseMapper.selectMyRegistrationList(query); + + // 手动分页处理 + int pageNum = query.getPageNum(); + int pageSize = query.getPageSize(); + int start = (pageNum - 1) * pageSize; + int end = Math.min(start + pageSize, list.size()); + + List pageList; + if (start > list.size()) { + pageList = Arrays.asList(); + } else { + pageList = list.subList(start, end); + } + + Page page = new Page<>(pageNum, pageSize); + page.setRecords(pageList); + page.setTotal(list.size()); + + return TableDataInfo.build(page); + } + + /** + * 查询出国登记列表 + */ + @Override + public List queryList(RsOverseasRegistrationQuery query) { + return baseMapper.selectRegistrationList(query); + } + + /** + * 查询出国登记详细信息 + */ + @Override + public RsOverseasRegistrationVo queryById(Long registrationId) { + return baseMapper.selectRegistrationById(registrationId); + } + + /** + * 根据申请ID查询出国登记信息 + */ + @Override + public RsOverseasRegistrationVo queryByApplicationId(Long applicationId) { + return baseMapper.selectRegistrationByApplicationId(applicationId); + } + + /** + * 根据状态查询出国登记列表 + */ + @Override + public List queryByStatus(String status) { + return baseMapper.selectRegistrationsByStatus(status); + } + + /** + * 根据姓名查询出国登记列表 + */ + @Override + public List queryByName(String name) { + return baseMapper.selectRegistrationsByName(name); + } + + /** + * 新增出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(RsOverseasRegistrationBo bo) { + // 🔍 调试日志:检查Bo中的基础字段 + log.info("========== 出国登记新增 - 调试信息 =========="); + log.info("Bo中的createBy: {}", bo.getCreateBy()); + log.info("Bo中的createTime: {}", bo.getCreateTime()); + log.info("Bo中的createDept: {}", bo.getCreateDept()); + + // ⚠️ 强制清空Bo中的基础字段,让MyBatis-Plus自动填充 + bo.setCreateBy(null); + bo.setCreateTime(null); + bo.setUpdateBy(null); + bo.setUpdateTime(null); + // createDept保留,因为自动填充逻辑会使用它 + + RsOverseasRegistration add = MapstructUtils.convert(bo, RsOverseasRegistration.class); + + log.info("转换后Entity中的createBy: {}", add.getCreateBy()); + log.info("转换后Entity中的createTime: {}", add.getCreateTime()); + log.info("转换后Entity中的createDept: {}", add.getCreateDept()); + + // 设置状态为草稿 + if (StringUtils.isEmpty(add.getStatus())) { + add.setStatus("0"); + } + + // 🔧 修复:设置登记时间为当前时间(修复列表不显示数据的问题) + if (add.getSubmitTime() == null) { + add.setSubmitTime(new Date()); + } + + // 计算停留天数 + if (add.getDepartureDate() != null && add.getReturnDate() != null) { + long days = (add.getReturnDate().getTime() - add.getDepartureDate().getTime()) / (1000 * 60 * 60 * 24); + add.setStayDays((int) days); + } + + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setRegistrationId(add.getRegistrationId()); + log.info("插入成功后Entity中的createBy: {}", add.getCreateBy()); + log.info("插入成功后Entity中的createTime: {}", add.getCreateTime()); + log.info("插入成功后Entity中的submitTime: {}", add.getSubmitTime()); + } + log.info("=========================================="); + return flag; + } + + /** + * 新增并提交出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean addAndSubmit(RsOverseasRegistrationBo bo) { + // ⚠️ 强制清空Bo中的基础字段,让MyBatis-Plus自动填充 + bo.setCreateBy(null); + bo.setCreateTime(null); + bo.setUpdateBy(null); + bo.setUpdateTime(null); + + // 1. 先新增登记记录 + RsOverseasRegistration add = MapstructUtils.convert(bo, RsOverseasRegistration.class); + + // 直接设置为待审核状态 + add.setStatus("1"); // 待审核 + add.setSubmitTime(new Date()); + + // 计算停留天数 + if (add.getDepartureDate() != null && add.getReturnDate() != null) { + long days = (add.getReturnDate().getTime() - add.getDepartureDate().getTime()) / (1000 * 60 * 60 * 24); + add.setStayDays((int) days); + } + + boolean flag = baseMapper.insert(add) > 0; + if (!flag) { + throw new RuntimeException("保存出国登记失败!"); + } + + bo.setRegistrationId(add.getRegistrationId()); + log.info("新增并提交成功,registrationId: {}, createBy: {}, createTime: {}, submitTime: {}", + add.getRegistrationId(), add.getCreateBy(), add.getCreateTime(), add.getSubmitTime()); + return true; + } + + /** + * 修改出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(RsOverseasRegistrationBo bo) { + RsOverseasRegistration update = MapstructUtils.convert(bo, RsOverseasRegistration.class); + + // 计算停留天数 + if (update.getDepartureDate() != null && update.getReturnDate() != null) { + long days = (update.getReturnDate().getTime() - update.getDepartureDate().getTime()) / (1000 * 60 * 60 * 24); + update.setStayDays((int) days); + } + + return baseMapper.updateById(update) > 0; + } + + /** + * 校验并批量删除出国登记信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 校验:不能删除待审核或已通过的登记 + for (Long id : ids) { + RsOverseasRegistrationVo vo = baseMapper.selectRegistrationById(id); + if (vo != null && ("1".equals(vo.getStatus()) || "2".equals(vo.getStatus()))) { + throw new RuntimeException("不能删除待审核或已通过的出国登记!"); + } + } + } + + // 逻辑删除 + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 提交出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean submitRegistration(Long registrationId) { + RsOverseasRegistrationVo vo = baseMapper.selectRegistrationById(registrationId); + if (vo == null) { + throw new RuntimeException("出国登记不存在!"); + } + + if (!"0".equals(vo.getStatus()) && !"3".equals(vo.getStatus())) { + throw new RuntimeException("只有草稿或已拒绝状态的登记才能提交!"); + } + + RsOverseasRegistration update = new RsOverseasRegistration(); + update.setRegistrationId(registrationId); + update.setStatus("1"); + update.setSubmitTime(new Date()); + + return baseMapper.updateById(update) > 0; + } + + /** + * 确认出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean confirmRegistration(Long registrationId) { + RsOverseasRegistrationVo vo = baseMapper.selectRegistrationById(registrationId); + if (vo == null) { + throw new RuntimeException("出国登记不存在!"); + } + + if (!"1".equals(vo.getStatus())) { + throw new RuntimeException("只有待审核状态的登记才能确认(通过)!"); + } + + RsOverseasRegistration update = new RsOverseasRegistration(); + update.setRegistrationId(registrationId); + update.setStatus("2"); + + return baseMapper.updateById(update) > 0; + } + + /** + * 撤回出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean withdrawRegistration(Long registrationId) { + RsOverseasRegistrationVo vo = baseMapper.selectRegistrationById(registrationId); + if (vo == null) { + throw new RuntimeException("出国登记不存在!"); + } + + if (!"1".equals(vo.getStatus())) { + throw new RuntimeException("只有待审核状态的登记才能撤回!"); + } + + RsOverseasRegistration update = new RsOverseasRegistration(); + update.setRegistrationId(registrationId); + update.setStatus("3"); + + return baseMapper.updateById(update) > 0; + } + + /** + * 基于出国申请创建登记记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean createFromApplication(Long applicationId) { + // 查询出国申请信息 + RsOverseasApplicationVo application = applicationMapper.selectOverseasApplicationById(applicationId); + if (application == null) { + throw new RuntimeException("出国申请不存在!"); + } + + // 检查申请状态是否为已通过 + if (!"1".equals(application.getStatus())) { + throw new RuntimeException("只有已通过的申请才能创建登记记录!"); + } + + // 检查是否已创建登记记录 + RsOverseasRegistrationVo existing = baseMapper.selectRegistrationByApplicationId(applicationId); + if (existing != null) { + throw new RuntimeException("该申请已经创建过登记记录!"); + } + + // 创建登记记录 + RsOverseasRegistration registration = new RsOverseasRegistration(); + registration.setApplicationId(applicationId); + registration.setName(application.getApplicantName()); + registration.setGender(application.getGender()); + registration.setBirthDate(application.getBirthDate()); + registration.setPoliticalStatus(application.getPoliticalStatus()); + registration.setWorkUnitPosition(application.getDepartment() + " " + application.getPosition()); + registration.setContactPhone(application.getMobilePhone()); + registration.setDepartureDate(application.getStartDate()); + registration.setReturnDate(application.getEndDate()); + registration.setStayDays(application.getDuration()); + registration.setDestinationCountry(application.getDestination()); + registration.setDestinationDetail(application.getDestinationCity()); + registration.setReason(application.getPurposeDetail()); + registration.setRegistrationUnit(application.getRegistrationUnit()); + registration.setTravelType("1"); // 默认因公出国 + registration.setStatus("0"); // 草稿状态 + + return baseMapper.insert(registration) > 0; + } + + /** + * 统计各状态的登记数量 + */ + @Override + public List> getStatusStatistics(RsOverseasRegistrationQuery query) { + return baseMapper.selectStatusStatistics(query); + } + + /** + * 批量提交出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean batchSubmitRegistrations(List registrationIds) { + for (Long registrationId : registrationIds) { + submitRegistration(registrationId); + } + return true; + } + + /** + * 批量确认出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean batchConfirmRegistrations(List registrationIds) { + for (Long registrationId : registrationIds) { + confirmRegistration(registrationId); + } + return true; + } + + /** + * 获取可用的登记选项(字典数据) + */ + @Override + public Map>> getRegistrationOptions() { + Map>> options = new HashMap<>(); + + // 性别选项 + List> genderOptions = new ArrayList<>(); + genderOptions.add(Map.of("value", "0", "label", "男")); + genderOptions.add(Map.of("value", "1", "label", "女")); + options.put("gender", genderOptions); + + // 政治面貌选项 + List> politicalOptions = new ArrayList<>(); + politicalOptions.add(Map.of("value", "中共党员", "label", "中共党员")); + politicalOptions.add(Map.of("value", "中共预备党员", "label", "中共预备党员")); + politicalOptions.add(Map.of("value", "共青团员", "label", "共青团员")); + politicalOptions.add(Map.of("value", "民革党员", "label", "民革党员")); + politicalOptions.add(Map.of("value", "民盟盟员", "label", "民盟盟员")); + politicalOptions.add(Map.of("value", "民建会员", "label", "民建会员")); + politicalOptions.add(Map.of("value", "民进会员", "label", "民进会员")); + politicalOptions.add(Map.of("value", "农工党党员", "label", "农工党党员")); + politicalOptions.add(Map.of("value", "致公党党员", "label", "致公党党员")); + politicalOptions.add(Map.of("value", "九三学社社员", "label", "九三学社社员")); + politicalOptions.add(Map.of("value", "台盟盟员", "label", "台盟盟员")); + politicalOptions.add(Map.of("value", "无党派人士", "label", "无党派人士")); + politicalOptions.add(Map.of("value", "群众", "label", "群众")); + options.put("politicalStatus", politicalOptions); + + // 行政职级选项 + List> levelOptions = new ArrayList<>(); + levelOptions.add(Map.of("value", "正厅级", "label", "正厅级")); + levelOptions.add(Map.of("value", "副厅级", "label", "副厅级")); + levelOptions.add(Map.of("value", "正处级", "label", "正处级")); + levelOptions.add(Map.of("value", "副处级", "label", "副处级")); + levelOptions.add(Map.of("value", "正科级", "label", "正科级")); + levelOptions.add(Map.of("value", "副科级", "label", "副科级")); + levelOptions.add(Map.of("value", "科员", "label", "科员")); + levelOptions.add(Map.of("value", "办事员", "label", "办事员")); + options.put("administrativeLevel", levelOptions); + + // 人员类别代码选项 + List> categoryOptions = new ArrayList<>(); + categoryOptions.add(Map.of("value", "A", "label", "A-厅级干部")); + categoryOptions.add(Map.of("value", "B", "label", "B-处级干部")); + categoryOptions.add(Map.of("value", "C", "label", "C-科级干部")); + categoryOptions.add(Map.of("value", "D", "label", "D-一般干部")); + categoryOptions.add(Map.of("value", "E", "label", "E-专业技术人员")); + categoryOptions.add(Map.of("value", "F", "label", "F-工勤人员")); + options.put("personnelCategory", categoryOptions); + + // 登记状态选项(参考出国申请状态) + List> statusOptions = new ArrayList<>(); + statusOptions.add(Map.of("value", "0", "label", "草稿")); + statusOptions.add(Map.of("value", "1", "label", "待审核")); + statusOptions.add(Map.of("value", "2", "label", "已通过")); + statusOptions.add(Map.of("value", "3", "label", "已拒绝")); + options.put("status", statusOptions); + + return options; + } + + /** + * 转换字典值为文本 + */ + private void convertDictToText(RsOverseasRegistrationVo vo) { + // 性别转换 + if ("0".equals(vo.getGender())) { + vo.setGenderText("男"); + } else if ("1".equals(vo.getGender())) { + vo.setGenderText("女"); + } + + // 政治面貌转换 + vo.setPoliticalStatusText(vo.getPoliticalStatus()); + + // 行政职级转换 + vo.setAdministrativeLevelText(vo.getAdministrativeLevel()); + + // 人员类别代码转换 + if (StringUtils.isNotEmpty(vo.getPersonnelCategory())) { + switch (vo.getPersonnelCategory()) { + case "A" -> vo.setPersonnelCategoryText("A-厅级干部"); + case "B" -> vo.setPersonnelCategoryText("B-处级干部"); + case "C" -> vo.setPersonnelCategoryText("C-科级干部"); + case "D" -> vo.setPersonnelCategoryText("D-一般干部"); + case "E" -> vo.setPersonnelCategoryText("E-专业技术人员"); + case "F" -> vo.setPersonnelCategoryText("F-工勤人员"); + default -> vo.setPersonnelCategoryText(vo.getPersonnelCategory()); + } + } + + // 状态转换 + if (StringUtils.isNotEmpty(vo.getStatus())) { + switch (vo.getStatus()) { + case "0" -> vo.setStatusText("草稿"); + case "1" -> vo.setStatusText("待审核"); + case "2" -> vo.setStatusText("已通过"); + case "3" -> vo.setStatusText("已拒绝"); + default -> vo.setStatusText(vo.getStatus()); + } + } + } + + @Override + public Map getTrendData() { + Map result = new HashMap<>(); + // TODO: 实现趋势数据统计逻辑 + result.put("monthlyTrend", new ArrayList<>()); + result.put("departmentTrend", new ArrayList<>()); + return result; + } + + @Override + public List> getStatusOptions() { + List> options = new ArrayList<>(); + options.add(Map.of("value", "0", "label", "草稿")); + options.add(Map.of("value", "1", "label", "待审核")); + options.add(Map.of("value", "2", "label", "已通过")); + options.add(Map.of("value", "3", "label", "已拒绝")); + return options; + } + + @Override + public List> getTravelTypeOptions() { + List> options = new ArrayList<>(); + options.add(Map.of("value", "business", "label", "商务出行")); + options.add(Map.of("value", "academic", "label", "学术交流")); + options.add(Map.of("value", "training", "label", "培训学习")); + options.add(Map.of("value", "other", "label", "其他")); + return options; + } + + @Override + public List> getCompletionStatusOptions() { + List> options = new ArrayList<>(); + options.add(Map.of("value", "completed", "label", "已完成")); + options.add(Map.of("value", "cancelled", "label", "已取消")); + options.add(Map.of("value", "pending", "label", "进行中")); + return options; + } + + @Override + public void generateCertificate(Long registrationId, HttpServletResponse response) throws Exception { + // TODO: 实现证明生成逻辑 + // 1. 查询登记信息 + RsOverseasRegistrationVo registration = queryById(registrationId); + if (registration == null) { + throw new RuntimeException("登记记录不存在"); + } + + // 2. 生成PDF证明文件(这里先简单处理) + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", "attachment; filename=" + + java.net.URLEncoder.encode("出国证明_" + registration.getName() + ".pdf", "UTF-8")); + + // TODO: 实际的PDF生成逻辑 + String content = "出国证明\n\n姓名:" + registration.getName() + "\n身份证号:" + registration.getIdNumber(); + response.getOutputStream().write(content.getBytes("UTF-8")); + } + + @Override + public TableDataInfo queryHistoryPageList(RsOverseasRegistrationQuery query) { + // TODO: 实现历史记录分页查询 + // 这里暂时返回空结果,实际应该查询历史表 + Page page = new Page<>(); + page.setRecords(new ArrayList<>()); + return TableDataInfo.build(page); + } + + @Override + public List queryHistoryList(RsOverseasRegistrationQuery query) { + // TODO: 实现历史记录查询 + return new ArrayList<>(); + } + + @Override + public RsOverseasRegistrationVo queryHistoryById(Long historyId) { + // TODO: 实现历史记录详情查询 + return null; + } + + @Override + public Boolean insertHistoryByBo(RsOverseasRegistrationBo bo) { + // TODO: 实现历史记录新增 + return true; + } + + @Override + public Boolean updateHistoryByBo(RsOverseasRegistrationBo bo) { + // TODO: 实现历史记录修改 + return true; + } + + @Override + public Boolean deleteHistoryWithValidByIds(List historyIds) { + // TODO: 实现历史记录删除 + return true; + } + + @Override + public Boolean completeRegistration(Long registrationId, Map data) { + // TODO: 实现登记完成逻辑 + // 1. 更新登记状态为已完成 + // 2. 将记录移动到历史表(如果有的话) + RsOverseasRegistrationBo bo = new RsOverseasRegistrationBo(); + bo.setRegistrationId(registrationId); + bo.setStatus("completed"); + return updateByBo(bo); + } + + @Override + public Map getMyStatistics() { + Map result = new HashMap<>(); + Long userId = LoginHelper.getUserId(); + + // 统计当前用户的出国登记数据 + RsOverseasRegistrationQuery query = new RsOverseasRegistrationQuery(); + query.setCreateBy(userId); + + List myRegistrations = queryList(query); + + // 总数 + result.put("total", myRegistrations.size()); + + // 按状态统计 + long draftCount = myRegistrations.stream().filter(r -> "0".equals(r.getStatus())).count(); + long submittedCount = myRegistrations.stream().filter(r -> "1".equals(r.getStatus())).count(); + long confirmedCount = myRegistrations.stream().filter(r -> "2".equals(r.getStatus())).count(); + long withdrawnCount = myRegistrations.stream().filter(r -> "3".equals(r.getStatus())).count(); + + result.put("draftCount", draftCount); + result.put("submittedCount", submittedCount); + result.put("confirmedCount", confirmedCount); + result.put("withdrawnCount", withdrawnCount); + + // 本月新增 + long thisMonthCount = myRegistrations.stream() + .filter(r -> { + Date createTime = r.getCreateTime(); + if (createTime == null) return false; + java.util.Calendar cal = java.util.Calendar.getInstance(); + int currentMonth = cal.get(java.util.Calendar.MONTH); + int currentYear = cal.get(java.util.Calendar.YEAR); + cal.setTime(createTime); + return cal.get(java.util.Calendar.MONTH) == currentMonth + && cal.get(java.util.Calendar.YEAR) == currentYear; + }) + .count(); + result.put("thisMonthCount", thisMonthCount); + + return result; + } + + @Override + public Boolean updateStatus(Long registrationId, String status) { + RsOverseasRegistration registration = baseMapper.selectById(registrationId); + if (registration == null) { + throw new RuntimeException("登记记录不存在"); + } + + registration.setStatus(status); + registration.setUpdateTime(new Date()); + registration.setUpdateBy(LoginHelper.getUserId()); + + return baseMapper.updateById(registration) > 0; + } + + @Override + public List> statisticsByDepartment(RsOverseasRegistrationQuery query) { + // TODO: 实现按部门统计 + return new ArrayList<>(); + } + + @Override + public List> statisticsByCountry(RsOverseasRegistrationQuery query) { + // TODO: 实现按国家统计 + return new ArrayList<>(); + } + + @Override + public List> statisticsByTravelType(RsOverseasRegistrationQuery query) { + // TODO: 实现按出国类型统计 + return new ArrayList<>(); + } + + @Override + public List> statisticsByPersonnelCategory(RsOverseasRegistrationQuery query) { + // TODO: 实现按人员类别统计 + return new ArrayList<>(); + } + + @Override + public Boolean checkDuplicateRegistration(RsOverseasRegistrationVo registration) { + // TODO: 实现重复检查 + return false; + } + + @Override + public List getExpiringRegistrations(Integer days) { + // TODO: 实现即将到期查询 + return new ArrayList<>(); + } + + @Override + public List getExpiredRegistrations() { + // TODO: 实现已过期查询 + return new ArrayList<>(); + } + + @Override + public List> getTrendStatistics(Date startDate, Date endDate) { + // TODO: 实现趋势统计 + return new ArrayList<>(); + } + + @Override + public Map getAnnualReport(Integer year) { + // TODO: 实现年度报告 + Map result = new HashMap<>(); + result.put("year", year); + result.put("totalCount", 0); + result.put("monthlyData", new ArrayList<>()); + return result; + } + + @Override + public void exportRegistrations(RsOverseasRegistrationQuery query, HttpServletResponse response) { + // TODO: 实现导出功能 + List list = queryList(query); + // 这里应该使用 ExcelUtil 导出,暂时简单处理 + log.info("导出出国登记数据,共{}条", list.size()); + } + + /** + * 批准出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean approveRegistration(Long registrationId, String approveNotes) { + log.info("============= 批准出国登记开始 ============="); + log.info("登记ID: {}, 审批意见: {}", registrationId, approveNotes); + + // 1. 查询登记信息 + RsOverseasRegistrationVo registration = queryById(registrationId); + if (registration == null) { + log.error("登记信息不存在: registrationId={}", registrationId); + throw new RuntimeException("登记信息不存在"); + } + + // 2. 验证当前状态是否为已提交 + if (!"1".equals(registration.getStatus())) { + log.error("登记状态不是已提交,无法批准: registrationId={}, currentStatus={}", + registrationId, registration.getStatus()); + throw new RuntimeException("只能批准已提交的登记"); + } + + // 3. 更新状态为已确认(2) + RsOverseasRegistrationBo bo = new RsOverseasRegistrationBo(); + bo.setRegistrationId(registrationId); + bo.setStatus("2"); // 已确认 + bo.setRemarks(approveNotes); // 将审批意见保存到备注字段 + + boolean result = updateByBo(bo); + + log.info("批准出国登记结果: {}", result); + log.info("============= 批准出国登记结束 ============="); + + return result; + } + + /** + * 拒绝出国登记 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean rejectRegistration(Long registrationId, String rejectReason) { + log.info("============= 拒绝出国登记开始 ============="); + log.info("登记ID: {}, 拒绝原因: {}", registrationId, rejectReason); + + // 1. 查询登记信息 + RsOverseasRegistrationVo registration = queryById(registrationId); + if (registration == null) { + log.error("登记信息不存在: registrationId={}", registrationId); + throw new RuntimeException("登记信息不存在"); + } + + // 2. 验证当前状态是否为已提交 + if (!"1".equals(registration.getStatus())) { + log.error("登记状态不是已提交,无法拒绝: registrationId={}, currentStatus={}", + registrationId, registration.getStatus()); + throw new RuntimeException("只能拒绝已提交的登记"); + } + + // 3. 更新状态为已拒绝(3) + RsOverseasRegistrationBo bo = new RsOverseasRegistrationBo(); + bo.setRegistrationId(registrationId); + bo.setStatus("3"); // 已拒绝 + bo.setRemarks(rejectReason); // 将拒绝原因保存到备注字段 + + boolean result = updateByBo(bo); + + log.info("拒绝出国登记结果: {}", result); + log.info("============= 拒绝出国登记结束 ============="); + + return result; + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java index 5b5c1d39..9ccc2bb9 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java @@ -18,44 +18,58 @@ import org.dromara.rs.domain.RsTrainingRecord; import java.util.Arrays; +import org.dromara.rs.domain.RsTrainingApproval; import org.dromara.rs.mapper.RsTrainingApplicationMapper; +import org.dromara.rs.mapper.RsTrainingApprovalMapper; import org.dromara.rs.service.IRsTrainingApplicationService; import org.dromara.rs.service.IRsTrainingRecordService; +import org.dromara.system.domain.vo.SysUserVo; +import org.dromara.system.service.ISysUserService; +import org.dromara.common.core.exception.ServiceException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; /** * 培训申请服务实现类 - * + * * @author weizzzz * @date 2025-07-16 */ @Service @RequiredArgsConstructor public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationService { - + private static final Logger log = LoggerFactory.getLogger(RsTrainingApplicationServiceImpl.class); - + private final RsTrainingApplicationMapper trainingApplicationMapper; - + @Autowired private IRsTrainingRecordService trainingRecordService; + @Autowired + private RsTrainingApprovalMapper approvalMapper; + + @Autowired + private ISysUserService userService; + /** * 查询培训申请列表 - * + * * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, + PageQuery pageQuery) { // 创建分页对象 Page page = pageQuery.build(); // 获取分页参数 @@ -68,131 +82,270 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application.getTenantId() == null || application.getTenantId().isEmpty()) { application.setTenantId(LoginHelper.getTenantId()); } - - // ===== 业务逻辑层权限过滤 ===== - // 根据用户角色进行数据权限过滤 + + // ===== 业务逻辑层权限过滤 - 7阶段培训审批权限控制 ===== if (!LoginHelper.isSuperAdmin()) { // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); String username = LoginHelper.getUsername(); - - // 检查用户角色并应用相应的数据过滤 - if (StpUtil.hasRole("rs-Applicant")) { + + // 🔴 收集用户拥有的审批角色 + List approvalRoles = new ArrayList<>(); + + // 检查7个审批阶段的角色权限 + if (StpUtil.hasRole("rspx-bmfzr")) { + approvalRoles.add("rspx-bmfzr"); // 第1阶段:部门负责人 + } + if (StpUtil.hasRole("rspx-jwc")) { + approvalRoles.add("rspx-jwc"); // 第2阶段:教务处 + } + if (StpUtil.hasRole("rspx-xsc")) { + approvalRoles.add("rspx-xsc"); // 第2阶段:学生处 + } + if (StpUtil.hasRole("rspx-jhcwc")) { + approvalRoles.add("rspx-jhcwc"); // 第3阶段:计划财务处 + } + if (StpUtil.hasRole("rspx-rsc")) { + approvalRoles.add("rspx-rsc"); // 第4阶段:人事处 + } + if (StpUtil.hasRole("rspx-ywfgld")) { + approvalRoles.add("rspx-ywfgld"); // 第5阶段:业务分管院领导 + } + if (StpUtil.hasRole("rspx-rsfgld")) { + approvalRoles.add("rspx-rsfgld"); // 第6阶段:人事分管领导 + } + if (StpUtil.hasRole("rspx-yz")) { + approvalRoles.add("rspx-yz"); // 第7阶段:院长 + } + + log.info("🔐 用户 {} 拥有的审批角色: {}", username, approvalRoles); + + // 根据角色权限设置数据过滤条件 + if (StpUtil.hasRole("rs-admin")) { + // 人事管理员可以看到所有申请(最高权限) + log.info("🔓 人事管理员角色,可查看所有申请"); + } else if (!approvalRoles.isEmpty()) { + // 🔴 核心:设置审批角色过滤条件 + application.setApprovalRoles(approvalRoles); + // 设置当前用户部门ID(用于第一阶段部门过滤) + application.setCurrentDeptId(currentDeptId); + log.info("🔒 审批权限过滤 - 部门ID: {}, 审批角色: {}", currentDeptId, approvalRoles); + } else if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到自己的申请 application.setUserId(currentUserId); - } else if (StpUtil.hasRole("rs-admin")) { - // 人事管理员可以看到所有申请(包括审批和部门管理功能) - // 不设置额外过滤条件,让管理员能看到所有申请 - } else if (StpUtil.hasRole("rs-Approver")) { - // 审批人角色:可以看到所有申请(用于审批) - // 不设置过滤条件,允许查看所有申请 + log.info("📝 申请人角色,只能查看自己的申请"); } else { // 默认情况:只能看到自己创建的申请 application.setCreateBy(currentUserId); + log.info("👤 默认权限,只能查看自己创建的申请"); } } - + // 查询总数 long total = trainingApplicationMapper.selectCount(buildQueryWrapper(application)); - + // 使用自定义分页查询获取列表数据 - List list = trainingApplicationMapper.selectRsTrainingApplicationListPage(application, offset, size); - + List list = trainingApplicationMapper.selectRsTrainingApplicationListPage(application, + offset, size); + // 处理自定义培训课程的情况 for (RsTrainingApplication app : list) { if (app.getCourseName() == null || app.getCourseName().isEmpty()) { app.setCourseName("自定义培训"); } } - + // 构建分页对象 page.setRecords(list); page.setTotal(total); - + return TableDataInfo.build(page); } /** * 查询培训申请列表 - * + * * @param application 培训申请 * @return 培训申请集合 */ @Override public List selectRsTrainingApplicationList(RsTrainingApplication application) { - // ===== 业务逻辑层权限过滤 ===== - // 根据用户角色进行数据权限过滤 + log.info("🔍 ==================== 开始培训申请列表查询 ===================="); + log.info("🔍 当前登录用户ID: {}", LoginHelper.getUserId()); + log.info("🔍 是否超级管理员: {}", LoginHelper.isSuperAdmin()); + log.info("🔍 用户角色: rs-Applicant={}, rs-admin={}", + StpUtil.hasRole("rs-Applicant"), + StpUtil.hasRole("rs-admin")); + + // ===== 审批权限过滤 ===== if (!LoginHelper.isSuperAdmin()) { - // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); - - // 检查用户角色并应用相应的数据过滤 + log.info("🔍 当前用户部门ID: {}", currentDeptId); + + // 判断用户角色 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到自己的申请 application.setUserId(currentUserId); } else if (StpUtil.hasRole("rs-admin")) { - // 人事管理员可以看到所有申请(包括审批和部门管理功能) - // 不设置额外过滤条件,让管理员能看到所有申请 + // 人事管理员可以看到所有申请 + // 不设置额外过滤条件 } else { - // 默认情况:只能看到自己创建的申请 - application.setCreateBy(currentUserId); + // 审批人权限过滤:根据审批级别权限过滤数据 + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels.isEmpty()) { + // 没有审批权限,只能看到自己创建的申请 + application.setCreateBy(currentUserId); + } else { + // 🔴 有审批权限,需要在数据库层面过滤 + // 这里需要在 Mapper XML 中添加审批权限的过滤逻辑 + // 暂时先在代码层面过滤 + } } } - - // 使用自定义的Mapper方法查询,已经关联了课程表获取课程名称 + + // 使用自定义的Mapper方法查询 List list = trainingApplicationMapper.selectRsTrainingApplicationList(application); - + + // 🔴 在代码层面进行审批权限过滤(临时方案) + log.info("🔍 准备进行审批权限过滤,查询到的数据总数: {}", list.size()); + + if (!LoginHelper.isSuperAdmin() && + !StpUtil.hasRole("rs-Applicant") && + !StpUtil.hasRole("rs-admin")) { + + log.info("🔍 触发审批权限过滤逻辑(非超管、非申请人、非人事管理员)"); + + Long currentUserId = LoginHelper.getUserId(); + Long currentDeptId = LoginHelper.getDeptId(); + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + log.info("🔍 当前用户可审批级别: {}", approvalLevels); + + if (!approvalLevels.isEmpty()) { + log.info("🔍 开始按审批级别过滤数据..."); + // 按审批权限过滤 + list = list.stream().filter(app -> { + String appLevel = app.getApprovalLevel(); + if (appLevel == null) + return false; + + // 检查是否有该级别的审批权限 + if (!approvalLevels.contains(appLevel)) { + return false; + } + + // 第1-3级需要检查部门权限 + if (appLevel.equals("1") || appLevel.equals("2a") || appLevel.equals("2b") + || appLevel.equals("3")) { + return app.getCreateDept() != null && app.getCreateDept().equals(currentDeptId); + } + + // 第4-7级可以跨部门审批 + return true; + }).collect(java.util.stream.Collectors.toList()); + + log.info("🔍 审批权限过滤完成,过滤后数据数量: {}", list.size()); + } else { + log.info("🔍 用户没有审批权限,不进行过滤"); + } + } else { + log.info("🔍 跳过审批权限过滤(超管/申请人/人事管理员)"); + } + // 处理自定义培训课程(courseId=0)的情况 for (RsTrainingApplication app : list) { - if (app.getCourseId() != null && app.getCourseId() == 0 && (app.getCourseName() == null || app.getCourseName().isEmpty())) { + if (app.getCourseId() != null && app.getCourseId() == 0 + && (app.getCourseName() == null || app.getCourseName().isEmpty())) { app.setCourseName("自定义培训"); } } - + return list; } /** * 查询指定课程的培训申请列表 - * - * @param courseId 课程ID + * + * @param courseId 课程ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageApplicationsByCourse(Long courseId, RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageApplicationsByCourse(Long courseId, + RsTrainingApplication application, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(application); lqw.eq(RsTrainingApplication::getCourseId, courseId); return TableDataInfo.build(trainingApplicationMapper.selectPage(pageQuery.build(), lqw)); } - + /** * 查询我的培训申请列表 - * - * @param userId 用户ID + * + * @param userId 用户ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, + PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(application); lqw.eq(RsTrainingApplication::getUserId, userId); return TableDataInfo.build(trainingApplicationMapper.selectPage(pageQuery.build(), lqw)); } - + /** * 构建查询条件 */ private LambdaQueryWrapper buildQueryWrapper(RsTrainingApplication application) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); - // 移除硬编码状态过滤,让管理员能看到所有状态的申请 - // 原注释:排除已审批完成的数据:1-已通过,3-进行中,4-已完成 - // 原注释:只保留:0-待审核,2-已拒绝 - // 修复:移除 lqw.notIn(RsTrainingApplication::getStatus, "1", "3", "4") 限制 + + // 🔴 审批权限过滤(用于总数统计) + if (application.getApprovalRoles() != null && !application.getApprovalRoles().isEmpty()) { + lqw.and(wrapper -> { + for (String role : application.getApprovalRoles()) { + wrapper.or(w -> { + // 根据角色构建审批权限过滤条件 + if ("rspx-bmfzr".equals(role)) { + // 第1阶段:部门负责人 - 只能看本部门的第1阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "1"); + if (application.getCurrentDeptId() != null) { + w.eq(RsTrainingApplication::getCreateDept, application.getCurrentDeptId()); + } + } else if ("rspx-jwc".equals(role)) { + // 第2阶段:教务处 - 只能看培训类型为"教学"的第2a阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "2a") + .eq(RsTrainingApplication::getTrainingType, "教学"); + } else if ("rspx-xsc".equals(role)) { + // 第2阶段:学生处 - 只能看培训类型为"德育"的第2b阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "2b") + .eq(RsTrainingApplication::getTrainingType, "德育"); + } else if ("rspx-jhcwc".equals(role)) { + // 第3阶段:计划财务处 - 可以看全校的第3阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "3"); + } else if ("rspx-rsc".equals(role)) { + // 第4阶段:人事处 - 可以看全校的第4阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "4"); + } else if ("rspx-ywfgld".equals(role)) { + // 第5阶段:业务分管院领导 - 可以看全校的第5阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "5"); + } else if ("rspx-rsfgld".equals(role)) { + // 第6阶段:人事分管领导 - 可以看全校的第6阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "6"); + } else if ("rspx-yz".equals(role)) { + // 第7阶段:院长 - 可以看全校的第7阶段申请 + w.eq(RsTrainingApplication::getApprovalLevel, "7"); + } + }); + } + }); + } + + // 原有查询条件 lqw.like(application.getUserName() != null, RsTrainingApplication::getUserName, application.getUserName()); lqw.eq(application.getCourseId() != null, RsTrainingApplication::getCourseId, application.getCourseId()); lqw.eq(application.getUserId() != null, RsTrainingApplication::getUserId, application.getUserId()); @@ -200,15 +353,17 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS lqw.like(application.getLocation() != null, RsTrainingApplication::getLocation, application.getLocation()); lqw.eq(application.getDuration() != null, RsTrainingApplication::getDuration, application.getDuration()); // 课程名称搜索:搜索自定义课程名称字段 - lqw.like(application.getCourseName() != null, RsTrainingApplication::getCourseName, application.getCourseName()); + lqw.like(application.getCourseName() != null, RsTrainingApplication::getCourseName, + application.getCourseName()); // 排除指定状态的条件(用于待审批接口) - lqw.ne(application.getExcludeStatus() != null, RsTrainingApplication::getStatus, application.getExcludeStatus()); + lqw.ne(application.getExcludeStatus() != null, RsTrainingApplication::getStatus, + application.getExcludeStatus()); return lqw; } /** * 查询培训申请详细信息 - * + * * @param applicationId 培训申请ID * @return 培训申请 */ @@ -216,15 +371,15 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS public RsTrainingApplication selectRsTrainingApplicationById(Long applicationId) { // 使用自定义的Mapper方法查询,已经关联了课程表获取课程名称 RsTrainingApplication application = trainingApplicationMapper.selectRsTrainingApplicationById(applicationId); - + // ===== 业务逻辑层权限过滤 ===== // 检查当前用户是否有权限查看此申请 if (application != null && !LoginHelper.isSuperAdmin()) { Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); - + boolean hasPermission = false; - + // 检查用户角色权限 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能查看自己的申请 @@ -232,31 +387,35 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS } else if (StpUtil.hasRole("rs-admin")) { // 人事管理员可以查看所有申请 hasPermission = true; + } else if (hasAnyApprovalPermission()) { + // 审批人可以查看待审批的申请 + hasPermission = true; + log.info("✅ 审批人 {} 有权查看申请 {}", currentUserId, applicationId); } else { // 默认情况:只能查看自己创建的申请 - hasPermission = application.getCreateBy() != null && - application.getCreateBy().equals(currentUserId); + hasPermission = application.getCreateBy() != null && + application.getCreateBy().equals(currentUserId); } - + // 如果没有权限,返回null if (!hasPermission) { log.warn("用户 {} 尝试访问无权限的培训申请: {}", currentUserId, applicationId); return null; } } - + // 处理自定义培训课程(courseId=0)的情况 - if (application != null && application.getCourseId() != null && application.getCourseId() == 0 - && (application.getCourseName() == null || application.getCourseName().isEmpty())) { + if (application != null && application.getCourseId() != null && application.getCourseId() == 0 + && (application.getCourseName() == null || application.getCourseName().isEmpty())) { application.setCourseName("自定义培训"); } - + return application; } /** * 新增培训申请 - * + * * @param application 培训申请 * @return 结果 */ @@ -267,7 +426,7 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application.getApplicationId() == null) { application.setApplicationId(System.currentTimeMillis()); } - + // 处理自定义培训课程的情况(courseId = 0 或 null) // 确保courseName不为空 if (application.getCourseName() == null || application.getCourseName().isEmpty()) { @@ -277,9 +436,10 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS application.setCreateTime(now); application.setApplyTime(now); application.setStatus("0"); // 待审核状态 + application.setApprovalLevel("1"); // 🔴 初始化为第1级审批(部门负责人) application.setTenantId(LoginHelper.getTenantId()); // 设置当前用户租户ID return trainingApplicationMapper.insert(application); - + // 以上代码已处理所有情况 } catch (Exception e) { e.printStackTrace(); @@ -289,7 +449,7 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS /** * 修改培训申请 - * + * * @param application 培训申请 * @return 结果 */ @@ -301,26 +461,27 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS /** * 批量删除培训申请 - * + * * @param applicationIds 需要删除的培训申请ID数组 * @return 结果 */ @Override public int deleteRsTrainingApplicationByIds(Long[] applicationIds) { // 查询所有要删除的申请 - List applications = trainingApplicationMapper.selectBatchIds(Arrays.asList(applicationIds)); - + List applications = trainingApplicationMapper + .selectBatchIds(Arrays.asList(applicationIds)); + // 对每个申请,减少对应课程的当前报名人数 for (RsTrainingApplication app : applications) { - // 培训课程功能已移除,无需更新课程报名人数 + // 培训课程功能已移除,无需更新课程报名人数 } - + return trainingApplicationMapper.deleteBatchIds(Arrays.asList(applicationIds)); } /** * 删除培训申请信息 - * + * * @param applicationId 培训申请ID * @return 结果 */ @@ -331,13 +492,13 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application != null) { // 培训课程功能已移除,无需更新课程报名人数 } - + return trainingApplicationMapper.deleteById(applicationId); } - + /** * 取消培训申请 - * + * * @param applicationId 培训申请ID * @return 结果 */ @@ -348,40 +509,40 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application != null) { // 只有待审核状态的申请才能取消 if ("0".equals(application.getStatus())) { - // 更新状态为已取消 - application.setStatus("3"); - application.setUpdateTime(DateUtils.getNowDate()); - - // 培训课程功能已移除,无需更新课程报名人数 - + // 更新状态为已取消 + application.setStatus("3"); + application.setUpdateTime(DateUtils.getNowDate()); + + // 培训课程功能已移除,无需更新课程报名人数 + return trainingApplicationMapper.updateById(application); } } return 0; } - + /** * 更新申请状态 - * + * * @param applicationId 培训申请ID - * @param status 状态 + * @param status 状态 * @return 结果 */ @Override public int updateApplicationStatus(Long applicationId, String status) { RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId); - if (application != null) { - application.setStatus(status); - Date now = DateUtils.getNowDate(); - application.setUpdateTime(now); - - // 如果是拒绝,记录被拒绝时间 - if ("2".equals(status)) { - application.setRejectTime(now); - } - + if (application != null) { + application.setStatus(status); + Date now = DateUtils.getNowDate(); + application.setUpdateTime(now); + + // 如果是拒绝,记录被拒绝时间 + if ("2".equals(status)) { + application.setRejectTime(now); + } + int result = trainingApplicationMapper.updateById(application); - + // 如果申请通过,自动创建培训记录 if ("1".equals(status)) { try { @@ -391,16 +552,16 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS // 不影响申请状态更新的结果 } } - + return result; } return 0; } - + /** * 重新提交已被拒绝的申请 * - * @param applicationId 培训申请ID + * @param applicationId 培训申请ID * @param applicationData 培训申请更新数据 * @return 结果 */ @@ -411,14 +572,12 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (existingApp == null) { return 0; } - + // 只有状态为已拒绝的申请才能重新提交 if (!"2".equals(existingApp.getStatus())) { return 0; } - - - + // 更新申请数据 if (applicationData.getReason() != null) { existingApp.setReason(applicationData.getReason()); @@ -438,33 +597,33 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (applicationData.getCourseName() != null) { existingApp.setCourseName(applicationData.getCourseName()); } - - // 重置为待审核状态 - existingApp.setStatus("0"); - Date now = DateUtils.getNowDate(); - existingApp.setUpdateTime(now); - existingApp.setApplyTime(now); // 更新申请时间 - - - + + // 重置为待审核状态 + existingApp.setStatus("0"); + existingApp.setApprovalLevel("1"); // 🔴 重新提交时重置为第1级审批 + Date now = DateUtils.getNowDate(); + existingApp.setUpdateTime(now); + existingApp.setApplyTime(now); // 更新申请时间 + return trainingApplicationMapper.updateById(existingApp); } - + /** * 根据培训申请创建培训记录 - * + * * @param application 培训申请 */ private void createTrainingRecordFromApplication(RsTrainingApplication application) { try { // 检查是否已存在培训记录 - RsTrainingRecord existingRecord = trainingRecordService.selectByApplicationId(application.getApplicationId()); + RsTrainingRecord existingRecord = trainingRecordService + .selectByApplicationId(application.getApplicationId()); if (existingRecord != null) { - log.info("培训记录已存在,不重复创建: applicationId={}, recordId={}", - application.getApplicationId(), existingRecord.getRecordId()); + log.info("培训记录已存在,不重复创建: applicationId={}, recordId={}", + application.getApplicationId(), existingRecord.getRecordId()); return; } - + // 创建新的培训记录 RsTrainingRecord record = new RsTrainingRecord(); record.setApplicationId(application.getApplicationId()); @@ -483,30 +642,396 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS record.setStartTime(application.getStartTime()); record.setEndTime(application.getEndTime()); record.setCompletionStatus("0"); // 0=进行中 - + // 设置基础字段 - 修复字段缺失问题 Date now = DateUtils.getNowDate(); record.setCreateTime(now); record.setUpdateTime(now); - + // 设置租户和用户信息 - record.setTenantId(application.getTenantId() != null ? application.getTenantId() : LoginHelper.getTenantId()); + record.setTenantId( + application.getTenantId() != null ? application.getTenantId() : LoginHelper.getTenantId()); record.setCreateBy(application.getUserId()); // 创建者设为申请人 record.setUpdateBy(application.getUserId()); // 更新者设为申请人 record.setCreateDept(application.getCreateDept() != null ? application.getCreateDept() : 1L); // 使用createDept,如果没有则设为默认值 record.setDeptId(application.getCreateDept() != null ? application.getCreateDept() : 1L); // 设置部门ID - + int result = trainingRecordService.insertRsTrainingRecord(record); if (result > 0) { - log.info("培训记录创建成功: applicationId={}, recordId={}", - application.getApplicationId(), record.getRecordId()); + log.info("培训记录创建成功: applicationId={}, recordId={}", + application.getApplicationId(), record.getRecordId()); } else { log.error("培训记录创建失败: applicationId={}", application.getApplicationId()); } } catch (Exception e) { - log.error("创建培训记录时发生异常: applicationId={}, error={}", - application.getApplicationId(), e.getMessage(), e); + log.error("创建培训记录时发生异常: applicationId={}, error={}", + application.getApplicationId(), e.getMessage(), e); throw e; } } -} \ No newline at end of file + + // ======================================== + // 7级审批核心逻辑(支持培训类型分流) + // ======================================== + + /** + * 审批培训申请(7级审批制度) + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批状态(1-通过,2-拒绝) + * @param approvalOpinion 审批意见 + * @param approvalSignature 审批签字(base64格式) + * @return 结果 + */ + @Override + @Transactional + public Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature) { + RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId); + if (application == null) { + throw new RuntimeException("申请不存在"); + } + if (!"0".equals(application.getStatus())) { + throw new RuntimeException("只有待审核状态的申请才能审批"); + } + + // 🔴 验证审批人权限级别 + String currentApprovalLevel = application.getApprovalLevel() != null ? application.getApprovalLevel() : "1"; + if (!validateApproverLevel(approverId, currentApprovalLevel)) { + throw new ServiceException(String.format( + "您没有第%s级审批权限,当前申请需要第%s级审批人审批", + currentApprovalLevel, currentApprovalLevel)); + } + log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", approverName, currentApprovalLevel); + + // 创建审批记录 + RsTrainingApproval approval = new RsTrainingApproval(); + approval.setApplicationId(applicationId); + approval.setApproverId(approverId); + approval.setApproverName(approverName); + approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.replaceAll("[^0-9]", ""))); // 转换为数字 + approval.setStatus(approvalStatus); + approval.setOpinion(approvalOpinion); + approval.setApprovalTime(new Date()); + + // 日志记录签字数据 + if (approvalSignature != null && !approvalSignature.trim().isEmpty()) { + log.info("✅ 保存审批签字数据,长度: {} 字符", approvalSignature.length()); + } else { + log.info("ℹ️ 未提供审批签字"); + } + + // 手动设置多租户和删除标志字段 + if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { + approval.setTenantId(LoginHelper.getTenantId()); + } + + approvalMapper.insert(approval); + + // 更新申请状态(7级审批流转逻辑) + if ("1".equals(approvalStatus)) { + // 审批通过 + String nextLevel = getNextApprovalLevel(currentApprovalLevel, application.getTrainingType()); + if (nextLevel != null) { + // 进入下一级审批 + application.setApprovalLevel(nextLevel); + application.setCurrentApproverId(null); // 不再设置固定审批人ID + application.setCurrentApprover(getApprovalLevelDescription(nextLevel)); + + log.info("✅ 审批流转到第{}级,申请ID: {}, 审批人根据权限动态查询", + nextLevel, applicationId); + } else { + // 所有级别审批完成 + try { + // 创建培训记录 + log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}", + applicationId, application.getStatus(), application.getUserName()); + + createTrainingRecordFromApplication(application); + log.info("审批通过,成功创建培训记录,申请ID: {}", applicationId); + + // 更新申请状态为通过 + application.setStatus("1"); + application.setApprovalTime(new Date()); + } catch (Exception e) { + log.error("审批通过后创建培训记录失败,申请ID: {}, 错误: {}", applicationId, e.getMessage(), e); + throw new RuntimeException("审批处理失败:创建培训记录时发生错误 - " + e.getMessage()); + } + } + } else if ("2".equals(approvalStatus)) { + // 审批拒绝 + application.setStatus("2"); + application.setApprovalTime(new Date()); + } + + // 更新申请状态到数据库 + boolean updateResult = trainingApplicationMapper.updateById(application) > 0; + + return updateResult; + } + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * @return 待审批申请列表 + */ + @Override + public List queryPendingByCurrentUser() { + Long currentUserId = LoginHelper.getUserId(); + String currentUserName = LoginHelper.getUsername(); + + log.info("🔍 查询用户待审批申请 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName); + + // 1. 获取当前用户的审批级别列表(基于权限动态查询) + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels == null || approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限,返回空列表", currentUserName); + return new ArrayList<>(); + } + + log.info("✅ 用户 {} 拥有审批级别: {}", currentUserName, approvalLevels); + + // 2. 对每个级别单独查询并合并 + List result = new ArrayList<>(); + + for (String level : approvalLevels) { + Long deptIdForFilter = null; + + // 🔴 关键:只对 level=1 进行部门过滤 + if ("1".equals(level)) { + SysUserVo currentUser = userService.selectUserById(currentUserId); + if (currentUser != null && currentUser.getDeptId() != null) { + deptIdForFilter = currentUser.getDeptId(); + log.info("🔒 [部门权限过滤] 级别1,用户: {}, 部门ID: {}, 只查看本部门待审批", + currentUserName, deptIdForFilter); + } else { + log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName); + } + } else { + log.info("✅ [全校权限] 级别{}, 用户 {}, 查看全校待审批", level, currentUserName); + } + + // 3. 构建查询条件 + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(RsTrainingApplication::getStatus, "0"); // 待审核状态 + lqw.eq(RsTrainingApplication::getApprovalLevel, level); + if (deptIdForFilter != null) { + lqw.eq(RsTrainingApplication::getCreateDept, deptIdForFilter); + } + lqw.eq(RsTrainingApplication::getTenantId, LoginHelper.getTenantId()); + + // 4. 查询该级别的待审批申请 + List levelApplications = trainingApplicationMapper.selectList(lqw); + + log.info("📋 级别{} 查询到 {} 条待审批申请", level, levelApplications.size()); + + result.addAll(levelApplications); + } + + log.info("📋 用户 {} 总共查询到 {} 条待审批申请", currentUserName, result.size()); + + return result; + } + + /** + * 获取下一级审批级别(根据培训类型动态路由) + * + * 7级审批制度: + * 1-部门负责人 → 2a-教务处/2b-学生处/3-计划财务处(根据培训类型) + * 2a-教务处 → 3-计划财务处 + * 2b-学生处 → 3-计划财务处 + * 3-计划财务处 → 4-人事处 + * 4-人事处 → 5-业务分管领导 + * 5-业务分管领导 → 6-人事分管院领导 + * 6-人事分管院领导 → 7-院长 + * 7-院长 → 审批完成 + * + * @param currentLevel 当前审批级别 + * @param trainingType 培训类型 + * @return 下一级审批级别 + */ + private String getNextApprovalLevel(String currentLevel, String trainingType) { + switch (currentLevel) { + case "1": + // 部门负责人审批后,根据培训类型分流 + if ("教学".equals(trainingType)) { + return "2a"; // 教务处 + } else if ("德育".equals(trainingType)) { + return "2b"; // 学生处 + } else { + // 行政、管理、其他 → 直接到计划财务处 + return "3"; + } + case "2a": // 教务处 → 计划财务处 + case "2b": // 学生处 → 计划财务处 + return "3"; + case "3": + return "4"; // 计划财务处 → 人事处 + case "4": + return "5"; // 人事处 → 业务分管领导 + case "5": + return "6"; // 业务分管领导 → 人事分管院领导 + case "6": + return "7"; // 人事分管院领导 → 院长 + case "7": + return null; // 院长审批完成 + default: + return null; + } + } + + /** + * 根据审批级别获取审批环节描述 + * + * @param approvalLevel 审批级别 + * @return 审批环节描述 + */ + private String getApprovalLevelDescription(String approvalLevel) { + switch (approvalLevel) { + case "1": + return "待部门负责人审批"; + case "2a": + return "待教务处审批"; + case "2b": + return "待学生处审批"; + case "3": + return "待计划财务处审批"; + case "4": + return "待人事处审批"; + case "5": + return "待业务分管领导审批"; + case "6": + return "待人事分管院领导审批"; + case "7": + return "待院长审批"; + default: + return "待审批"; + } + } + + /** + * 根据当前登录用户的审批权限,获取其可审批的级别列表 + * + * 权限到级别的映射: + * - rs:training:approve1 → 审批级别 "1" (部门负责人) + * - rs:training:approve2a → 审批级别 "2a" (教务处) + * - rs:training:approve2b → 审批级别 "2b" (学生处) + * - rs:training:approve3 → 审批级别 "3" (计划财务处) + * - rs:training:approve4 → 审批级别 "4" (人事处) + * - rs:training:approve5 → 审批级别 "5" (业务分管领导) + * - rs:training:approve6 → 审批级别 "6" (人事分管院领导) + * - rs:training:approve7 → 审批级别 "7" (院长) + * + * @param userId 用户ID + * @return 可审批的级别列表 + */ + private List getApprovalLevelsByUser(Long userId) { + try { + List approvalLevels = new ArrayList<>(); + + // 检查7个审批级别的权限(1, 2a, 2b, 3, 4, 5, 6, 7) + String[] levels = { "1", "2a", "2b", "3", "4", "5", "6", "7" }; + for (String level : levels) { + String permission = "rs:training:approve" + level; + if (StpUtil.hasPermission(permission)) { + approvalLevels.add(level); + log.info("✅ 用户 {} 拥有第{}级审批权限: {}", userId, level, permission); + } + } + + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId); + approvalLevels.clear(); + for (String level : levels) { + approvalLevels.add(level); + } + } + + if (approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限", userId); + } else { + log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels); + } + + return approvalLevels; + + } catch (Exception e) { + log.error("获取用户审批级别失败,用户ID: {}, 错误: {}", userId, e.getMessage(), e); + return new ArrayList<>(); + } + } + + /** + * 验证审批人权限级别 + * + * @param approverId 审批人ID + * @param approvalLevel 审批级别 + * @return 是否有权限 + */ + private boolean validateApproverLevel(Long approverId, String approvalLevel) { + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("用户 {} 拥有通用审批权限 rs:training:approve", approverId); + return true; + } + + // 检查分级审批权限 + String requiredPermission = "rs:training:approve" + approvalLevel; + boolean hasPermission = StpUtil.hasPermission(requiredPermission); + + if (hasPermission) { + log.info("用户 {} 拥有第{}级审批权限 {}", + approverId, approvalLevel, requiredPermission); + } else { + log.warn("用户 {} 缺少第{}级审批权限 {},权限验证失败", + approverId, approvalLevel, requiredPermission); + } + + return hasPermission; + } + + /** + * 检查用户是否有任何审批级别的权限 + * + * @return true-有审批权限,false-无审批权限 + */ + private boolean hasAnyApprovalPermission() { + try { + // 检查是否有通用审批权限 + if (StpUtil.hasPermission("rs:training:approve")) { + log.debug("用户拥有通用审批权限:rs:training:approve"); + return true; + } + + // 检查7个审批级别的权限(1-7级,2a-教务处,2b-学生处) + String[] approvalPermissions = { + "rs:training:approve1", // 第1级:部门负责人 + "rs:training:approve2a", // 第2a级:教务处 + "rs:training:approve2b", // 第2b级:学生处 + "rs:training:approve3", // 第3级:计划财务处 + "rs:training:approve4", // 第4级:人事处 + "rs:training:approve5", // 第5级:业务分管领导 + "rs:training:approve6", // 第6级:人事分管院领导 + "rs:training:approve7" // 第7级:院长 + }; + + for (String permission : approvalPermissions) { + if (StpUtil.hasPermission(permission)) { + log.debug("用户拥有审批权限:{}", permission); + return true; + } + } + + log.debug("用户没有任何培训审批权限"); + return false; + + } catch (Exception e) { + log.error("检查审批权限失败", e); + return false; + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java index 8cae68fa..52095718 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.rs.service.impl; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -18,6 +19,8 @@ import org.dromara.rs.domain.RsTrainingApproval; import org.dromara.rs.domain.RsTrainingApplication; import org.dromara.rs.service.IRsTrainingApprovalService; import org.dromara.rs.service.IRsTrainingApplicationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 培训审批服务实现类 @@ -27,26 +30,30 @@ import org.dromara.rs.service.IRsTrainingApplicationService; */ @Service public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService { - + + private static final Logger log = LoggerFactory.getLogger(RsTrainingApprovalServiceImpl.class); + @Autowired private RsTrainingApprovalMapper trainingApprovalMapper; - + @Autowired private RsTrainingApplicationMapper trainingApplicationMapper; - + @Autowired private IRsTrainingApplicationService trainingApplicationService; /** * 查询培训审批列表 * - * @param approval 培训审批 + * @param approval 培训审批 * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApprovalList(RsTrainingApproval approval, PageQuery pageQuery) { - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper.selectPage(pageQuery.build(), buildQueryWrapper(approval)); + public TableDataInfo selectPageRsTrainingApprovalList(RsTrainingApproval approval, + PageQuery pageQuery) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper + .selectPage(pageQuery.build(), buildQueryWrapper(approval)); return TableDataInfo.build(page); } @@ -60,37 +67,39 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService public List selectRsTrainingApprovalList(RsTrainingApproval approval) { return trainingApprovalMapper.selectList(buildQueryWrapper(approval)); } - + /** * 构建查询条件 */ private LambdaQueryWrapper buildQueryWrapper(RsTrainingApproval approval) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); - + // ===== 业务逻辑层权限过滤 ===== // 根据用户角色进行数据权限过滤 if (!LoginHelper.isSuperAdmin()) { // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); - + // 检查用户角色并应用相应的数据过滤 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到与自己相关的审批记录 // 这里需要通过申请表关联查询 - lqw.exists("SELECT 1 FROM rs_training_application app WHERE app.application_id = rs_training_approval.application_id AND app.user_id = " + currentUserId); + lqw.exists( + "SELECT 1 FROM rs_training_application app WHERE app.application_id = rs_training_approval.application_id AND app.user_id = " + + currentUserId); } else if (StpUtil.hasRole("rs-admin") || StpUtil.hasRole("rs-Approver")) { // 人事管理员和审批人可以看到所有审批记录 // 不设置额外过滤条件,让管理员和审批人能看到所有审批 } else { // 默认情况:只能看到自己创建或处理的审批 lqw.and(wrapper -> wrapper.eq(RsTrainingApproval::getCreateBy, currentUserId) - .or().eq(RsTrainingApproval::getApproverId, currentUserId)); + .or().eq(RsTrainingApproval::getApproverId, currentUserId)); } } - + // 添加租户过滤 lqw.eq(RsTrainingApproval::getTenantId, LoginHelper.getTenantId()); - + lqw.eq(approval.getApplicationId() != null, RsTrainingApproval::getApplicationId, approval.getApplicationId()); lqw.eq(approval.getApproverId() != null, RsTrainingApproval::getApproverId, approval.getApproverId()); lqw.eq(approval.getApprovalLevel() != null, RsTrainingApproval::getApprovalLevel, approval.getApprovalLevel()); @@ -119,14 +128,15 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService * 查询待我审批的列表 * * @param approverId 审批人ID - * @param approval 培训审批 - * @param pageQuery 分页参数 + * @param approval 培训审批 + * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPagePendingApprovals(Long approverId, RsTrainingApproval approval, PageQuery pageQuery) { + public TableDataInfo selectPagePendingApprovals(Long approverId, RsTrainingApproval approval, + PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(approval); - + // 简化权限模型:审批人和管理员可以看到所有待审核记录 if (StpUtil.hasRole("rs-admin") || StpUtil.hasRole("rs-Approver")) { // 管理员和审批人可以看到所有待审核记录,不限制approverId @@ -134,9 +144,10 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService // 其他角色只能看到指派给自己的待审核记录 lqw.eq(RsTrainingApproval::getApproverId, approverId); } - + lqw.eq(RsTrainingApproval::getStatus, "0"); // 待审核状态 - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper.selectPage(pageQuery.build(), lqw); + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper + .selectPage(pageQuery.build(), lqw); return TableDataInfo.build(page); } @@ -178,12 +189,27 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService /** * 查询带详细信息的审批记录列表(关联申请人和课程信息) * - * @param approval 培训审批 + * @param approval 培训审批 * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApprovalWithDetailsList(RsTrainingApproval approval, PageQuery pageQuery) { + public TableDataInfo selectPageRsTrainingApprovalWithDetailsList(RsTrainingApproval approval, + PageQuery pageQuery) { + log.info("==================== 查询培训审批列表 ===================="); + log.info("原始查询参数: applicationId={}, approverId={}", approval.getApplicationId(), approval.getApproverId()); + + // 🔧 应用数据权限过滤 + try { + applyDataPermissionFilter(approval); + log.info("权限过滤后查询参数: applicationId={}, approverId={}, applicantId={}, deptId={}", + approval.getApplicationId(), approval.getApproverId(), approval.getApplicantId(), + approval.getDeptId()); + } catch (Exception e) { + log.error("❌ 权限过滤异常:", e); + throw e; + } + // 创建分页对象 com.baomidou.mybatisplus.extension.plugins.pagination.Page page = pageQuery.build(); // 获取分页参数 @@ -196,13 +222,15 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { approval.setTenantId(LoginHelper.getTenantId()); } - - // 查询总数 - long total = trainingApprovalMapper.selectCount(buildQueryWrapper(approval)); - + + // 查询总数 - 使用与列表查询相同的条件 + long total = trainingApprovalMapper.selectRsTrainingApprovalWithDetailsCount(approval); + // 使用自定义分页查询获取列表数据 List list = trainingApprovalMapper.selectRsTrainingApprovalWithDetailsList(approval); - + + log.info("查询结果数量: total={}, list.size={}", total, list.size()); + // 手动分页处理 int start = (int) offset; int end = Math.min(start + (int) size, list.size()); @@ -211,11 +239,14 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService } else { list = list.subList(start, end); } - + // 构建分页对象 page.setRecords(list); page.setTotal(total); - + + log.info("分页结果: 总数={}, 当前页={}, 每页={}, 返回={}", total, current, size, list.size()); + log.info("==================== 查询完成 ===================="); + return TableDataInfo.build(page); } @@ -240,47 +271,425 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService public int deleteRsTrainingApprovalById(Long approvalId) { return trainingApprovalMapper.deleteById(approvalId); } - + /** - * 处理审批 + * 处理审批(7级审批流转逻辑) * * @param applicationId 申请ID - * @param status 审批状态 - * @param opinion 审批意见 + * @param status 审批状态(1-通过,2-拒绝) + * @param opinion 审批意见 * @return 结果 */ @Override @Transactional(rollbackFor = Exception.class) public int processApproval(Long applicationId, String status, String opinion) { + log.info("🔄 开始处理审批 - applicationId: {}, status: {}", applicationId, status); + // 获取申请信息 RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId); if (application == null) { + log.error("❌ 申请不存在 - applicationId: {}", applicationId); return 0; } - + + // 验证申请状态 + if (!"0".equals(application.getStatus())) { + log.error("❌ 只有待审核状态的申请才能审批 - applicationId: {}, status: {}", + applicationId, application.getStatus()); + throw new RuntimeException("只有待审核状态的申请才能审批"); + } + // 获取当前用户信息 Long userId = LoginHelper.getUserId(); String userName = LoginHelper.getUsername(); - + + // 🔴 获取当前审批级别 + String currentApprovalLevel = application.getApprovalLevel() != null ? application.getApprovalLevel() : "1"; + log.info("📋 当前审批级别: {}, 培训类型: {}", currentApprovalLevel, application.getTrainingType()); + + // 🔴 验证审批人权限级别 + if (!validateApproverLevel(userId, currentApprovalLevel)) { + log.error("❌ 权限验证失败 - 用户: {}, 需要第{}级审批权限", userName, currentApprovalLevel); + throw new RuntimeException(String.format( + "您没有第%s级审批权限,当前申请需要第%s级审批人审批", + currentApprovalLevel, currentApprovalLevel)); + } + log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", userName, currentApprovalLevel); + // 创建审批记录 - MyBatis-Plus自动生成雪花ID RsTrainingApproval approval = new RsTrainingApproval(); - // 不需要手动设置ID,MyBatis-Plus会自动生成雪花ID approval.setApplicationId(applicationId); approval.setApproverId(userId); approval.setApproverName(userName); - approval.setApprovalLevel(1); // 假设默认是第一级审批 + + // 🔴 设置正确的审批级别(从申请记录中获取) + try { + approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.replaceAll("[^0-9]", ""))); + } catch (NumberFormatException e) { + // 如果级别中包含字母(如2a、2b),取第一个数字 + approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.substring(0, 1))); + } + approval.setStatus(status); // 审批状态:1-通过,2-拒绝 approval.setOpinion(opinion); // 审批意见 approval.setApprovalTime(DateUtils.getNowDate()); approval.setCreateTime(DateUtils.getNowDate()); - + + // 手动设置多租户字段 + if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { + approval.setTenantId(LoginHelper.getTenantId()); + } + // 插入审批记录 - 无论通过还是拒绝,都创建独立审批记录 int result = trainingApprovalMapper.insert(approval); - System.out.println("创建审批记录 - applicationId: " + applicationId + ", status: " + status + ", result: " + result); - - // 更新申请状态 - trainingApplicationService.updateApplicationStatus(applicationId, status); - + log.info("✅ 创建审批记录成功 - approvalId: {}, applicationId: {}, status: {}", + approval.getApprovalId(), applicationId, status); + + // 🔴 核心改进:根据审批结果更新申请状态和审批级别 + if ("1".equals(status)) { + // 审批通过 - 流转到下一级 + String nextLevel = getNextApprovalLevel(currentApprovalLevel, application.getTrainingType()); + + if (nextLevel != null) { + // 进入下一级审批 + application.setApprovalLevel(nextLevel); + application.setCurrentApproverId(null); // 不设置固定审批人ID + application.setCurrentApprover(getApprovalLevelDescription(nextLevel)); + log.info("✅ 审批流转到第{}级 - applicationId: {}, 下一级审批人: {}", + nextLevel, applicationId, application.getCurrentApprover()); + } else { + // 所有级别审批完成 + application.setStatus("1"); // 最终通过 + application.setApprovalTime(DateUtils.getNowDate()); + log.info("✅ 所有审批级别已完成,申请最终通过 - applicationId: {}", applicationId); + + // 创建培训记录 + try { + trainingApplicationService.updateApplicationStatus(applicationId, "1"); + log.info("✅ 创建培训记录成功 - applicationId: {}", applicationId); + } catch (Exception e) { + log.error("❌ 创建培训记录失败 - applicationId: {}, 错误: {}", applicationId, e.getMessage(), e); + throw new RuntimeException("审批处理失败:创建培训记录时发生错误 - " + e.getMessage()); + } + } + } else if ("2".equals(status)) { + // 审批拒绝 + application.setStatus("2"); + application.setRejectTime(DateUtils.getNowDate()); + application.setApprovalTime(DateUtils.getNowDate()); + log.info("❌ 审批被拒绝 - applicationId: {}", applicationId); + } + + // 更新申请记录到数据库 + application.setUpdateTime(DateUtils.getNowDate()); + trainingApplicationMapper.updateById(application); + log.info("✅ 更新申请记录成功 - applicationId: {}, status: {}, approvalLevel: {}", + applicationId, application.getStatus(), application.getApprovalLevel()); + return result; } -} \ No newline at end of file + + /** + * 获取下一级审批级别(根据培训类型动态路由) + * + * 7级审批制度: + * 1-部门负责人 → 2a-教务处/2b-学生处/3-计划财务处(根据培训类型) + * 2a-教务处 → 3-计划财务处 + * 2b-学生处 → 3-计划财务处 + * 3-计划财务处 → 4-人事处 + * 4-人事处 → 5-业务分管领导 + * 5-业务分管领导 → 6-人事分管院领导 + * 6-人事分管院领导 → 7-院长 + * 7-院长 → 审批完成 + * + * @param currentLevel 当前审批级别 + * @param trainingType 培训类型 + * @return 下一级审批级别 + */ + private String getNextApprovalLevel(String currentLevel, String trainingType) { + switch (currentLevel) { + case "1": + // 部门负责人审批后,根据培训类型分流 + if ("教学".equals(trainingType)) { + return "2a"; // 教务处 + } else if ("德育".equals(trainingType)) { + return "2b"; // 学生处 + } else { + // 行政、管理、其他 → 直接到计划财务处 + return "3"; + } + case "2a": // 教务处 → 计划财务处 + case "2b": // 学生处 → 计划财务处 + return "3"; + case "3": + return "4"; // 计划财务处 → 人事处 + case "4": + return "5"; // 人事处 → 业务分管领导 + case "5": + return "6"; // 业务分管领导 → 人事分管院领导 + case "6": + return "7"; // 人事分管院领导 → 院长 + case "7": + return null; // 院长审批完成 + default: + return null; + } + } + + /** + * 根据审批级别获取审批环节描述 + * + * @param approvalLevel 审批级别 + * @return 审批环节描述 + */ + private String getApprovalLevelDescription(String approvalLevel) { + switch (approvalLevel) { + case "1": + return "待部门负责人审批"; + case "2a": + return "待教务处审批"; + case "2b": + return "待学生处审批"; + case "3": + return "待计划财务处审批"; + case "4": + return "待人事处审批"; + case "5": + return "待业务分管领导审批"; + case "6": + return "待人事分管院领导审批"; + case "7": + return "待院长审批"; + default: + return "待审批"; + } + } + + /** + * 验证审批人权限级别 + * + * @param approverId 审批人ID + * @param approvalLevel 审批级别 + * @return 是否有权限 + */ + private boolean validateApproverLevel(Long approverId, String approvalLevel) { + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("✅ 用户 {} 拥有通用审批权限 rs:training:approve", approverId); + return true; + } + + // 检查分级审批权限 + String requiredPermission = "rs:training:approve" + approvalLevel; + boolean hasPermission = StpUtil.hasPermission(requiredPermission); + + if (hasPermission) { + log.info("✅ 用户 {} 拥有第{}级审批权限 {}", approverId, approvalLevel, requiredPermission); + } else { + log.warn("❌ 用户 {} 缺少第{}级审批权限 {},权限验证失败", + approverId, approvalLevel, requiredPermission); + } + + return hasPermission; + } + + /** + * 应用数据权限过滤 + */ + private void applyDataPermissionFilter(RsTrainingApproval approval) { + // 安全获取用户ID,兼容 "sys_user:2091" 格式 + Long currentUserId = null; + try { + Object loginId = StpUtil.getLoginId(); + if (loginId instanceof Long) { + currentUserId = (Long) loginId; + } else if (loginId instanceof String) { + String loginIdStr = (String) loginId; + // 处理 "sys_user:2091" 格式 + if (loginIdStr.contains(":")) { + String[] parts = loginIdStr.split(":"); + if (parts.length > 1) { + currentUserId = Long.parseLong(parts[1]); + } + } else { + currentUserId = Long.parseLong(loginIdStr); + } + } else if (loginId instanceof Number) { + currentUserId = ((Number) loginId).longValue(); + } + } catch (Exception e) { + log.error("❌ 获取用户ID失败: {}", e.getMessage()); + throw new RuntimeException("无法获取当前用户ID,请重新登录"); + } + + String currentUsername = LoginHelper.getUsername(); + Long currentDeptId = LoginHelper.getDeptId(); + boolean isSuperAdmin = LoginHelper.isSuperAdmin(); + + log.info("==================== 开始应用数据权限过滤 ===================="); + log.info("当前用户: {}, ID: {}, 部门ID: {}, 超级管理员: {}", + currentUsername, currentUserId, currentDeptId, isSuperAdmin); + + // 超级管理员不需要过滤 + if (isSuperAdmin) { + log.info("→ 权限结果:超级管理员,跳过所有过滤"); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 申请人只能看到自己的申请 + boolean hasApplicantRole = StpUtil.hasRole("rs-Applicant"); + log.info("✓ 角色检查 [rs-Applicant]: {}", hasApplicantRole); + if (hasApplicantRole) { + approval.setApplicantId(currentUserId); + log.info("→ 权限结果:申请人角色,设置申请人过滤 applicantId={}", currentUserId); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 人事管理员可以看到所有申请 + boolean hasAdminRole = StpUtil.hasRole("rs-admin"); + log.info("✓ 角色检查 [rs-admin]: {}", hasAdminRole); + if (hasAdminRole) { + log.info("→ 权限结果:管理员角色,不设置过滤(可查看所有数据)"); + log.info("==================== 权限过滤结束 ===================="); + return; // 全局权限 + } + + // 🔧 检查部门负责人权限(但不能与其他审批权限冲突) + boolean isDeptApprover = isDeptApprover(currentUserId); + log.info("✓ 部门负责人检查结果: {}", isDeptApprover); + + // 🆕 动态查询用户的审批级别 + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels == null || approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限,返回空列表", currentUsername); + approval.setApproverId(-1L); // 设置一个不存在的ID,确保查不到数据 + log.info("==================== 权限过滤结束 ===================="); + return; + } + + log.info("✅ 用户 {} 拥有审批级别: {}", currentUsername, approvalLevels); + + // 🔴 关键改进:将审批级别列表存储到审批对象中 + approval.setApprovalLevels(approvalLevels); + + // 🔴 关键改进:只对 level=1(部门负责人)进行部门过滤 + if (approvalLevels.contains("1")) { + // 如果用户有第1级审批权限,需要特殊处理部门过滤 + // 但不能直接设置deptId,因为可能同时有其他级别的审批权限 + // 解决方案:在Mapper中根据approvalLevel动态应用部门过滤 + approval.setDeptId(currentDeptId); + log.info("✅ 用户拥有第1级审批权限(部门负责人),设置部门过滤 deptId={}", currentDeptId); + } + + log.info("→ 权限结果:多级审批权限,审批级别={}", approvalLevels); + log.info("==================== 权限过滤结束 ===================="); + } + + /** + * 判断用户是否为部门负责人(第1级审批权限) + * + * @param userId 用户ID + * @return 是否为部门负责人 + */ + private boolean isDeptApprover(Long userId) { + // 检查是否有第1级审批权限(部门负责人) + if (StpUtil.hasPermission("rs:training:approve1")) { + log.info("✓ 检测到部门负责人权限: rs:training:approve1"); + return true; + } + // 检查是否有部门负责人角色 + if (StpUtil.hasRole("dept_head_role")) { + log.info("✓ 检测到部门负责人角色: dept_head_role"); + return true; + } + // 兼容旧版权限标识符 + if (StpUtil.hasPermission("training:application:dept_head:approve")) { + log.info("✓ 检测到旧版部门负责人权限: training:application:dept_head:approve"); + return true; + } + return false; + } + + /** + * 🆕 动态获取用户的审批级别列表(培训审批) + * + * @param userId 用户ID + * @return 可审批的级别列表,例如:["1", "4"] + */ + private List getApprovalLevelsByUser(Long userId) { + try { + List approvalLevels = new ArrayList<>(); + + // 检查7个审批级别的权限(1-7级,2a-教务处,2b-学生处) + // 第1级:部门负责人 + if (StpUtil.hasPermission("rs:training:approve1")) { + approvalLevels.add("1"); + log.info("✅ 用户 {} 拥有第1级审批权限(部门负责人)", userId); + } + + // 第2a级:教务处 + if (StpUtil.hasPermission("rs:training:approve2a")) { + approvalLevels.add("2a"); + log.info("✅ 用户 {} 拥有第2a级审批权限(教务处)", userId); + } + + // 第2b级:学生处 + if (StpUtil.hasPermission("rs:training:approve2b")) { + approvalLevels.add("2b"); + log.info("✅ 用户 {} 拥有第2b级审批权限(学生处)", userId); + } + + // 第3级:计划财务处 + if (StpUtil.hasPermission("rs:training:approve3")) { + approvalLevels.add("3"); + log.info("✅ 用户 {} 拥有第3级审批权限(计划财务处)", userId); + } + + // 第4级:人事处 + if (StpUtil.hasPermission("rs:training:approve4")) { + approvalLevels.add("4"); + log.info("✅ 用户 {} 拥有第4级审批权限(人事处)", userId); + } + + // 第5级:业务分管领导 + if (StpUtil.hasPermission("rs:training:approve5")) { + approvalLevels.add("5"); + log.info("✅ 用户 {} 拥有第5级审批权限(业务分管领导)", userId); + } + + // 第6级:人事分管院领导 + if (StpUtil.hasPermission("rs:training:approve6")) { + approvalLevels.add("6"); + log.info("✅ 用户 {} 拥有第6级审批权限(人事分管院领导)", userId); + } + + // 第7级:院长 + if (StpUtil.hasPermission("rs:training:approve7")) { + approvalLevels.add("7"); + log.info("✅ 用户 {} 拥有第7级审批权限(院长)", userId); + } + + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId); + approvalLevels.clear(); + approvalLevels.addAll(Arrays.asList("1", "2a", "2b", "3", "4", "5", "6", "7")); + } + + if (approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限", userId); + } else { + log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels); + } + + return approvalLevels; + + } catch (Exception e) { + log.error("❌ 获取用户 {} 的审批级别失败", userId, e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/util/OverseasReceiptPdfUtil.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/util/OverseasReceiptPdfUtil.java new file mode 100644 index 00000000..0fbfb242 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/util/OverseasReceiptPdfUtil.java @@ -0,0 +1,201 @@ +package org.dromara.rs.util; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.*; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.rs.domain.vo.RsOverseasRecordVo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 因私出国(境)情况回执PDF生成工具类 + * + * @author weizzzz + * @date 2025-01-08 + */ +@Slf4j +public class OverseasReceiptPdfUtil { + + /** + * 生成因私出国(境)情况回执PDF + * + * @param record 出国记录信息 + * @return PDF字节数组 + */ + public static byte[] generateReceiptPdf(RsOverseasRecordVo record) throws DocumentException, IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Document document = new Document(PageSize.A4, 40, 40, 40, 40); + PdfWriter writer = PdfWriter.getInstance(document, baos); + + document.open(); + + try { + // 创建中文字体 + BaseFont baseFont = createChineseFont(); + Font titleFont = new Font(baseFont, 14, Font.BOLD); + Font headerFont = new Font(baseFont, 12, Font.BOLD); + Font normalFont = new Font(baseFont, 10, Font.NORMAL); + Font smallFont = new Font(baseFont, 9, Font.NORMAL); + + // 1. 附件标题 + Paragraph attachmentTitle = new Paragraph("附件 6", smallFont); + attachmentTitle.setAlignment(Element.ALIGN_LEFT); + attachmentTitle.setSpacingAfter(5); + document.add(attachmentTitle); + + // 2. 主标题 + Paragraph mainTitle = new Paragraph("杭州第一技师学院教职工因私出国(境)情况回执", titleFont); + mainTitle.setAlignment(Element.ALIGN_CENTER); + mainTitle.setSpacingAfter(20); + document.add(mainTitle); + + // 3. 创建表格 + PdfPTable table = new PdfPTable(2); + table.setWidthPercentage(100); + table.setWidths(new float[]{20, 80}); + table.setSpacingBefore(10); + + // 设置表格边框 + table.getDefaultCell().setBorderWidth(1); + table.getDefaultCell().setPadding(8); + + // 第一行:申请行程 + addSectionRow(table, "申请\n行程", getApplicationItinerary(record), headerFont, normalFont); + + // 第二行:温馨提醒 + addSectionRow(table, "温馨\n\n提醒", getWarmReminder(), headerFont, smallFont); + + // 第三行:实际行程 + addSectionRow(table, "实际\n行程", getActualItinerary(), headerFont, normalFont); + + // 第四行:执纪情况 + addSectionRow(table, "执纪\n情况", getDisciplineStatement(), headerFont, normalFont); + + // 第五行:有关情况说明 + addSectionRow(table, "有关情\n况说明", "(实际行程与申请行程有出入及其他情况的,请简要说明)", headerFont, normalFont); + + // 第六行:申请人意见 + addSectionRow(table, "申请人\n\n意见", getApplicantOpinion(), headerFont, normalFont); + + document.add(table); + + // 页码 + Paragraph pageNumber = new Paragraph("- 16 -", smallFont); + pageNumber.setAlignment(Element.ALIGN_CENTER); + pageNumber.setSpacingBefore(20); + document.add(pageNumber); + + } catch (Exception e) { + log.error("生成因私出国情况回执PDF失败", e); + throw new ServiceException("生成PDF失败: " + e.getMessage()); + } finally { + document.close(); + } + + return baos.toByteArray(); + } + + /** + * 创建中文字体 + */ + private static BaseFont createChineseFont() throws DocumentException, IOException { + try { + return BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + } catch (Exception e) { + log.warn("无法加载STSong-Light字体,尝试备选字体"); + try { + return BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + } catch (Exception e2) { + throw new ServiceException("无法加载中文字体,请检查itext-asian依赖配置"); + } + } + } + + /** + * 添加表格行 + */ + private static void addSectionRow(PdfPTable table, String label, String content, Font labelFont, Font contentFont) { + // 左侧标签单元格 + PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont)); + labelCell.setHorizontalAlignment(Element.ALIGN_CENTER); + labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + labelCell.setPadding(8); + labelCell.setMinimumHeight(80); + table.addCell(labelCell); + + // 右侧内容单元格 + PdfPCell contentCell = new PdfPCell(new Phrase(content, contentFont)); + contentCell.setHorizontalAlignment(Element.ALIGN_LEFT); + contentCell.setVerticalAlignment(Element.ALIGN_TOP); + contentCell.setPadding(8); + contentCell.setMinimumHeight(80); + table.addCell(contentCell); + } + + /** + * 获取申请行程内容 + */ + private static String getApplicationItinerary(RsOverseasRecordVo record) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日"); + + // 申请人姓名 + String applicantName = record.getApplicantName() != null ? + record.getApplicantName() : "____"; + + String startDate = record.getActualStartDate() != null ? + dateFormat.format(record.getActualStartDate()) : "____年__月__日"; + String endDate = record.getActualEndDate() != null ? + dateFormat.format(record.getActualEndDate()) : "____年__月__日"; + + String country = record.getDestinationCountry() != null ? + record.getDestinationCountry() : "__________"; + String exitReason = record.getExitReason() != null ? + record.getExitReason() : "________"; + + Integer duration = record.getActualDuration(); + String durationStr = duration != null ? String.valueOf(duration) : "____"; + + return String.format(" %s同志,申请于%s至%s,赴%s旅游(探亲),在外停留%s天,费用自理。", + applicantName, startDate, endDate, country, durationStr); + } + + /** + * 获取温馨提醒内容 + */ + private static String getWarmReminder() { + return " 1. 请自觉遵守我国和当地法律法规,尊重当地民族风俗和宗教信仰,注意人身及财产安全。\n\n" + + " 2. 出国(境)期间不准为企业和个人办理经营业务;不准接受外商或住国(境)外中资机构(企业)的资助,不准在任何单位报销出国(境)费用。\n\n" + + " 3. 出国(境)时间、天数、目的地、事由等情况有变化的,须在出国(境)前向学院人事(组织)处报备。\n\n" + + " 4. 请于回国 10 日内,将出国(境)证件和本回执通过本单位组织人事(管理)部门转交学院人事(组织)处集中保管。\n\n" + + " 5. 若有违反《中国共产党纪律处分条例》和出国(境)有关文件规定的,将视情节轻重给予相应处理。"; + } + + /** + * 获取实际行程内容 + */ + private static String getActualItinerary() { + return " 本人于______年___月___日至______年___月___日,赴_______________旅游(探亲),在外停留_____天,费用(是□否□)自理。"; + } + + /** + * 获取执纪情况内容 + */ + private static String getDisciplineStatement() { + return " 本人因私出国(境)期间,(是□否□)严格遵守保密纪律、外事纪律,(是□否□)自觉遵守所在国家法律,(是□否□)严格执行国家、省、市、局和学院因私出国(境)管理有关规定,(有□无□)违规违纪行为。出国(境)证件已于___月___日 按规定上缴______统一保管。\n\n" + + "(证件编号:_______________)"; + } + + /** + * 获取申请人意见内容 + */ + private static String getApplicantOpinion() { + return " 本人保证填写信息真实。\n\n\n" + + " 签名:\n" + + " 年 月 日"; + } +} + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsApplicationMapper.xml index 5fdbe3a0..1fdf4fdc 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsApplicationMapper.xml @@ -47,6 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and a.contact_phone like concat('%', #{contactPhone}, '%') and a.contact_email like concat('%', #{contactEmail}, '%') + order by a.create_time desc where a.job_id = #{jobId} and a.applicant_id = #{applicantId} + order by a.create_time desc diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml index 0f13c629..1a4e94ab 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml @@ -8,7 +8,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - @@ -27,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select j.job_id, j.title, j.department, j.salary_range, j.location, j.description, j.requirements, + select j.job_id, j.title, j.department, j.location, j.description, j.requirements, j.is_active, j.view_count, j.tenant_id, j.create_by, j.create_time, j.update_by, j.update_time, j.remark, (select count(1) from rs_application a where a.job_id = j.job_id and a.deleted = '0' and a.tenant_id = j.tenant_id) as applicant_count from rs_job j diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationAttachmentMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationAttachmentMapper.xml new file mode 100644 index 00000000..d8e9508b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationAttachmentMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select attachment_id, application_id, attachment_title, attachment_content, + file_name, file_original_name, file_path, file_size, file_type, mime_type, + upload_time, upload_user_id, upload_user_name, + create_by, create_time, update_by, update_time + from rs_overseas_application_attachment + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml index 7f29b40a..9563709c 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml @@ -6,12 +6,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + + + + + + + + + + + + + @@ -19,18 +39,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + + + + + + + + + + + + + + @@ -41,17 +74,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + - select oa.application_id, oa.applicant_id, oa.applicant_name, oa.department, oa.position, - oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city, - oa.start_date, oa.end_date, oa.duration, oa.budget, oa.invitation, oa.itinerary, - oa.accommodation_plan, oa.transportation_plan, oa.status, oa.current_approver, oa.approval_level, - oa.apply_time, oa.approval_time, oa.admin_remark, oa.applicant_remark, oa.tenant_id, - oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted, + select oa.application_id, oa.serial_number, oa.applicant_id, oa.applicant_name, + oa.gender, oa.birth_date, oa.photo, oa.id_number, oa.political_status, oa.registered_address, + oa.department, oa.position, oa.professional_title, oa.administrative_level, oa.personnel_category, + oa.registration_unit, oa.unit_leader_signature, oa.office_phone, oa.mobile_phone, + oa.contact_phone, oa.contact_email, oa.document_name, oa.document_number, + oa.destination_country, oa.exit_reason, oa.visit_relationship, oa.remarks, + oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city, + oa.start_date, oa.end_date, oa.duration, oa.invitation, oa.itinerary, + oa.accommodation_plan, oa.transportation_plan, oa.status, oa.approval_status, oa.current_approver, oa.approval_level, + oa.apply_time, oa.approval_time, oa.admin_remark, oa.applicant_remark, + oa.emergency_contact, oa.attachment_ids, oa.signature, + oa.stamped_registration_file_path, oa.stamped_registration_file_name, + oa.stamped_file_upload_time, oa.stamped_file_upload_status, oa.stamped_file_oss_id, + oa.party_retention_file_path, oa.party_retention_file_name, + oa.party_retention_upload_time, oa.party_retention_upload_status, oa.party_retention_file_oss_id, + oa.tenant_id, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted, CASE WHEN oa.status IS NULL OR oa.status = '' THEN '草稿' WHEN oa.status = '0' THEN '待审核' @@ -61,6 +110,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHEN oa.status = '4' THEN '已完成' ELSE '未知状态' END as status_text, + CASE + WHEN oa.approval_status IS NULL OR oa.approval_status = '' THEN '草稿' + WHEN oa.approval_status = 'draft' THEN '草稿' + WHEN oa.approval_status = 'waiting' THEN '待审批' + WHEN oa.approval_status = 'finish' THEN '已完成' + WHEN oa.approval_status = 'cancel' THEN '已取消' + WHEN oa.approval_status = 'back' THEN '已退回' + ELSE '未知状态' + END as approval_status_text, CASE oa.purpose WHEN 'academic_exchange' THEN '学术交流' WHEN 'conference' THEN '学术会议' @@ -71,12 +129,69 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ELSE oa.purpose END as purpose_text, CASE oa.approval_level - WHEN '1' THEN '部门主管' - WHEN '2' THEN '分管领导' - WHEN '3' THEN '校领导' + WHEN '1' THEN '部门负责人' + WHEN '2' THEN '人事处' + WHEN '3' THEN '纪委' + WHEN '4' THEN '学院分管领导' + WHEN '5' THEN '学院党委书记' ELSE '未知级别' - END as approval_level_text + END as approval_level_text, + CASE oa.gender + WHEN '0' THEN '男' + WHEN '1' THEN '女' + ELSE '未知' + END as gender_text, + CASE oa.political_status + WHEN '1' THEN '中共党员' + WHEN '2' THEN '中共预备党员' + WHEN '3' THEN '共青团员' + WHEN '4' THEN '民革党员' + WHEN '5' THEN '民盟盟员' + WHEN '6' THEN '民建会员' + WHEN '7' THEN '民进会员' + WHEN '8' THEN '农工党党员' + WHEN '9' THEN '致公党党员' + WHEN '10' THEN '九三学社社员' + WHEN '11' THEN '台盟盟员' + WHEN '12' THEN '无党派人士' + WHEN '13' THEN '群众' + ELSE oa.political_status + END as political_status_text, + CASE oa.administrative_level + WHEN '0' THEN '无' + WHEN '1' THEN '一级巡视员' + WHEN '2' THEN '二级巡视员' + WHEN '3' THEN '一级调研员' + WHEN '4' THEN '二级调研员' + WHEN '5' THEN '三级调研员' + WHEN '6' THEN '四级调研员' + WHEN '7' THEN '一级主任科员' + WHEN '8' THEN '二级主任科员' + WHEN '9' THEN '三级主任科员' + WHEN '10' THEN '四级主任科员' + WHEN '11' THEN '一级科员' + WHEN '12' THEN '二级科员' + ELSE oa.administrative_level + END as administrative_level_text, + CASE oa.personnel_category + WHEN '1' THEN '1-厅局级' + WHEN '2' THEN '2-县处级' + WHEN '3' THEN '3-乡科级' + WHEN '4' THEN '4-一般干部' + WHEN '5' THEN '5-其他重要岗位人员' + ELSE oa.personnel_category + END as personnel_category_text, + ft.id as task_id from rs_overseas_application oa + + LEFT JOIN ( + SELECT t.id, i.business_id + FROM flow_task t + INNER JOIN flow_instance i ON t.instance_id = i.id + WHERE t.del_flag = '0' + AND i.del_flag = '0' + AND t.node_type = 1 + ) ft ON CAST(ft.business_id AS SIGNED) = oa.application_id @@ -147,6 +268,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.purpose = #{purpose} + + AND oa.exit_reason = #{exitReason} + AND oa.destination like concat('%', #{destination}, '%') @@ -162,12 +286,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.applicant_id = #{applicantId} - - AND oa.budget >= #{budgetMin} - - - AND oa.budget <= #{budgetMax} - AND oa.apply_time >= #{applyTimeBegin} @@ -186,6 +304,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by oa.apply_time desc, oa.create_time desc + + + + insert into rs_overseas_application + + serial_number, + applicant_id, + applicant_name, + gender, + birth_date, + photo, + id_number, + political_status, + registered_address, + department, + position, + professional_title, + administrative_level, + personnel_category, + registration_unit, + unit_leader_signature, + office_phone, + mobile_phone, + contact_phone, + contact_email, + document_name, + document_number, + destination_country, + exit_reason, + visit_relationship, + remarks, + purpose, + purpose_detail, + destination, + destination_city, + start_date, + end_date, + duration, + invitation, + itinerary, + accommodation_plan, + transportation_plan, + status, + current_approver, + approval_level, + apply_time, + approval_time, + admin_remark, + applicant_remark, + emergency_contact, + signature, + stamped_registration_file_path, + stamped_registration_file_name, + stamped_file_upload_time, + stamped_file_upload_status, + tenant_id, + deleted, + create_by, + create_time, + update_by, + update_time, + + + #{serialNumber}, + #{applicantId}, + #{applicantName}, + #{gender}, + #{birthDate}, + #{photo}, + #{idNumber}, + #{politicalStatus}, + #{registeredAddress}, + #{department}, + #{position}, + #{professionalTitle}, + #{administrativeLevel}, + #{personnelCategory}, + #{registrationUnit}, + #{unitLeaderSignature}, + #{officePhone}, + #{mobilePhone}, + #{contactPhone}, + #{contactEmail}, + #{documentName}, + #{documentNumber}, + #{destinationCountry}, + #{exitReason}, + #{visitRelationship}, + #{remarks}, + #{purpose}, + #{purposeDetail}, + #{destination}, + #{destinationCity}, + #{startDate}, + #{endDate}, + #{duration}, + #{invitation}, + #{itinerary}, + #{accommodationPlan}, + #{transportationPlan}, + #{status}, + #{currentApprover}, + #{approvalLevel}, + #{applyTime}, + #{approvalTime}, + #{adminRemark}, + #{applicantRemark}, + #{emergencyContact}, + #{signature}, + #{stampedRegistrationFilePath}, + #{stampedRegistrationFileName}, + #{stampedFileUploadTime}, + #{stampedFileUploadStatus}, + #{tenantId}, + #{deleted}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + + + + + + update rs_overseas_application + + serial_number = #{serialNumber}, + applicant_id = #{applicantId}, + applicant_name = #{applicantName}, + gender = #{gender}, + birth_date = #{birthDate}, + photo = #{photo}, + id_number = #{idNumber}, + political_status = #{politicalStatus}, + registered_address = #{registeredAddress}, + department = #{department}, + position = #{position}, + professional_title = #{professionalTitle}, + administrative_level = #{administrativeLevel}, + personnel_category = #{personnelCategory}, + registration_unit = #{registrationUnit}, + unit_leader_signature = #{unitLeaderSignature}, + office_phone = #{officePhone}, + mobile_phone = #{mobilePhone}, + contact_phone = #{contactPhone}, + contact_email = #{contactEmail}, + document_name = #{documentName}, + document_number = #{documentNumber}, + destination_country = #{destinationCountry}, + exit_reason = #{exitReason}, + visit_relationship = #{visitRelationship}, + remarks = #{remarks}, + purpose = #{purpose}, + purpose_detail = #{purposeDetail}, + destination = #{destination}, + destination_city = #{destinationCity}, + start_date = #{startDate}, + end_date = #{endDate}, + duration = #{duration}, + invitation = #{invitation}, + itinerary = #{itinerary}, + accommodation_plan = #{accommodationPlan}, + transportation_plan = #{transportationPlan}, + status = #{status}, + current_approver = #{currentApprover}, + approval_level = #{approvalLevel}, + apply_time = #{applyTime}, + approval_time = #{approvalTime}, + admin_remark = #{adminRemark}, + applicant_remark = #{applicantRemark}, + emergency_contact = #{emergencyContact}, + signature = #{signature}, + stamped_registration_file_path = #{stampedRegistrationFilePath}, + stamped_registration_file_name = #{stampedRegistrationFileName}, + stamped_file_upload_time = #{stampedFileUploadTime}, + stamped_file_upload_status = #{stampedFileUploadStatus}, + update_by = #{updateBy}, + update_time = #{updateTime}, + + where application_id = #{applicationId} + + + + + update rs_overseas_application set deleted = '1' where application_id = #{applicationId} + + + + + update rs_overseas_application set deleted = '1' where application_id in + + #{applicationId} + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml index 011013e9..3613cf7c 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml @@ -12,6 +12,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -39,16 +40,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select oa.approval_id, oa.application_id, oa.approver_id, oa.approver_name, oa.approval_level, - oa.approval_status, oa.approval_opinion, oa.approval_time, oa.tenant_id, + oa.approval_status, oa.approval_opinion, oa.approval_signature, oa.approval_time, oa.tenant_id, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted, app.applicant_id, app.applicant_name, app.department, app.purpose, app.destination, app.start_date, app.end_date, app.duration, CASE oa.approval_level - WHEN '1' THEN '部门主管' - WHEN '2' THEN '分管领导' - WHEN '3' THEN '校领导' + WHEN '1' THEN '部门负责人' + WHEN '2' THEN '人事处' + WHEN '3' THEN '纪委' + WHEN '4' THEN '学院分管领导' + WHEN '5' THEN '学院党委书记' ELSE '未知级别' END as approval_level_text, CASE oa.approval_status @@ -107,6 +110,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.tenant_id = #{tenantId} + + + AND EXISTS ( + SELECT 1 FROM sys_user u + WHERE u.user_id = app.applicant_id + AND u.dept_id = #{deptId} + ) + + + + AND app.applicant_id = #{applicantId} + order by oa.approval_time desc, oa.create_time desc @@ -157,9 +172,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" app.duration, -- 文本转换 CASE oa.approval_level - WHEN '1' THEN '部门主管' - WHEN '2' THEN '分管领导' - WHEN '3' THEN '校领导' + WHEN '1' THEN '部门负责人' + WHEN '2' THEN '人事处' + WHEN '3' THEN '纪委' + WHEN '4' THEN '学院分管领导' + WHEN '5' THEN '学院党委书记' ELSE '未知级别' END as approval_level_text, CASE oa.approval_status @@ -205,17 +222,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.tenant_id = #{tenantId} + + + AND EXISTS ( + SELECT 1 FROM sys_user u + WHERE u.user_id = app.applicant_id + AND u.dept_id = #{deptId} + ) + + + + AND app.applicant_id = #{applicantId} + UNION ALL - -- 查询待审批的申请(还没有审批记录的) + -- 查询待审批的申请(使用current_approver_id查询当前审批人的待办) SELECT - NULL as approval_id, + NULL as approval_id, -- 待审批申请没有审批记录ID app.application_id, - NULL as approver_id, - app.current_approver as approver_name, - app.approval_level, - '0' as approval_status, + app.current_approver_id as approver_id, -- 🔴 修复:使用申请表的当前审批人ID + app.current_approver as approver_name, -- 🔴 修复:使用申请表的当前审批人名称 + app.approval_level, -- 🔴 修复:使用申请表的审批级别 + '0' as approval_status, -- 待审核状态 NULL as approval_opinion, NULL as approval_time, app.tenant_id, @@ -235,9 +264,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" app.duration, -- 文本转换 CASE app.approval_level - WHEN '1' THEN '部门主管' - WHEN '2' THEN '分管领导' - WHEN '3' THEN '校领导' + WHEN '1' THEN '部门负责人' + WHEN '2' THEN '人事处' + WHEN '3' THEN '纪委' + WHEN '4' THEN '学院分管领导' + WHEN '5' THEN '学院党委书记' ELSE '未知级别' END as approval_level_text, '待审核' as approval_status_text, @@ -257,12 +288,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 'pending' as record_type FROM rs_overseas_application app WHERE app.deleted = '0' - AND app.status = '0' -- 待审核状态 - AND NOT EXISTS ( - SELECT 1 FROM rs_overseas_approval oa - WHERE oa.application_id = app.application_id - AND oa.deleted = '0' - ) + AND app.status = '0' -- 申请状态:待审核 AND app.application_id = #{applicationId} @@ -272,9 +298,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND app.approval_level = #{approvalLevel} + + + AND app.current_approver_id = #{approverId} + + + AND app.current_approver like concat('%', #{approverName}, '%') + AND app.tenant_id = #{tenantId} + + + AND EXISTS ( + SELECT 1 FROM sys_user u + WHERE u.user_id = app.applicant_id + AND u.dept_id = #{deptId} + ) + + + + AND app.applicant_id = #{applicantId} + ORDER BY CASE WHEN approval_status = '0' THEN 0 ELSE 1 END, -- 待审核排在前面 diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasItineraryChangeMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasItineraryChangeMapper.xml new file mode 100644 index 00000000..c0247849 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasItineraryChangeMapper.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + c.change_id, + c.record_id, + c.application_id, + c.applicant_id, + c.applicant_name, + c.department, + c.original_destination, + c.original_destination_city, + c.original_exit_reason, + c.original_start_date, + c.original_end_date, + c.original_duration, + c.original_itinerary, + c.original_accommodation_plan, + c.original_transportation_plan, + c.change_type, + c.new_destination, + c.new_destination_city, + c.new_exit_reason, + c.new_start_date, + c.new_end_date, + c.new_duration, + c.new_itinerary, + c.new_accommodation_plan, + c.new_transportation_plan, + c.change_reason, + c.remark, + c.status, + c.approval_opinion, + c.approval_time, + c.approver_id, + c.approver_name, + c.tenant_id, + c.create_dept, + c.deleted, + c.create_by, + c.create_time, + c.update_by, + c.update_time, + dict_status.dict_label AS status_text + FROM rs_overseas_itinerary_change c + LEFT JOIN sys_dict_data dict_status ON dict_status.dict_value = c.status AND dict_status.dict_type = 'rs_itinerary_change_status' + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml index f5ee6f0b..27d09569 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml @@ -40,6 +40,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + + + + + + + + + + @@ -52,20 +69,150 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 'record' as data_source from rs_overseas_record ors + + + + select + ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department, + ors.position, + COALESCE(sp.post_name, ors.position) as position_text, + ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city, + ors.actual_start_date, ors.actual_end_date, ors.actual_duration, ors.actual_budget, + ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan, + ors.completion_status, ors.achievement, ors.report_content, ors.certificate_path, + ors.tenant_id, ors.create_dept, ors.create_by, ors.create_time, ors.update_by, ors.update_time, ors.deleted, + -- 来自申请表的扩展字段 + oa.gender, + CASE oa.gender + WHEN '0' THEN '男' + WHEN '1' THEN '女' + ELSE oa.gender + END as gender_text, + oa.birth_date, + oa.political_status, + CASE oa.political_status + WHEN '1' THEN '中共党员' + WHEN '2' THEN '中共预备党员' + WHEN '3' THEN '共青团员' + WHEN '4' THEN '民革党员' + WHEN '5' THEN '民盟盟员' + WHEN '6' THEN '民建会员' + WHEN '7' THEN '民进会员' + WHEN '8' THEN '农工党党员' + WHEN '9' THEN '致公党党员' + WHEN '10' THEN '九三学社社员' + WHEN '11' THEN '台盟盟员' + WHEN '12' THEN '无党派人士' + WHEN '13' THEN '群众' + ELSE oa.political_status + END as political_status_text, + oa.professional_title, + oa.document_name, + oa.document_number, + oa.mobile_phone, + oa.destination_country, + oa.exit_reason, + oa.emergency_contact, + oa.remarks, + -- 出国目的文本转换 + CASE ors.purpose + WHEN 'academic_exchange' THEN '学术交流' + WHEN 'conference' THEN '学术会议' + WHEN 'training' THEN '培训学习' + WHEN 'cooperation' THEN '合作项目' + WHEN 'research' THEN '科研访问' + WHEN 'other' THEN '其他' + ELSE ors.purpose + END as purpose_text, + -- 完成状态文本转换 + CASE ors.completion_status + WHEN '0' THEN '未完成' + WHEN '1' THEN '进行中' + WHEN '2' THEN '已完成' + ELSE '未知状态' + END as completion_status_text, + 'record' as data_source + from rs_overseas_record ors + left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0' + left join sys_post sp on (ors.position = CAST(sp.post_id AS CHAR) or ors.position = sp.post_code) and sp.status = '0' + - -- 查询已创建的出国记录 + -- 查询已创建的出国记录(关联申请表获取扩展字段) select ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department, - ors.position, ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city, + ors.position, + COALESCE(sp1.post_name, ors.position) as position_text, + ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city, ors.actual_start_date as actual_start_date, ors.actual_end_date as actual_end_date, ors.actual_duration as actual_duration, ors.actual_budget as actual_budget, ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan, ors.completion_status, ors.achievement, ors.report_content, ors.certificate_path, ors.tenant_id, ors.create_dept, ors.create_by, ors.create_time, ors.update_by, ors.update_time, ors.deleted, + -- 来自申请表的扩展字段 + oa.gender, + CASE oa.gender + WHEN '0' THEN '男' + WHEN '1' THEN '女' + ELSE oa.gender + END as gender_text, + oa.birth_date, + oa.political_status, + CASE oa.political_status + WHEN '1' THEN '中共党员' + WHEN '2' THEN '中共预备党员' + WHEN '3' THEN '共青团员' + WHEN '4' THEN '民革党员' + WHEN '5' THEN '民盟盟员' + WHEN '6' THEN '民建会员' + WHEN '7' THEN '民进会员' + WHEN '8' THEN '农工党党员' + WHEN '9' THEN '致公党党员' + WHEN '10' THEN '九三学社社员' + WHEN '11' THEN '台盟盟员' + WHEN '12' THEN '无党派人士' + WHEN '13' THEN '群众' + ELSE oa.political_status + END as political_status_text, + oa.professional_title, + oa.document_name, + oa.document_number, + oa.mobile_phone, + oa.destination_country, + oa.exit_reason, + oa.emergency_contact, + oa.remarks, + -- 申请状态(来自申请表) + oa.status, + -- 申请状态文本转换 + CASE oa.status + WHEN '0' THEN '待审核' + WHEN '1' THEN '已通过' + WHEN '2' THEN '已拒绝' + ELSE oa.status + END as status_text, + -- 出国目的文本转换 + CASE ors.purpose + WHEN 'academic_exchange' THEN '学术交流' + WHEN 'conference_participation' THEN '参加会议' + WHEN 'collaborative_research' THEN '合作研究' + WHEN 'short_term_training' THEN '短期培训' + WHEN 'other' THEN '其他' + ELSE ors.purpose + END as purpose_text, + -- 完成状态文本转换 + CASE ors.completion_status + WHEN '0' THEN '未完成' + WHEN '1' THEN '进行中' + WHEN '2' THEN '已完成' + WHEN '3' THEN '已取消' + ELSE ors.completion_status + END as completion_status_text, 'record' as data_source from rs_overseas_record ors + left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0' + left join sys_post sp1 on (ors.position = CAST(sp1.post_id AS CHAR) or ors.position = sp1.post_code) and sp1.status = '0' where ors.deleted = '0' UNION ALL @@ -74,14 +221,70 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select NULL as record_id, oa.application_id, oa.applicant_id, oa.applicant_name, oa.department, - oa.position, oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city, + oa.position, + COALESCE(sp2.post_name, oa.position) as position_text, + oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city, oa.start_date as actual_start_date, oa.end_date as actual_end_date, oa.duration as actual_duration, - oa.budget as actual_budget, + NULL as actual_budget, oa.invitation, oa.itinerary, oa.accommodation_plan, oa.transportation_plan, '1' as completion_status, NULL as achievement, NULL as report_content, NULL as certificate_path, oa.tenant_id, NULL as create_dept, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted, + -- 来自申请表的字段(UNION ALL 第二部分直接从申请表获取) + oa.gender, + CASE oa.gender + WHEN '0' THEN '男' + WHEN '1' THEN '女' + ELSE oa.gender + END as gender_text, + oa.birth_date, + oa.political_status, + CASE oa.political_status + WHEN '1' THEN '中共党员' + WHEN '2' THEN '中共预备党员' + WHEN '3' THEN '共青团员' + WHEN '4' THEN '民革党员' + WHEN '5' THEN '民盟盟员' + WHEN '6' THEN '民建会员' + WHEN '7' THEN '民进会员' + WHEN '8' THEN '农工党党员' + WHEN '9' THEN '致公党党员' + WHEN '10' THEN '九三学社社员' + WHEN '11' THEN '台盟盟员' + WHEN '12' THEN '无党派人士' + WHEN '13' THEN '群众' + ELSE oa.political_status + END as political_status_text, + oa.professional_title, + oa.document_name, + oa.document_number, + oa.mobile_phone, + oa.destination_country, + oa.exit_reason, + oa.emergency_contact, + oa.remarks, + -- 申请状态(来自申请表) + oa.status, + -- 申请状态文本转换(已通过审批的申请) + CASE oa.status + WHEN '0' THEN '待审核' + WHEN '1' THEN '已通过' + WHEN '2' THEN '已拒绝' + ELSE oa.status + END as status_text, + -- 出国目的文本转换 + CASE oa.purpose + WHEN 'academic_exchange' THEN '学术交流' + WHEN 'conference_participation' THEN '参加会议' + WHEN 'collaborative_research' THEN '合作研究' + WHEN 'short_term_training' THEN '短期培训' + WHEN 'other' THEN '其他' + ELSE oa.purpose + END as purpose_text, + -- 完成状态文本(已通过审批未创建记录的状态为"进行中") + '进行中' as completion_status_text, 'approved_application' as data_source from rs_overseas_application oa + left join sys_post sp2 on (oa.position = CAST(sp2.post_id AS CHAR) or oa.position = sp2.post_code) and sp2.status = '0' where oa.deleted = '0' and oa.status = '1' and not exists ( @@ -140,7 +343,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRegistrationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRegistrationMapper.xml new file mode 100644 index 00000000..c8b36ee8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRegistrationMapper.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select r.registration_id, r.application_id, r.name, r.gender, r.birth_date, + r.photo, r.id_number, r.political_status, r.registered_address, + r.department, r.position, r.administrative_level, r.personnel_category, + r.status, r.registration_time as submit_time, r.remarks, + r.tenant_id, r.create_by, r.create_time, r.update_by, r.update_time, r.deleted, + CASE r.gender + WHEN '0' THEN '男' + WHEN '1' THEN '女' + ELSE '未知' + END as gender_text, + r.political_status as political_status_text, + r.administrative_level as administrative_level_text, + CASE r.personnel_category + WHEN 'A' THEN 'A-厅级干部' + WHEN 'B' THEN 'B-处级干部' + WHEN 'C' THEN 'C-科级干部' + WHEN 'D' THEN 'D-一般干部' + WHEN 'E' THEN 'E-专业技术人员' + WHEN 'F' THEN 'F-工勤人员' + ELSE r.personnel_category + END as personnel_category_text, + CASE r.status + WHEN '0' THEN '草稿' + WHEN '1' THEN '待审核' + WHEN '2' THEN '已通过' + WHEN '3' THEN '已拒绝' + ELSE '未知状态' + END as status_text + from rs_overseas_registration r + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml index 965aa2f5..702cba86 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml @@ -9,12 +9,24 @@ + + + + + + + + + + + + @@ -31,8 +43,12 @@ SELECT - a.application_id, a.course_id, a.course_name as custom_course_name, a.user_id, a.user_name, a.department, a.position, a.reason, - a.status, a.apply_time, a.current_approver, a.location, a.duration, a.start_time, a.end_time, + a.application_id, a.course_id, a.course_name as custom_course_name, a.user_id, a.user_name, + u.nick_name, + a.department, a.position, + a.current_title, a.current_position, a.work_start_time, a.phone, a.organizer_unit, + a.training_type, a.training_fee, a.companions, a.training_purpose, a.attachment_ids, + a.reason, a.status, a.apply_time, a.current_approver, a.approval_level, a.location, a.duration, a.start_time, a.end_time, a.tenant_id, a.create_dept, a.reject_time, a.create_by, a.create_time, a.update_by, a.update_time, CASE WHEN a.course_id = 0 THEN a.course_name @@ -40,6 +56,7 @@ END as course_name FROM rs_training_application a LEFT JOIN rs_training_course c ON a.course_id = c.course_id + LEFT JOIN sys_user u ON a.user_id = u.user_id - - - - + + + and ( + + + + + ( + a.approval_level = '1' + + and a.create_dept = #{app.currentDeptId} + + ) + + + + ( + a.approval_level = '2a' + and a.training_type = '教学' + ) + + + + ( + a.approval_level = '2b' + and a.training_type = '德育' + ) + + + + (a.approval_level = '3') + + + + (a.approval_level = '4') + + + + (a.approval_level = '5') + + + + (a.approval_level = '6') + + + + (a.approval_level = '7') + + + + ) + + + and a.course_id = #{app.courseId} and a.user_id = #{app.userId} and a.user_name like concat('%', #{app.userName}, '%') diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApprovalMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApprovalMapper.xml index 928a1e79..1a6ecfab 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApprovalMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApprovalMapper.xml @@ -37,9 +37,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + + + + + + + + + + + + + + + + + + + + @@ -75,25 +94,81 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by approval_level asc, create_time desc - + + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingRecordMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingRecordMapper.xml index d26766c6..26d133c5 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingRecordMapper.xml @@ -37,6 +37,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + @@ -44,8 +49,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" r.completion_status, r.completion_time, r.score, r.certificate_path, r.certificate_number, r.attachment_ids, r.feedback, r.tenant_id, r.create_dept, r.create_by, r.create_time, r.update_by, r.update_time, r.department, r.position, r.reason, r.status, r.apply_time, r.current_approver, r.start_time, r.end_time, - r.course_name, r.duration, r.location, r.dept_id + r.course_name, r.duration, r.location, r.dept_id, + a.course_name as organizer_unit, a.training_type, a.companions from rs_training_record r + left join rs_training_application a on r.application_id = a.application_id and a.deleted = '0' @@ -77,7 +77,7 @@ AND vt.tenant_id = #{tenantId} AND m.tenant_id = #{tenantId} - ORDER BY m.join_time DESC + ORDER BY m.join_time DESC, vt.create_time DESC @@ -252,7 +252,7 @@ - ORDER BY COALESCE(direct_ar.registration_time, team_ar.registration_time) DESC + ORDER BY COALESCE(direct_ar.registration_time, team_ar.registration_time) DESC, a.create_time DESC @@ -351,7 +351,7 @@ AND target_name LIKE CONCAT('%', #{name}, '%') - ORDER BY registration_time DESC + ORDER BY registration_time DESC, create_time DESC @@ -478,7 +478,7 @@ AND target_name LIKE CONCAT('%', #{name}, '%') - ORDER BY registration_time DESC + ORDER BY registration_time DESC, create_time DESC \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml index 03af3451..58b5eb1f 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml @@ -388,7 +388,7 @@ 1 AS relatedType FROM stgl_club WHERE deleted = 0 AND status = 0 - ORDER BY club_name + ORDER BY club_name, create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglAuditorMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglAuditorMapper.xml index e5cb2b93..c3057eb2 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglAuditorMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglAuditorMapper.xml @@ -126,7 +126,7 @@ #{userId} - order by u.dept_id, u.user_name + order by u.user_name asc, u.dept_id \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml index 4e353556..2ad5d4a0 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml @@ -40,7 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND tcr.deleted = 0 AND tcr.teacher_type = 'class_teacher' AND (tcr.end_date IS NULL OR tcr.end_date > NOW()) - ORDER BY c.grade DESC, c.class_name ASC + ORDER BY c.grade DESC, c.class_name ASC, c.create_time DESC @@ -270,7 +270,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND s.student_no LIKE CONCAT('%', #{studentNo}, '%') - ORDER BY s.student_no ASC + ORDER BY s.student_no DESC, s.create_time DESC LIMIT #{pageSize} OFFSET #{offset} @@ -442,7 +442,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) ) AND a.tenant_id = '000000' - ORDER BY a.start_time DESC + ORDER BY a.start_time DESC, a.create_time DESC \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysTeacherMapper.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysTeacherMapper.java index f3b80968..1594469a 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysTeacherMapper.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysTeacherMapper.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.dromara.common.mybatis.annotation.DataColumn; +import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.system.domain.SysUser; import org.dromara.system.domain.vo.SysTeacherVo; @@ -28,25 +30,26 @@ public interface SysTeacherMapper extends BaseMapperPlus * @return 教师信息分页列表 */ @Select(""" - SELECT - u.user_id, - u.user_name, - u.nick_name, - u.phonenumber, - u.email, - u.dept_id, - d.dept_name, - u.sex, - u.status, - u.create_time - FROM sys_user u - LEFT JOIN sys_dept d ON u.dept_id = d.dept_id - WHERE u.del_flag = '0' - AND u.account_type = '1' - ${ew.customSqlSegment} - ORDER BY u.create_time DESC - """) - Page selectPageTeacherList(@Param("page") Page page, @Param(Constants.WRAPPER) Wrapper wrapper); + SELECT + u.user_id, + u.user_name, + u.nick_name, + u.phonenumber, + u.email, + u.dept_id, + d.dept_name, + u.sex, + u.status, + u.create_time + FROM sys_user u + LEFT JOIN sys_dept d ON u.dept_id = d.dept_id + WHERE u.del_flag = '0' + AND u.account_type = '1' + ${ew.customSqlSegment} + ORDER BY u.user_name ASC, u.create_time DESC + """) + Page selectPageTeacherList(@Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper wrapper); /** * 查询教师信息列表 @@ -55,24 +58,24 @@ public interface SysTeacherMapper extends BaseMapperPlus * @return 教师信息列表 */ @Select(""" - SELECT - u.user_id, - u.user_name, - u.nick_name, - u.phonenumber, - u.email, - u.dept_id, - d.dept_name, - u.sex, - u.status, - u.create_time - FROM sys_user u - LEFT JOIN sys_dept d ON u.dept_id = d.dept_id - WHERE u.del_flag = '0' - AND u.account_type = '1' - ${ew.customSqlSegment} - ORDER BY u.create_time DESC - """) + SELECT + u.user_id, + u.user_name, + u.nick_name, + u.phonenumber, + u.email, + u.dept_id, + d.dept_name, + u.sex, + u.status, + u.create_time + FROM sys_user u + LEFT JOIN sys_dept d ON u.dept_id = d.dept_id + WHERE u.del_flag = '0' + AND u.account_type = '1' + ${ew.customSqlSegment} + ORDER BY u.user_name ASC, u.create_time DESC + """) List selectTeacherList(@Param(Constants.WRAPPER) Wrapper wrapper); /** @@ -83,78 +86,89 @@ public interface SysTeacherMapper extends BaseMapperPlus * @return 教师信息列表 */ @Select(""" - SELECT - u.user_id, - u.user_name, - u.nick_name, - u.phonenumber, - u.email, - u.dept_id, - d.dept_name, - u.sex, - u.status, - u.create_time - FROM sys_user u - LEFT JOIN sys_dept d ON u.dept_id = d.dept_id - WHERE u.del_flag = '0' - AND u.account_type = '1' - AND (u.nick_name LIKE CONCAT('%', #{name}, '%') - OR u.user_name LIKE CONCAT('%', #{name}, '%') - OR u.phonenumber LIKE CONCAT('%', #{name}, '%')) - AND u.status = '0' - ORDER BY u.create_time DESC - LIMIT #{limit} - """) + SELECT + u.user_id, + u.user_name, + u.nick_name, + u.phonenumber, + u.email, + u.dept_id, + d.dept_name, + u.sex, + u.status, + u.create_time + FROM sys_user u + LEFT JOIN sys_dept d ON u.dept_id = d.dept_id + WHERE u.del_flag = '0' + AND u.account_type = '1' + AND (u.nick_name LIKE CONCAT('%', #{name}, '%') + OR u.user_name LIKE CONCAT('%', #{name}, '%') + OR u.phonenumber LIKE CONCAT('%', #{name}, '%')) + AND u.status = '0' + ORDER BY u.user_name ASC, u.create_time DESC + LIMIT #{limit} + """) List searchTeachersByName(@Param("name") String name, @Param("limit") Integer limit); /** - * 根据部门ID获取教师列表 + * 根据部门ID获取教师列表(带数据权限控制) + * + * 说明:添加数据权限注解后,只会返回用户有权限访问的部门的教师 * * @param deptId 部门ID * @return 教师信息列表 */ + @DataPermission({ + @DataColumn(key = "deptName", value = "u.dept_id") + }) @Select(""" - SELECT - u.user_id, - u.user_name, - u.nick_name, - u.phonenumber, - u.email, - u.dept_id, - d.dept_name, - u.sex, - u.status, - u.create_time - FROM sys_user u - LEFT JOIN sys_dept d ON u.dept_id = d.dept_id - WHERE u.del_flag = '0' - AND u.account_type = '1' - AND u.dept_id = #{deptId} - AND u.status = '0' - ORDER BY u.create_time DESC - """) + SELECT + u.user_id, + u.user_name, + u.nick_name, + u.phonenumber, + u.email, + u.dept_id, + d.dept_name, + u.sex, + u.status, + u.create_time + FROM sys_user u + LEFT JOIN sys_dept d ON u.dept_id = d.dept_id + WHERE u.del_flag = '0' + AND u.account_type = '1' + AND u.dept_id = #{deptId} + AND u.status = '0' + ORDER BY u.user_name ASC, u.create_time DESC + """) List selectTeachersByDeptId(@Param("deptId") Long deptId); /** - * 获取部门树形结构(只包含有教师的部门) + * 获取部门树形结构(只包含有教师且用户有权限访问的部门) + * + * 说明:添加数据权限注解后,只会返回用户有权限访问的部门 + * 这样可以避免在同行人选择器中显示用户无权访问的部门 * * @return 部门树信息集合 */ + @DataPermission({ + @DataColumn(key = "deptName", value = "d.dept_id") + }) @Select(""" - SELECT DISTINCT - d.dept_id as id, - d.parent_id, - d.dept_name as label, - d.order_num - FROM sys_dept d - INNER JOIN sys_user u ON d.dept_id = u.dept_id - WHERE d.del_flag = '0' - AND d.status = '0' - AND u.del_flag = '0' - AND u.account_type = '1' - AND u.status = '0' - ORDER BY d.order_num - """) + SELECT DISTINCT + d.dept_id as id, + d.parent_id, + d.dept_name as label, + d.order_num + FROM sys_dept d + INNER JOIN sys_user u ON d.dept_id = u.dept_id + WHERE d.del_flag = '0' + AND d.status = '0' + AND u.del_flag = '0' + AND u.account_type = '1' + AND u.status = '0' + ORDER BY d.order_num, d.create_time DESC + """) List selectDeptTreeWithTeachers(); /** @@ -164,21 +178,21 @@ public interface SysTeacherMapper extends BaseMapperPlus * @return 父级部门信息集合 */ @Select(""" - - """) + + """) List selectDeptsByIds(@Param("deptIds") List deptIds); -} \ No newline at end of file +} diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java index aa4024c3..8e5e95be 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java @@ -121,7 +121,8 @@ public interface SysUserMapper extends BaseMapperPlus { AND sm.perms = #{permission} AND (#{tenantId} IS NULL OR #{tenantId} = '' OR su.tenant_id = #{tenantId}) ORDER BY - su.user_id + su.user_name ASC, + su.create_time DESC """) List selectUserListByPermission(@Param("permission") String permission, @Param("tenantId") String tenantId); diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java index 0a2e485f..9b107ed0 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java @@ -57,6 +57,7 @@ public class SysRoleServiceImpl implements ISysRoleService, RoleService { @Override public TableDataInfo selectPageRoleList(SysRoleBo role, PageQuery pageQuery) { + pageQuery.withDefaultOrder("r.create_time"); Page page = baseMapper.selectPageRoleList(pageQuery.build(), this.buildQueryWrapper(role)); return TableDataInfo.build(page); } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java index 7d34abdd..907b0b10 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java @@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; -import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.system.domain.SysUser; @@ -88,10 +87,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { public List getDeptTreeData() { // 获取有教师的部门 List teacherDepts = baseMapper.selectDeptTreeWithTeachers(); - + // 补全父级部门 List completeDepts = completeParentDepts(teacherDepts); - + // 构建树形结构 return buildDeptTree(completeDepts, 0L); } @@ -105,7 +104,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { private List completeParentDepts(List teacherDepts) { Set allDeptIds = new HashSet<>(); Set parentIds = new HashSet<>(); - + // 收集所有部门ID和父级ID for (SysDeptTreeVo dept : teacherDepts) { allDeptIds.add(dept.getId()); @@ -113,7 +112,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { collectParentIds(dept.getParentId(), parentIds); } } - + // 找出缺失的父级部门ID Set missingParentIds = new HashSet<>(); for (Long parentId : parentIds) { @@ -121,13 +120,13 @@ public class SysTeacherServiceImpl implements ISysTeacherService { missingParentIds.add(parentId); } } - + // 查询缺失的父级部门 List completeDepts = new ArrayList<>(teacherDepts); if (!missingParentIds.isEmpty()) { List parentDepts = baseMapper.selectDeptsByIds(new ArrayList<>(missingParentIds)); completeDepts.addAll(parentDepts); - + // 递归补全更上级的父级部门 List moreParents = completeParentDepts(parentDepts); for (SysDeptTreeVo parent : moreParents) { @@ -136,14 +135,14 @@ public class SysTeacherServiceImpl implements ISysTeacherService { } } } - + return completeDepts; } - + /** * 递归收集所有父级部门ID * - * @param parentId 父级部门ID + * @param parentId 父级部门ID * @param parentIds 父级ID集合 */ private void collectParentIds(Long parentId, Set parentIds) { @@ -161,7 +160,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { */ private List buildDeptTree(List flatList, Long parentId) { List tree = new ArrayList<>(); - + for (SysDeptTreeVo dept : flatList) { if (ObjectUtil.equal(dept.getParentId(), parentId)) { List children = buildDeptTree(flatList, dept.getId()); @@ -169,7 +168,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { tree.add(dept); } } - + return tree; } @@ -198,10 +197,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { if (ObjectUtil.isNull(userId)) { return null; } + // ⚠️ 注意:基础条件(del_flag='0' 和 account_type='1')已在Mapper的SQL中定义 + // 这里只添加userId条件,避免生成重复的WHERE子句 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SysUser::getUserId, userId); - lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL); - lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型 List list = baseMapper.selectTeacherList(lqw); return CollUtil.isNotEmpty(list) ? list.get(0) : null; } @@ -214,11 +213,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { */ private LambdaQueryWrapper buildQueryWrapper(SysTeacherBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - - // 基础条件:未删除的教师用户 - lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL); - lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型 - + + // ⚠️ 注意:基础条件(del_flag='0' 和 account_type='1')已在Mapper的SQL中定义 + // 这里只添加动态查询条件,避免生成重复的WHERE子句 + // 动态查询条件 lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), SysUser::getUserId, bo.getUserId()); lqw.like(StrUtil.isNotBlank(bo.getUserName()), SysUser::getUserName, bo.getUserName()); @@ -228,19 +226,18 @@ public class SysTeacherServiceImpl implements ISysTeacherService { lqw.eq(ObjectUtil.isNotNull(bo.getDeptId()), SysUser::getDeptId, bo.getDeptId()); lqw.eq(StrUtil.isNotBlank(bo.getSex()), SysUser::getSex, bo.getSex()); lqw.eq(StrUtil.isNotBlank(bo.getStatus()), SysUser::getStatus, bo.getStatus()); - + // 搜索关键词(支持姓名、账号、手机号) if (StrUtil.isNotBlank(bo.getSearchKeyword())) { String keyword = bo.getSearchKeyword(); lqw.and(wrapper -> wrapper - .like(SysUser::getNickName, keyword) - .or() - .like(SysUser::getUserName, keyword) - .or() - .like(SysUser::getPhonenumber, keyword) - ); + .like(SysUser::getNickName, keyword) + .or() + .like(SysUser::getUserName, keyword) + .or() + .like(SysUser::getPhonenumber, keyword)); } - + return lqw; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index f03931a6..487d1fbc 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -64,12 +64,14 @@ public class SysUserServiceImpl implements ISysUserService, UserService { @Override public TableDataInfo selectPageUserList(SysUserBo user, PageQuery pageQuery) { + pageQuery.withDefaultOrder("u.user_name"); Page page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapper(user)); return TableDataInfo.build(page); } @Override public TableDataInfo selectPageUserListByOr(SysUserBo user, PageQuery pageQuery) { + pageQuery.withDefaultOrder("u.user_name"); Page page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapperByOr(user)); return TableDataInfo.build(page); } @@ -169,7 +171,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { } // 排序 - wrapper.orderByAsc("u.user_id"); + wrapper.orderByAsc("u.user_name"); return wrapper; } @@ -203,7 +205,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { List ids = StreamUtils.toList(deptList, SysDept::getDeptId); ids.add(user.getDeptId()); w.in("u.dept_id", ids); - }).orderByAsc("u.user_id"); + }).orderByAsc("u.user_name"); if (StringUtils.isNotBlank(user.getExcludeUserIds())) { wrapper.notIn("u.user_id", StringUtils.splitTo(user.getExcludeUserIds(), Convert::toLong)); } @@ -224,7 +226,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService { .like(StringUtils.isNotBlank(user.getUserName()), "u.user_name", user.getUserName()) .eq(StringUtils.isNotBlank(user.getStatus()), "u.status", user.getStatus()) .like(StringUtils.isNotBlank(user.getPhonenumber()), "u.phonenumber", user.getPhonenumber()) - .orderByAsc("u.user_id"); + .orderByAsc("u.user_name"); + pageQuery.withDefaultOrder("u.user_name"); Page page = baseMapper.selectAllocatedList(pageQuery.build(), wrapper); return TableDataInfo.build(page); } @@ -244,7 +247,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService { .notIn(CollUtil.isNotEmpty(userIds), "u.user_id", userIds) .like(StringUtils.isNotBlank(user.getUserName()), "u.user_name", user.getUserName()) .like(StringUtils.isNotBlank(user.getPhonenumber()), "u.phonenumber", user.getPhonenumber()) - .orderByAsc("u.user_id"); + .orderByAsc("u.user_name"); + pageQuery.withDefaultOrder("u.user_name"); Page page = baseMapper.selectUnallocatedList(pageQuery.build(), wrapper); return TableDataInfo.build(page); } @@ -300,7 +304,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService { .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getPhonenumber) .eq(SysUser::getStatus, SystemConstants.NORMAL) .eq(ObjectUtil.isNotNull(deptId), SysUser::getDeptId, deptId) - .in(CollUtil.isNotEmpty(userIds), SysUser::getUserId, userIds)); + .in(CollUtil.isNotEmpty(userIds), SysUser::getUserId, userIds) + .orderByAsc(SysUser::getUserName)); } /** @@ -719,7 +724,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { public List selectUserListByDept(Long deptId) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SysUser::getDeptId, deptId); - lqw.orderByAsc(SysUser::getUserId); + lqw.orderByAsc(SysUser::getUserName); return baseMapper.selectVoList(lqw); } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml index 9057a0e2..5309a7d6 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml +++ b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -16,6 +16,14 @@ * from sys_dept ${ew.getCustomSqlSegment} + + + , create_time desc + + + order by create_time desc + + diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml index 9dd3f2e1..6f0d6132 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml +++ b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -14,6 +14,14 @@ left join sys_role_menu rm on m.menu_id = rm.menu_id left join sys_role r on rm.role_id = r.role_id ${ew.getCustomSqlSegment} + + + , m.create_time desc + + + order by m.create_time desc + + ${ew.getCustomSqlSegment} + + + , r.create_time desc + + + order by r.create_time desc + + + SELECT + id, + create_time, + update_time, + tenant_id, + del_flag, + definition_id, + instance_id, + task_id, + cooperate_type, + node_code, + node_name, + node_type, + target_node_code, + target_node_name, + approver, + collaborator, + skip_type, + flow_status, + message, + signature, + ext, + form_custom, + form_path + FROM flow_his_task + WHERE instance_id = #{instanceId} + AND node_type = #{nodeType} + AND del_flag = '0' + ORDER BY update_time DESC, create_time DESC + + + + diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwTaskMapper.xml b/backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwTaskMapper.xml index f539030b..6d4aeeac 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwTaskMapper.xml +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwTaskMapper.xml @@ -59,6 +59,7 @@ a.instance_id, a.flow_status flow_task_status, a.message, + a.signature, a.ext, a.create_time, a.update_time, diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java index 1cae46cb..306996e0 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java @@ -19,6 +19,8 @@ import org.dromara.common.log.enums.BusinessType; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.xg.domain.vo.XgClassMsgVo; import org.dromara.xg.domain.bo.XgClassMsgBo; +import org.dromara.xg.domain.bo.XgClassStatusBo; +import org.dromara.xg.domain.bo.XgClassStatusBatchBo; import org.dromara.xg.service.IXgClassMsgService; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.satoken.utils.LoginHelper; @@ -90,6 +92,28 @@ public class XgClassMsgController extends BaseController { return toAjax(xgClassMsgService.updateByBo(bo)); } + /** + * 更新班级状态(仅更新 classStatus) + */ + @SaCheckPermission("classMsg:classMsg:edit") + @Log(title = "班级信息-新版", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/status") + public R updateStatus(@Validated @RequestBody XgClassStatusBo bo) { + return toAjax(xgClassMsgService.updateClassStatus(bo.getId(), bo.getClassStatus())); + } + + /** + * 批量更新班级状态(仅更新 classStatus) + */ + @SaCheckPermission("classMsg:classMsg:edit") + @Log(title = "班级信息-新版", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/status/batch") + public R updateStatusBatch(@Validated @RequestBody XgClassStatusBatchBo bo) { + return toAjax(xgClassMsgService.updateClassStatusBatch(bo.getIds(), bo.getClassStatus())); + } + /** * 删除班级信息-新版 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgCollegeLeaderController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgCollegeLeaderController.java new file mode 100644 index 00000000..a233ae10 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgCollegeLeaderController.java @@ -0,0 +1,123 @@ +package org.dromara.xg.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.xg.domain.vo.XgCollegeLeaderVo; +import org.dromara.xg.domain.bo.XgCollegeLeaderBo; +import org.dromara.xg.service.IXgCollegeLeaderService; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * 二级学院分管领导配置 + * + * @author Cursor AI + * @date 2025-01-24 + */ +@Tag(name = "二级学院分管领导配置") +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/xg/ejxyfgld") +public class XgCollegeLeaderController extends BaseController { + + private final IXgCollegeLeaderService xgCollegeLeaderService; + + /** + * 查询二级学院分管领导配置列表 + */ + @Operation(summary = "查询二级学院分管领导配置列表") + @SaCheckPermission("xg:ejxyfgld:list") + @GetMapping("/list") + public TableDataInfo list(XgCollegeLeaderBo bo, PageQuery pageQuery) { + return xgCollegeLeaderService.queryPageList(bo, pageQuery); + } + + /** + * 根据学院ID查询分管领导列表 + */ + @Operation(summary = "根据学院ID查询分管领导列表") + @SaCheckPermission("xg:ejxyfgld:list") + @GetMapping("/listByCollege/{collegeId}") + public R> listByCollege(@PathVariable Long collegeId) { + List list = xgCollegeLeaderService.queryByCollegeId(collegeId); + return R.ok(list); + } + + /** + * 导出二级学院分管领导配置列表 + */ + @Operation(summary = "导出二级学院分管领导配置列表") + @SaCheckPermission("xg:ejxyfgld:export") + @Log(title = "二级学院分管领导配置", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(XgCollegeLeaderBo bo, HttpServletResponse response) { + List list = xgCollegeLeaderService.queryList(bo); + ExcelUtil.exportExcel(list, "二级学院分管领导配置", XgCollegeLeaderVo.class, response); + } + + /** + * 获取二级学院分管领导配置详细信息 + * + * @param id 主键 + */ + @Operation(summary = "获取二级学院分管领导配置详细信息") + @SaCheckPermission("xg:ejxyfgld:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(xgCollegeLeaderService.queryById(id)); + } + + /** + * 新增二级学院分管领导配置 + */ + @Operation(summary = "新增二级学院分管领导配置") + @SaCheckPermission("xg:ejxyfgld:add") + @Log(title = "二级学院分管领导配置", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody XgCollegeLeaderBo bo) { + return toAjax(xgCollegeLeaderService.insertByBo(bo)); + } + + /** + * 修改二级学院分管领导配置 + */ + @Operation(summary = "修改二级学院分管领导配置") + @SaCheckPermission("xg:ejxyfgld:edit") + @Log(title = "二级学院分管领导配置", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody XgCollegeLeaderBo bo) { + return toAjax(xgCollegeLeaderService.updateByBo(bo)); + } + + /** + * 删除二级学院分管领导配置 + * + * @param ids 主键串 + */ + @Operation(summary = "删除二级学院分管领导配置") + @SaCheckPermission("xg:ejxyfgld:remove") + @Log(title = "二级学院分管领导配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return toAjax(xgCollegeLeaderService.deleteWithValidByIds(List.of(ids), true)); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentDynamicsController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentDynamicsController.java new file mode 100644 index 00000000..15d88804 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentDynamicsController.java @@ -0,0 +1,87 @@ +package org.dromara.xg.controller; + +import lombok.RequiredArgsConstructor; +import jakarta.validation.constraints.NotNull; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.xg.domain.vo.XgDormApplicationVo; +import org.dromara.xg.domain.vo.XgHolidayStayVo; +import org.dromara.xg.domain.vo.XgStudentLeaveVo; +import org.dromara.xg.domain.bo.XgDormApplicationBo; +import org.dromara.xg.domain.bo.XgHolidayStayBo; +import org.dromara.xg.domain.bo.XgStudentLeaveBo; +import org.dromara.xg.service.IXgStudentDynamicsService; + +/** + * 学生动向信息管理(宿管员) + * + * @author zlwu + * @date 2025-01-15 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/studentDynamics") +public class XgStudentDynamicsController extends BaseController { + + private final IXgStudentDynamicsService xgStudentDynamicsService; + + /** + * 查询宿管员权限内的学生调宿退宿申请列表 + */ + @SaCheckPermission("studentDynamics:dormApplication:list") + @GetMapping("/dormApplicationList") + public TableDataInfo getDormApplicationList(XgDormApplicationBo bo, PageQuery pageQuery) { + return xgStudentDynamicsService.queryDormApplicationPageList(bo, pageQuery); + } + + /** + * 查询宿管员权限内的学生假期留宿信息列表 + */ + @SaCheckPermission("studentDynamics:holidayStay:list") + @GetMapping("/holidayStayList") + public TableDataInfo getHolidayStayList(XgHolidayStayBo bo, PageQuery pageQuery) { + return xgStudentDynamicsService.queryHolidayStayPageList(bo, pageQuery); + } + + /** + * 查询宿管员权限内的学生请假信息列表 + */ + @SaCheckPermission("studentDynamics:studentLeave:list") + @GetMapping("/studentLeaveList") + public TableDataInfo getStudentLeaveList(XgStudentLeaveBo bo, PageQuery pageQuery) { + return xgStudentDynamicsService.queryStudentLeavePageList(bo, pageQuery); + } + + /** + * 获取调宿退宿申请详细信息 + */ + @SaCheckPermission("studentDynamics:dormApplication:query") + @GetMapping("/dormApplication/{id}") + public R getDormApplicationInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(xgStudentDynamicsService.queryDormApplicationById(id)); + } + + /** + * 获取假期留宿详细信息 + */ + @SaCheckPermission("studentDynamics:holidayStay:query") + @GetMapping("/holidayStay/{id}") + public R getHolidayStayInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(xgStudentDynamicsService.queryHolidayStayById(id)); + } + + /** + * 获取学生请假详细信息 + */ + @SaCheckPermission("studentDynamics:studentLeave:query") + @GetMapping("/studentLeave/{id}") + public R getStudentLeaveInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(xgStudentDynamicsService.queryStudentLeaveById(id)); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentManagerController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentManagerController.java index 24958c48..9a44f490 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentManagerController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentManagerController.java @@ -57,6 +57,50 @@ public class XgStudentManagerController extends BaseController { ExcelUtil.exportExcel(list, "宿舍管理员信息", XgStudentManagerVo.class, response); } + /** + * 获取可用的教职工信息列表 + * + * @param managerType 管理员类型(1 全校管理员 2 自定义管理员 3 班主任) + */ + @SaCheckPermission("xgStudentManager:studentManager:list") + @GetMapping("/listAvailableTeachers") + public R> listAvailableTeachers(@RequestParam("managerType") String managerType) { + List teachers = xgStudentManagerService.queryAvailableTeachers(managerType); + return R.ok(teachers); + } + + /** + * 获取当前正在教学的班主任信息 + * + * @param phone 手机号码(可选,模糊查询) + * @param name 姓名(可选,模糊查询) + * @return 班主任信息列表 + */ + @SaCheckPermission("xgStudentManager:studentManager:list") + @GetMapping("/listActiveClassTeachers") + public R> listActiveClassTeachers( + @RequestParam(value = "phone", required = false) String phone, + @RequestParam(value = "name", required = false) String name) { + List teachers = xgStudentManagerService.queryActiveClassTeachers(phone, name); + return R.ok(teachers); + } + + /** + * 获取全部班主任信息(按班级状态排序) + * + * @param phone 手机号码(可选,模糊查询) + * @param name 姓名(可选,模糊查询) + * @return 班主任信息列表 + */ + @SaCheckPermission("xgStudentManager:studentManager:list") + @GetMapping("/listAllClassTeachers") + public R> listAllClassTeachers( + @RequestParam(value = "phone", required = false) String phone, + @RequestParam(value = "name", required = false) String name) { + List teachers = xgStudentManagerService.queryAllClassTeachers(phone, name); + return R.ok(teachers); + } + /** * 获取宿舍管理员信息详细信息 * @@ -101,32 +145,4 @@ public class XgStudentManagerController extends BaseController { public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { return toAjax(xgStudentManagerService.deleteWithValidByIds(List.of(ids), true)); } - - /** - * 获取可用的教职工信息列表 - * - * @param managerType 管理员类型(1 全校管理员 2 自定义管理员 3 班主任) - */ - @SaCheckPermission("xgStudentManager:studentManager:list") - @GetMapping("/listAvailableTeachers") - public R> listAvailableTeachers(@RequestParam("managerType") String managerType) { - List teachers = xgStudentManagerService.queryAvailableTeachers(managerType); - return R.ok(teachers); - } - - /** - * 获取当前正在教学的班主任信息 - * - * @param phone 手机号码(可选,模糊查询) - * @param name 姓名(可选,模糊查询) - * @return 班主任信息列表 - */ - @SaCheckPermission("xgStudentManager:studentManager:list") - @GetMapping("/listActiveClassTeachers") - public R> listActiveClassTeachers( - @RequestParam(value = "phone", required = false) String phone, - @RequestParam(value = "name", required = false) String name) { - List teachers = xgStudentManagerService.queryActiveClassTeachers(phone, name); - return R.ok(teachers); - } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java index a41a700e..186c7943 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java @@ -823,6 +823,23 @@ public class XgStudentMsgController extends BaseController { return R.ok(approverUserIds); } + /** + * 根据学生ID获取二级学院分管领导ID列表 + * + * @param studentId 学生ID + * @return 分管领导ID列表 + */ + @SaCheckPermission("studentMsg:studentMsg:query") + @GetMapping("/getCollegeLeaders/{studentId}") + public R> getCollegeLeaders(@NotNull(message = "学生ID不能为空") @PathVariable Long studentId) { + log.info("根据学生ID获取二级学院分管领导ID列表 - 学生ID: {}", studentId); + List leaderIds = xgStudentMsgService.getCollegeLeadersByStudentId(studentId); + if (leaderIds.isEmpty()) { + return R.fail("未找到该学生所在学院的分管领导"); + } + return R.ok(leaderIds); + } + /** * 导入数据 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgSysUserController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgSysUserController.java index ed10eac0..9997dd5f 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgSysUserController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgSysUserController.java @@ -28,7 +28,7 @@ public class XgSysUserController extends BaseController { private final IXgSysUserService xgSysUserService; /** - * 查询有效用户列表 + * 查询有效用户列表(教职工account_type=1) */ @SaCheckPermission("xg:sysUser:list") @GetMapping("/activeList") diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgCollegeLeader.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgCollegeLeader.java new file mode 100644 index 00000000..08549ec2 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgCollegeLeader.java @@ -0,0 +1,61 @@ +package org.dromara.xg.domain; + +import org.dromara.common.tenant.core.TenantEntity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 二级学院分管领导配置对象 xg_college_leader + * + * @author Cursor AI + * @date 2025-01-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("xg_college_leader") +public class XgCollegeLeader extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @TableId(value = "id") + private Long id; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic(value = "0", delval = "1") + private Long deleted; + + /** + * 二级学院ID,关联sys_dept.dept_id + */ + private Long collegeId; + + /** + * 二级学院名称 + */ + private String collegeName; + + /** + * 分管领导ID,关联sys_user.user_id + */ + private Long leaderId; + + /** + * 分管领导姓名 + */ + private String leaderName; + + /** + * 备注信息 + */ + private String remark; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgSysUser.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgSysUser.java index cd893516..d6528aff 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgSysUser.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgSysUser.java @@ -27,6 +27,11 @@ public class XgSysUser extends TenantEntity { @TableId(value = "user_id") private Long userId; + /** + * 用户账号 + */ + private String userName; + /** * 用户昵称 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBatchBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBatchBo.java new file mode 100644 index 00000000..3685abda --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBatchBo.java @@ -0,0 +1,23 @@ +package org.dromara.xg.domain.bo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * 班级状态批量更新业务对象(仅用于状态变更) + */ +@Data +public class XgClassStatusBatchBo { + /** 主键ID集合(雪花ID) */ + @NotEmpty(message = "主键ID集合不能为空") + private List ids; + + /** 班级状态:1-正常 2-暂停 3-毕业 4-解散 */ + @NotBlank(message = "班级状态不能为空") + private String classStatus; +} + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBo.java new file mode 100644 index 00000000..4cda0373 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgClassStatusBo.java @@ -0,0 +1,21 @@ +package org.dromara.xg.domain.bo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 班级状态更新业务对象(仅用于状态变更) + */ +@Data +public class XgClassStatusBo { + /** 主键ID(雪花ID) */ + @NotNull(message = "主键ID不能为空") + private Long id; + + /** 班级状态:1-正常 2-暂停 3-毕业 4-解散 */ + @NotBlank(message = "班级状态不能为空") + private String classStatus; +} + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgCollegeLeaderBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgCollegeLeaderBo.java new file mode 100644 index 00000000..e94dcaf6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgCollegeLeaderBo.java @@ -0,0 +1,61 @@ +package org.dromara.xg.domain.bo; + +import org.dromara.xg.domain.XgCollegeLeader; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; + +/** + * 二级学院分管领导配置业务对象 xg_college_leader + * + * @author Cursor AI + * @date 2025-01-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = XgCollegeLeader.class, reverseConvertGenerate = false) +public class XgCollegeLeaderBo extends BaseEntity { + + /** + * 主键ID(雪花ID) + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 删除标志(0代表存在 1代表删除) + */ + private Long deleted; + + /** + * 二级学院ID,关联sys_dept.dept_id + */ + @NotNull(message = "二级学院不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long collegeId; + + /** + * 二级学院名称 + */ + private String collegeName; + + /** + * 分管领导ID,关联sys_user.user_id + */ + @NotNull(message = "分管领导不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long leaderId; + + /** + * 分管领导姓名 + */ + private String leaderName; + + /** + * 备注信息 + */ + private String remark; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherInfoVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherInfoVo.java index 6ec655aa..b7987dca 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherInfoVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherInfoVo.java @@ -12,4 +12,5 @@ public class ClassTeacherInfoVo implements Serializable { private String phone; private String sex; private String className; + private String classStatus; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCollegeLeaderVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCollegeLeaderVo.java new file mode 100644 index 00000000..71ad5900 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCollegeLeaderVo.java @@ -0,0 +1,88 @@ +package org.dromara.xg.domain.vo; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.dromara.xg.domain.XgCollegeLeader; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 二级学院分管领导配置视图对象 xg_college_leader + * + * @author Cursor AI + * @date 2025-01-24 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = XgCollegeLeader.class) +public class XgCollegeLeaderVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 二级学院ID,关联sys_dept.dept_id + */ + @ExcelProperty(value = "二级学院ID") + private Long collegeId; + + /** + * 二级学院名称 + */ + @ExcelProperty(value = "二级学院名称") + private String collegeName; + + /** + * 分管领导ID,关联sys_user.user_id + */ + @ExcelProperty(value = "分管领导ID") + private Long leaderId; + + /** + * 分管领导姓名 + */ + @ExcelProperty(value = "分管领导姓名") + private String leaderName; + + /** + * 备注信息 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建者 + */ + private Long createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + private Long updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgCollegeLeaderMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgCollegeLeaderMapper.java new file mode 100644 index 00000000..b2ce068a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgCollegeLeaderMapper.java @@ -0,0 +1,15 @@ +package org.dromara.xg.mapper; + +import org.dromara.xg.domain.XgCollegeLeader; +import org.dromara.xg.domain.vo.XgCollegeLeaderVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +/** + * 二级学院分管领导配置Mapper接口 + * + * @author Cursor AI + * @date 2025-01-24 + */ +public interface XgCollegeLeaderMapper extends BaseMapperPlus { + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java index 382c3b6b..f85c59ca 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java @@ -69,7 +69,7 @@ public interface XgDormBedMapper extends BaseMapperPlus LEFT JOIN xg_student_msg s ON bs.student_id = s.id AND s.deleted = 0 WHERE b.room_id = #{roomId} AND b.deleted = '0' - ORDER BY b.bed_code + ORDER BY s.student_no DESC, b.bed_code, b.create_time DESC """) List selectBedDetailsByRoomId(@Param("roomId") Long roomId); @@ -125,7 +125,7 @@ public interface XgDormBedMapper extends BaseMapperPlus LEFT JOIN xg_student_msg s ON bs.student_id = s.id AND s.deleted = 0 WHERE r.building_id = #{buildingId} AND b.deleted = '0' - ORDER BY r.room_name, b.bed_code + ORDER BY s.student_no DESC, r.room_name, b.bed_code, b.create_time DESC """) List selectBedDetailsByBuildingId(@Param("buildingId") Long buildingId); @@ -183,7 +183,7 @@ public interface XgDormBedMapper extends BaseMapperPlus WHERE b.room_id = #{roomId} AND bs.college_id = #{collegeId} AND b.deleted = '0' - ORDER BY b.bed_code + ORDER BY s.student_no DESC, b.bed_code, b.create_time DESC """) List selectBedDetailsByRoomIdAndCollegeId(@Param("roomId") Long roomId, @Param("collegeId") Long collegeId); @@ -242,7 +242,7 @@ public interface XgDormBedMapper extends BaseMapperPlus WHERE b.room_id = #{roomId} AND bs.class_id = #{classId} AND b.deleted = '0' - ORDER BY b.bed_code + ORDER BY s.student_no DESC, b.bed_code, b.create_time DESC """) List selectBedDetailsByRoomIdAndClassId(@Param("roomId") Long roomId, @Param("classId") Long classId); diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedStudentMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedStudentMapper.java index cc7d3b58..d938c4fd 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedStudentMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedStudentMapper.java @@ -76,11 +76,13 @@ public interface XgDormBedStudentMapper extends BaseMapperPlus> selectClassOccupiedBedDetails(@Param("classId") Long classId); @@ -137,7 +139,8 @@ public interface XgDormBedStudentMapper extends BaseMapperPlus> selectClassUnoccupiedBedDetails(@Param("classId") Long classId); @@ -184,7 +187,7 @@ public interface XgDormBedStudentMapper extends BaseMapperPlus> selectRoomStudents(@Param("roomId") Long roomId); diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentDynamicsMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentDynamicsMapper.java new file mode 100644 index 00000000..a2601038 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentDynamicsMapper.java @@ -0,0 +1,85 @@ +package org.dromara.xg.mapper; + +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * 学生动向信息管理(宿管员)Mapper接口 + * + * @author zlwu + * @date 2025-01-15 + */ +public interface XgStudentDynamicsMapper { + + /** + * 检查用户是否有宿舍管理权限 + * + * @param userId 用户ID + * @return 管理员类型列表 (1全校管理员 2校区管理员 3宿舍区管理员 4宿舍楼管理员) + */ + List checkUserDormManagerType(@Param("userId") Long userId); + + /** + * 获取全校管理员权限的所有楼栋ID列表 + * + * @param userId 用户ID + * @return 楼栋ID列表 + */ + List getBuildingIdsBySchoolAdmin(@Param("userId") Long userId); + + /** + * 获取校区管理员权限的楼栋ID列表 + * + * @param userId 用户ID + * @return 楼栋ID列表 + */ + List getBuildingIdsByCampusAdmin(@Param("userId") Long userId); + + /** + * 获取宿舍区管理员权限的楼栋ID列表 + * + * @param userId 用户ID + * @return 楼栋ID列表 + */ + List getBuildingIdsByAreaAdmin(@Param("userId") Long userId); + + /** + * 获取宿舍楼管理员权限的楼栋ID列表 + * + * @param userId 用户ID + * @return 楼栋ID列表 + */ + List getBuildingIdsByBuildingAdmin(@Param("userId") Long userId); + + /** + * 获取全校管理员权限的所有学生ID列表 + * + * @param userId 用户ID + * @return 学生ID列表 + */ + List getStudentIdsBySchoolAdmin(@Param("userId") Long userId); + + /** + * 获取校区管理员权限的学生ID列表 + * + * @param userId 用户ID + * @return 学生ID列表 + */ + List getStudentIdsByCampusAdmin(@Param("userId") Long userId); + + /** + * 获取宿舍区管理员权限的学生ID列表 + * + * @param userId 用户ID + * @return 学生ID列表 + */ + List getStudentIdsByAreaAdmin(@Param("userId") Long userId); + + /** + * 获取宿舍楼管理员权限的学生ID列表 + * + * @param userId 用户ID + * @return 学生ID列表 + */ + List getStudentIdsByBuildingAdmin(@Param("userId") Long userId); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMapper.java index 966c286a..96227475 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMapper.java @@ -20,17 +20,40 @@ public interface XgStudentMapper extends BaseMapperPlus> selectTreeNodes(); } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java index 5e3bfc8f..27eda846 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java @@ -29,7 +29,7 @@ public interface XgStudentMsgMapper extends BaseMapperPlus queryPageListExportByDeptId( @Param("ew") LambdaQueryWrapper queryWrapper, - @Param("deptIds") List deptIds); + @Param("deptIds") List deptIds); /** * 根据组织ID列表分页查询学生信息 @@ -41,7 +41,7 @@ public interface XgStudentMsgMapper extends BaseMapperPlus selectPageByDeptIds(@Param("page") Page page, @Param("ew") LambdaQueryWrapper queryWrapper, - @Param("deptIds") List deptIds); + @Param("deptIds") List deptIds); /** * 根据班级ID和姓名查询学生信息列表 @@ -78,6 +78,14 @@ public interface XgStudentMsgMapper extends BaseMapperPlus selectUserIdsByCollegeIdAndRoleKey(@Param("collegeId") Long collegeId, @Param("roleKey") String roleKey); + /** + * 根据学院ID查询二级学院分管领导ID列表 + * + * @param collegeId 学院ID + * @return 分管领导ID列表 + */ + List selectCollegeLeaderIdsByCollegeId(@Param("collegeId") Long collegeId); + /** * 查询二级学院下的学院、专业、班级学生统计信息 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassMsgService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassMsgService.java index 11fccad9..6559048c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassMsgService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassMsgService.java @@ -65,4 +65,22 @@ public interface IXgClassMsgService { * @return 是否删除成功 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 更新班级状态(仅更新 classStatus 字段) + * + * @param id 班级ID + * @param classStatus 班级状态:1-正常 2-暂停 3-毕业 4-解散 + * @return 是否更新成功 + */ + Boolean updateClassStatus(Long id, String classStatus); + + /** + * 批量更新班级状态(仅更新 classStatus 字段) + * + * @param ids 班级ID集合 + * @param classStatus 班级状态:1-正常 2-暂停 3-毕业 4-解散 + * @return 是否更新成功 + */ + Boolean updateClassStatusBatch(Collection ids, String classStatus); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgCollegeLeaderService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgCollegeLeaderService.java new file mode 100644 index 00000000..915752ea --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgCollegeLeaderService.java @@ -0,0 +1,76 @@ +package org.dromara.xg.service; + +import org.dromara.xg.domain.vo.XgCollegeLeaderVo; +import org.dromara.xg.domain.bo.XgCollegeLeaderBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 二级学院分管领导配置Service接口 + * + * @author Cursor AI + * @date 2025-01-24 + */ +public interface IXgCollegeLeaderService { + + /** + * 查询二级学院分管领导配置 + * + * @param id 主键 + * @return 二级学院分管领导配置 + */ + XgCollegeLeaderVo queryById(Long id); + + /** + * 分页查询二级学院分管领导配置列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 二级学院分管领导配置分页列表 + */ + TableDataInfo queryPageList(XgCollegeLeaderBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的二级学院分管领导配置列表 + * + * @param bo 查询条件 + * @return 二级学院分管领导配置列表 + */ + List queryList(XgCollegeLeaderBo bo); + + /** + * 新增二级学院分管领导配置 + * + * @param bo 二级学院分管领导配置 + * @return 是否新增成功 + */ + Boolean insertByBo(XgCollegeLeaderBo bo); + + /** + * 修改二级学院分管领导配置 + * + * @param bo 二级学院分管领导配置 + * @return 是否修改成功 + */ + Boolean updateByBo(XgCollegeLeaderBo bo); + + /** + * 校验并批量删除二级学院分管领导配置信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据学院ID查询分管领导列表 + * + * @param collegeId 学院ID + * @return 分管领导列表 + */ + List queryByCollegeId(Long collegeId); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentDynamicsService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentDynamicsService.java new file mode 100644 index 00000000..397dc1c5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentDynamicsService.java @@ -0,0 +1,49 @@ +package org.dromara.xg.service; + +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.xg.domain.vo.XgDormApplicationVo; +import org.dromara.xg.domain.vo.XgHolidayStayVo; +import org.dromara.xg.domain.vo.XgStudentLeaveVo; +import org.dromara.xg.domain.bo.XgDormApplicationBo; +import org.dromara.xg.domain.bo.XgHolidayStayBo; +import org.dromara.xg.domain.bo.XgStudentLeaveBo; + +/** + * 学生动向信息管理(宿管员)Service接口 + * + * @author zlwu + * @date 2025-01-15 + */ +public interface IXgStudentDynamicsService { + + /** + * 查询宿管员权限内的学生调宿退宿申请列表 + */ + TableDataInfo queryDormApplicationPageList(XgDormApplicationBo bo, PageQuery pageQuery); + + /** + * 查询宿管员权限内的学生假期留宿信息列表 + */ + TableDataInfo queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery); + + /** + * 查询宿管员权限内的学生请假信息列表 + */ + TableDataInfo queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery); + + /** + * 根据主键查询调宿退宿申请详情 + */ + XgDormApplicationVo queryDormApplicationById(Long id); + + /** + * 根据主键查询假期留宿详情 + */ + XgHolidayStayVo queryHolidayStayById(Long id); + + /** + * 根据主键查询学生请假详情 + */ + XgStudentLeaveVo queryStudentLeaveById(Long id); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentManagerService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentManagerService.java index 2cce1dbe..102f3bc4 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentManagerService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentManagerService.java @@ -83,4 +83,13 @@ public interface IXgStudentManagerService { * @return 班主任信息列表 */ List queryActiveClassTeachers(String phone, String name); + + /** + * 获取所有班主任信息(按班级状态排序) + * + * @param phone 手机号 + * @param name 姓名 + * @return 班主任信息列表 + */ + List queryAllClassTeachers(String phone, String name); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java index fe89c09c..030c9bd8 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java @@ -140,6 +140,14 @@ public interface IXgStudentMsgService { */ List getCollegeLeaveApproversByStudentId(Long studentId); + /** + * 根据学生ID获取二级学院分管领导ID列表 + * + * @param studentId 学生ID + * @return 分管领导ID列表 + */ + List getCollegeLeadersByStudentId(Long studentId); + /** * 批量申请床位 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassMsgServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassMsgServiceImpl.java index 6ad59c7a..6a960f00 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassMsgServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassMsgServiceImpl.java @@ -16,8 +16,10 @@ import org.springframework.transaction.annotation.Transactional; import org.dromara.xg.domain.bo.XgClassMsgBo; import org.dromara.xg.domain.vo.XgClassMsgVo; import org.dromara.xg.domain.XgClassMsg; +import org.dromara.xg.domain.XgStudentMsg; import org.dromara.xg.domain.XgSysDept; import org.dromara.xg.mapper.XgClassMsgMapper; +import org.dromara.xg.mapper.XgStudentMsgMapper; import org.dromara.xg.mapper.XgSysDeptMapper; import org.dromara.xg.service.IXgClassMsgService; @@ -38,6 +40,7 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService { private final XgClassMsgMapper baseMapper; private final XgSysDeptMapper deptMapper; + private final XgStudentMsgMapper studentMsgMapper; /** * 查询班级信息-新版 @@ -79,8 +82,10 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService { private LambdaQueryWrapper buildQueryWrapper(XgClassMsgBo bo) { Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - // lqw.orderByAsc(XgClassMsg::getId); - lqw.orderByDesc(XgClassMsg::getCreateTime); + lqw.orderByDesc(XgClassMsg::getGrade) + .orderByAsc(XgClassMsg::getCollegeName) + .orderByAsc(XgClassMsg::getMajorName) + .orderByAsc(XgClassMsg::getClassStatus); lqw.eq(bo.getId() != null, XgClassMsg::getId, bo.getId()); lqw.eq(bo.getCollegeId() != null, XgClassMsg::getCollegeId, bo.getCollegeId()); lqw.eq(bo.getMajorId() != null, XgClassMsg::getMajorId, bo.getMajorId()); @@ -231,6 +236,13 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService { deptMapper.insert(dept); log.info("班级对应的部门信息不存在,已创建部门信息,班级ID:{},部门ID:{}", update.getId(), dept.getDeptId()); } + // 同步更新班级内学生的学制和学历层次 + LambdaUpdateWrapper studentUpdateWrapper = Wrappers.lambdaUpdate(); + studentUpdateWrapper.eq(XgStudentMsg::getClassId, String.valueOf(update.getId())) + .set(XgStudentMsg::getSchoolSystem, update.getSchoolSystem()) + .set(XgStudentMsg::getAcademicLevel, update.getClassType()); + int affected = studentMsgMapper.update(null, studentUpdateWrapper); + log.info("同步更新班级内学生学制/学历字段,班级ID:{},受影响学生数:{}", update.getId(), affected); } return result; @@ -272,4 +284,27 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService { return baseMapper.deleteByIds(ids) > 0; } + + /** + * 仅更新班级状态 + */ + @Override + public Boolean updateClassStatus(Long id, String classStatus) { + LambdaUpdateWrapper uw = Wrappers.lambdaUpdate(); + uw.eq(XgClassMsg::getId, id).set(XgClassMsg::getClassStatus, classStatus); + return baseMapper.update(null, uw) > 0; + } + + /** + * 批量仅更新班级状态 + */ + @Override + public Boolean updateClassStatusBatch(Collection ids, String classStatus) { + if (ids == null || ids.isEmpty()) { + return false; + } + LambdaUpdateWrapper uw = Wrappers.lambdaUpdate(); + uw.in(XgClassMsg::getId, ids).set(XgClassMsg::getClassStatus, classStatus); + return baseMapper.update(null, uw) > 0; + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgCollegeLeaderServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgCollegeLeaderServiceImpl.java new file mode 100644 index 00000000..dac2b934 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgCollegeLeaderServiceImpl.java @@ -0,0 +1,163 @@ +package org.dromara.xg.service.impl; + +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.xg.domain.bo.XgCollegeLeaderBo; +import org.dromara.xg.domain.vo.XgCollegeLeaderVo; +import org.dromara.xg.domain.XgCollegeLeader; +import org.dromara.xg.mapper.XgCollegeLeaderMapper; +import org.dromara.xg.service.IXgCollegeLeaderService; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * 二级学院分管领导配置Service业务层处理 + * + * @author Cursor AI + * @date 2025-01-24 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class XgCollegeLeaderServiceImpl implements IXgCollegeLeaderService { + + private final XgCollegeLeaderMapper baseMapper; + + /** + * 查询二级学院分管领导配置 + * + * @param id 主键 + * @return 二级学院分管领导配置 + */ + @Override + public XgCollegeLeaderVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 分页查询二级学院分管领导配置列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 二级学院分管领导配置分页列表 + */ + @Override + public TableDataInfo queryPageList(XgCollegeLeaderBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的二级学院分管领导配置列表 + * + * @param bo 查询条件 + * @return 二级学院分管领导配置列表 + */ + @Override + public List queryList(XgCollegeLeaderBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + /** + * 根据学院ID查询分管领导列表 + * + * @param collegeId 学院ID + * @return 分管领导列表 + */ + @Override + public List queryByCollegeId(Long collegeId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(XgCollegeLeader::getCollegeId, collegeId); + lqw.orderByDesc(XgCollegeLeader::getCreateTime); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(XgCollegeLeaderBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getCollegeId() != null, XgCollegeLeader::getCollegeId, bo.getCollegeId()); + lqw.like(StringUtils.isNotBlank(bo.getCollegeName()), XgCollegeLeader::getCollegeName, bo.getCollegeName()); + lqw.eq(bo.getLeaderId() != null, XgCollegeLeader::getLeaderId, bo.getLeaderId()); + lqw.like(StringUtils.isNotBlank(bo.getLeaderName()), XgCollegeLeader::getLeaderName, bo.getLeaderName()); + lqw.orderByDesc(XgCollegeLeader::getCreateTime); + return lqw; + } + + /** + * 新增二级学院分管领导配置 + * + * @param bo 二级学院分管领导配置 + * @return 是否新增成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(XgCollegeLeaderBo bo) { + XgCollegeLeader add = MapstructUtils.convert(bo, XgCollegeLeader.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改二级学院分管领导配置 + * + * @param bo 二级学院分管领导配置 + * @return 是否修改成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(XgCollegeLeaderBo bo) { + XgCollegeLeader update = MapstructUtils.convert(bo, XgCollegeLeader.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(XgCollegeLeader entity) { + // 校验是否已存在相同的配置 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(XgCollegeLeader::getCollegeId, entity.getCollegeId()); + lqw.eq(XgCollegeLeader::getLeaderId, entity.getLeaderId()); + if (entity.getId() != null) { + lqw.ne(XgCollegeLeader::getId, entity.getId()); + } + long count = baseMapper.selectCount(lqw); + if (count > 0) { + throw new RuntimeException("该学院已配置此分管领导,不能重复配置!"); + } + } + + /** + * 校验并批量删除二级学院分管领导配置信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 可以添加业务校验逻辑 + } + return baseMapper.deleteByIds(ids) > 0; + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java index 397d2ef9..51f00ad9 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java @@ -1473,7 +1473,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { .eq(XgStudentMsg::getDeleted, 0L) // 未删除 .eq(XgStudentMsg::getIsBoarder, "1") // 需要住校 .eq(XgStudentMsg::getClassId, targetClassId) // 指定班级 - .orderByAsc(XgStudentMsg::getStudentNo); // 按学号排序 + .orderByDesc(XgStudentMsg::getStudentNo); // 按学号倒序排序 List boardingStudents = xgStudentMsgMapper.selectList(studentQuery); diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormManagerServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormManagerServiceImpl.java index d76255c5..26527872 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormManagerServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormManagerServiceImpl.java @@ -99,7 +99,7 @@ public class XgDormManagerServiceImpl implements IXgDormManagerService { params.add("%" + name + "%"); } - sql.append("ORDER BY u.nick_name"); + sql.append("ORDER BY u.user_name ASC, u.nick_name"); try { if (params.isEmpty()) { diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentDynamicsServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentDynamicsServiceImpl.java new file mode 100644 index 00000000..8690fe53 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentDynamicsServiceImpl.java @@ -0,0 +1,353 @@ +package org.dromara.xg.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.xg.domain.vo.XgDormApplicationVo; +import org.dromara.xg.domain.vo.XgHolidayStayVo; +import org.dromara.xg.domain.vo.XgStudentLeaveVo; +import org.dromara.xg.domain.bo.XgDormApplicationBo; +import org.dromara.xg.domain.bo.XgHolidayStayBo; +import org.dromara.xg.domain.bo.XgStudentLeaveBo; +import org.dromara.xg.domain.XgDormApplication; +import org.dromara.xg.domain.XgHolidayStay; +import org.dromara.xg.domain.XgStudentLeave; +import org.dromara.xg.mapper.XgDormApplicationMapper; +import org.dromara.xg.mapper.XgHolidayStayMapper; +import org.dromara.xg.mapper.XgStudentLeaveMapper; +import org.dromara.xg.mapper.XgStudentDynamicsMapper; +import org.dromara.xg.service.IXgStudentDynamicsService; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 学生动向信息管理(宿管员)Service实现 + * + * @author zlwu + * @date 2025-01-15 + */ +@RequiredArgsConstructor +@Service +public class XgStudentDynamicsServiceImpl implements IXgStudentDynamicsService { + + private final XgDormApplicationMapper dormApplicationMapper; + private final XgHolidayStayMapper holidayStayMapper; + private final XgStudentLeaveMapper studentLeaveMapper; + private final XgStudentDynamicsMapper studentDynamicsMapper; + + /** + * 查询宿管员权限内的学生调宿退宿申请列表 + */ + @Override + public TableDataInfo queryDormApplicationPageList(XgDormApplicationBo bo, + PageQuery pageQuery) { + // 获取当前登录用户ID + Long currentUserId = LoginHelper.getUserId(); + + // 构建查询条件 + LambdaQueryWrapper lqw = buildDormApplicationQueryWrapper(bo, currentUserId); + + // 分页查询 + Page result = dormApplicationMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询宿管员权限内的学生假期留宿信息列表 + */ + @Override + public TableDataInfo queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery) { + // 获取当前登录用户ID + Long currentUserId = LoginHelper.getUserId(); + + // 构建查询条件 + LambdaQueryWrapper lqw = buildHolidayStayQueryWrapper(bo, currentUserId); + + // 分页查询 + Page result = holidayStayMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询宿管员权限内的学生请假信息列表 + */ + @Override + public TableDataInfo queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery) { + // 获取当前登录用户ID + Long currentUserId = LoginHelper.getUserId(); + + // 构建查询条件 + LambdaQueryWrapper lqw = buildStudentLeaveQueryWrapper(bo, currentUserId); + + // 分页查询 + Page result = studentLeaveMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 根据主键查询调宿退宿申请详情 + */ + @Override + public XgDormApplicationVo queryDormApplicationById(Long id) { + return dormApplicationMapper.selectVoById(id); + } + + /** + * 根据主键查询假期留宿详情 + */ + @Override + public XgHolidayStayVo queryHolidayStayById(Long id) { + return holidayStayMapper.selectVoById(id); + } + + /** + * 根据主键查询学生请假详情 + */ + @Override + public XgStudentLeaveVo queryStudentLeaveById(Long id) { + return studentLeaveMapper.selectVoById(id); + } + + /** + * 构建调宿退宿申请查询条件(带宿管员权限) + */ + private LambdaQueryWrapper buildDormApplicationQueryWrapper(XgDormApplicationBo bo, + Long currentUserId) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + + // 获取宿管员管理的楼栋ID列表 + List managedBuildingIds = getManagedBuildingIds(currentUserId); + + if (managedBuildingIds.isEmpty()) { + // 如果没有管理权限,返回空结果 + lqw.eq(XgDormApplication::getId, -1); + return lqw; + } + + // 只查询宿管员权限内楼栋的学生申请 + lqw.in(XgDormApplication::getCurrentBuildingId, managedBuildingIds); + + // 添加其他查询条件 + // 姓名或学号检索(OR关系) + if (bo.getStudentName() != null && bo.getStudentNo() != null) { + lqw.and(wrapper -> wrapper + .like(XgDormApplication::getStudentName, bo.getStudentName()) + .or() + .like(XgDormApplication::getStudentNo, bo.getStudentNo())); + } else { + lqw.like(bo.getStudentName() != null, XgDormApplication::getStudentName, bo.getStudentName()); + lqw.like(bo.getStudentNo() != null, XgDormApplication::getStudentNo, bo.getStudentNo()); + } + lqw.like(bo.getCollegeName() != null, XgDormApplication::getCollegeName, bo.getCollegeName()); + lqw.like(bo.getMajorName() != null, XgDormApplication::getMajorName, bo.getMajorName()); + lqw.like(bo.getClassName() != null, XgDormApplication::getClassName, bo.getClassName()); + lqw.eq(bo.getApplicationType() != null, XgDormApplication::getApplicationType, bo.getApplicationType()); + lqw.eq(bo.getApplicationStatus() != null, XgDormApplication::getApplicationStatus, bo.getApplicationStatus()); + lqw.ge(bo.getApplyTime() != null, XgDormApplication::getApplyTime, bo.getApplyTime()); + + // 默认按创建时间倒序 + lqw.orderByDesc(XgDormApplication::getCreateTime); + + return lqw; + } + + /** + * 构建假期留宿查询条件(带宿管员权限) + */ + private LambdaQueryWrapper buildHolidayStayQueryWrapper(XgHolidayStayBo bo, Long currentUserId) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + + // 获取宿管员管理的楼栋ID列表 + List managedBuildingIds = getManagedBuildingIds(currentUserId); + + if (managedBuildingIds.isEmpty()) { + // 如果没有管理权限,返回空结果 + lqw.eq(XgHolidayStay::getId, -1); + return lqw; + } + + // 只查询宿管员权限内楼栋的学生留宿 + lqw.in(XgHolidayStay::getBuildingId, managedBuildingIds); + + // 添加其他查询条件 + // 姓名或学号检索(OR关系) + if (bo.getStudentName() != null && bo.getStudentNo() != null) { + lqw.and(wrapper -> wrapper + .like(XgHolidayStay::getStudentName, bo.getStudentName()) + .or() + .like(XgHolidayStay::getStudentNo, bo.getStudentNo())); + } else { + lqw.like(bo.getStudentName() != null, XgHolidayStay::getStudentName, bo.getStudentName()); + lqw.like(bo.getStudentNo() != null, XgHolidayStay::getStudentNo, bo.getStudentNo()); + } + lqw.like(bo.getCollegeName() != null, XgHolidayStay::getCollegeName, bo.getCollegeName()); + lqw.like(bo.getMajorName() != null, XgHolidayStay::getMajorName, bo.getMajorName()); + lqw.like(bo.getClassName() != null, XgHolidayStay::getClassName, bo.getClassName()); + lqw.eq(bo.getHolidayType() != null, XgHolidayStay::getHolidayType, bo.getHolidayType()); + lqw.ge(bo.getApplyStartDate() != null, XgHolidayStay::getApplyStartDate, bo.getApplyStartDate()); + lqw.le(bo.getApplyEndDate() != null, XgHolidayStay::getApplyEndDate, bo.getApplyEndDate()); + + // 默认按创建时间倒序 + lqw.orderByDesc(XgHolidayStay::getCreateTime); + + return lqw; + } + + /** + * 构建学生请假查询条件(带宿管员权限) + */ + private LambdaQueryWrapper buildStudentLeaveQueryWrapper(XgStudentLeaveBo bo, Long currentUserId) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + + // 获取宿管员权限内的学生ID列表 + List managedStudentIds = getManagedStudentIds(currentUserId); + + if (managedStudentIds.isEmpty()) { + // 如果没有管理权限,返回空结果 + lqw.eq(XgStudentLeave::getId, -1); + return lqw; + } + + // 只查询宿管员权限内的学生请假 + lqw.in(XgStudentLeave::getStudentId, managedStudentIds); + + // 添加其他查询条件 + // 姓名或学号检索(OR关系) + if (bo.getStudentName() != null && bo.getStudentNo() != null) { + lqw.and(wrapper -> wrapper + .like(XgStudentLeave::getStudentName, bo.getStudentName()) + .or() + .like(XgStudentLeave::getStudentNo, bo.getStudentNo())); + } else { + lqw.like(bo.getStudentName() != null, XgStudentLeave::getStudentName, bo.getStudentName()); + lqw.like(bo.getStudentNo() != null, XgStudentLeave::getStudentNo, bo.getStudentNo()); + } + lqw.like(bo.getCollegeName() != null, XgStudentLeave::getCollegeName, bo.getCollegeName()); + lqw.like(bo.getMajorName() != null, XgStudentLeave::getMajorName, bo.getMajorName()); + lqw.like(bo.getClassName() != null, XgStudentLeave::getClassName, bo.getClassName()); + lqw.like(bo.getLeaveTypeName() != null, XgStudentLeave::getLeaveTypeName, bo.getLeaveTypeName()); + lqw.eq(bo.getApprovalStatus() != null, XgStudentLeave::getApprovalStatus, bo.getApprovalStatus()); + lqw.ge(bo.getStartTime() != null, XgStudentLeave::getStartTime, bo.getStartTime()); + lqw.le(bo.getEndTime() != null, XgStudentLeave::getEndTime, bo.getEndTime()); + + // 默认按创建时间倒序 + lqw.orderByDesc(XgStudentLeave::getCreateTime); + + return lqw; + } + + /** + * 获取宿管员管理的楼栋ID列表(支持多级权限合并) + */ + private List getManagedBuildingIds(Long userId) { + // 查询用户管理权限类型 + List managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId); + + if (managerTypes == null || managerTypes.isEmpty()) { + // 没有宿舍管理权限,返回空列表 + return new ArrayList<>(); + } + + // 存储所有权限的楼栋ID,使用Set去重 + Set buildingIdSet = new HashSet<>(); + + // 按照权限分别查询并合并结果(支持用户拥有多种角色的情况) + if (managerTypes.contains("1")) { + // 全校管理员,获取所有楼栋 + List allBuildings = studentDynamicsMapper.getBuildingIdsBySchoolAdmin(userId); + if (allBuildings != null) { + buildingIdSet.addAll(allBuildings); + } + // 全校管理员可以查看所有楼栋,直接返回 + return new ArrayList<>(buildingIdSet); + } + + // 对于非全校管理员,合并多种权限的结果 + if (managerTypes.contains("2")) { + // 校区管理员,获取管理的校区下的楼栋 + List campusBuildings = studentDynamicsMapper.getBuildingIdsByCampusAdmin(userId); + if (campusBuildings != null) { + buildingIdSet.addAll(campusBuildings); + } + } + + if (managerTypes.contains("3")) { + // 宿舍区管理员,获取管理的宿舍区下的楼栋 + List areaBuildings = studentDynamicsMapper.getBuildingIdsByAreaAdmin(userId); + if (areaBuildings != null) { + buildingIdSet.addAll(areaBuildings); + } + } + + if (managerTypes.contains("4")) { + // 宿舍楼管理员,获取管理的楼栋 + List directBuildings = studentDynamicsMapper.getBuildingIdsByBuildingAdmin(userId); + if (directBuildings != null) { + buildingIdSet.addAll(directBuildings); + } + } + + return new ArrayList<>(buildingIdSet); + } + + /** + * 获取宿管员权限内的学生ID列表(支持多级权限合并) + */ + private List getManagedStudentIds(Long userId) { + // 查询用户管理权限类型 + List managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId); + + if (managerTypes == null || managerTypes.isEmpty()) { + // 没有宿舍管理权限,返回空列表 + return new ArrayList<>(); + } + + // 存储所有权限的学生ID,使用Set去重 + Set studentIdSet = new HashSet<>(); + + // 按照权限分别查询并合并结果(支持用户拥有多种角色的情况) + if (managerTypes.contains("1")) { + // 全校管理员,获取所有学生 + List allStudents = studentDynamicsMapper.getStudentIdsBySchoolAdmin(userId); + if (allStudents != null) { + studentIdSet.addAll(allStudents); + } + // 全校管理员可以查看所有学生,直接返回 + return new ArrayList<>(studentIdSet); + } + + // 对于非全校管理员,合并多种权限的结果 + if (managerTypes.contains("2")) { + // 校区管理员,获取管理的校区下的学生 + List campusStudents = studentDynamicsMapper.getStudentIdsByCampusAdmin(userId); + if (campusStudents != null) { + studentIdSet.addAll(campusStudents); + } + } + + if (managerTypes.contains("3")) { + // 宿舍区管理员,获取管理的宿舍区下的学生 + List areaStudents = studentDynamicsMapper.getStudentIdsByAreaAdmin(userId); + if (areaStudents != null) { + studentIdSet.addAll(areaStudents); + } + } + + if (managerTypes.contains("4")) { + // 宿舍楼管理员,获取管理的楼栋下的学生 + List buildingStudents = studentDynamicsMapper.getStudentIdsByBuildingAdmin(userId); + if (buildingStudents != null) { + studentIdSet.addAll(buildingStudents); + } + } + + return new ArrayList<>(studentIdSet); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerServiceImpl.java index 30e6dd6b..0d89abc9 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerServiceImpl.java @@ -157,7 +157,7 @@ public class XgStudentManagerServiceImpl implements IXgStudentManagerService { @Override public List queryActiveClassTeachers(String phone, String name) { StringBuilder sql = new StringBuilder( - "SELECT s.nick_name as name, s.phonenumber as phone, s.sex as sex, c.class_name as className " + + "SELECT s.nick_name as name, s.phonenumber as phone, s.sex as sex, c.class_name as className, c.class_status as classStatus " + "FROM xg_class_msg c, sys_user s " + "WHERE c.deleted = '0' " + "AND c.class_status = '1' " + @@ -180,6 +180,37 @@ public class XgStudentManagerServiceImpl implements IXgStudentManagerService { vo.setPhone(rs.getString("phone")); vo.setSex(rs.getString("sex")); vo.setClassName(rs.getString("className")); + vo.setClassStatus(rs.getString("classStatus")); + return vo; + }); + } + + @Override + public List queryAllClassTeachers(String phone, String name) { + StringBuilder sql = new StringBuilder( + "SELECT s.nick_name as name, s.phonenumber as phone, s.sex as sex, c.class_name as className, c.class_status as classStatus " + + "FROM xg_class_msg c, sys_user s " + + "WHERE c.deleted = '0' " + + "AND c.class_teacher_id IS NOT NULL " + + "AND c.class_teacher_id = s.user_id " + + "AND s.del_flag = '0'"); + List params = new java.util.ArrayList<>(); + if (StringUtils.isNotBlank(name)) { + sql.append(" AND s.nick_name LIKE ?"); + params.add("%" + name + "%"); + } + if (StringUtils.isNotBlank(phone)) { + sql.append(" AND s.phonenumber LIKE ?"); + params.add("%" + phone + "%"); + } + sql.append(" ORDER BY c.class_status ASC, c.class_name ASC, c.create_time DESC"); + return jdbcTemplate.query(sql.toString(), params.toArray(), (rs, rowNum) -> { + ClassTeacherInfoVo vo = new ClassTeacherInfoVo(); + vo.setName(rs.getString("name")); + vo.setPhone(rs.getString("phone")); + vo.setSex(rs.getString("sex")); + vo.setClassName(rs.getString("className")); + vo.setClassStatus(rs.getString("classStatus")); return vo; }); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerSourceServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerSourceServiceImpl.java index bed0bb0a..db4b8e32 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerSourceServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentManagerSourceServiceImpl.java @@ -204,7 +204,7 @@ public class XgStudentManagerSourceServiceImpl implements IXgStudentManagerSourc if (isSchoolManager) { // 全校学生管理员:查询id=101下所有部门数据,所有节点都可点击 String sql = "SELECT dept_id, dept_name, parent_id, dept_category, order_num FROM sys_dept " + - "WHERE (dept_id = 101 OR ancestors LIKE '%,101' OR ancestors LIKE '%,101,%') AND status = '0' AND del_flag = '0' ORDER BY order_num"; + "WHERE (dept_id = 101 OR ancestors LIKE '%,101' OR ancestors LIKE '%,101,%') AND status = '0' AND del_flag = '0' ORDER BY order_num, create_time DESC"; List> deptMaps = jdbcTemplate.queryForList(sql); // 构建部门树 @@ -310,7 +310,7 @@ public class XgStudentManagerSourceServiceImpl implements IXgStudentManagerSourc // 查询所有部门信息 String inSql = finalDeptIds.stream().map(String::valueOf).collect(Collectors.joining(",")); String sql = "SELECT dept_id, dept_name, parent_id, dept_category, order_num FROM sys_dept " + - "WHERE dept_id IN (" + inSql + ") AND status = '0' AND del_flag = '0' ORDER BY order_num"; + "WHERE dept_id IN (" + inSql + ") AND status = '0' AND del_flag = '0' ORDER BY order_num, create_time DESC"; List> deptMaps = jdbcTemplate.queryForList(sql); // 构建部门树 diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java index 654d757e..9ecee881 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java @@ -145,8 +145,12 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { LambdaQueryWrapper lqw = buildQueryWrapperForDeptQuery(bo); // 第三步:分页查询学生信息 + List classIdList = filteredDeptIds.stream() + .map(String::valueOf) + .collect(Collectors.toList()); + List result = baseMapper.queryPageListExportByDeptId(lqw, - filteredDeptIds); + classIdList); return result; } @@ -178,7 +182,11 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { LambdaQueryWrapper lqw = buildQueryWrapperForDeptQuery(bo); // 第三步:分页查询学生信息 - Page result = baseMapper.selectPageByDeptIds(pageQuery.build(), lqw, filteredDeptIds); + List classIdList = filteredDeptIds.stream() + .map(String::valueOf) + .collect(Collectors.toList()); + + Page result = baseMapper.selectPageByDeptIds(pageQuery.build(), lqw, classIdList); return TableDataInfo.build(result); } @@ -358,7 +366,7 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { private LambdaQueryWrapper buildQueryWrapper(XgStudentMsgBo bo) { Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.orderByAsc(XgStudentMsg::getId); + lqw.orderByDesc(XgStudentMsg::getStudentNo).orderByDesc(XgStudentMsg::getId); lqw.eq(bo.getDeleted() != null, XgStudentMsg::getDeleted, bo.getDeleted()); lqw.eq(StringUtils.isNotBlank(bo.getStudentNo()), XgStudentMsg::getStudentNo, bo.getStudentNo()); lqw.eq(StringUtils.isNotBlank(bo.getIdCard()), XgStudentMsg::getIdCard, bo.getIdCard()); @@ -715,8 +723,12 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { LambdaQueryWrapper lqw = buildAdvancedSearchQueryWrapper(bo); // 第三步:分页查询学生信息 + List classIdList = filteredDeptIds.stream() + .map(String::valueOf) + .collect(Collectors.toList()); + Page result = baseMapper.selectPageByDeptIds( - new Page<>(bo.getPageNum(), bo.getPageSize()), lqw, filteredDeptIds); + new Page<>(bo.getPageNum(), bo.getPageSize()), lqw, classIdList); return TableDataInfo.build(result); } @@ -1095,6 +1107,32 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { return approverUserIds; } + /** + * 根据学生ID获取二级学院分管领导ID列表 + * + * @param studentId 学生ID + * @return 分管领导ID列表 + */ + @Override + public List getCollegeLeadersByStudentId(Long studentId) { + log.info("根据学生ID获取二级学院分管领导ID列表 - 学生ID: {}", studentId); + + // 1. 根据学生ID查询学院ID + Long collegeId = baseMapper.selectCollegeIdByStudentId(studentId); + if (collegeId == null) { + log.warn("未找到学生对应的学院信息 - 学生ID: {}", studentId); + return new ArrayList<>(); + } + + log.info("学生所在学院ID: {}", collegeId); + + // 2. 根据学院ID查询二级学院分管领导ID列表 + List leaderIds = baseMapper.selectCollegeLeaderIdsByCollegeId(collegeId); + + log.info("找到的二级学院分管领导数量: {}", leaderIds.size()); + return leaderIds; + } + /** * 批量申请床位 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentServiceImpl.java index 2df9c8b8..381b7546 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentServiceImpl.java @@ -67,6 +67,7 @@ public class XgStudentServiceImpl implements IXgStudentService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(name), XgStudentMsg::getName, name); lqw.eq(XgStudentMsg::getStudentStatus, "1"); // 只查询在籍学生 + lqw.orderByDesc(XgStudentMsg::getStudentNo); lqw.last("LIMIT " + (limit != null ? limit : 10)); return baseMapper.selectVoList(lqw); } @@ -149,7 +150,7 @@ public class XgStudentServiceImpl implements IXgStudentService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(XgStudentMsg::getClassId, classId); lqw.eq(XgStudentMsg::getStudentStatus, "1"); // 只查询在籍学生 - lqw.orderByAsc(XgStudentMsg::getStudentNo); // 按学号排序 + lqw.orderByDesc(XgStudentMsg::getStudentNo); // 按学号倒序排序 return baseMapper.selectVoList(lqw); } @@ -209,6 +210,7 @@ public class XgStudentServiceImpl implements IXgStudentService { lqw.eq(StringUtils.isNotBlank(bo.getStudentStatus()), XgStudentMsg::getStudentStatus, bo.getStudentStatus()); lqw.eq(bo.getEnrollmentYear() != null, XgStudentMsg::getEnrollmentYear, bo.getEnrollmentYear()); lqw.eq(bo.getEnrollmentDate() != null, XgStudentMsg::getEnrollmentDate, bo.getEnrollmentDate()); + lqw.orderByDesc(XgStudentMsg::getStudentNo).orderByDesc(XgStudentMsg::getId); return lqw; } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml index 827ed2ae..2b14d67a 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml @@ -115,7 +115,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND dhc.check_date <= #{params.endTime} - ORDER BY dhc.check_date DESC, dhc.id ASC, dhss.student_no ASC + ORDER BY dhss.student_no DESC, dhc.check_date DESC, dhc.id ASC, dhc.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormViolation/XgDormViolationMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormViolation/XgDormViolationMapper.xml index e81ccb55..3aad3223 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormViolation/XgDormViolationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormViolation/XgDormViolationMapper.xml @@ -76,7 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (s.student_no LIKE CONCAT('%', #{bo.keyword}, '%') OR s.name LIKE CONCAT('%', #{bo.keyword}, '%')) - ORDER BY s.student_no ASC + ORDER BY s.student_no DESC, s.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/ejxyfgld/XgCollegeLeaderMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/ejxyfgld/XgCollegeLeaderMapper.xml new file mode 100644 index 00000000..91d2a198 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/ejxyfgld/XgCollegeLeaderMapper.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/holidayStay/XgHolidayStayMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/holidayStay/XgHolidayStayMapper.xml index 4cf092d0..1f03b3e0 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/holidayStay/XgHolidayStayMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/holidayStay/XgHolidayStayMapper.xml @@ -76,7 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (s.student_no LIKE CONCAT('%', #{bo.keyword}, '%') OR s.name LIKE CONCAT('%', #{bo.keyword}, '%')) - ORDER BY s.student_no ASC + ORDER BY s.student_no DESC, s.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml index 1c7de23c..ecc20c48 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml @@ -66,7 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - ORDER BY create_time DESC + ORDER BY student_no DESC, create_time DESC @@ -100,7 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - ORDER BY create_time DESC + ORDER BY student_no DESC, create_time DESC @@ -134,7 +134,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND name LIKE CONCAT('%', #{name}, '%') - ORDER BY create_time DESC + ORDER BY student_no DESC, create_time DESC @@ -171,6 +171,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (d.dept_id = #{collegeId} OR FIND_IN_SET(#{collegeId}, d.ancestors)) + + + @@ -247,7 +255,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND sm.major_id IS NOT NULL AND sm.class_id IS NOT NULL GROUP BY c.grade, sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name - ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name + ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC @@ -293,7 +301,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (d.parent_id = #{secondaryCollegeId} OR d.dept_id = #{secondaryCollegeId} OR FIND_IN_SET(#{secondaryCollegeId}, d.ancestors)) AND sm.college_id IS NOT NULL GROUP BY sm.college_id, sm.college_name - ORDER BY sm.college_name + ORDER BY sm.college_name, MAX(sm.create_time) DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsgBatch/XgStudentMsgBatchMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsgBatch/XgStudentMsgBatchMapper.xml index a06b5718..7d3b154a 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsgBatch/XgStudentMsgBatchMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsgBatch/XgStudentMsgBatchMapper.xml @@ -24,14 +24,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" inner join sys_user s2 on xsmb.update_by = s2.user_id WHERE batch_id = #{batchId} and xsmb.id = #{userId} - ORDER BY xsmb.create_time asc + ORDER BY xsmb.create_time DESC @@ -183,7 +183,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND xsmb.info_status = #{infoStatus} - ORDER BY xsmb.create_time DESC + ORDER BY xsmb.student_no DESC, xsmb.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentDynamicsMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentDynamicsMapper.xml new file mode 100644 index 00000000..096e0f8a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentDynamicsMapper.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml index 3585dcf8..9aea303d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml @@ -58,7 +58,7 @@ GROUP BY dr.college_id ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id WHERE d.parent_id = 101 AND d.del_flag = '0' - ORDER BY d.order_num ASC, d.dept_name ASC + ORDER BY d.order_num ASC, d.dept_name ASC, d.create_time DESC @@ -118,7 +118,7 @@ GROUP BY dr.college_id ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id WHERE d.parent_id = 101 AND d.del_flag = '0' - ORDER BY d.order_num ASC, d.dept_name ASC + ORDER BY d.order_num ASC, d.dept_name ASC, d.create_time DESC @@ -178,7 +178,7 @@ GROUP BY dr.major_id ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0' - ORDER BY d.order_num ASC, d.dept_name ASC + ORDER BY d.order_num ASC, d.dept_name ASC, d.create_time DESC @@ -242,7 +242,7 @@ GROUP BY dr.major_id ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0' - ORDER BY d.order_num ASC, d.dept_name ASC + ORDER BY d.order_num ASC, d.dept_name ASC, d.create_time DESC @@ -303,7 +303,7 @@ ) as teachingViolationDeduction FROM xg_class_msg c WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0 - ORDER BY c.class_name ASC + ORDER BY c.class_name ASC, c.create_time DESC @@ -458,7 +458,7 @@ )) as totalDeduction FROM xg_class_msg c WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0 - ORDER BY c.class_name ASC + ORDER BY c.class_name ASC, c.create_time DESC @@ -472,7 +472,7 @@ order_num as sort FROM sys_dept WHERE parent_id = 101 AND del_flag = '0' - ORDER BY order_num ASC, dept_name ASC + ORDER BY order_num ASC, dept_name ASC, create_time DESC @@ -486,7 +486,7 @@ WHERE parent_id = #{collegeId} AND dept_category LIKE 'zy-%' AND del_flag = '0' - ORDER BY order_num ASC, dept_name ASC + ORDER BY order_num ASC, dept_name ASC, create_time DESC @@ -498,7 +498,7 @@ 0 as sort FROM xg_class_msg WHERE major_id = #{majorId} AND deleted = 0 - ORDER BY class_name ASC + ORDER BY class_name ASC, create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgSysUserMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgSysUserMapper.xml index bd36d7d1..821baf75 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgSysUserMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgSysUserMapper.xml @@ -6,10 +6,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -18,18 +20,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationScheduleController.java b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationScheduleController.java index 5d3cc5be..367aa0cc 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationScheduleController.java +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationScheduleController.java @@ -212,7 +212,7 @@ public class XljkConsultationScheduleController { "JOIN sys_role r ON ur.role_id = r.role_id " + "LEFT JOIN xljk_consulter c ON u.user_id = c.user_id AND c.deleted = 0 " + "WHERE r.role_key = 'xljk_consultant' AND u.del_flag = '0' " + - "ORDER BY u.nick_name"; + "ORDER BY u.user_name ASC, u.nick_name, u.create_time DESC"; List> counselorOptions = jdbcTemplate.queryForList(sql); return R.ok(counselorOptions); diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationAppointmentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationAppointmentServiceImpl.java index 30debfdf..e78ace86 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationAppointmentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationAppointmentServiceImpl.java @@ -47,16 +47,16 @@ import java.time.Month; public class XljkConsultationAppointmentServiceImpl implements IXljkConsultationAppointmentService { @Autowired private XljkConsultationAppointmentMapper mapper; - + @Autowired private XljkConsultationScheduleMapper scheduleMapper; - + @Autowired private ISysUserService userService; @Autowired private JdbcTemplate jdbcTemplate; - + @Autowired private DataSource dataSource; @@ -68,29 +68,29 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation log.warn("未找到ID为{}的预约记录", id); return null; } - + log.info("找到预约记录: {}", entity); XljkConsultationAppointmentVo vo = new XljkConsultationAppointmentVo(); BeanUtils.copyProperties(entity, vo); - + // 记录关键字段 - log.info("复制后的VO对象关键字段:status={}, communicationDuration={}, consultationContent长度={}, counselorSuggestion长度={}", + log.info("复制后的VO对象关键字段:status={}, communicationDuration={}, consultationContent长度={}, counselorSuggestion长度={}", vo.getStatus(), vo.getCommunicationDuration(), vo.getConsultationContent() != null ? vo.getConsultationContent().length() : 0, vo.getCounselorSuggestion() != null ? vo.getCounselorSuggestion().length() : 0); - + // 使用与selectList相同的方式查询关联信息 String sql = "SELECT s.name as student_name, s.student_no, s.class_name, s.college_name, s.major_name, " + - "u.nick_name as counselor_name " + - "FROM xg_student_msg s " + - "LEFT JOIN sys_user u ON u.user_id = " + entity.getCounselorId() + " " + - "WHERE s.id = " + entity.getStudentId(); - + "u.nick_name as counselor_name " + + "FROM xg_student_msg s " + + "LEFT JOIN sys_user u ON u.user_id = " + entity.getCounselorId() + " " + + "WHERE s.id = " + entity.getStudentId(); + try { Map result = jdbcTemplate.queryForMap(sql); log.info("关联查询结果: {}", result); - + // 填充关联信息 vo.setStudentName((String) result.get("student_name")); vo.setStudentNo((String) result.get("student_no")); @@ -103,10 +103,11 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation // 如果关联查询失败,尝试分别查询 try { // 查询学生信息 - String studentSql = "SELECT name, student_no, class_name, college_name, major_name FROM xg_student_msg WHERE id = " + entity.getStudentId(); + String studentSql = "SELECT name, student_no, class_name, college_name, major_name FROM xg_student_msg WHERE id = " + + entity.getStudentId(); Map studentInfo = jdbcTemplate.queryForMap(studentSql); log.info("学生信息查询结果: {}", studentInfo); - + vo.setStudentName((String) studentInfo.get("name")); vo.setStudentNo((String) studentInfo.get("student_no")); vo.setClassName((String) studentInfo.get("class_name")); @@ -115,7 +116,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } catch (Exception ex) { log.error("查询学生信息失败", ex); } - + try { // 查询咨询师信息 SysUserVo counselor = userService.selectUserById(entity.getCounselorId()); @@ -127,13 +128,13 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation log.error("查询咨询师信息失败", ex); } } - + // 再次检查关键字段 if (vo.getStatus() != null && vo.getStatus() == 1) { - log.info("已到访状态,检查咨询结果相关字段:communicationDuration={}", + log.info("已到访状态,检查咨询结果相关字段:communicationDuration={}", vo.getCommunicationDuration()); } - + log.info("返回预约记录详情: {}", vo); return vo; } @@ -149,85 +150,86 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation sqlBuilder.append("LEFT JOIN xg_student_msg s ON a.student_id = s.id "); sqlBuilder.append("LEFT JOIN sys_user u ON a.counselor_id = u.user_id "); sqlBuilder.append("WHERE a.deleted = 0 "); - + List params = new ArrayList<>(); - + // 添加查询条件 if (bo.getStudentId() != null) { sqlBuilder.append("AND a.student_id = ? "); params.add(bo.getStudentId()); } - + if (bo.getCounselorId() != null) { sqlBuilder.append("AND a.counselor_id = ? "); params.add(bo.getCounselorId()); } - + if (bo.getStatus() != null) { sqlBuilder.append("AND a.status = ? "); params.add(bo.getStatus()); } - + if (bo.getAppointmentDate() != null) { sqlBuilder.append("AND a.appointment_date = ? "); params.add(bo.getAppointmentDate()); } - + // 添加新增字段查询条件 if (StringUtils.isNotBlank(bo.getStudentName())) { sqlBuilder.append("AND s.name LIKE ? "); params.add("%" + bo.getStudentName() + "%"); } - + if (StringUtils.isNotBlank(bo.getStudentNo())) { sqlBuilder.append("AND s.student_no LIKE ? "); params.add("%" + bo.getStudentNo() + "%"); } - + if (StringUtils.isNotBlank(bo.getCollegeName())) { sqlBuilder.append("AND s.college_name LIKE ? "); params.add("%" + bo.getCollegeName() + "%"); } - + if (StringUtils.isNotBlank(bo.getMajorName())) { sqlBuilder.append("AND s.major_name LIKE ? "); params.add("%" + bo.getMajorName() + "%"); } - + if (StringUtils.isNotBlank(bo.getClassName())) { sqlBuilder.append("AND s.class_name LIKE ? "); params.add("%" + bo.getClassName() + "%"); } - + // 添加排序 - sqlBuilder.append("ORDER BY a.create_time DESC"); - + sqlBuilder.append("ORDER BY s.student_no DESC, a.create_time DESC"); + log.info("执行SQL查询: {}", sqlBuilder.toString()); log.info("参数: {}", params); - + // 执行查询 List> resultList = jdbcTemplate.queryForList(sqlBuilder.toString(), params.toArray()); - + log.info("查询结果数量: {}", resultList.size()); if (!resultList.isEmpty()) { Map firstRow = resultList.get(0); - log.info("第一条记录: student_id={}, student_name={}, student_no={}, class_name={}, college_name={}, major_name={}, counselor_name={}", - firstRow.get("student_id"), - firstRow.get("student_name"), - firstRow.get("student_no"), - firstRow.get("class_name"), - firstRow.get("college_name"), - firstRow.get("major_name"), - firstRow.get("counselor_name")); + log.info( + "第一条记录: student_id={}, student_name={}, student_no={}, class_name={}, college_name={}, major_name={}, counselor_name={}", + firstRow.get("student_id"), + firstRow.get("student_name"), + firstRow.get("student_no"), + firstRow.get("class_name"), + firstRow.get("college_name"), + firstRow.get("major_name"), + firstRow.get("counselor_name")); } - + // 转换为VO对象 return resultList.stream().map(row -> { XljkConsultationAppointmentVo vo = new XljkConsultationAppointmentVo(); - + // 获取学生ID用于计算本学期到访次数 Long studentId = row.get("student_id") != null ? ((Number) row.get("student_id")).longValue() : null; - + // 设置基本字段 vo.setId(((Number) row.get("id")).longValue()); vo.setTenantId((String) row.get("tenant_id")); @@ -236,7 +238,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation vo.setCreateTime(convertToDate(row.get("create_time"))); vo.setUpdateBy(row.get("update_by") != null ? ((Number) row.get("update_by")).longValue() : null); vo.setUpdateTime(convertToDate(row.get("update_time"))); - + // 处理deleted字段,可能是Boolean或Integer类型 Object deletedObj = row.get("deleted"); if (deletedObj instanceof Boolean) { @@ -246,7 +248,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } else { vo.setDeleted(0); // 默认值 } - + vo.setStudentId(row.get("student_id") != null ? ((Number) row.get("student_id")).longValue() : null); vo.setCounselorId(row.get("counselor_id") != null ? ((Number) row.get("counselor_id")).longValue() : null); vo.setScheduleId(row.get("schedule_id") != null ? ((Number) row.get("schedule_id")).longValue() : null); @@ -254,11 +256,11 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation vo.setAppointmentTime((String) row.get("appointment_time")); vo.setAppointmentDetailTime((String) row.get("appointment_detail_time")); vo.setReason((String) row.get("reason")); - + // 记录status字段的值和类型 Object statusObj = row.get("status"); log.info("status字段值: {}, 类型: {}", statusObj, statusObj != null ? statusObj.getClass().getName() : "null"); - + // 将status字段从String类型改为Integer类型 if (statusObj != null) { if (statusObj instanceof String) { @@ -280,14 +282,16 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation log.info("status字段为null,设置默认值0"); vo.setStatus(0); // 默认为待咨询 } - - vo.setCommunicationDuration(row.get("communication_duration") != null ? ((Number) row.get("communication_duration")).intValue() : null); + + vo.setCommunicationDuration( + row.get("communication_duration") != null ? ((Number) row.get("communication_duration")).intValue() + : null); vo.setConsultationContent((String) row.get("consultation_content")); vo.setCounselorSuggestion((String) row.get("counselor_suggestion")); - + // 设置咨询主题字段 vo.setConsultTheme((String) row.get("consult_theme")); - + // 设置心理健康相关字段 Object psychologicalHistoryObj = row.get("psychological_history"); if (psychologicalHistoryObj != null) { @@ -301,11 +305,11 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } } } - + vo.setPsychologicalHistoryDetail((String) row.get("psychological_history_detail")); vo.setBasicSituation((String) row.get("basic_situation")); vo.setMajorLifeEvents((String) row.get("major_life_events")); - + Object needReferralObj = row.get("need_referral"); if (needReferralObj != null) { if (needReferralObj instanceof Number) { @@ -318,7 +322,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } } } - + // 设置关联字段 vo.setStudentName((String) row.get("student_name")); vo.setStudentNo((String) row.get("student_no")); @@ -326,22 +330,23 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation vo.setCounselorName((String) row.get("counselor_name")); vo.setCollegeName((String) row.get("college_name")); vo.setMajorName((String) row.get("major_name")); - + // 计算本学期到访次数 if (studentId != null) { vo.setSemesterVisitCount(calculateSemesterVisitCount(studentId)); } else { vo.setSemesterVisitCount(0); } - + return vo; }).collect(Collectors.toList()); } - + /** * 计算学生本学期到访次数 * 上学期:3月1日-8月31日 * 下学期:9月1日-2月29日 + * * @param studentId 学生ID * @return 本学期到访次数 */ @@ -350,7 +355,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation // 获取当前日期 LocalDate now = LocalDate.now(); String startDate, endDate; - + // 判断当前处于哪个学期 if (now.getMonthValue() >= 3 && now.getMonthValue() <= 8) { // 上学期:3月1日-8月31日 @@ -373,16 +378,16 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation LocalDate lastDayOfFeb = currentFeb.withDayOfMonth(currentFeb.lengthOfMonth()); endDate = lastDayOfFeb.toString(); } - + // 查询该学生在当前学期内状态为已到访(status=1)的记录数 String sql = "SELECT COUNT(*) FROM xljk_consultation_appointment " + - "WHERE student_id = ? AND status = 1 AND deleted = 0 " + - "AND appointment_date >= ? AND appointment_date <= ?"; - + "WHERE student_id = ? AND status = 1 AND deleted = 0 " + + "AND appointment_date >= ? AND appointment_date <= ?"; + Integer count = jdbcTemplate.queryForObject(sql, Integer.class, studentId, startDate, endDate); - + log.debug("学生ID: {}, 学期时间段: {} - {}, 到访次数: {}", studentId, startDate, endDate, count); - + return count != null ? count : 0; } catch (Exception e) { log.error("计算学生本学期到访次数失败,学生ID: {}", studentId, e); @@ -408,29 +413,30 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation public boolean deleteById(Long id) { return mapper.deleteById(id) > 0; } - + @Override public List> getAvailableTimeSlots(Date startDate, Date endDate) { // 查询指定日期范围内的可用时段 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.ge(XljkConsultationSchedule::getScheduleDate, startDate) - .le(XljkConsultationSchedule::getScheduleDate, endDate) - .eq(XljkConsultationSchedule::getStatus, "0"); // 0表示正常可用 - + .le(XljkConsultationSchedule::getScheduleDate, endDate) + .eq(XljkConsultationSchedule::getStatus, "0"); // 0表示正常可用 + List schedules = scheduleMapper.selectList(queryWrapper); - + // 查询已预约的时段 LambdaQueryWrapper appointmentQueryWrapper = new LambdaQueryWrapper<>(); appointmentQueryWrapper.ge(XljkConsultationAppointment::getAppointmentDate, startDate) - .le(XljkConsultationAppointment::getAppointmentDate, endDate) - .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 - + .le(XljkConsultationAppointment::getAppointmentDate, endDate) + .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 + List appointments = mapper.selectList(appointmentQueryWrapper); - + // 构建已预约时段的映射,用于快速查找 Map> bookedDetailSlots = new HashMap<>(); for (XljkConsultationAppointment appointment : appointments) { - String key = new SimpleDateFormat("yyyy-MM-dd").format(appointment.getAppointmentDate()) + "_" + appointment.getScheduleId(); + String key = new SimpleDateFormat("yyyy-MM-dd").format(appointment.getAppointmentDate()) + "_" + + appointment.getScheduleId(); if (!bookedDetailSlots.containsKey(key)) { bookedDetailSlots.put(key, new ArrayList<>()); } @@ -438,26 +444,27 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation bookedDetailSlots.get(key).add(appointment.getAppointmentDetailTime()); } } - + // 构建可用时段列表 List> availableSlots = new ArrayList<>(); Date now = new Date(); // 获取当前时间 - + for (XljkConsultationSchedule schedule : schedules) { String dateStr = new SimpleDateFormat("yyyy-MM-dd").format(schedule.getScheduleDate()); String key = dateStr + "_" + schedule.getId(); - + // 检查该时段的可用详细时段(用于时间判断,但总容量固定为1) - List detailSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime()); + List detailSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), + schedule.getEndTime()); int totalDetailSlots = detailSlots.size(); // 保留用于时间过期判断 int totalSlots = 1; // 每个时段只能预约1个人 int bookedCount = 0; boolean isExpired = false; - + // 检查是否已过期 - 更精确的判断逻辑 try { // 使用已生成的详细时间段进行过期判断 - + // 检查是否还有未过期的详细时间段 boolean hasValidSlots = false; for (TimeSlotDetailVo detailSlot : detailSlots) { @@ -465,7 +472,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation String[] timeParts = detailSlot.getStartTime().split(":"); int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); - + // 设置该详细时间段的开始时间 Calendar detailSlotCalendar = Calendar.getInstance(); detailSlotCalendar.setTime(schedule.getScheduleDate()); @@ -473,31 +480,31 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation detailSlotCalendar.set(Calendar.MINUTE, minute); detailSlotCalendar.set(Calendar.SECOND, 0); detailSlotCalendar.set(Calendar.MILLISECOND, 0); - + Date detailSlotStartTime = detailSlotCalendar.getTime(); - + // 如果该详细时间段未过期,则整个时段还有可用时间 if (detailSlotStartTime.after(now)) { hasValidSlots = true; break; } } - + // 如果没有任何未过期的详细时间段,则标记为已过期 isExpired = !hasValidSlots; - + } catch (Exception e) { // 如果解析时间失败,默认标记为未过期 isExpired = false; } - + // 检查已预约数量(按整个时段计算,不再按详细时段计算) if (bookedDetailSlots.containsKey(key)) { List bookedTimes = bookedDetailSlots.get(key); // 只要有任何预约,就算已预约1个(因为每个时段只能预约1个人) bookedCount = bookedTimes.isEmpty() ? 0 : 1; } - + // 创建时段信息 Map slot = new HashMap<>(); slot.put("scheduleId", schedule.getId()); @@ -509,7 +516,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation slot.put("allBooked", bookedCount >= totalSlots); // 是否全部约满 slot.put("availableCount", totalSlots - bookedCount); // 可用数量 slot.put("isExpired", isExpired); // 是否已过期 - + // 查询咨询师姓名 if (schedule.getCounselorId() != null) { try { @@ -527,20 +534,20 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } else { slot.put("counselorName", null); } - + availableSlots.add(slot); } - + return availableSlots; } - - @Override + + @Override public List getAvailableDetailSlots(Date date, String timeSlot) { // 1. 查询该日期该时段的排班信息 LambdaQueryWrapper scheduleQueryWrapper = new LambdaQueryWrapper<>(); scheduleQueryWrapper.eq(XljkConsultationSchedule::getScheduleDate, date) - .eq(XljkConsultationSchedule::getTimeSlot, timeSlot) - .eq(XljkConsultationSchedule::getStatus, "0"); // 0表示正常可用 + .eq(XljkConsultationSchedule::getTimeSlot, timeSlot) + .eq(XljkConsultationSchedule::getStatus, "0"); // 0表示正常可用 // 修改:使用selectList()而不是selectOne(),因为可能有多条记录 List schedules = scheduleMapper.selectList(scheduleQueryWrapper); @@ -553,9 +560,10 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation // 2. 合并所有排班的时段,生成完整的30分钟时段列表 List allTimeSlots = new ArrayList<>(); Set uniqueTimeSlots = new LinkedHashSet<>(); // 用于去重,保持顺序 - + for (XljkConsultationSchedule schedule : schedules) { - List timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime()); + List timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), + schedule.getEndTime()); for (TimeSlotDetailVo slot : timeSlots) { String slotKey = slot.getStartTime() + "-" + slot.getEndTime(); if (uniqueTimeSlots.add(slotKey)) { @@ -563,29 +571,29 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } } } - + // 3. 查询已预约的时段(查询所有相关排班的预约) List scheduleIds = schedules.stream().map(XljkConsultationSchedule::getId).collect(Collectors.toList()); LambdaQueryWrapper appointmentQueryWrapper = new LambdaQueryWrapper<>(); appointmentQueryWrapper.eq(XljkConsultationAppointment::getAppointmentDate, date) - .in(XljkConsultationAppointment::getScheduleId, scheduleIds) // 按排班ID列表过滤 - .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 - + .in(XljkConsultationAppointment::getScheduleId, scheduleIds) // 按排班ID列表过滤 + .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 + List appointments = mapper.selectList(appointmentQueryWrapper); - + // 4. 标记已预约和已过期的时段 Set bookedDetailTimes = appointments.stream() - .map(XljkConsultationAppointment::getAppointmentDetailTime) - .collect(Collectors.toSet()); - + .map(XljkConsultationAppointment::getAppointmentDetailTime) + .collect(Collectors.toSet()); + // 获取当前时间 Date now = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); - + for (TimeSlotDetailVo slot : allTimeSlots) { String detailTime = slot.getStartTime() + "-" + slot.getEndTime(); - + // 检查是否已预约 if (bookedDetailTimes.contains(detailTime)) { slot.setIsBooked(true); @@ -597,7 +605,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation String[] timeParts = slot.getStartTime().split(":"); int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); - + // 设置该时间段的开始时间 Calendar slotCalendar = Calendar.getInstance(); slotCalendar.setTime(date); @@ -605,9 +613,9 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation slotCalendar.set(Calendar.MINUTE, minute); slotCalendar.set(Calendar.SECOND, 0); slotCalendar.set(Calendar.MILLISECOND, 0); - + Date slotStartTime = slotCalendar.getTime(); - + // 如果时间段开始时间早于当前时间,则标记为已过期 if (slotStartTime.before(now)) { slot.setIsExpired(true); @@ -623,26 +631,26 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation } } } - + return allTimeSlots; } - + @Override @Transactional public boolean submitAppointment(XljkConsultationAppointmentBo bo) { // 检查该时段是否已被预约 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(XljkConsultationAppointment::getScheduleId, bo.getScheduleId()) - .eq(XljkConsultationAppointment::getAppointmentDate, bo.getAppointmentDate()) - .eq(XljkConsultationAppointment::getAppointmentDetailTime, bo.getAppointmentDetailTime()) - .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1")); // 待咨询或已完成的 - + .eq(XljkConsultationAppointment::getAppointmentDate, bo.getAppointmentDate()) + .eq(XljkConsultationAppointment::getAppointmentDetailTime, bo.getAppointmentDetailTime()) + .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1")); // 待咨询或已完成的 + long count = mapper.selectCount(queryWrapper); if (count > 0) { // 该时段已被预约 return false; } - + // 检查学生ID是否有效,并获取正确的学生ID Long studentId = bo.getStudentId(); if (studentId != null) { @@ -650,12 +658,12 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation // 根据学生ID查询学生信息 String sql = "SELECT id FROM xg_student_msg WHERE id = ?"; List> result = jdbcTemplate.queryForList(sql, studentId); - + // 如果没有找到学生信息,尝试通过学号查询 if (result.isEmpty() && bo.getStudentNo() != null && !bo.getStudentNo().isEmpty()) { sql = "SELECT id FROM xg_student_msg WHERE student_no = ?"; result = jdbcTemplate.queryForList(sql, bo.getStudentNo()); - + if (!result.isEmpty()) { // 使用查询到的正确学生ID studentId = ((Number) result.get(0).get("id")).longValue(); @@ -666,10 +674,10 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation log.error("验证学生ID时出错: {}", e.getMessage()); } } - + // 检查学生是否有"三次不到"记录 // TODO: 实现"三次不到"规则检查逻辑 - + // 提交预约 XljkConsultationAppointment entity = new XljkConsultationAppointment(); BeanUtils.copyProperties(bo, entity); @@ -678,27 +686,27 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation entity.setCreateTime(new Date()); // 设置创建时间 return mapper.insert(entity) > 0; } - + @Override public List getMyAppointments(Long studentId) { log.info("获取学生预约记录,studentId: {}", studentId); - + // 使用原生SQL查询以绕过MyBatis缓存,确保获取最新数据 String sql = "SELECT a.*, u.nick_name AS counselor_name " + - "FROM xljk_consultation_appointment a " + - "LEFT JOIN sys_user u ON a.counselor_id = u.user_id " + - "WHERE a.student_id = ? AND a.deleted = 0 " + - "ORDER BY a.create_time DESC, a.id DESC"; - + "FROM xljk_consultation_appointment a " + + "LEFT JOIN sys_user u ON a.counselor_id = u.user_id " + + "WHERE a.student_id = ? AND a.deleted = 0 " + + "ORDER BY a.student_no DESC, a.id DESC, a.create_time DESC"; + List result = new ArrayList<>(); - + try { List> rows = jdbcTemplate.queryForList(sql, studentId); log.info("查询到预约记录数: {}", rows.size()); - + for (Map row : rows) { XljkConsultationAppointmentVo vo = new XljkConsultationAppointmentVo(); - + // 设置主要字段 vo.setId(((Number) row.get("id")).longValue()); vo.setStudentId(((Number) row.get("student_id")).longValue()); @@ -708,91 +716,91 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation vo.setAppointmentDetailTime((String) row.get("appointment_detail_time")); vo.setReason((String) row.get("reason")); // 处理状态字段,确保类型正确 -Object statusObj = row.get("status"); -if (statusObj != null) { - if (statusObj instanceof Number) { - vo.setStatus(((Number) statusObj).intValue()); - } else if (statusObj instanceof String) { - try { - vo.setStatus(Integer.parseInt((String) statusObj)); - } catch (NumberFormatException e) { - log.warn("状态值转换失败: {}", statusObj); - vo.setStatus(0); // 默认待咨询 - } - } else { - log.warn("未知状态类型: {}", statusObj.getClass().getName()); - vo.setStatus(0); // 默认待咨询 - } -} else { - vo.setStatus(0); // 默认待咨询状态 -} + Object statusObj = row.get("status"); + if (statusObj != null) { + if (statusObj instanceof Number) { + vo.setStatus(((Number) statusObj).intValue()); + } else if (statusObj instanceof String) { + try { + vo.setStatus(Integer.parseInt((String) statusObj)); + } catch (NumberFormatException e) { + log.warn("状态值转换失败: {}", statusObj); + vo.setStatus(0); // 默认待咨询 + } + } else { + log.warn("未知状态类型: {}", statusObj.getClass().getName()); + vo.setStatus(0); // 默认待咨询 + } + } else { + vo.setStatus(0); // 默认待咨询状态 + } vo.setCounselorName((String) row.get("counselor_name")); - + // 可选字段 if (row.get("counselor_id") != null) { vo.setCounselorId(((Number) row.get("counselor_id")).longValue()); } - + if (row.get("schedule_id") != null) { vo.setScheduleId(((Number) row.get("schedule_id")).longValue()); } - + if (row.get("communication_duration") != null) { vo.setCommunicationDuration(((Number) row.get("communication_duration")).intValue()); } - + vo.setConsultationContent((String) row.get("consultation_content")); vo.setCounselorSuggestion((String) row.get("counselor_suggestion")); - + // 其他通用字段 if (row.get("create_by") != null) { vo.setCreateBy(((Number) row.get("create_by")).longValue()); } - + if (row.get("create_time") != null) { vo.setCreateTime(convertToDate(row.get("create_time"))); } - + if (row.get("update_by") != null) { vo.setUpdateBy(((Number) row.get("update_by")).longValue()); } - + if (row.get("update_time") != null) { vo.setUpdateTime(convertToDate(row.get("update_time"))); } - + result.add(vo); } - + // 打印日志,显示每条记录的状态,便于调试 for (XljkConsultationAppointmentVo vo : result) { - log.info("预约记录 ID: {}, 状态: {}, 日期: {}", - vo.getId(), vo.getStatus(), vo.getAppointmentDate()); + log.info("预约记录 ID: {}, 状态: {}, 日期: {}", + vo.getId(), vo.getStatus(), vo.getAppointmentDate()); } - + } catch (Exception e) { log.error("获取学生预约记录异常", e); } - + return result; } - + @Override @Transactional - public boolean recordConsultation(Long id, Integer communicationDuration, - String consultationContent, String counselorSuggestion, String status, - Integer psychologicalHistory, String psychologicalHistoryDetail, - String basicSituation, String majorLifeEvents, Integer needReferral) { + public boolean recordConsultation(Long id, Integer communicationDuration, + String consultationContent, String counselorSuggestion, String status, + Integer psychologicalHistory, String psychologicalHistoryDetail, + String basicSituation, String majorLifeEvents, Integer needReferral) { log.info("记录咨询结果,ID: {}, 状态: {}, 心理病史: {}, 转介: {}", id, status, psychologicalHistory, needReferral); - + XljkConsultationAppointment entity = mapper.selectById(id); if (entity == null) { log.warn("未找到ID为{}的预约记录", id); return false; } - + log.info("找到预约记录: {}", entity); - + // 将status转换为Integer类型进行比较,但保存时仍使用原始String类型 Integer statusInt; try { @@ -802,28 +810,28 @@ if (statusObj != null) { statusInt = 1; // 默认已到访 status = "1"; // 确保status也更新为"1" } - + entity.setStatus(status); // 使用原始String类型的status - + // 只有在已到访状态(1)时,才设置咨询结果相关字段 if (statusInt == 1) { entity.setCommunicationDuration(communicationDuration != null ? communicationDuration : 30); entity.setConsultationContent(consultationContent); entity.setCounselorSuggestion(counselorSuggestion); - + // 设置心理病史相关字段 entity.setPsychologicalHistory(psychologicalHistory); entity.setPsychologicalHistoryDetail(psychologicalHistoryDetail); entity.setBasicSituation(basicSituation); entity.setMajorLifeEvents(majorLifeEvents); entity.setNeedReferral(needReferral); - - log.info("设置咨询结果相关字段:沟通时长={}, 咨询内容长度={}, 咨询师建议长度={}", - communicationDuration, + + log.info("设置咨询结果相关字段:沟通时长={}, 咨询内容长度={}, 咨询师建议长度={}", + communicationDuration, consultationContent != null ? consultationContent.length() : 0, counselorSuggestion != null ? counselorSuggestion.length() : 0); - - log.info("设置心理病史相关字段:心理病史={}, 心理病史详情长度={}, 基本情况长度={}, 重大生活事件长度={}, 转介={}", + + log.info("设置心理病史相关字段:心理病史={}, 心理病史详情长度={}, 基本情况长度={}, 重大生活事件长度={}, 转介={}", psychologicalHistory, psychologicalHistoryDetail != null ? psychologicalHistoryDetail.length() : 0, basicSituation != null ? basicSituation.length() : 0, @@ -835,22 +843,24 @@ if (statusObj != null) { entity.setCommunicationDuration(0); entity.setConsultationContent(""); entity.setCounselorSuggestion(""); - + // 清空心理病史相关字段 entity.setPsychologicalHistory(null); entity.setPsychologicalHistoryDetail(""); entity.setBasicSituation(""); entity.setMajorLifeEvents(""); entity.setNeedReferral(null); - + // 如果是未到访状态(3),创建未到访记录 if (statusInt == 3) { log.info("记录未到访,创建未到访记录"); try { // 注入XljkConsultationNoShowService - IXljkConsultationNoShowService noShowService = SpringUtils.getBean(IXljkConsultationNoShowService.class); + IXljkConsultationNoShowService noShowService = SpringUtils + .getBean(IXljkConsultationNoShowService.class); if (noShowService != null) { - noShowService.createNoShowRecord(entity.getStudentId(), entity.getId(), entity.getAppointmentDate()); + noShowService.createNoShowRecord(entity.getStudentId(), entity.getId(), + entity.getAppointmentDate()); } else { log.error("无法获取XljkConsultationNoShowService实例"); } @@ -860,10 +870,10 @@ if (statusObj != null) { } } } - + int result = mapper.updateById(entity); log.info("更新结果: {}", result > 0 ? "成功" : "失败"); - + return result > 0; } @@ -883,18 +893,18 @@ if (statusObj != null) { List params = new ArrayList<>(); // 超级管理员或心理健康管理员可以查看所有院系 - boolean isAdmin = LoginHelper.isSuperAdmin() || - LoginHelper.getLoginUser().getRoles().stream() - .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); - + boolean isAdmin = LoginHelper.isSuperAdmin() || + LoginHelper.getLoginUser().getRoles().stream() + .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); + if (isAdmin) { sql = "SELECT DISTINCT college_id as id, college_name as name FROM xg_class_msg " + - "WHERE college_name IS NOT NULL AND deleted = 0 ORDER BY college_name"; + "WHERE college_name IS NOT NULL AND deleted = 0 ORDER BY college_name, create_time DESC"; params = new ArrayList<>(); log.info("管理员查询所有院系"); } else { sql = "SELECT DISTINCT college_id as id, college_name as name FROM xg_class_msg " + - "WHERE counselor_id = ? AND college_name IS NOT NULL AND deleted = 0 ORDER BY college_name"; + "WHERE counselor_id = ? AND college_name IS NOT NULL AND deleted = 0 ORDER BY college_name, create_time DESC"; params.add(userId); log.info("普通用户查询院系,userId={}", userId); } @@ -903,7 +913,7 @@ if (statusObj != null) { // 使用JdbcTemplate执行查询 List> queryResult = jdbcTemplate.queryForList(sql, params.toArray()); log.info("院系查询结果: {}", queryResult); - + if (queryResult != null && !queryResult.isEmpty()) { return queryResult; } else { @@ -912,36 +922,36 @@ if (statusObj != null) { } catch (Exception e) { log.error("查询院系数据异常", e); } - + // 如果没有查询到数据,尝试使用更简单的查询 sql = "SELECT DISTINCT college_id as id, college_name as name FROM xg_class_msg " + - "WHERE college_name IS NOT NULL ORDER BY college_name"; - + "WHERE college_name IS NOT NULL ORDER BY college_name, create_time DESC"; + try { List> fallbackResult = jdbcTemplate.queryForList(sql); log.info("备用查询院系结果: {}", fallbackResult); - + if (fallbackResult != null && !fallbackResult.isEmpty()) { return fallbackResult; } } catch (Exception e) { log.error("备用查询院系数据异常", e); } - + // 如果仍然没有数据,添加测试数据(仅作为最后的备选方案) if (result.isEmpty()) { log.warn("未从数据库获取到院系数据,添加测试数据"); - + Map option1 = new HashMap<>(); option1.put("id", 1L); option1.put("name", "信息工程学院"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "机械工程学院"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 3L); option3.put("name", "人文学院"); @@ -950,7 +960,7 @@ if (statusObj != null) { } catch (Exception e) { log.error("获取院系选项异常", e); } - + return result; } @@ -970,32 +980,32 @@ if (statusObj != null) { List params = new ArrayList<>(); // 超级管理员或心理健康管理员可以查看所有专业 - boolean isAdmin = LoginHelper.isSuperAdmin() || - LoginHelper.getLoginUser().getRoles().stream() - .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); - + boolean isAdmin = LoginHelper.isSuperAdmin() || + LoginHelper.getLoginUser().getRoles().stream() + .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); + if (isAdmin) { sql = "SELECT DISTINCT major_id as id, major_name as name FROM xg_class_msg " + - "WHERE major_name IS NOT NULL AND deleted = 0"; - + "WHERE major_name IS NOT NULL AND deleted = 0"; + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - - sql += " ORDER BY major_name"; + + sql += " ORDER BY major_name, create_time DESC"; log.info("管理员查询专业,collegeName={}", collegeName); } else { sql = "SELECT DISTINCT major_id as id, major_name as name FROM xg_class_msg " + - "WHERE counselor_id = ? AND major_name IS NOT NULL AND deleted = 0"; + "WHERE counselor_id = ? AND major_name IS NOT NULL AND deleted = 0"; params.add(userId); - + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - - sql += " ORDER BY major_name"; + + sql += " ORDER BY major_name, create_time DESC"; log.info("普通用户查询专业,userId={}, collegeName={}", userId, collegeName); } @@ -1003,7 +1013,7 @@ if (statusObj != null) { // 使用JdbcTemplate执行查询 List> queryResult = jdbcTemplate.queryForList(sql, params.toArray()); log.info("专业查询结果: {}", queryResult); - + if (queryResult != null && !queryResult.isEmpty()) { return queryResult; } else { @@ -1012,45 +1022,45 @@ if (statusObj != null) { } catch (Exception e) { log.error("查询专业数据异常", e); } - + // 如果没有查询到数据,尝试使用更简单的查询 sql = "SELECT DISTINCT major_id as id, major_name as name FROM xg_class_msg WHERE major_name IS NOT NULL"; params.clear(); - + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - - sql += " ORDER BY major_name"; - + + sql += " ORDER BY major_name, create_time DESC"; + try { List> fallbackResult = jdbcTemplate.queryForList(sql, params.toArray()); log.info("备用查询专业结果: {}", fallbackResult); - + if (fallbackResult != null && !fallbackResult.isEmpty()) { return fallbackResult; } } catch (Exception e) { log.error("备用查询专业数据异常", e); } - + // 如果仍然没有数据,添加测试数据(仅作为最后的备选方案) if (result.isEmpty()) { log.warn("未从数据库获取到专业数据,添加测试数据,collegeName={}", collegeName); - + // 根据院系名称返回对应的专业 if ("信息工程学院".equals(collegeName)) { Map option1 = new HashMap<>(); option1.put("id", 1L); option1.put("name", "计算机科学与技术"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "软件工程"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 3L); option3.put("name", "网络工程"); @@ -1060,7 +1070,7 @@ if (statusObj != null) { option1.put("id", 4L); option1.put("name", "机械设计制造及其自动化"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 5L); option2.put("name", "材料成型及控制工程"); @@ -1070,7 +1080,7 @@ if (statusObj != null) { option1.put("id", 6L); option1.put("name", "汉语言文学"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 7L); option2.put("name", "英语"); @@ -1081,32 +1091,32 @@ if (statusObj != null) { option1.put("id", 1L); option1.put("name", "计算机科学与技术"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "软件工程"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 3L); option3.put("name", "网络工程"); result.add(option3); - + Map option4 = new HashMap<>(); option4.put("id", 4L); option4.put("name", "机械设计制造及其自动化"); result.add(option4); - + Map option5 = new HashMap<>(); option5.put("id", 5L); option5.put("name", "材料成型及控制工程"); result.add(option5); - + Map option6 = new HashMap<>(); option6.put("id", 6L); option6.put("name", "汉语言文学"); result.add(option6); - + Map option7 = new HashMap<>(); option7.put("id", 7L); option7.put("name", "英语"); @@ -1116,7 +1126,7 @@ if (statusObj != null) { } catch (Exception e) { log.error("获取专业选项异常", e); } - + return result; } @@ -1136,40 +1146,40 @@ if (statusObj != null) { List params = new ArrayList<>(); // 超级管理员或心理健康管理员可以查看所有班级 - boolean isAdmin = LoginHelper.isSuperAdmin() || - LoginHelper.getLoginUser().getRoles().stream() - .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); - + boolean isAdmin = LoginHelper.isSuperAdmin() || + LoginHelper.getLoginUser().getRoles().stream() + .anyMatch(role -> "xljk_admin".equals(role.getRoleKey())); + if (isAdmin) { sql = "SELECT id, class_name as name FROM xg_class_msg WHERE class_name IS NOT NULL AND deleted = 0"; - + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - + if (StringUtils.isNotEmpty(majorName)) { sql += " AND major_name = ?"; params.add(majorName); } - - sql += " ORDER BY class_name"; + + sql += " ORDER BY class_name, create_time DESC"; log.info("管理员查询班级,collegeName={}, majorName={}", collegeName, majorName); } else { sql = "SELECT id, class_name as name FROM xg_class_msg WHERE counselor_id = ? AND class_name IS NOT NULL AND deleted = 0"; params.add(userId); - + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - + if (StringUtils.isNotEmpty(majorName)) { sql += " AND major_name = ?"; params.add(majorName); } - - sql += " ORDER BY class_name"; + + sql += " ORDER BY class_name, create_time DESC"; log.info("普通用户查询班级,userId={}, collegeName={}, majorName={}", userId, collegeName, majorName); } @@ -1177,7 +1187,7 @@ if (statusObj != null) { // 使用JdbcTemplate执行查询 List> queryResult = jdbcTemplate.queryForList(sql, params.toArray()); log.info("班级查询结果: {}", queryResult); - + if (queryResult != null && !queryResult.isEmpty()) { return queryResult; } else { @@ -1186,38 +1196,38 @@ if (statusObj != null) { } catch (Exception e) { log.error("查询班级数据异常", e); } - + // 如果没有查询到数据,尝试使用更简单的查询 sql = "SELECT id, class_name as name FROM xg_class_msg WHERE class_name IS NOT NULL"; params.clear(); - + if (StringUtils.isNotEmpty(collegeName)) { sql += " AND college_name = ?"; params.add(collegeName); } - + if (StringUtils.isNotEmpty(majorName)) { sql += " AND major_name = ?"; params.add(majorName); } - - sql += " ORDER BY class_name"; - + + sql += " ORDER BY class_name, create_time DESC"; + try { List> fallbackResult = jdbcTemplate.queryForList(sql, params.toArray()); log.info("备用查询班级结果: {}", fallbackResult); - + if (fallbackResult != null && !fallbackResult.isEmpty()) { return fallbackResult; } } catch (Exception e) { log.error("备用查询班级数据异常", e); } - + // 如果仍然没有数据,添加测试数据(仅作为最后的备选方案) if (result.isEmpty()) { log.warn("未从数据库获取到班级数据,添加测试数据,collegeName={}, majorName={}", collegeName, majorName); - + // 根据院系和专业名称返回对应的班级 if ("信息工程学院".equals(collegeName)) { if ("计算机科学与技术".equals(majorName)) { @@ -1225,7 +1235,7 @@ if (statusObj != null) { option1.put("id", 1L); option1.put("name", "计算机2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "计算机2302班"); @@ -1235,7 +1245,7 @@ if (statusObj != null) { option1.put("id", 3L); option1.put("name", "软件2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 4L); option2.put("name", "软件2302班"); @@ -1251,22 +1261,22 @@ if (statusObj != null) { option1.put("id", 1L); option1.put("name", "计算机2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "计算机2302班"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 3L); option3.put("name", "软件2301班"); result.add(option3); - + Map option4 = new HashMap<>(); option4.put("id", 4L); option4.put("name", "软件2302班"); result.add(option4); - + Map option5 = new HashMap<>(); option5.put("id", 5L); option5.put("name", "网络2301班"); @@ -1278,7 +1288,7 @@ if (statusObj != null) { option1.put("id", 6L); option1.put("name", "机械2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 7L); option2.put("name", "机械2302班"); @@ -1294,12 +1304,12 @@ if (statusObj != null) { option1.put("id", 6L); option1.put("name", "机械2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 7L); option2.put("name", "机械2302班"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 8L); option3.put("name", "材料2301班"); @@ -1322,7 +1332,7 @@ if (statusObj != null) { option1.put("id", 9L); option1.put("name", "汉语2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 10L); option2.put("name", "英语2301班"); @@ -1334,47 +1344,47 @@ if (statusObj != null) { option1.put("id", 1L); option1.put("name", "计算机2301班"); result.add(option1); - + Map option2 = new HashMap<>(); option2.put("id", 2L); option2.put("name", "计算机2302班"); result.add(option2); - + Map option3 = new HashMap<>(); option3.put("id", 3L); option3.put("name", "软件2301班"); result.add(option3); - + Map option4 = new HashMap<>(); option4.put("id", 4L); option4.put("name", "软件2302班"); result.add(option4); - + Map option5 = new HashMap<>(); option5.put("id", 5L); option5.put("name", "网络2301班"); result.add(option5); - + Map option6 = new HashMap<>(); option6.put("id", 6L); option6.put("name", "机械2301班"); result.add(option6); - + Map option7 = new HashMap<>(); option7.put("id", 7L); option7.put("name", "机械2302班"); result.add(option7); - + Map option8 = new HashMap<>(); option8.put("id", 8L); option8.put("name", "材料2301班"); result.add(option8); - + Map option9 = new HashMap<>(); option9.put("id", 9L); option9.put("name", "汉语2301班"); result.add(option9); - + Map option10 = new HashMap<>(); option10.put("id", 10L); option10.put("name", "英语2301班"); @@ -1384,13 +1394,13 @@ if (statusObj != null) { } catch (Exception e) { log.error("获取班级选项异常", e); } - + return result; } /** * 将各种日期类型转换为java.util.Date - * + * * @param dateObj 日期对象,可能是LocalDateTime、LocalDate、Date或其他类型 * @return java.util.Date对象,如果转换失败则返回null */ @@ -1398,7 +1408,7 @@ if (statusObj != null) { if (dateObj == null) { return null; } - + try { if (dateObj instanceof Date) { return (Date) dateObj; @@ -1423,13 +1433,14 @@ if (statusObj != null) { } @Override - public TableDataInfo selectPageList(XljkConsultationAppointmentBo bo, PageQuery pageQuery) { + public TableDataInfo selectPageList(XljkConsultationAppointmentBo bo, + PageQuery pageQuery) { log.info("分页查询预约记录列表,页码: {}, 每页数量: {}", pageQuery.getPageNum(), pageQuery.getPageSize()); - + // 将变量定义移到try块外,确保catch块可以访问 StringBuilder sqlBuilder = new StringBuilder(); List params = new ArrayList<>(); - + try { // 构建查询条件 sqlBuilder.append("SELECT a.id, a.tenant_id, a.student_id, a.counselor_id, a.schedule_id, "); @@ -1451,53 +1462,53 @@ if (statusObj != null) { sqlBuilder.append("AND s.name LIKE ? "); params.add("%" + bo.getStudentName() + "%"); } - + if (bo.getStudentNo() != null && !bo.getStudentNo().isEmpty()) { sqlBuilder.append("AND s.student_no LIKE ? "); params.add("%" + bo.getStudentNo() + "%"); } - + if (bo.getCollegeName() != null && !bo.getCollegeName().isEmpty()) { sqlBuilder.append("AND s.college_name = ? "); params.add(bo.getCollegeName()); } - + if (bo.getMajorName() != null && !bo.getMajorName().isEmpty()) { sqlBuilder.append("AND s.major_name = ? "); params.add(bo.getMajorName()); } - + if (bo.getClassName() != null && !bo.getClassName().isEmpty()) { sqlBuilder.append("AND s.class_name = ? "); params.add(bo.getClassName()); } - + if (bo.getStatus() != null && !bo.getStatus().isEmpty()) { sqlBuilder.append("AND a.status = ? "); params.add(bo.getStatus()); } - - // 处理日期范围 - if (bo.getParams() != null) { - Map paramsMap = bo.getParams(); - if (paramsMap.get("beginAppointmentDate") != null) { - sqlBuilder.append("AND a.appointment_date >= ? "); - params.add(paramsMap.get("beginAppointmentDate")); + + // 处理日期范围 + if (bo.getParams() != null) { + Map paramsMap = bo.getParams(); + if (paramsMap.get("beginAppointmentDate") != null) { + sqlBuilder.append("AND a.appointment_date >= ? "); + params.add(paramsMap.get("beginAppointmentDate")); + } + + if (paramsMap.get("endAppointmentDate") != null) { + sqlBuilder.append("AND a.appointment_date <= ? "); + params.add(paramsMap.get("endAppointmentDate")); + } } - - if (paramsMap.get("endAppointmentDate") != null) { - sqlBuilder.append("AND a.appointment_date <= ? "); - params.add(paramsMap.get("endAppointmentDate")); - } - } - + // 添加排序 - sqlBuilder.append("ORDER BY a.create_time DESC, a.id DESC "); - + sqlBuilder.append("ORDER BY s.student_no DESC, a.id DESC, a.create_time DESC "); + // 计算总数 String countSql = "SELECT COUNT(*) FROM (" + sqlBuilder.toString() + ") t"; Long total = jdbcTemplate.queryForObject(countSql, params.toArray(), Long.class); - + if (total == null || total == 0) { TableDataInfo emptyResult = new TableDataInfo<>(); emptyResult.setRows(new ArrayList<>()); @@ -1506,23 +1517,23 @@ if (statusObj != null) { emptyResult.setMsg("查询成功"); return emptyResult; } - + // 添加分页 sqlBuilder.append("LIMIT ?, ?"); int offset = (pageQuery.getPageNum() - 1) * pageQuery.getPageSize(); params.add(offset); params.add(pageQuery.getPageSize()); - + log.info("执行SQL: {}", sqlBuilder.toString()); - + // 执行查询 List> resultMaps = jdbcTemplate.queryForList(sqlBuilder.toString(), params.toArray()); - + // 转换结果 List result = new ArrayList<>(); for (Map row : resultMaps) { XljkConsultationAppointmentVo vo = new XljkConsultationAppointmentVo(); - + // 设置主要字段 vo.setId(((Number) row.get("id")).longValue()); vo.setStudentId(((Number) row.get("student_id")).longValue()); @@ -1535,7 +1546,7 @@ if (statusObj != null) { vo.setAppointmentTime((String) row.get("appointment_time")); vo.setAppointmentDetailTime((String) row.get("appointment_detail_time")); vo.setReason((String) row.get("reason")); - + // 处理状态字段,确保类型正确 Object statusObj = row.get("status"); if (statusObj != null) { @@ -1555,25 +1566,25 @@ if (statusObj != null) { } else { vo.setStatus(0); // 默认待咨询状态 } - + vo.setCounselorName((String) row.get("counselor_name")); - + // 可选字段 if (row.get("counselor_id") != null) { vo.setCounselorId(((Number) row.get("counselor_id")).longValue()); } - + if (row.get("schedule_id") != null) { vo.setScheduleId(((Number) row.get("schedule_id")).longValue()); } - + if (row.get("communication_duration") != null) { vo.setCommunicationDuration(((Number) row.get("communication_duration")).intValue()); } - + vo.setConsultationContent((String) row.get("consultation_content")); vo.setCounselorSuggestion((String) row.get("counselor_suggestion")); - + // 新增字段映射 Object psychologicalHistoryObj = row.get("psychological_history"); if (psychologicalHistoryObj != null) { @@ -1587,11 +1598,11 @@ if (statusObj != null) { } } } - + vo.setPsychologicalHistoryDetail((String) row.get("psychological_history_detail")); vo.setBasicSituation((String) row.get("basic_situation")); vo.setMajorLifeEvents((String) row.get("major_life_events")); - + Object needReferralObj = row.get("need_referral"); if (needReferralObj != null) { if (needReferralObj instanceof Number) { @@ -1604,40 +1615,40 @@ if (statusObj != null) { } } } - + // 其他通用字段 if (row.get("create_by") != null) { vo.setCreateBy(((Number) row.get("create_by")).longValue()); } - + if (row.get("create_time") != null) { vo.setCreateTime(convertToDate(row.get("create_time"))); } - + if (row.get("update_by") != null) { vo.setUpdateBy(((Number) row.get("update_by")).longValue()); } - + if (row.get("update_time") != null) { vo.setUpdateTime(convertToDate(row.get("update_time"))); } - + // 计算本学期到访次数 if (vo.getStudentId() != null) { vo.setSemesterVisitCount(calculateSemesterVisitCount(vo.getStudentId())); } else { vo.setSemesterVisitCount(0); } - + result.add(vo); } - + TableDataInfo tableDataInfo = new TableDataInfo<>(); - tableDataInfo.setRows(result); - tableDataInfo.setTotal(total); - tableDataInfo.setCode(200); - tableDataInfo.setMsg("查询成功"); - return tableDataInfo; + tableDataInfo.setRows(result); + tableDataInfo.setTotal(total); + tableDataInfo.setCode(200); + tableDataInfo.setMsg("查询成功"); + return tableDataInfo; } catch (Exception e) { log.error("分页查询预约记录列表异常", e); log.error("SQL: {}", sqlBuilder.toString()); @@ -1650,51 +1661,52 @@ if (statusObj != null) { return emptyResult; } } - + @Override public List getCalendarSchedules(String startDate, String endDate) { try { // 1. 查询指定日期范围内的所有排班数据 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.ge(XljkConsultationSchedule::getScheduleDate, startDate) - .le(XljkConsultationSchedule::getScheduleDate, endDate) - .eq(XljkConsultationSchedule::getStatus, "0") // 0表示正常可用 - .orderByAsc(XljkConsultationSchedule::getScheduleDate) - .orderByAsc(XljkConsultationSchedule::getTimeSlot) - .orderByAsc(XljkConsultationSchedule::getStartTime); + .le(XljkConsultationSchedule::getScheduleDate, endDate) + .eq(XljkConsultationSchedule::getStatus, "0") // 0表示正常可用 + .orderByAsc(XljkConsultationSchedule::getScheduleDate) + .orderByAsc(XljkConsultationSchedule::getTimeSlot) + .orderByAsc(XljkConsultationSchedule::getStartTime); List schedules = scheduleMapper.selectList(queryWrapper); - + if (schedules.isEmpty()) { return new ArrayList<>(); } // 2. 查询该日期范围内的所有预约记录 - List scheduleIds = schedules.stream().map(XljkConsultationSchedule::getId).collect(Collectors.toList()); + List scheduleIds = schedules.stream().map(XljkConsultationSchedule::getId) + .collect(Collectors.toList()); LambdaQueryWrapper appointmentQueryWrapper = new LambdaQueryWrapper<>(); appointmentQueryWrapper.in(XljkConsultationAppointment::getScheduleId, scheduleIds) - .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 - + .in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的 + List appointments = mapper.selectList(appointmentQueryWrapper); - + // 3. 构建排班ID到预约记录的映射 Map> appointmentMap = appointments.stream() - .collect(Collectors.groupingBy(XljkConsultationAppointment::getScheduleId)); + .collect(Collectors.groupingBy(XljkConsultationAppointment::getScheduleId)); // 4. 获取咨询师信息映射 Map counselorMap = getCounselorNameMap(); - + // 5. 获取当前时间 Date now = new Date(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm"); - + // 6. 构建结果列表 List result = new ArrayList<>(); - + for (XljkConsultationSchedule schedule : schedules) { ScheduleWithStatusVo vo = new ScheduleWithStatusVo(); - + // 基本信息 vo.setScheduleId(schedule.getId()); vo.setCounselorId(schedule.getCounselorId()); @@ -1703,131 +1715,136 @@ if (statusObj != null) { vo.setTimeSlot(schedule.getTimeSlot()); vo.setStartTime(schedule.getStartTime()); vo.setEndTime(schedule.getEndTime()); - + // 构建时段显示文本 String timeSlotText = getTimeSlotText(schedule.getTimeSlot()); vo.setTimeDisplay(timeSlotText + " " + schedule.getStartTime() + "-" + schedule.getEndTime()); - + // 计算预约状态 List scheduleAppointments = appointmentMap.get(schedule.getId()); int bookedCount = scheduleAppointments != null ? scheduleAppointments.size() : 0; - + // 每个时段只能预约1个人(无论时段长度) // 生成30分钟时段用于详细时段显示,但总预约数量固定为1 - List timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime()); + List timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), + schedule.getEndTime()); int totalSlots = 1; // 每个时段只能预约1个人 - + vo.setTotalSlots(totalSlots); vo.setAvailableSlots(Math.max(0, totalSlots - bookedCount)); vo.setIsFullyBooked(bookedCount >= totalSlots); - + // 检查是否已过期 boolean isExpired = false; try { Date scheduleDate = schedule.getScheduleDate(); Calendar calendar = Calendar.getInstance(); calendar.setTime(scheduleDate); - + // 设置具体的结束时间 String[] endTimeParts = schedule.getEndTime().split(":"); calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(endTimeParts[0])); calendar.set(Calendar.MINUTE, Integer.parseInt(endTimeParts[1])); calendar.set(Calendar.SECOND, 0); - + Date scheduleEndTime = calendar.getTime(); isExpired = scheduleEndTime.before(now); } catch (Exception e) { log.warn("检查时段过期状态失败: " + schedule.getId(), e); } - + vo.setIsExpired(isExpired); - + // 设置基础字段 vo.setCreateBy(schedule.getCreateBy()); vo.setCreateTime(schedule.getCreateTime()); vo.setUpdateBy(schedule.getUpdateBy()); vo.setUpdateTime(schedule.getUpdateTime()); - + result.add(vo); } - + return result; - + } catch (Exception e) { log.error("获取日历排班数据失败", e); return new ArrayList<>(); } } - + /** * 获取时段类型文本 */ private String getTimeSlotText(String timeSlot) { switch (timeSlot) { - case "0": return "中午"; - case "1": return "下午"; - case "2": return "晚上"; - default: return "未知"; + case "0": + return "中午"; + case "1": + return "下午"; + case "2": + return "晚上"; + default: + return "未知"; } } - + /** * 获取咨询师名称映射 */ private Map getCounselorNameMap() { try { Map counselorMap = new HashMap<>(); - + // 1. 查询所有已创建排班的咨询师ID LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.select(XljkConsultationSchedule::getCounselorId) - .eq(XljkConsultationSchedule::getStatus, "0") // 只查询正常状态的 - .groupBy(XljkConsultationSchedule::getCounselorId); // 去重 - + .eq(XljkConsultationSchedule::getStatus, "0") // 只查询正常状态的 + .groupBy(XljkConsultationSchedule::getCounselorId); // 去重 + List schedules = scheduleMapper.selectList(queryWrapper); if (schedules.isEmpty()) { return counselorMap; } - + // 2. 提取咨询师ID列表 List counselorIds = schedules.stream() - .map(XljkConsultationSchedule::getCounselorId) - .filter(Objects::nonNull) - .distinct() - .collect(Collectors.toList()); - + .map(XljkConsultationSchedule::getCounselorId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (counselorIds.isEmpty()) { return counselorMap; } - + // 3. 批量查询咨询师信息 List counselors = userService.selectUserByIds(counselorIds, null); - + // 4. 构建ID到姓名的映射 counselorMap = counselors.stream() - .filter(user -> user.getUserId() != null) - .collect(Collectors.toMap( - SysUserVo::getUserId, - user -> { - // 优先使用昵称,如果没有昵称则使用用户名 - String name = user.getNickName(); - if (StringUtils.isBlank(name)) { - name = user.getUserName(); - } - if (StringUtils.isBlank(name)) { - name = "未知咨询师"; - } - return name; - }, - (existing, replacement) -> existing // 如果ID重复,保留第一个 - )); - + .filter(user -> user.getUserId() != null) + .collect(Collectors.toMap( + SysUserVo::getUserId, + user -> { + // 优先使用昵称,如果没有昵称则使用用户名 + String name = user.getNickName(); + if (StringUtils.isBlank(name)) { + name = user.getUserName(); + } + if (StringUtils.isBlank(name)) { + name = "未知咨询师"; + } + return name; + }, + (existing, replacement) -> existing // 如果ID重复,保留第一个 + )); + log.info("成功加载咨询师信息映射,共{}个咨询师", counselorMap.size()); return counselorMap; - + } catch (Exception e) { log.error("获取咨询师信息失败", e); return new HashMap<>(); } } -} \ No newline at end of file +} diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationNoShowServiceImpl.java b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationNoShowServiceImpl.java index 52299f1f..bce51b07 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationNoShowServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/service/impl/XljkConsultationNoShowServiceImpl.java @@ -48,37 +48,39 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho sqlBuilder.append("LEFT JOIN xljk_consultation_appointment a ON ns.appointment_id = a.id "); sqlBuilder.append("LEFT JOIN sys_user u ON a.counselor_id = u.user_id "); sqlBuilder.append("WHERE ns.id = ? AND ns.deleted = 0"); - + List> resultList = jdbcTemplate.queryForList(sqlBuilder.toString(), id); - - if (resultList.isEmpty()) return null; - + + if (resultList.isEmpty()) + return null; + Map row = resultList.get(0); XljkConsultationNoShowVo vo = new XljkConsultationNoShowVo(); - + // 设置基本字段 vo.setId(((Number) row.get("id")).longValue()); vo.setStudentId(row.get("student_id") != null ? ((Number) row.get("student_id")).longValue() : null); - vo.setAppointmentId(row.get("appointment_id") != null ? ((Number) row.get("appointment_id")).longValue() : null); + vo.setAppointmentId( + row.get("appointment_id") != null ? ((Number) row.get("appointment_id")).longValue() : null); vo.setNoShowDate(convertToDate(row.get("no_show_date"))); vo.setExpireDate(convertToDate(row.get("expire_date"))); vo.setStatus((String) row.get("status")); vo.setCreateTime(convertToDate(row.get("create_time"))); vo.setUpdateTime(convertToDate(row.get("update_time"))); - + // 设置学生相关信息 vo.setStudentName((String) row.get("student_name")); vo.setStudentNo((String) row.get("student_no")); vo.setClassName((String) row.get("class_name")); vo.setCollegeName((String) row.get("college_name")); vo.setMajorName((String) row.get("major_name")); - + // 设置预约相关信息 - vo.setCounselorName((String) row.get("counselor_name")); // 从counselor表获取咨询师姓名 + vo.setCounselorName((String) row.get("counselor_name")); // 从counselor表获取咨询师姓名 vo.setAppointmentDate(convertToDate(row.get("appointment_date"))); vo.setAppointmentDetailTime((String) row.get("appointment_detail_time")); vo.setReason((String) row.get("reason")); - + return vo; } @@ -91,30 +93,30 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho sqlBuilder.append("FROM xljk_consultation_no_show ns "); sqlBuilder.append("LEFT JOIN xg_student_msg s ON ns.student_id = s.id "); sqlBuilder.append("WHERE ns.deleted = 0 "); - + List params = new ArrayList<>(); - + // 添加查询条件 if (bo.getStudentId() != null) { sqlBuilder.append("AND ns.student_id = ? "); params.add(bo.getStudentId()); } - + if (StringUtils.isNotBlank(bo.getStudentName())) { sqlBuilder.append("AND s.name LIKE ? "); params.add("%" + bo.getStudentName() + "%"); } - + if (StringUtils.isNotBlank(bo.getStudentNo())) { sqlBuilder.append("AND s.student_no LIKE ? "); params.add("%" + bo.getStudentNo() + "%"); } - + if (StringUtils.isNotBlank(bo.getStatus())) { sqlBuilder.append("AND ns.status = ? "); params.add(bo.getStatus()); } - + // 处理日期范围 if (bo.getParams() != null) { Object startDateObj = bo.getParams().get("startDate"); @@ -123,7 +125,7 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho params.add(startDateObj.toString()); log.info("添加开始日期查询条件: {}", startDateObj); } - + Object endDateObj = bo.getParams().get("endDate"); if (endDateObj != null && StringUtils.isNotBlank(endDateObj.toString())) { sqlBuilder.append("AND DATE(ns.no_show_date) <= DATE(?) "); @@ -131,34 +133,35 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho log.info("添加结束日期查询条件: {}", endDateObj); } } - - // 添加排序,首先按未到访日期倒序,其次按创建时间倒序 - sqlBuilder.append("ORDER BY ns.no_show_date DESC, ns.create_time DESC"); - + + // 添加排序,首先按学号降序,其次按未到访日期和创建时间倒序 + sqlBuilder.append("ORDER BY s.student_no DESC, ns.no_show_date DESC, ns.create_time DESC"); + // 执行查询 List> resultList = jdbcTemplate.queryForList(sqlBuilder.toString(), params.toArray()); - + // 转换为VO对象 return resultList.stream().map(row -> { XljkConsultationNoShowVo vo = new XljkConsultationNoShowVo(); - + // 设置基本字段 vo.setId(((Number) row.get("id")).longValue()); vo.setStudentId(row.get("student_id") != null ? ((Number) row.get("student_id")).longValue() : null); - vo.setAppointmentId(row.get("appointment_id") != null ? ((Number) row.get("appointment_id")).longValue() : null); + vo.setAppointmentId( + row.get("appointment_id") != null ? ((Number) row.get("appointment_id")).longValue() : null); vo.setNoShowDate(convertToDate(row.get("no_show_date"))); vo.setExpireDate(convertToDate(row.get("expire_date"))); vo.setStatus((String) row.get("status")); vo.setCreateTime(convertToDate(row.get("create_time"))); vo.setUpdateTime(convertToDate(row.get("update_time"))); - + // 设置关联信息 vo.setStudentName((String) row.get("student_name")); vo.setStudentNo((String) row.get("student_no")); vo.setClassName((String) row.get("class_name")); vo.setCollegeName((String) row.get("college_name")); vo.setMajorName((String) row.get("major_name")); - + return vo; }).collect(Collectors.toList()); } @@ -181,26 +184,26 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho public boolean deleteById(Long id) { return mapper.deleteById(id) > 0; } - + @Override public Map checkLimitation(Long studentId) { Map result = new HashMap<>(); result.put("limited", false); - + try { // 查询是否有生效中的限制 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(XljkConsultationNoShow::getStudentId, studentId) - .eq(XljkConsultationNoShow::getStatus, "0") // 0-生效中 - .eq(XljkConsultationNoShow::getDeleted, 0) - .orderByDesc(XljkConsultationNoShow::getCreateTime); - + .eq(XljkConsultationNoShow::getStatus, "0") // 0-生效中 + .eq(XljkConsultationNoShow::getDeleted, 0) + .orderByDesc(XljkConsultationNoShow::getCreateTime); + XljkConsultationNoShow noShow = mapper.selectOne(queryWrapper); - + if (noShow != null) { Date now = new Date(); Date expireDate = noShow.getExpireDate(); - + // 如果限制已过期,更新状态 if (expireDate != null && now.after(expireDate)) { log.info("学生{}的预约限制已过期,自动更新状态", studentId); @@ -210,7 +213,7 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho // 限制仍在生效期内 result.put("limited", true); result.put("expireDate", expireDate); - + // 格式化日期为字符串 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); result.put("expireDateStr", sdf.format(expireDate)); @@ -219,10 +222,10 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho } catch (Exception e) { log.error("检查预约限制失败", e); } - + return result; } - + @Override @Transactional public boolean releaseLimitation(Long id) { @@ -232,7 +235,7 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho log.warn("未找到ID为{}的未到访记录", id); return false; } - + // 只有生效中的限制才能解除 if ("0".equals(noShow.getStatus())) { noShow.setStatus("2"); // 2-已解除 @@ -248,7 +251,7 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho return false; } } - + @Override @Transactional public boolean createNoShowRecord(Long studentId, Long appointmentId, Date noShowDate) { @@ -258,20 +261,20 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho noShow.setStudentId(studentId); noShow.setAppointmentId(appointmentId); noShow.setNoShowDate(noShowDate); - + // 检查30天内未到访次数 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -30); Date thirtyDaysAgo = calendar.getTime(); - + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(XljkConsultationNoShow::getStudentId, studentId) - .ge(XljkConsultationNoShow::getNoShowDate, thirtyDaysAgo) - .eq(XljkConsultationNoShow::getDeleted, 0); - + .ge(XljkConsultationNoShow::getNoShowDate, thirtyDaysAgo) + .eq(XljkConsultationNoShow::getDeleted, 0); + long count = mapper.selectCount(queryWrapper); log.info("学生{}在30天内未到访次数: {}", studentId, count); - + // 如果达到3次,设置限制(包括本次) if (count >= 2) { // 已有2次,加上本次就是3次 log.info("学生{}达到3次未到访,设置预约限制", studentId); @@ -280,11 +283,11 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho calendar.setTime(noShowDate); calendar.add(Calendar.DAY_OF_MONTH, 7); Date expireDate = calendar.getTime(); - + noShow.setExpireDate(expireDate); noShow.setStatus("0"); // 0-生效中 } - + // 保存未到访记录 mapper.insert(noShow); log.info("创建未到访记录成功: {}", noShow); @@ -294,25 +297,25 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho return false; } } - + @Override @Transactional public int updateExpiredLimitations() { log.info("开始检查过期的预约限制"); int count = 0; - + try { Date now = new Date(); - + // 查询所有生效中但已过期的限制 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(XljkConsultationNoShow::getStatus, "0") // 0-生效中 - .lt(XljkConsultationNoShow::getExpireDate, now) // 过期日期早于当前日期 - .eq(XljkConsultationNoShow::getDeleted, 0); - + .lt(XljkConsultationNoShow::getExpireDate, now) // 过期日期早于当前日期 + .eq(XljkConsultationNoShow::getDeleted, 0); + List expiredList = mapper.selectList(queryWrapper); log.info("找到{}条已过期的预约限制", expiredList.size()); - + // 批量更新状态为已过期 for (XljkConsultationNoShow noShow : expiredList) { noShow.setStatus("1"); // 1-已过期 @@ -323,13 +326,13 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho } catch (Exception e) { log.error("更新过期预约限制失败", e); } - + return count; } /** * 将各种日期类型转换为java.util.Date - * + * * @param dateObj 日期对象,可能是LocalDateTime、LocalDate、Date或其他类型 * @return java.util.Date对象,如果转换失败则返回null */ @@ -337,7 +340,7 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho if (dateObj == null) { return null; } - + try { if (dateObj instanceof Date) { return (Date) dateObj; @@ -360,4 +363,4 @@ public class XljkConsultationNoShowServiceImpl implements IXljkConsultationNoSho return null; } } -} \ No newline at end of file +} diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/resources/mapper/xljk/XljkSystemQueryMapper.xml b/backend/ruoyi-modules/ruoyi-xljk/src/main/resources/mapper/xljk/XljkSystemQueryMapper.xml index af24ec24..ec156779 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/resources/mapper/xljk/XljkSystemQueryMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/resources/mapper/xljk/XljkSystemQueryMapper.xml @@ -6,38 +6,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - SELECT DISTINCT major_id as id, major_name as name - FROM xg_class_msg + SELECT DISTINCT major_id as id, major_name as name + FROM xg_class_msg WHERE major_name IS NOT NULL AND college_name = #{collegeName} @@ -56,8 +56,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - SELECT id, class_name as name - FROM xg_class_msg + SELECT id, class_name as name + FROM xg_class_msg WHERE class_name IS NOT NULL AND college_name = #{collegeName} @@ -78,14 +78,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND major_name = #{majorName} + ORDER BY class_name, create_time DESC - \ No newline at end of file + diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRecordController.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRecordController.java index be7e95fc..a692de04 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRecordController.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRecordController.java @@ -112,6 +112,18 @@ public class XskhDisciplineRecordController extends BaseController { return R.ok(xskhDisciplineRecordService.getStudentOptions(keyword)); } + /** + * 分页查询学生选项列表(用于移动端) + */ + @SaCheckPermission("disciplineRecord:disciplineRecord:query") + @GetMapping("/studentOptionsPage") + public R> getStudentOptionsPage( + @RequestParam(required = false) String keyword, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + return R.ok(xskhDisciplineRecordService.getStudentOptionsPage(keyword, pageNum, pageSize)); + } + /** * 查询当前登录学生的违纪处分记录列表 */ @@ -166,4 +178,14 @@ public class XskhDisciplineRecordController extends BaseController { @RequestParam String endTime) { return R.ok(xskhDisciplineRecordService.getMaxScoreByTypeAndPeriod(typeId, startTime, endTime)); } + + /** + * 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索) + */ + @SaCheckPermission("disciplineRecord:disciplineRecord:query") + @GetMapping("/myCreated") + public TableDataInfo getMyCreatedDisciplineRecords(XskhDisciplineRecordBo bo, + PageQuery pageQuery) { + return xskhDisciplineRecordService.queryMyCreatedPageList(bo, pageQuery); + } } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java index 75db4990..c93965ff 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java @@ -22,10 +22,14 @@ public interface XskhDisciplineRecordMapper extends BaseMapperPlus selectStudentOptions(String keyword); + List selectStudentOptions(@Param("keyword") String keyword, + @Param("pageNum") Integer pageNum, + @Param("pageSize") Integer pageSize); /** * 查询班主任班级的违纪处分记录列表(班主任专用) @@ -78,7 +82,7 @@ public interface XskhDisciplineRecordMapper extends BaseMapperPlus getStudentOptions(String keyword); + /** + * 分页查询学生选项列表(用于移动端) + * + * @param keyword 搜索关键词 + * @param pageNum 页码 + * @param pageSize 每页数量 + * @return 学生选项列表 + */ + List getStudentOptionsPage(String keyword, Integer pageNum, Integer pageSize); + /** * 查询当前登录学生的违纪处分记录列表 */ @@ -101,4 +114,13 @@ public interface IXskhDisciplineRecordService { * @return 最高总扣分 */ java.math.BigDecimal getMaxScoreByTypeAndPeriod(Long typeId, String startTime, String endTime); + + /** + * 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索) + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 违纪记录分页列表 + */ + TableDataInfo queryMyCreatedPageList(XskhDisciplineRecordBo bo, PageQuery pageQuery); } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java index 97a73cbf..69798c8e 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java @@ -165,7 +165,15 @@ public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordSer */ @Override public List getStudentOptions(String keyword) { - return baseMapper.selectStudentOptions(keyword); + return baseMapper.selectStudentOptions(keyword, null, null); + } + + /** + * 分页查询学生选项列表(用于移动端) + */ + @Override + public List getStudentOptionsPage(String keyword, Integer pageNum, Integer pageSize) { + return baseMapper.selectStudentOptions(keyword, pageNum, pageSize); } /** @@ -286,4 +294,46 @@ public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordSer log.info("违纪类型 {} 的最高总扣分为: {}", typeId, maxScore); return maxScore; } + + /** + * 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索) + */ + @Override + public TableDataInfo queryMyCreatedPageList(XskhDisciplineRecordBo bo, + PageQuery pageQuery) { + // 获取当前登录用户ID + Long currentUserId = LoginHelper.getUserId(); + log.info("查询用户 {} 创建的违纪记录列表", currentUserId); + + // 构建查询条件,强制只查询当前用户创建的记录 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + + // 核心条件:只查询当前用户创建的记录 + lqw.eq(XskhDisciplineRecord::getCreateBy, currentUserId); + + // 支持按学生姓名或学号模糊搜索(使用OR连接) + if (StringUtils.isNotBlank(bo.getStudentName()) || StringUtils.isNotBlank(bo.getStudentNo())) { + String keyword = StringUtils.isNotBlank(bo.getStudentName()) ? bo.getStudentName() : bo.getStudentNo(); + lqw.and(wrapper -> wrapper + .like(XskhDisciplineRecord::getStudentName, keyword) + .or() + .like(XskhDisciplineRecord::getStudentNo, keyword)); + } + + // 其他可选查询条件 + lqw.eq(bo.getStudentId() != null, XskhDisciplineRecord::getStudentId, bo.getStudentId()); + lqw.eq(bo.getCollegeId() != null, XskhDisciplineRecord::getCollegeId, bo.getCollegeId()); + lqw.eq(bo.getMajorId() != null, XskhDisciplineRecord::getMajorId, bo.getMajorId()); + lqw.eq(bo.getClassId() != null, XskhDisciplineRecord::getClassId, bo.getClassId()); + lqw.eq(bo.getDisciplineTypeId() != null, XskhDisciplineRecord::getDisciplineTypeId, bo.getDisciplineTypeId()); + lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XskhDisciplineRecord::getStatus, bo.getStatus()); + + // 按创建时间倒序排列 + lqw.orderByDesc(XskhDisciplineRecord::getCreateTime); + + // 执行分页查询 + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + log.info("查询到 {} 条记录", result.getTotal()); + return TableDataInfo.build(result); + } } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml index ac51253f..356face0 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml @@ -49,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select id, student_id, student_no, student_name, college_id, college_name, major_id, major_name, class_id, class_name, discipline_type_id, discipline_type_name, discipline_date, discipline_reason, discipline_content, score_deduction, status, violation_source, attachment_oss_ids, create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id from xskh_discipline_record - select id, student_no, name, college_id, college_name, major_id, major_name, class_id, class_name @@ -60,8 +60,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and (student_no like concat('%', #{keyword}, '%') or name like concat('%', #{keyword}, '%')) - order by student_no - limit 50 + order by student_no desc + + + limit ${(pageNum - 1) * pageSize}, #{pageSize, jdbcType=INTEGER} + + + limit 50 + + diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java index e0638e8b..0d2b94e7 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java @@ -10,6 +10,7 @@ import org.dromara.zsjy.domain.vo.InternshipInfoVO; import org.dromara.zsjy.domain.bo.InternshipInfoQueryBO; import org.dromara.zsjy.domain.bo.InternshipInfoAddBO; import org.dromara.zsjy.domain.bo.InternshipInfoEditBO; +import org.dromara.zsjy.domain.bo.InternshipInfoChangeBO; import org.dromara.zsjy.service.IInternshipInfoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -225,4 +226,29 @@ public class InternshipInfoController extends BaseController { return R.fail("获取实习信息失败:" + e.getMessage()); } } + + /** + * 实习信息变更 + */ + @Operation(summary = "实习信息变更", description = "变更学生实习信息,将原记录设置为失效,并新增一条新的实习记录") + @SaCheckPermission("zsjy:internship:change") + @PostMapping("/change") + public R changeInternship(@RequestBody @Validated InternshipInfoChangeBO changeBO) { + log.info("实习信息变更请求,参数:{}", changeBO); + try { + Boolean result = internshipInfoService.changeInternshipInfo(changeBO); + if (result) { + log.info("实习信息变更成功,原实习ID:{},学生ID:{}", + changeBO.getInternshipId(), changeBO.getStudentId()); + return R.ok(); + } else { + log.error("实习信息变更失败,原实习ID:{}", changeBO.getInternshipId()); + return R.fail("实习信息变更失败"); + } + } catch (Exception e) { + log.error("实习信息变更异常,原实习ID:{},错误信息:{}", + changeBO.getInternshipId(), e.getMessage(), e); + return R.fail("实习信息变更失败:" + e.getMessage()); + } + } } diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java index 523eafe1..2e5b7129 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java @@ -31,6 +31,11 @@ public class ZsjyInternshipInfo extends BaseEntity { @TableId(value = "internship_id") private Long internshipId; + /** + * 前一条实习记录ID(用于变更记录关联) + */ + private Long previousInternshipId; + /** * 学生ID */ @@ -166,5 +171,9 @@ public class ZsjyInternshipInfo extends BaseEntity { */ @TableLogic(value = "0", delval = "1") private Long deleted; + + /** + * 变更原因(用于记录实习信息变更的原因) + */ + private String changeReason; } - \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/InternshipInfoChangeBO.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/InternshipInfoChangeBO.java new file mode 100644 index 00000000..f9c1b84c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/InternshipInfoChangeBO.java @@ -0,0 +1,182 @@ +package org.dromara.zsjy.domain.bo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 实习信息变更业务对象 InternshipInfoChangeBO + *

用于实习信息变更功能,将原实习记录设置为"已变更"状态,并新增一条新的实习记录

+ * + * @author system + * @date 2025-01-03 + */ +@Data +@Schema(description = "实习信息变更业务对象") +public class InternshipInfoChangeBO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 原实习信息ID(要变更的实习记录ID) + */ + @Schema(description = "原实习信息ID") + @NotNull(message = "原实习信息ID不能为空") + private Long internshipId; + + /** + * 学生ID(用于验证权限) + */ + @Schema(description = "学生ID") + @NotNull(message = "学生ID不能为空") + private Long studentId; + + /** + * 变更原因(必填) + */ + @Schema(description = "变更原因") + @NotBlank(message = "变更原因不能为空") + private String changeReason; + + /** + * 实习状态:1-进行中,2-已完成,3-已暂停,4-已变更 + */ + @Schema(description = "实习状态:1-进行中,2-已完成,3-已暂停,4-已变更") + @NotBlank(message = "实习状态不能为空") + @Pattern(regexp = "^[1-4]$", message = "实习状态只能是1-进行中,2-已完成,3-已暂停,4-已变更") + private String internshipStatus; + + /** + * 实习单位名称 + */ + @Schema(description = "实习单位名称") + @NotBlank(message = "实习单位名称不能为空") + private String companyName; + + /** + * 实习单位地址 + */ + @Schema(description = "实习单位地址") + private String companyAddress; + + /** + * 单位联系人 + */ + @Schema(description = "单位联系人") + private String companyContact; + + /** + * 联系人电话 + */ + @Schema(description = "联系人电话") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确") + private String companyPhone; + + /** + * 行业类型 + */ + @Schema(description = "行业类型") + private String companyIndustry; + + /** + * 单位规模 + */ + @Schema(description = "单位规模") + private String companyScale; + + /** + * 实习岗位名称 + */ + @Schema(description = "实习岗位名称") + @NotBlank(message = "实习岗位名称不能为空") + private String positionName; + + /** + * 岗位职责 + */ + @Schema(description = "岗位职责") + private String positionDuties; + + /** + * 实习补贴/薪资 + */ + @Schema(description = "实习补贴/薪资") + @Positive(message = "实习补贴/薪资必须大于0") + private BigDecimal internshipAllowance; + + /** + * 工作时长 + */ + @Schema(description = "工作时长") + private String workHours; + + /** + * 住宿地址 + */ + @Schema(description = "住宿地址") + private String accommodationAddress; + + /** + * 住宿类型 + */ + @Schema(description = "住宿类型") + private String accommodationType; + + /** + * 住宿费用 + */ + @Schema(description = "住宿费用") + private BigDecimal accommodationCost; + + /** + * 实习开始时间 + */ + @Schema(description = "实习开始时间") + @JsonFormat(pattern = "yyyy-MM-dd") + @NotNull(message = "实习开始时间不能为空") + private Date startDate; + + /** + * 实习结束时间 + */ + @Schema(description = "实习结束时间") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date endDate; + + /** + * 联系人姓名 + */ + @Schema(description = "联系人姓名") + private String contactPersonName; + + /** + * 联系人电话 + */ + @Schema(description = "联系人电话") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确") + private String contactPersonPhone; + + /** + * 实习协议状态:1-已签署,2-未签署 + */ + @Schema(description = "实习协议状态:1-已签署,2-未签署") + @Pattern(regexp = "^[1-2]$", message = "实习协议状态只能是1-已签署,2-未签署") + private String agreementStatus; + + /** + * 备注信息 + */ + @Schema(description = "备注信息") + private String remark; +} + diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentInfoMapper.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentInfoMapper.java index 877d1e19..f85d5969 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentInfoMapper.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentInfoMapper.java @@ -39,7 +39,7 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus> groupByMajor(); /** @@ -47,7 +47,7 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus> groupByRegion(); /** @@ -55,7 +55,7 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus> countByGenderList(); /** @@ -82,7 +82,7 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus= DATE_SUB(CURDATE(), INTERVAL #{days} DAY) AND deleted = 0 GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY date") + @Select("SELECT DATE_FORMAT(create_time, '%Y-%m-%d') as date, COUNT(id) as count FROM zsjy_enrollment_info WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY) AND deleted = 0 GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY date DESC, MAX(create_time) DESC") List> countByDateRange(@Param("days") Integer days); /** @@ -110,7 +110,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByYear( @Param("startDate") Date startDate, @@ -142,7 +143,7 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByMonth( @Param("startDate") Date startDate, @@ -174,7 +175,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByDay( @Param("startDate") Date startDate, @@ -207,7 +209,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByMajor( @Param("startDate") Date startDate, @@ -241,7 +244,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByRegion( @Param("startDate") Date startDate, @@ -273,7 +277,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByGender( @Param("startDate") Date startDate, @@ -315,7 +320,8 @@ public interface ZsjyEnrollmentInfoMapper extends BaseMapperPlus statisticsByStatus( @Param("startDate") Date startDate, diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentProcessMapper.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentProcessMapper.java index 314a5f88..2d196254 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentProcessMapper.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyEnrollmentProcessMapper.java @@ -54,7 +54,8 @@ public interface ZsjyEnrollmentProcessMapper extends BaseMapperPlus> countByStatusList(); @@ -82,7 +83,7 @@ public interface ZsjyEnrollmentProcessMapper extends BaseMapperPlus selectMyAuditList(@Param("userId") Long userId); /** diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java index 660703d5..38a30d2d 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java @@ -6,6 +6,7 @@ import org.dromara.zsjy.domain.vo.InternshipInfoVO; import org.dromara.zsjy.domain.bo.InternshipInfoQueryBO; import org.dromara.zsjy.domain.bo.InternshipInfoAddBO; import org.dromara.zsjy.domain.bo.InternshipInfoEditBO; +import org.dromara.zsjy.domain.bo.InternshipInfoChangeBO; import jakarta.servlet.http.HttpServletResponse; import java.util.List; @@ -105,4 +106,13 @@ public interface IInternshipInfoService { * @return 当前学生的实习信息,如果没有则返回null */ InternshipInfoVO getCurrentStudentInternship(); + + /** + * 实习信息变更 + *

将原实习记录设置为"已变更"状态,并新增一条新的实习记录

+ * + * @param changeBO 变更参数 + * @return 是否成功 + */ + Boolean changeInternshipInfo(InternshipInfoChangeBO changeBO); } diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java index eba50d51..c13fdbca 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java @@ -20,6 +20,7 @@ import org.dromara.common.core.exception.ServiceException; import org.dromara.zsjy.domain.bo.InternshipInfoQueryBO; import org.dromara.zsjy.domain.bo.InternshipInfoAddBO; import org.dromara.zsjy.domain.bo.InternshipInfoEditBO; +import org.dromara.zsjy.domain.bo.InternshipInfoChangeBO; import org.dromara.zsjy.domain.vo.InternshipInfoVO; import org.dromara.zsjy.domain.ZsjyInternshipInfo; import org.dromara.zsjy.mapper.InternshipInfoMapper; @@ -508,4 +509,113 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService { log.info("成功获取当前学生实习信息,实习ID:{}", vo.getInternshipId()); return vo; } + + /** + * 实习信息变更 + *

业务逻辑:

+ *
    + *
  1. 将原实习记录的状态设置为"4已变更",标记为失效
  2. + *
  3. 新增一条实习记录,previous_internship_id指向原记录
  4. + *
  5. 保存变更原因到新记录的change_reason字段
  6. + *
+ * + * @param changeBO 变更参数 + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean changeInternshipInfo(InternshipInfoChangeBO changeBO) { + log.info("=== 实习信息变更开始 ==="); + log.info("1. 接收到的变更参数:{}", changeBO); + + // 1. 查询原实习记录 + ZsjyInternshipInfo oldInternshipInfo = zsjyInternshipInfoMapper.selectById(changeBO.getInternshipId()); + if (ObjectUtil.isNull(oldInternshipInfo)) { + log.error("原实习信息不存在,ID:{}", changeBO.getInternshipId()); + throw new ServiceException("原实习信息不存在"); + } + log.info("2. 查询到原实习记录:实习ID={}, 学生ID={}, 单位={}", + oldInternshipInfo.getInternshipId(), oldInternshipInfo.getStudentId(), + oldInternshipInfo.getCompanyName()); + + // 2. 验证学生ID是否匹配 + if (!oldInternshipInfo.getStudentId().equals(changeBO.getStudentId())) { + log.error("学生ID不匹配,原记录学生ID:{},变更请求学生ID:{}", + oldInternshipInfo.getStudentId(), changeBO.getStudentId()); + throw new ServiceException("学生ID不匹配,无法进行变更操作"); + } + + // 3. 检查原记录状态是否允许变更(只有"进行中"的实习可以变更) + if (!"1".equals(oldInternshipInfo.getInternshipStatus())) { + log.error("只有进行中的实习信息才能变更,当前状态:{}", oldInternshipInfo.getInternshipStatus()); + throw new ServiceException("只有进行中的实习信息才能变更"); + } + + // 4. 将原实习记录设置为"已变更"状态 + ZsjyInternshipInfo updateOldInfo = new ZsjyInternshipInfo(); + updateOldInfo.setInternshipId(oldInternshipInfo.getInternshipId()); + updateOldInfo.setInternshipStatus("4"); // 4=已变更 + int updateResult = zsjyInternshipInfoMapper.updateById(updateOldInfo); + log.info("3. 更新原实习记录状态为'已变更',结果:{}", updateResult > 0 ? "成功" : "失败"); + + if (updateResult <= 0) { + log.error("更新原实习记录状态失败"); + throw new ServiceException("更新原实习记录状态失败"); + } + + // 5. 创建新的实习记录 + ZsjyInternshipInfo newInternshipInfo = new ZsjyInternshipInfo(); + + // 复制学生基础信息(从原记录) + newInternshipInfo.setStudentId(oldInternshipInfo.getStudentId()); + newInternshipInfo.setStudentNo(oldInternshipInfo.getStudentNo()); + newInternshipInfo.setStudentName(oldInternshipInfo.getStudentName()); + newInternshipInfo.setGender(oldInternshipInfo.getGender()); + newInternshipInfo.setCollegeId(oldInternshipInfo.getCollegeId()); + newInternshipInfo.setMajorId(oldInternshipInfo.getMajorId()); + newInternshipInfo.setClassId(oldInternshipInfo.getClassId()); + newInternshipInfo.setPhone(oldInternshipInfo.getPhone()); + + // 设置变更关联信息 + newInternshipInfo.setPreviousInternshipId(oldInternshipInfo.getInternshipId()); // 关联原记录 + newInternshipInfo.setChangeReason(changeBO.getChangeReason()); // 变更原因 + + // 设置新的实习信息(来自changeBO) + newInternshipInfo.setInternshipStatus(changeBO.getInternshipStatus()); + newInternshipInfo.setCompanyName(changeBO.getCompanyName()); + newInternshipInfo.setCompanyAddress(changeBO.getCompanyAddress()); + newInternshipInfo.setCompanyContact(changeBO.getCompanyContact()); + newInternshipInfo.setCompanyPhone(changeBO.getCompanyPhone()); + newInternshipInfo.setCompanyIndustry(changeBO.getCompanyIndustry()); + newInternshipInfo.setCompanyScale(changeBO.getCompanyScale()); + newInternshipInfo.setPositionName(changeBO.getPositionName()); + newInternshipInfo.setPositionDuties(changeBO.getPositionDuties()); + newInternshipInfo.setInternshipAllowance(changeBO.getInternshipAllowance()); + newInternshipInfo.setWorkHours(changeBO.getWorkHours()); + newInternshipInfo.setAccommodationAddress(changeBO.getAccommodationAddress()); + newInternshipInfo.setAccommodationType(changeBO.getAccommodationType()); + newInternshipInfo.setAccommodationCost(changeBO.getAccommodationCost()); + newInternshipInfo.setStartDate(changeBO.getStartDate()); + newInternshipInfo.setEndDate(changeBO.getEndDate()); + newInternshipInfo.setStatus("0"); // 状态:0=正常 + + log.info("4. 准备插入新实习记录:学生ID={}, 新单位={}, 变更原因={}", + newInternshipInfo.getStudentId(), newInternshipInfo.getCompanyName(), + newInternshipInfo.getChangeReason()); + + // 6. 插入新记录 + int insertResult = zsjyInternshipInfoMapper.insert(newInternshipInfo); + log.info("5. 插入新实习记录,结果:{}", insertResult > 0 ? "成功" : "失败"); + log.info("6. 新实习记录ID:{}", newInternshipInfo.getInternshipId()); + + if (insertResult <= 0) { + log.error("插入新实习记录失败"); + throw new ServiceException("插入新实习记录失败"); + } + + boolean success = insertResult > 0; + log.info("=== 实习信息变更结束,结果:{} ===", success ? "成功" : "失败"); + + return success; + } } diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/InternshipInfoMapper.xml b/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/InternshipInfoMapper.xml index 7a42d5f0..2dae10a7 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/InternshipInfoMapper.xml +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/InternshipInfoMapper.xml @@ -153,7 +153,7 @@ resultMap="InternshipInfoVOResult"> - ORDER BY s.class_name, s.student_no + ORDER BY s.student_no DESC, s.class_name, s.create_time DESC @@ -176,7 +176,7 @@ AND c.class_status = '1' AND c.class_teacher_id = #{teacherId} AND c.tenant_id = '000000' - ORDER BY c.grade DESC, c.class_name + ORDER BY c.grade DESC, c.class_name, c.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml b/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml index 092e5fce..53662a7b 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml @@ -335,7 +335,7 @@ AND m.submit_time <= #{bo.submitTimeEnd} - ORDER BY m.review_time DESC, m.submit_time DESC + ORDER BY m.review_time DESC, m.submit_time DESC, m.create_time DESC diff --git a/backend/script/sql/update/20250723/delete-evaluation-published-status.sql b/backend/script/sql/update/20250723/delete-evaluation-published-status.sql deleted file mode 100644 index 782535b2..00000000 --- a/backend/script/sql/update/20250723/delete-evaluation-published-status.sql +++ /dev/null @@ -1,20 +0,0 @@ --- ========================================== --- 删除心理测评状态字典中的"已发布"状态 --- 版本:V1.0.0 --- 功能:根据需求调整,心理健康测评只保留"草稿"和"已完成"两种状态 --- 作者:Cursor AI --- 时间:2025-07-23 --- ========================================== - --- 设置字符集 -SET NAMES utf8mb4; - --- 删除"已发布"状态的字典数据 -DELETE FROM `sys_dict_data` WHERE `dict_type` = 'evaluation_status' AND `dict_value` = '1'; - --- 更新字典顺序 -UPDATE `sys_dict_data` SET `dict_sort` = 1 WHERE `dict_type` = 'evaluation_status' AND `dict_value` = '2'; - --- 验证结果 -SELECT '删除心理测评状态字典中的已发布状态完成!' AS result; -SELECT * FROM `sys_dict_data` WHERE `dict_type` = 'evaluation_status' ORDER BY `dict_sort`; \ No newline at end of file diff --git a/backend/script/sql/update/20250723/fix-xljk-admin-permissions.sql b/backend/script/sql/update/20250723/fix-xljk-admin-permissions.sql deleted file mode 100644 index 2d6de0c0..00000000 --- a/backend/script/sql/update/20250723/fix-xljk-admin-permissions.sql +++ /dev/null @@ -1,62 +0,0 @@ --- ========================================== --- 修复心理健康管理员角色权限 --- 版本:V1.0.0 --- 功能:为心理健康管理员角色添加心理健康测评的查询权限 --- 作者:Cursor AI --- 时间:2025-07-23 --- ========================================== - --- 设置字符集 -SET NAMES utf8mb4; - --- 1. 检查心理健康管理员角色是否存在 -SELECT @role_id := role_id FROM sys_role WHERE role_key = 'xljk_admin' LIMIT 1; - --- 2. 如果角色不存在,创建心理健康管理员角色 -INSERT INTO sys_role (role_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, del_flag, create_by, create_time, remark) -SELECT 117, '心理健康管理员', 'xljk_admin', 7, '1', '1', '1', '0', '0', 1, NOW(), '心理健康管理员' -WHERE NOT EXISTS (SELECT 1 FROM sys_role WHERE role_key = 'xljk_admin'); - --- 3. 重新获取角色ID -SELECT @role_id := role_id FROM sys_role WHERE role_key = 'xljk_admin' LIMIT 1; - --- 4. 为心理健康管理员角色分配心理健康测评菜单权限 --- 检查是否已经有菜单权限 -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT @role_id, 2300 -FROM dual -WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = @role_id AND menu_id = 2300); - -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT @role_id, 2310 -FROM dual -WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = @role_id AND menu_id = 2310); - --- 5. 为心理健康管理员角色分配心理健康测评查询和导出权限 -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT @role_id, 2311 -FROM dual -WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = @role_id AND menu_id = 2311); - -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT @role_id, 2315 -FROM dual -WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = @role_id AND menu_id = 2315); - --- 6. 为心理健康管理员角色分配心理健康测评统计权限 -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT @role_id, menu_id -FROM sys_menu -WHERE perms = 'xljk:evaluation:statistics' -AND NOT EXISTS ( - SELECT 1 FROM sys_role_menu - WHERE role_id = @role_id - AND menu_id = (SELECT menu_id FROM sys_menu WHERE perms = 'xljk:evaluation:statistics' LIMIT 1) -); - --- 7. 验证结果 -SELECT '心理健康管理员角色权限修复完成!' AS result; -SELECT CONCAT('心理健康管理员角色ID: ', @role_id) AS role_info; -SELECT CONCAT('已分配菜单权限: ', COUNT(*)) AS menu_count -FROM sys_role_menu -WHERE role_id = @role_id; \ No newline at end of file diff --git a/docs/原型文件/人事管理/recruitment-prototype.html b/docs/原型文件/人事管理/recruitment-prototype.html index e530ed27..d36e96b1 100644 --- a/docs/原型文件/人事管理/recruitment-prototype.html +++ b/docs/原型文件/人事管理/recruitment-prototype.html @@ -612,18 +612,6 @@ -
-
- - -
-
@@ -813,7 +801,6 @@ 职位名称 部门 - 薪资范围 工作地点 发布日期 状态 @@ -826,7 +813,6 @@ 高级Java开发工程师 教学部门 - 15k-25k 杭州 2025-07-01 招聘中 @@ -911,11 +897,6 @@
-
- - -
-
diff --git a/docs/变更管理/code-adjust-record-2025-01-08-cursor.md b/docs/变更管理/code-adjust-record-2025-01-08-cursor.md deleted file mode 100644 index 5964cba7..00000000 --- a/docs/变更管理/code-adjust-record-2025-01-08-cursor.md +++ /dev/null @@ -1,75 +0,0 @@ -# 代码调整记录 - 2025-01-08 - Cursor AI - -## 📝 变更概述 -**变更时间**: 2025-01-08 15:30:00 -**变更人员**: Cursor AI -**变更类型**: 数据库结构修复 -**影响模块**: 社团管理 - 活动报名功能 - -## 🔴 **问题描述** -**问题现象**: 活动报名接口数据插入失败,报错缺少字段 -**根本原因**: `stgl_activity_registration` 数据库表结构与实体类不匹配,缺失多个字段 - -**缺失字段清单**: -- `remark` - 申请理由(非必填) -- `gender` - 性别 -- `phone` - 联系电话 -- `email` - 邮箱 -- `department` - 院系 -- `major` - 专业 -- `class_name` - 班级 - -## ✅ **解决方案** - -### 1. 创建修复SQL脚本 -**文件**: `docs/设计文档/database/社团管理/修复活动报名表字段.sql` -- 添加7个缺失字段到 `stgl_activity_registration` 表 -- 新增性能优化索引 (`idx_gender`, `idx_phone`, `idx_department`) -- 包含验证和检查语句 - -### 2. 更新建表脚本 -**文件**: `docs/设计文档/database/社团管理/社团管理建表脚本.sql` -- 在 `stgl_activity_registration` 表中补充完整字段定义 -- 调整字段顺序,符合逻辑分组(基本信息、审核信息、成绩信息、学生个人信息) -- 新增对应索引 - -## 📊 **影响范围** -- ✅ **后端**: StglActivityRegistration 实体类字段完整匹配 -- ✅ **数据库**: 表结构字段补全,支持完整业务数据 -- ✅ **API**: 活动报名相关接口正常工作 -- ✅ **前端**: 报名表单数据可正常提交和保存 - -## 🧪 **验证方法** -```sql --- 1. 执行修复脚本 -source docs/设计文档/database/社团管理/修复活动报名表字段.sql - --- 2. 验证表结构 -DESCRIBE stgl_activity_registration; - --- 3. 测试插入数据 --- 使用活动报名API接口测试完整数据提交 -``` - -## 📋 **待执行任务** -- [x] 分析问题原因 -- [x] 创建修复SQL脚本 -- [x] 更新建表脚本 -- [x] 记录变更日志 -- [ ] **需要执行**: 在数据库中运行修复脚本 -- [ ] **需要测试**: 验证活动报名功能正常 - -## 🔗 **相关文件** -- `backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivityRegistration.java` -- `backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityRegistrationMapper.xml` -- `docs/设计文档/database/社团管理/社团管理建表脚本.sql` -- `docs/设计文档/database/社团管理/修复活动报名表字段.sql` (新增) - -## 💡 **经验总结** -1. **数据库与实体类一致性**: 开发时必须确保数据库表结构与实体类字段完全匹配 -2. **渐进式开发问题**: 实体类后续添加字段时,必须同步更新数据库表结构 -3. **文档同步重要性**: 建表脚本必须与实际代码保持同步,避免部署时出现问题 -4. **字段分组设计**: 表字段应该按业务逻辑分组,提高可读性和维护性 - ---- -*本记录严格按照项目规范要求,代码变更与文档同步更新* \ No newline at end of file diff --git a/docs/变更管理/code-adjust-record-2025-01-14-cursor.md b/docs/变更管理/code-adjust-record-2025-01-14-cursor.md deleted file mode 100644 index 756d27de..00000000 --- a/docs/变更管理/code-adjust-record-2025-01-14-cursor.md +++ /dev/null @@ -1,241 +0,0 @@ -# 代码调整记录 - 2025-01-14 - cursor - -## 📝 变更概述 -**变更日期**: 2025-01-14 15:30:00 -**处理人员**: cursor -**变更类型**: 前端功能修复 + UI优化 - -## 🎯 变更内容 - -### 1. 修复预约时段状态切换功能 (XljkConsultationSchedule) - -**问题描述**: -- 心理健康咨询预约时段管理页面,点击编辑按钮后状态切换异常 -- 无论当前状态如何,都会变成停用状态 -- 显示双重状态标签,界面异常 - -**根本原因分析**: -1. **状态值类型不一致**: 前端接收的`status`是字符串类型(`"0"`, `"1"`),但切换逻辑使用数字比较(`=== 1`) -2. **字典数据缺失**: `schedule_status`字典未正确配置,导致显示异常 -3. **状态更新接口数据类型错误**: 发送数字状态但接收端可能期望字符串 - -**修复方案**: - -#### 1.1 修复状态切换逻辑 -**文件**: `web/src/views/xljk/consultation/schedule/index.vue` - -**修改前**: -```typescript -const handleUpdate = async (row?: XljkConsultationScheduleVO) => { - if (row && row.id) { - try { - await updateXljkSchedule({ - id: row.id, - status: row.status === 1 ? 0 : 1 // 错误:字符串与数字比较 - } as XljkConsultationScheduleForm); - ElMessage.success('状态更新成功'); - getList(); - } catch (error) { - console.error('更新时段状态失败', error); - ElMessage.error('状态更新失败'); - } - } -}; -``` - -**修改后**: -```typescript -const handleUpdate = async (row?: XljkConsultationScheduleVO) => { - if (row && row.id) { - try { - // 修复:将字符串状态正确转换为数字进行比较 - const currentStatus = parseInt(row.status as string); - const newStatus = currentStatus === 1 ? 0 : 1; - - console.log(`状态切换调试 - 当前状态: ${row.status} (${typeof row.status}), 转换后: ${currentStatus}, 新状态: ${newStatus}`); - - await updateXljkSchedule({ - id: row.id, - status: newStatus.toString() - } as XljkConsultationScheduleForm); - - ElMessage.success(`状态已${newStatus === 0 ? '启用' : '停用'}`); - getList(); - } catch (error) { - console.error('更新时段状态失败', error); - ElMessage.error('状态更新失败'); - } - } -}; -``` - -**修复要点**: -- ✅ 使用 `parseInt()` 将字符串状态转换为数字进行比较 -- ✅ 添加调试日志输出,便于问题排查 -- ✅ 改进用户提示消息,明确显示状态变更结果 -- ✅ 确保发送的状态值类型一致性 - -#### 1.2 创建字典数据配置 -**文件**: `backend/script/sql/xljk-dict-data.sql` - -**新增内容**: -```sql --- 预约时段状态字典 -INSERT INTO sys_dict_type VALUES (102, '000000', '预约时段状态', 'schedule_status', 103, 1, sysdate(), NULL, NULL, '预约时段状态列表'); - --- 预约时段状态字典数据 -INSERT INTO sys_dict_data VALUES (300, '000000', 1, '正常', '0', 'schedule_status', '', 'success', 'Y', 103, 1, sysdate(), NULL, NULL, '正常状态'); -INSERT INTO sys_dict_data VALUES (301, '000000', 2, '停用', '1', 'schedule_status', '', 'danger', 'N', 103, 1, sysdate(), NULL, NULL, '停用状态'); -``` - -**配置要点**: -- ✅ 创建 `schedule_status` 字典类型 -- ✅ 配置状态值:`0-正常`(success样式)、`1-停用`(danger样式) -- ✅ 同时创建相关的预约状态和时段类型字典 - -### 2. 优化预约时段管理列表列宽 (2025-01-14 15:45:00) - -**调整目标**: 缩小上午时段、下午时段、创建时间3个列的列宽,优化页面布局 - -**列宽调整对比**: - -| 字段名称 | 调整前 | 调整后 | 变化 | -|----------|--------|--------|------| -| **上午时段** | `min-width="150"` | `min-width="120"` | -30px | -| **下午时段** | `min-width="150"` | `min-width="120"` | -30px | -| **创建时间** | `width="150"` | `width="120"` | -30px | - -**修改位置**: `web/src/views/xljk/consultation/schedule/index.vue` (第84-102行) - -**修改内容**: -```vue - - - - - - - - -``` - -**优化效果**: -- ✅ 总体节省空间:90px (30px × 3列) -- ✅ 上午/下午时段列保持响应式 (`min-width`) -- ✅ 创建时间列固定宽度更紧凑 -- ✅ 为其他列提供更多显示空间 -- ✅ 提升页面整体布局效率 - -**表格布局更新**: -- **原总最小宽度**: 1115px -- **新总最小宽度**: 1025px -- **空间优化**: 节省90px宽度 - -### 3. 移除预约限制管理页面创建时间字段 (2025-01-14 16:00:00) - -**调整目标**: 移除预约限制管理(未到访记录)列表界面的创建时间字段显示 - -**页面位置**: `web/src/views/xljk/consultation/noshow/index.vue` - -**移除字段**: -```vue - - - - -``` - -**修改位置**: 第92-96行 - -**优化效果**: -- ✅ **简化界面**: 移除不必要的时间信息显示 -- ✅ **节省空间**: 释放150px列宽空间 -- ✅ **聚焦核心**: 突出学生信息、限制状态等关键信息 -- ✅ **提升体验**: 减少信息冗余,提高页面简洁度 - -**当前保留字段**: -- 学生姓名、学号、院系、专业、班级 -- 未到访日期、限制到期日、状态 -- 操作(查看、解除限制) - -**表格布局优化**: -- **移除列宽**: 150px -- **界面更简洁**: 专注于业务核心信息 -- **操作便捷**: 保留查看详情和解除限制功能 - -## 🔧 技术细节 - -### 问题原因深度分析 - -1. **JavaScript类型转换问题**: - ```javascript - // 问题代码 - "1" === 1 // false (字符串与数字比较) - - // 修复代码 - parseInt("1") === 1 // true (转换后比较) - ``` - -2. **数据流类型不一致**: - ``` - 后端返回 → status: "0" (字符串) - 前端比较 → status === 1 (数字) - 结果 → 永远返回false,总是执行 : 1 分支 - ``` - -3. **字典配置缺失**: - ```vue - - ``` - 当 `schedule_status` 字典为空时,`dict-tag` 组件无法正确渲染,可能显示原始值或双重标签。 - -### 测试验证 - -**验证步骤**: -1. ✅ 启动前端开发服务器 -2. ✅ 导航至心理健康 → 咨询管理 → 预约时段 -3. ✅ 检查状态列显示是否正常(应显示"正常"或"停用") -4. ✅ 点击编辑按钮测试状态切换 -5. ✅ 观察控制台调试日志 -6. ✅ 验证状态在正常/停用之间正确切换 - -**预期结果**: -- 状态列正确显示"正常"/"停用"标签 -- 点击编辑按钮后状态正确切换 -- 用户提示消息明确显示变更结果 -- 控制台输出调试信息供问题排查 - -## 📊 影响范围 - -**直接影响**: -- ✅ 心理健康咨询预约时段管理功能恢复正常 -- ✅ 状态切换功能可正确启用/停用时段 - -**间接影响**: -- ✅ 改善用户体验,操作反馈更清晰 -- ✅ 为类似的状态切换功能提供标准修复模式 -- ✅ 完善了心理健康模块的字典数据配置 - -## 🚀 后续优化建议 - -1. **统一类型处理**: 考虑在前端建立统一的状态值处理工具函数 -2. **完善错误处理**: 添加更详细的错误处理和用户提示 -3. **字典数据管理**: 建立字典数据的统一管理和维护机制 -4. **测试用例**: 为状态切换功能添加单元测试和集成测试 - -## 📋 变更文件清单 - -| 文件路径 | 变更类型 | 说明 | -|----------|----------|------| -| `web/src/views/xljk/consultation/schedule/index.vue` | 修改 | 修复状态切换逻辑 + 优化列宽布局 | -| `web/src/views/xljk/consultation/noshow/index.vue` | 修改 | 移除创建时间字段显示 | -| `backend/script/sql/xljk-dict-data.sql` | 新增 | 创建心理健康模块字典数据配置 | -| `docs/变更管理/code-adjust-record-2025-01-14-cursor.md` | 新增 | 本变更记录文档 | - ---- - -**验证完成**: ✅ 功能测试通过 -**文档更新**: ✅ 已同步更新 -**代码审查**: ✅ 自检完成 \ No newline at end of file diff --git a/docs/变更管理/code-adjust-record-2025-01-15-cursor.md b/docs/变更管理/code-adjust-record-2025-01-15-cursor.md deleted file mode 100644 index 92d8fe70..00000000 --- a/docs/变更管理/code-adjust-record-2025-01-15-cursor.md +++ /dev/null @@ -1,66 +0,0 @@ -# 代码调整记录 - 2025-01-15 - cursor - -## 🔧 数据库字段修复 - -### 📅 变更时间 -2025-01-15 14:30:00 - -### 🎯 变更目标 -修复 `stgl_activity` 表缺失的 `remark` 字段 - -### ⚠️ 问题描述 -在社团活动管理功能中出现以下错误: -``` -Unknown column 'sa.remark' in 'field list' -``` - -**错误原因分析:** -1. 实体类 `StglActivity.java` 中定义了 `remark` 字段 -2. MyBatis映射文件 `StglActivityMapper.xml` 中的SQL查询包含了 `sa.remark` 字段 -3. 但数据库表 `stgl_activity` 中实际缺少 `remark` 字段 - -### 🔍 修复过程 - -#### 1. 问题定位 -- 检查实体类:`StglActivity.java` 第216-217行定义了remark字段 -- 检查映射文件:`StglActivityMapper.xml` 第59行和第40行引用了remark字段 -- 检查数据库表:`stgl_activity` 表确实缺少remark字段 - -#### 2. 修复方案 -为 `stgl_activity` 表添加缺失的 `remark` 字段 - -#### 3. 执行的SQL语句 -```sql -ALTER TABLE `stgl_activity` -ADD COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '备注' AFTER `import_source`; -``` - -#### 4. 修复结果验证 -- ✅ remark字段成功添加到stgl_activity表 -- ✅ 字段类型:varchar(500) -- ✅ 字段注释:备注 -- ✅ 字段位置:import_source字段之后 - -### 📁 相关文件 -- **实体类**: `backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java` -- **映射文件**: `backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml` -- **修复脚本**: `backend/script/sql/fix-stgl-activity-remark-field.sql` -- **建表脚本**: `docs/设计文档/database/社团管理/社团管理建表脚本.sql` - -### 🎯 影响范围 -- **功能模块**: 社团活动管理 -- **影响接口**: 所有涉及stgl_activity表查询的接口 -- **数据完整性**: 修复后代码与数据库结构保持一致 - -### ✅ 验证结果 -修复完成后,社团活动管理相关功能应该能正常运行,不再出现字段缺失错误。 - -### 📝 经验教训 -1. **数据库同步**: 确保实体类、映射文件与数据库表结构完全一致 -2. **建表脚本**: 需要检查建表脚本是否包含所有必要字段 -3. **字段映射**: MyBatis映射文件中的字段必须在数据库表中实际存在 - ---- -**修复人员**: Cursor AI -**修复时间**: 2025-01-15 14:30:00 -**状态**: ✅ 已完成 \ No newline at end of file diff --git a/docs/变更管理/code-adjust-record-2025-01-21-cursor.md b/docs/变更管理/code-adjust-record-2025-01-21-cursor.md deleted file mode 100644 index 7b5a8f9b..00000000 --- a/docs/变更管理/code-adjust-record-2025-01-21-cursor.md +++ /dev/null @@ -1,137 +0,0 @@ -# 代码调整记录 - 2025-01-21 - Cursor AI - -## 📋 变更概述 - -**时间**: 2025-01-21 10:30:00 -**操作人**: Cursor AI -**变更类型**: 🐛 Bug修复 -**影响范围**: 志愿者成员管理模块 - -## 🔍 问题描述 - -**问题现象**: 当前登录用户`2025LY23117`是志愿者马拉松工作组的队长,该队伍总共有3个人,但在志愿者成员管理页面只能看到一个成员。 - -**根本原因**: 志愿者角色类型(roleType)在前后端定义不一致,导致权限过滤逻辑失效。 - -### 问题详情 - -| 组件 | 队长值 | 队员值 | 状态 | -|------|--------|--------|------| -| 前端字典(修复前) | 1 | 2 | ❌ 错误 | -| 后端权限过滤 | 0 | 1 | ✅ 正确 | -| 数据库SQL映射 | 0 | 1 | ✅ 正确 | -| 字典表(修复前) | 1 | 2 | ❌ 错误 | - -**影响**: -- 权限过滤方法`getCaptainTeamIds`查找`roleType=0`时找不到队长记录 -- 查询条件被设置为`relationId=-1L`,导致返回空结果 -- 用户只能看到第一个队伍的数据 - -## 🛠️ 解决方案 - -**采用方案A**: 统一使用数据库SQL的定义(队长=0,队员=1) - -### 修复内容 - -#### 1. 修改字典表数据 -**文件**: `backend/script/sql/stgl_volunteer_role_dict.sql` -- 队长的`dict_value`从'1'改为'0' -- 队员的`dict_value`从'2'改为'1' - -#### 2. 修改前端字典定义 -**文件**: `web/src/views/stgl/volunteer/member/index.vue` -- 第358-361行:更新手动定义的字典 -- 第432行:修改默认角色值为'1'(队员) -- 第242行:移除parseInt转换,保持字符串类型 -- 第632行:修改类型转换为String() - -#### 3. 创建数据库更新脚本 -**文件**: `script/update-volunteer-role-dict.sql` -- 创建线上数据库更新脚本 -- 包含验证查询和完成提示 - -## 📝 修改细节 - -### 代码变更 - -```javascript -// 修复前 -const stgl_volunteer_role = ref([ - { value: '1', label: '队长' }, - { value: '2', label: '队员' } -]); - -// 修复后 -const stgl_volunteer_role = ref([ - { value: '0', label: '队长' }, - { value: '1', label: '队员' } -]); -``` - -```sql --- 修复前字典数据 -VALUES (..., '队长', '1', ...) -VALUES (..., '队员', '2', ...) - --- 修复后字典数据 -VALUES (..., '队长', '0', ...) -VALUES (..., '队员', '1', ...) -``` - -### 权限过滤逻辑验证 - -后端权限过滤逻辑保持不变(已正确): -```java -captainBo.setRoleType(0); // 队长角色 = 0 -``` - -数据库SQL映射保持不变(已正确): -```xml -WHEN 0 THEN '队长' -WHEN 1 THEN '队员' -``` - -## 🎯 预期效果 - -修复完成后: -1. ✅ 志愿者队长能够正确查看所属队伍的所有成员 -2. ✅ 权限过滤逻辑正常工作 -3. ✅ 前后端角色类型定义统一 -4. ✅ 字典数据显示正确 - -## 📋 部署步骤 - -1. **执行数据库更新脚本**: - ```bash - mysql -u username -p database < script/update-volunteer-role-dict.sql - ``` - -2. **重新构建前端**: - ```bash - cd web - npm run build - ``` - -3. **重启后端服务**(如有缓存) - -4. **验证功能**: - - 使用队长账号登录 - - 访问志愿者成员管理页面 - - 确认能看到所有队伍成员 - -## 🔍 验证要点 - -- [ ] 队长能查看所属队伍的所有成员 -- [ ] 角色显示正确(队长/队员) -- [ ] 新增成员时角色选择正常 -- [ ] 权限过滤按预期工作 - -## 📚 相关文档 - -- **问题分析**: 本文档"问题描述"部分 -- **技术规范**: `.cursor/cursorrules-backend.md` - 权限管理规范 -- **API文档**: `docs/开发文档/API/志愿者管理API.md` - -## 🏷️ 标签 - -`#志愿者管理` `#权限过滤` `#角色类型` `#前后端统一` `#字典数据` `#Bug修复` \ No newline at end of file diff --git a/docs/开发文档/API/API.md.backup-20250701-163220 b/docs/开发文档/API/API.md.backup-20250701-163220 index 419203e4..473956c1 100644 --- a/docs/开发文档/API/API.md.backup-20250701-163220 +++ b/docs/开发文档/API/API.md.backup-20250701-163220 @@ -1447,6 +1447,8 @@ admin 用户具有系统最高权限 `*:*:*`,可以访问所有功能模块和 **权限要求**: `pm:repairOrder:assign` +**排序规则**: 返回的维修人员选项按用户名(`user_name`)升序排列,便于快速定位目标用户 + **请求参数**: 无 **响应示例**: diff --git a/docs/开发文档/API/API文档基础规范.md b/docs/开发文档/API/API文档基础规范.md index d5820838..66f204fa 100644 --- a/docs/开发文档/API/API文档基础规范.md +++ b/docs/开发文档/API/API文档基础规范.md @@ -198,6 +198,8 @@ 4. **单条数据接口必须使用 R 类型包装返回结果** 5. **操作类接口必须使用 R 类型包装返回结果** 6. **必须包含 clientid、Content-Type 等必要请求头** +7. **查询类接口默认按创建时间倒序排列,若已有排序字段则在末尾追加 `create_time DESC` 保证新增数据优先展示** +7. **所有列表查询接口默认按 `create_time` 倒序排列;如接口自定义其他排序字段,必须在末尾追加 `create_time desc` 作为稳定排序项** --- diff --git a/docs/开发文档/API/API文档总览.md b/docs/开发文档/API/API文档总览.md index 06e458c5..8f78b7ee 100644 --- a/docs/开发文档/API/API文档总览.md +++ b/docs/开发文档/API/API文档总览.md @@ -121,9 +121,17 @@ - `student_status = '1'` (在籍状态) - `class_id = 入参的classId` (班级ID) - `name LIKE '%入参的name%'` (姓名模糊查询,可选) -- **排序方式**: 按创建时间倒序排列 +- **排序方式**: 按学号降序排列 +- **最新变更**(2025-11-13): 所有学生信息列表接口默认按学号降序排序,便于优先查看学号靠后的学生 - **权限要求**: `studentMsg:studentMsg:query` +### 2025-11-13 - 学生列表排序策略统一 +- **心理健康测评**: `GET /xljk/evaluation/detail/students/{classId}` 现在按学号降序返回班级学生,满足最新业务要求 +- **学生考核奖励**: 学生候选下拉接口默认按学号降序排列,保持后台与移动端行为一致 +- **心理咨询预约**: 预约记录列表接口默认按学号降序,随后按ID降序排序,并以创建时间倒序兜底保证顺序稳定 +- **预约限制功能**: 未到访记录分页默认先按学号降序,再按未到访日期、创建时间倒序 +- **学生信息批次收集**: 批次学生列表默认按学号降序,当学号一致时以创建时间倒序作为补充 + ### 2025-07-24 - 学生信息管理界面重构 - **界面重构**: 学生信息管理新增弹窗调整为多tab页形式 - **功能增强**: 新增照片上传功能,支持完整的学生信息录入 @@ -143,6 +151,13 @@ ## 🔗 API文档导航 +### 👨‍💼 人事管理模块 + +| API文档 | 描述 | 状态 | 更新时间 | +|---------|------|------|----------| +| [出国申请管理API](./出国申请管理API.md) | 出国申请审批流程管理(三级审批) | ✅ 完成 | 2025-01-28 | +| **[出国登记管理API](./出国登记管理API.md)** | **出国登记信息管理(已优化字段)** | **✅ 完成** | **2025-09-30** | + ### 👥 学工管理模块 | API文档 | 描述 | 状态 | 更新时间 | diff --git a/docs/开发文档/API/人事招聘模块API.md b/docs/开发文档/API/人事招聘模块API.md index a593ccff..43edba16 100644 --- a/docs/开发文档/API/人事招聘模块API.md +++ b/docs/开发文档/API/人事招聘模块API.md @@ -21,7 +21,6 @@ | job_id | bigint(20) | 是 | 职位ID,主键 | | title | varchar(100) | 是 | 职位名称 | | department | varchar(50) | 是 | 部门 | -| salary_range | varchar(50) | 是 | 薪资范围 | | location | varchar(100) | 是 | 工作地点 | | description | text | 是 | 职位描述 | | requirements | text | 是 | 任职要求 | @@ -130,7 +129,6 @@ "jobId": 1, "title": "软件工程师", "department": "tech", - "salaryRange": "15k-25k", "location": "北京", "isActive": "0", "publishTime": "2025-07-01 10:00:00", @@ -181,7 +179,6 @@ "jobId": 1, "title": "软件工程师", "department": "tech", - "salaryRange": "15k-25k", "location": "北京", "description": "负责系统设计和开发...", "requirements": "本科及以上学历,3年以上相关经验...", @@ -208,7 +205,7 @@ |-------|------|------|------| | title | string | 是 | 职位名称 | | department | string | 是 | 部门 | -| salaryRange | string | 是 | 薪资范围 | + | location | string | 是 | 工作地点 | | description | string | 是 | 职位描述 | | requirements | string | 是 | 任职要求 | @@ -236,7 +233,6 @@ | jobId | long | 是 | 职位ID | | title | string | 是 | 职位名称 | | department | string | 是 | 部门 | -| salaryRange | string | 是 | 薪资范围 | | location | string | 是 | 工作地点 | | description | string | 是 | 职位描述 | | requirements | string | 是 | 任职要求 | @@ -382,7 +378,7 @@ rs/ 1. 进入职位管理页面 (`/rs/job`) 2. 点击"新增"按钮添加职位 -3. 填写职位信息,包括职位名称、部门、薪资范围等 +3. 填写职位信息,包括职位名称、部门、工作地点等 4. 点击"确定"保存职位信息 ### 申请管理页面 diff --git a/docs/开发文档/API/出国申请管理API.md b/docs/开发文档/API/出国申请管理API.md index dd9347ce..7cc420f4 100644 --- a/docs/开发文档/API/出国申请管理API.md +++ b/docs/开发文档/API/出国申请管理API.md @@ -24,9 +24,26 @@ **模块名称**:overseas / 出国申请管理 **基础路径**:`/api/rs/overseas` -**版本**:v1.0 +**版本**:v1.1 **负责人**:weizzzz -**更新时间**:2025-01-28 +**更新时间**:2025-10-28 + +### 🆕 最近更新 + +**v1.1 (2025-10-28) - 分级审批权限升级** +- ✅ **审批权限升级为五级审批制度** + - 新增 `rs:overseas:approve1-5` 五个分级审批权限 + - 保留 `rs:overseas:approve` 作为通用审批权限 +- ✅ **Controller层采用OR模式权限验证** + - 用户拥有任一审批权限即可访问审批接口 + - 解决部门负责人403权限错误问题 +- ✅ **Service层增加审批级别验证** + - 新增 `validateApproverLevel()` 方法 + - 精确验证用户权限是否匹配申请的审批级别 + - 防止越级审批 +- ✅ **前端按钮权限优化** + - 审批按钮支持多级权限显示 + - 改善用户体验,按钮显示逻辑更合理 ### 模块功能 @@ -59,12 +76,107 @@ - `rs:overseas:record:create` - 创建记录权限 - `rs:overseas:record:complete` - 完成记录权限 -#### 出国审批权限 -- **权限前缀**:`rs:overseas:approval:` -- **权限列表**: +#### 出国审批权限(🆕 2025-10-28 更新为五级审批) +- **权限前缀**:`rs:overseas:` +- **审批记录查看权限**: - `rs:overseas:approval:list` - 查询审批记录列表权限 - `rs:overseas:approval:query` - 查询审批记录详情权限 - `rs:overseas:approval:export` - 导出审批记录权限 + +- **分级审批权限**(五级审批制度): + - `rs:overseas:approve` - 通用审批权限(超级管理员/高级审批者,可审批任意级别) + - `rs:overseas:approve1` - 第1级:部门负责人审批权限 + - `rs:overseas:approve2` - 第2级:人事处审批权限 + - `rs:overseas:approve3` - 第3级:纪委审批权限 + - `rs:overseas:approve4` - 第4级:学院分管领导审批权限 + - `rs:overseas:approve5` - 第5级:学院党委书记审批权限 +- **数据返回顺序**:审批人下拉接口(`/system/user/list-by-permission`)默认按用户名(`user_name`)升序排序,确保审批人选择器顺序一致(2025-11-13) + +- **权限使用说明**: + - 审批接口采用 **OR模式**,用户拥有以上任一权限即可调用接口 + - 具体审批权限在业务层验证,必须匹配申请的 `approval_level` + - 部门负责人通过 `rs:overseas:approve1` 权限识别,自动应用部门数据过滤 + +### 🔐 数据权限控制 + +#### 权限分级说明 + +出国申请系统采用**基于角色和部门的多级数据权限控制**,确保用户只能访问其权限范围内的数据。 + +#### 角色权限矩阵 + +| 角色 | 角色标识 | 数据访问范围 | 可见申请范围 | 备注 | +|------|----------|------------|-------------|------| +| **超级管理员** | `admin` | 全局数据 | 所有申请 | 不受任何限制 | +| **人事管理员** | `rs-admin` | 全局数据 | 所有申请 | 仅当不是部门负责人时 | +| **申请人** | `rs-Applicant` | 个人数据 | 仅自己的申请 | 只能查看和管理自己的申请 | +| **部门负责人** | 具有`rs:overseas:approve1`权限 | 本部门数据 | 本部门所有申请 | **优先级最高** | +| **普通审批人** | 其他审批权限 | 分配数据 | 分配给自己的审批 | 只能看到分配的审批任务 | + +#### 部门权限过滤规则(🆕 2025-01-28) + +**核心原则**:部门负责人**只能看到本部门成员提交的申请**,不能看到其他部门的申请。 + +**实现逻辑**: +1. **判断用户是否为部门负责人**(三种方式任选其一): + - 具有 `rs:overseas:approve1` 权限 + - 具有 `dept_head_role` 角色 + - 具有 `overseas:application:dept_head:approve` 权限(兼容旧版) + +2. **应用部门过滤**: + - 查询条件:`申请人的dept_id = 当前用户的dept_id` + - SQL实现:通过 `sys_user` 表关联,筛选申请人所属部门 + - **重要**:不限制审批人(approverId),只限制申请人部门(deptId) + +3. **权限优先级**(从高到低): + ``` + 超级管理员 > 申请人(自己的数据)> 部门负责人(本部门)> 人事管理员(全局)> 普通审批人(分配数据) + ``` + +**示例场景**: + +假设有以下用户和部门: +- **张三**:`dept_id = 001`(测试部门A),角色:申请人 +- **李四**:`dept_id = 001`(测试部门A),角色:部门负责人 +- **王五**:`dept_id = 002`(测试部门B),角色:部门负责人 +- **赵六**:`dept_id = 003`(测试部门C),角色:部门负责人 + +**张三提交申请后:** +- ✅ **李四**(同部门):可以看到张三的申请 +- ❌ **王五**(不同部门):不能看到张三的申请 +- ❌ **赵六**(不同部门):不能看到张三的申请 + +**技术实现**: + +```java +// Service层权限过滤逻辑 +private void applyDeptPermissionFilter(RsOverseasApprovalQuery query) { + // 1. 判断是否为部门负责人 + boolean isDeptApprover = isDeptApprover(currentUserId); + + // 2. 如果是部门负责人,只设置部门过滤 + if (isDeptApprover) { + query.setDeptId(currentDeptId); // 只应用部门过滤 + return; // 不设置approverId,避免过度限制 + } +} +``` + +```xml + + + AND EXISTS ( + SELECT 1 FROM sys_user u + WHERE u.user_id = app.applicant_id + AND u.dept_id = #{deptId} + ) + +``` + +**相关文档**: +- 测试指南:`docs/测试文档/出国申请部门权限过滤测试指南.md` +- 验证脚本:`部门权限过滤验证脚本.sql` +- 代码实现:`RsOverseasApprovalServiceImpl.java` --- @@ -95,6 +207,7 @@ | 取消申请 | PUT | `/rs/overseas/application/cancel/{id}` | `rs:overseas:application:cancel` | 取消出国申请 | | 审批申请 | POST | `/rs/overseas/application/approve` | `rs:overseas:application:approve` | 审批出国申请(通过/拒绝) | | 修复记录 | POST | `/rs/overseas/application/fix/records` | `rs:overseas:application:approve` | 修复已审批通过但未创建记录的申请 | +| **下载党籍保留PDF模板** | GET | `/rs/overseas/record/downloadPartyRetention` | `rs:overseas:query` | **新增**:下载党籍保留申请PDF空白模板(线下手工填写)(2025-10-17) | #### 出国记录管理接口 @@ -151,6 +264,7 @@ interface OverseasApplicationVO { accommodationPlan: string; // 住宿安排 transportationPlan: string; // 交通安排 status: string; // 申请状态编码 + approvalStatus: string; // 工作流状态编码 currentApprover: string; // 当前审批人 approvalLevel: string; // 审批级别 applyTime: string; // 申请时间 @@ -161,6 +275,7 @@ interface OverseasApplicationVO { updateTime: string; // 更新时间 // 显示字段 statusText: string; // 状态文本 + approvalStatusText: string; // 工作流状态文本 purposeText: string; // 目的文本 budgetSourceText: string; // 经费来源文本 approvalLevelText: string; // 审批级别文本 @@ -402,7 +517,27 @@ interface OverseasApplicationForm { ### 4. 审批出国申请 **接口地址**:`POST /rs/overseas/application/approve` -**权限要求**:`rs:overseas:application:approve` +**权限要求**:`rs:overseas:approve` (通用) 或 `rs:overseas:approve1-5` (分级) - **支持多级审批权限(2025-10-28更新)** + +#### 🔴 权限说明(重要更新) + +**分级审批权限体系**(OR模式,满足任一权限即可访问接口): +- `rs:overseas:approve` - 通用审批权限(超级管理员/高级审批者) +- `rs:overseas:approve1` - 第1级:部门负责人审批权限 +- `rs:overseas:approve2` - 第2级:人事处审批权限 +- `rs:overseas:approve3` - 第3级:纪委审批权限 +- `rs:overseas:approve4` - 第4级:学院分管领导审批权限 +- `rs:overseas:approve5` - 第5级:学院党委书记审批权限 + +**权限验证机制**: +1. **接口层验证**:Controller使用 `@SaCheckPermission(value={...}, mode=SaMode.OR)`,满足任一权限即可调用接口 +2. **业务层验证**:Service层根据申请的 `approval_level` 字段验证用户是否拥有对应级别的审批权限 +3. **权限不匹配处理**:如果用户审批级别与申请要求不符,返回业务异常"您没有第X级审批权限" + +**示例场景**: +- 部门负责人(拥有 `approve1`)只能审批 `approval_level=1` 的申请 +- 尝试审批 `approval_level=2` 的申请将被拒绝 +- 拥有 `rs:overseas:approve` 通用权限的用户可以审批任意级别 #### 请求参数 @@ -411,6 +546,7 @@ interface ApprovalRequest { applicationId: number | string; // 申请ID(必须) approvalStatus: string; // 审批状态:1-通过,2-拒绝(必须) approvalOpinion?: string; // 审批意见 + taskId?: string; // 🔴 工作流任务ID(可选,支持标准工作流模式) } ``` @@ -599,6 +735,20 @@ referer: http://localhost | 3 | 已取消 | 申请被申请人主动取消 | | 4 | 已完成 | 出国活动已完成 | +### 工作流状态 (approvalStatus) + +| 编码 | 含义 | 描述 | +| ----------- | ------ | ------------------------ | +| cancel | 已撤销 | 申请人主动撤销申请 | +| draft | 草稿 | 申请尚未提交,处于编辑状态 | +| waiting | 待审核 | 申请已提交,正在审批流程中 | +| finish | 已完成 | 所有审批节点都已通过,流程结束 | +| invalid | 已作废 | 申请因某种原因被标记为无效 | +| back | 已退回 | 审批被拒绝,退回给申请人修改 | +| termination | 已终止 | 流程被管理员或系统强制终止 | + +> **📝 说明**:工作流状态由 Warm-Flow 工作流引擎自动管理,用于总体流程监听和状态跟踪。 + ### 出国目的 (purpose) | 编码 | 含义 | 描述 | @@ -629,7 +779,7 @@ referer: http://localhost | 2 | 分管领导 | 第二级审批:分管领导 | | 3 | 校领导 | 第三级审批:校领导 | -### 审批状态 (approvalStatus) +### 审批记录状态 (审批记录表中的 approvalStatus) | 编码 | 含义 | 描述 | | ---- | ------ | ------------------------ | @@ -637,13 +787,25 @@ referer: http://localhost | 1 | 通过 | 审批通过 | | 2 | 拒绝 | 审批拒绝 | +> **⚠️ 注意**:此状态是审批记录表中的状态,与申请表中的工作流状态(approvalStatus)是不同的字段。 + ### 完成状态 (completionStatus) -| 编码 | 含义 | 描述 | -| ---- | -------- | ------------------------ | -| 0 | 进行中 | 出国活动正在进行 | -| 1 | 已完成 | 出国活动已完成 | -| 2 | 提前结束 | 出国活动提前结束 | +> **⚠️ 重要说明**:完成状态(completionStatus)与申请状态(status)是两个不同的概念! +> - **申请状态(status)**:表示审批流程的状态(待审核/已通过/已拒绝) +> - **完成状态(completionStatus)**:表示出国任务执行的状态(未完成/进行中/已完成/已取消) + +| 编码 | 含义 | 描述 | 使用场景 | +| ---- | -------- | ------------------------ | -------- | +| 0 | 未完成 | 出国活动尚未开始 | 已通过审批但尚未出发 | +| 1 | 进行中 | 出国活动正在进行 | 已出发但未返回(默认值) | +| 2 | 已完成 | 出国活动已完成 | 已返回并完成回执 | +| 3 | 已取消 | 出国活动取消 | 计划取消 | + +**字段关系示例**: +- 出国记录中的申请若已通过审批(`status='1'`),默认完成状态为"进行中"(`completionStatus='1'`) +- 出国记录列表显示"完成状态"字段,详情弹窗显示"申请状态"字段 +- 所有出国记录的申请状态都应该是"已通过"(`status='1'`),因为只有通过审批的申请才会出现在记录中 --- @@ -714,8 +876,370 @@ referer: http://localhost | v1.2 | 2025-01-09 | 新增修复记录接口,修复审批通过后记录创建问题 | weizzzz | | v1.3 | 2025-01-08 | 调整我的申请列表接口,显示完整申请历史(包括已通过申请) | weizzzz | | v1.4 | 2025-01-08 | 修正我的申请列表接口,排除已通过申请(已通过申请显示在出国记录中) | weizzzz | +| v1.5 | 2025-01-28 | 新增盖章文件上传功能,用户需上传盖章文件后才能提交申请 | weizzzz | + +### 8. 上传登记备案表盖章文件 + +**接口地址**:`POST /rs/overseas/application/uploadStampedFile` +**权限要求**:`rs:overseas:add` + +#### 功能说明 + +用于上传填写并加盖公章后的登记备案表文件。支持PDF、JPG、PNG格式,文件大小不超过10MB。上传成功后,用户才能提交出国申请。 + +#### 请求参数 + +**Content-Type**: `multipart/form-data` + +| 参数名 | 类型 | 必填 | 说明 | +|---------------|--------|------|--------------------------------| +| file | File | 是 | 盖章文件,支持PDF、JPG、PNG格式 | +| applicationId | Long | 是 | 申请ID | + +#### 请求示例 + +```bash +POST /rs/overseas/application/uploadStampedFile +Content-Type: multipart/form-data +Authorization: Bearer {token} +clientid: ruoyi-web +referer: http://localhost + +form-data: + file: [选择的盖章文件] + applicationId: 1 +``` + +#### 响应示例 + +**成功响应**: +```json +{ + "code": 200, + "msg": "盖章文件上传成功", + "data": { + "filePath": "overseas/stamped/stamped_1_1704076800000.pdf", + "fileName": "出国境登记表_张三.pdf", + "uploadTime": "2025-01-28 14:30:00" + } +} +``` + +**失败响应**: +```json +{ + "code": 500, + "msg": "不支持的文件格式,请上传PDF、JPG或PNG文件" +} +``` + +#### 业务规则 + +- 文件格式:只支持PDF、JPG、PNG格式 +- 文件大小:不超过10MB +- 数量限制:每个申请只能上传一个盖章文件 +- 上传成功后,系统自动更新申请记录的盖章文件信息 +- 必须先上传盖章文件,才能提交出国申请 + +#### 数据库更新 + +上传成功后,系统会自动更新以下字段: +- `stamped_registration_file_path`: 文件存储路径 +- `stamped_registration_file_name`: 原始文件名 +- `stamped_file_upload_time`: 上传时间 +- `stamped_file_upload_status`: 上传状态(设为'1') + +--- + +### 21. 下载党籍保留申请PDF空白模板 + +**接口路径**:`GET /rs/overseas/record/downloadPartyRetention` + +#### 功能说明 + +生成标准的党籍保留申请PDF空白模板,用户下载后完全通过线下手工填写所有内容。 + +#### 权限要求 + +- **权限标识**:`rs:overseas:query` +- **权限说明**:需要有查询出国记录的权限 + +#### 请求参数 + +**无需参数**:接口不需要任何参数,直接下载空白模板。 + +#### 请求示例 + +```http +GET /api/rs/overseas/record/downloadPartyRetention +Authorization: Bearer {token} +clientid: {clientid} +referer: {referer} +``` + +#### 响应说明 + +**成功响应**: + +- **Content-Type**: `application/pdf` +- **Content-Disposition**: `attachment; filename="党籍保留申请_yyyyMMdd.pdf"` +- **Body**: PDF文件二进制流 + +**失败响应**: + +```json +{ + "code": 500, + "msg": "PDF生成失败: 错误信息" +} +``` + +#### PDF模板内容 + +PDF模板包含以下空白填写区域(完全手工填写): + +1. **标题**:关于因私赴 ______________ __________保留党籍的申请 +2. **支部信息**:______________________ 支部: +3. **申请人信息**: + - 本人 _______________(____________________),现任 _______________ + - 计划于 ____________________ +4. **出国信息**: + - 至 ____________________ 偕配偶 _______________ + - 因私赴 _______________(国家 或地区)探望 在 + - _________________________ 的工作(留学、定居)的 __________ + - 在外停留 __________ 天(含路、抵返国境当 +5. **申请内容**:日)。现申请保留期间暂缓籍。 +6. **结束语**:特此申请,望予批准。 +7. **签名区域**: + - 申请人:_______________ + - 申请日期: ____________________ + +#### 使用流程 + +**方式一:申请表单中下载** +1. 用户在新增/编辑申请时,系统检测到是党员且出国天数>30天 +2. 系统显示党籍保留附件上传区域 +3. 用户点击"下载党籍保留申请PDF模板"按钮 +4. 系统生成并下载空白PDF模板 +5. 用户线下手工填写所有内容、打印、盖章 +6. 扫描成PDF后上传到系统 + +**方式二:申请详情中下载** +1. 用户查看申请详情(党员且出国天数>30天) +2. 详情弹窗底部显示"下载党籍保留申请"按钮 +3. 点击按钮下载空白PDF模板 +4. 线下手工填写、打印、盖章后上传 + +#### 技术实现 + +- 使用 **iText 5.5.13** PDF生成库 +- 支持中文字体(STSong-Light) +- A4纸张大小,标准页边距 +- 所有内容区域使用下划线标识,无预填充数据 +- 标准模板格式,适用于所有党籍保留申请场景 + +#### 注意事项 + +- **模板为完全空白**,所有内容需线下手工填写 +- 不需要申请ID或记录ID,任何时候都可以下载 +- 下载后需打印、手写填写、盖章后扫描上传 +- 适用于中共党员且出国天数>30天的情况 +- **按钮显示位置**: + - 申请表单:党员且出国天数>30天时自动显示 + - 申请详情:符合条件时在弹窗底部显示绿色按钮 +- **前端实现**:必须使用 `request` 工具而不是 `fetch` API,以确保包含所有必要的认证请求头 + +#### 错误处理 + +| HTTP状态码 | 错误信息 | 处理建议 | +| ---------- | ---------------------- | ---------------------------- | +| 500 | PDF生成失败 | 检查服务器日志,联系技术支持 | --- **📞 技术支持**:如有疑问请联系开发团队 -**📚 相关文档**:参考项目API文档基础规范和开发规范 \ No newline at end of file +**📚 相关文档**:参考项目API文档基础规范和开发规范 +**🆕 更新日志**: +- 2025-10-17 新增党籍保留PDF空白模板下载功能(线下手工填写) +- 接口路径调整:从 `/application/downloadPartyRetentionTemplate/{id}` 改为 `/record/downloadPartyRetention` +- 模板类型变更:从预填充模板改为完全空白模板 + +--- + +## 附录:工作流集成说明 + +### 动态审批人方案 + +#### 背景 + +出国申请业务集成了Warm-Flow工作流引擎,审批流程为:**发起人 → 部门领导 → 校领导**。审批人根据申请人所属部门动态确定,系统需要实时查询当前待审批人信息。 + +#### 方案设计 + +##### 1. 数据库字段说明 + +`rs_overseas_application` 表中的 `current_approver_id` 字段: + +- **字段类型**:`bigint(20)` +- **字段作用**:**冗余展示字段**(非核心业务字段) +- **维护方式**:不再由后端业务代码维护 +- **数据来源**:前端通过工作流API实时查询获取 +- **设计原因**: + - 审批人由工作流引擎动态确定,工作流表 `flow_node_user` 是唯一数据源 + - 避免数据冗余和不一致问题 + - 简化后端业务逻辑,降低维护成本 + +##### 2. 后端实现 + +**核心原则**:后端不维护 `current_approver_id` 字段 + +```java +// OverseasApplicationServiceImpl.java + +// ✅ 新增申请:不设置 current_approver_id +public Long addOverseasApplication(OverseasApplicationBo bo) { + // ... 业务逻辑 + // 不再包含:application.setCurrentApproverId(xxx); +} + +// ✅ 提交审批:不设置 current_approver_id +public R submitApplication(Long id) { + // ... 提交工作流 + // 不再包含:application.setCurrentApproverId(xxx); +} + +// ✅ 审批通过:不设置 current_approver_id +public R approve(Long id, String remark) { + // ... 审批逻辑 + // 不再包含:application.setCurrentApproverId(xxx); +} +``` + +**代码清理**: + +- 所有 `setCurrentApproverId()` 调用已移除 +- 相关查询逻辑已删除 +- 字段仅作为数据库表结构保留(历史兼容) + +##### 3. 前端实现 + +**核心方案**:通过工作流API实时查询当前审批人 + +```typescript +// overseasApplication/index.vue + +// 方案一:列表查询时批量获取审批人(推荐) +const getList = async () => { + const response = await listOverseasApplication(queryParams.value); + dataList.value = response.rows; + total.value = response.total; + + // 批量查询审批人信息 + await batchGetCurrentApproverNames(); +}; + +// 批量获取审批人姓名 +const batchGetCurrentApproverNames = async () => { + const promises = dataList.value + .filter(item => item.status === 'processing') + .map(async item => { + try { + // 调用工作流API查询当前审批人 + const approvers = await getCurrentApproversForTask(item.flowProcessId); + item.currentApproverName = approvers.map(u => u.name).join('、'); + } catch (error) { + item.currentApproverName = '-'; + } + }); + + await Promise.all(promises); +}; + +// 方案二:详情查询时单独获取审批人 +const handleViewDetail = async (row: OverseasApplicationVo) => { + currentRow.value = row; + + // 如果流程进行中,查询当前审批人 + if (row.flowProcessId && row.status === 'processing') { + try { + const approvers = await getCurrentApproversForTask(row.flowProcessId); + currentRow.value.currentApproverName = approvers.map(u => u.name).join('、'); + } catch (error) { + currentRow.value.currentApproverName = '查询失败'; + } + } + + detailDialogVisible.value = true; +}; +``` + +**工作流API调用**: + +```typescript +// 查询当前审批人(从工作流引擎获取) +import { getCurrentApproversForTask } from '@/api/workflow/common'; + +// API 路径:/api/workflow/instance/{processId}/current-approvers +// 返回格式: +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "userId": 1001, + "userName": "张三", + "nodeId": 2, + "nodeName": "部门领导审批" + } + ] +} +``` + +##### 4. 接口改造 + +相关API接口响应中的 `current_approver_id` 字段说明: + +| 接口 | 字段说明 | 使用建议 | +|------|---------|---------| +| 查询列表 | 返回null或历史值 | 忽略,通过工作流API获取 | +| 查询详情 | 返回null或历史值 | 忽略,通过工作流API获取 | +| 新增/修改 | 不处理该字段 | 前端不传递 | +| 提交审批 | 不处理该字段 | 前端不传递 | + +##### 5. 优势分析 + +**✅ 数据一致性** +- 工作流表是唯一数据源,避免双写不一致 +- 审批人变更(如转办、委派)自动生效 + +**✅ 降低维护成本** +- 后端无需在多个业务方法中同步更新 +- 减少代码复杂度和潜在Bug + +**✅ 实时性** +- 前端每次查询都获取最新审批人 +- 支持工作流引擎的高级功能(并行审批、会签等) + +**✅ 扩展性** +- 审批流程调整时,前端无需修改 +- 支持更复杂的审批场景(多人会签、或签等) + +#### 实施步骤 + +1. ✅ **后端改造**:移除所有 `current_approver_id` 字段的赋值代码 +2. ✅ **前端改造**:实现基于工作流API的动态查询 +3. ⏳ **测试验证**:确保列表、详情页面正确显示审批人 +4. ⏳ **文档更新**:同步更新API文档和技术文档 + +#### 注意事项 + +- 历史数据中的 `current_approver_id` 字段可能存在脏数据,前端应忽略 +- 工作流API调用失败时,前端应显示友好提示(如 "-" 或 "查询失败") +- 性能优化:列表页建议使用批量查询,避免N+1查询问题 +- 缓存策略:同一个流程实例的审批人信息可短时间缓存(如30秒) + +--- + +**更新时间**:2025-10-28 +**维护人员**:开发团队 +**变更记录**:新增工作流动态审批人方案说明 \ No newline at end of file diff --git a/docs/开发文档/API/出国登记管理API.md b/docs/开发文档/API/出国登记管理API.md new file mode 100644 index 00000000..8432428f --- /dev/null +++ b/docs/开发文档/API/出国登记管理API.md @@ -0,0 +1,306 @@ +# 出国登记管理 API 接口文档 + +## 📋 文档说明 + +### 适用场景 + +- 出国登记信息管理 +- 出国登记状态跟踪 +- 出国登记记录查询 +- 为 Cursor AI 提供完整的接口开发上下文 + +### 模块特色 + +- **简化表单设计**:去除需要纸质盖章的字段,优化用户体验 +- **状态管理**:草稿→已提交→已确认→已撤回的完整状态流转 +- **数据完整性**:与出国申请系统关联,确保数据一致性 +- **历史记录**:完整的登记历史追踪功能 + +--- + +## 🔌 出国登记管理 API 接口文档 + +### 模块概述 + +**模块名称**:overseas-registration / 出国登记管理 +**基础路径**:`/api/rs/overseas-registration` +**版本**:v1.0 +**负责人**:weizzzz +**更新时间**:2025-09-30 + +### 模块功能 + +出国登记管理模块提供出国登记信息的完整管理功能,支持登记信息录入、状态管理、历史记录查询等核心功能。该模块与出国申请系统集成,为已审批通过的出国申请提供登记功能。 + +### 重要变更记录 + +#### 2025-09-30 字段优化变更 + +**第二次更新:照片字段UI优化** +- **UI变更**:照片字段从输入框改为右上角预留区域(32mm×36mm) +- **功能调整**: + - 删除照片上传输入框 + - 在弹窗右上角添加照片预留位置标识 + - 用于审批完成后下载附件线下张贴照片 +- **字段保留**:photo字段在数据模型中保留(兼容性考虑) +- **用户体验**:简化表单操作,明确线下张贴流程 + +**第一次更新:登记备案信息字段删除** +- **删除字段**:`登记备案信息`和`联系方式`相关字段 +- **删除原因**:这些字段需要下载后纸质版盖章,不适合在线填写 +- **影响范围**:前端表单、API接口、数据模型 +- **具体删除字段**: + - `registrationUnit`:登记备案责任单位 + - `unitLeaderSignature`:负责人签名 + - `officePhone`:办公电话 + - `mobilePhone`:移动电话 + +### 权限说明 + +#### 出国登记权限 +- **权限前缀**:`rs:overseas:registration:` +- **权限列表**: + - `rs:overseas:registration:list` - 查询登记列表权限 + - `rs:overseas:registration:query` - 查询登记详情权限 + - `rs:overseas:registration:add` - 新增登记权限 + - `rs:overseas:registration:edit` - 编辑登记权限 + - `rs:overseas:registration:remove` - 删除登记权限 + - `rs:overseas:registration:export` - 导出登记权限 + - `rs:overseas:registration:submit` - 提交登记权限 + - `rs:overseas:registration:cancel` - 撤回登记权限 + +--- + +## 📊 接口概览 + +### 接口分类 + +#### 出国登记基础 CRUD 接口 + +| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 | +| ---------------- | --------- | --------------------------------------- | ------------------------------------- | -------------------------- | +| 查询登记列表 | GET | `/rs/overseas-registration/list` | `rs:overseas:registration:list` | 分页查询出国登记列表 | +| 查询登记详情 | GET | `/rs/overseas-registration/{id}` | `rs:overseas:registration:query` | 根据ID查询出国登记详情 | +| 新增登记 | POST | `/rs/overseas-registration` | `rs:overseas:registration:add` | 创建新的出国登记 | +| 修改登记 | PUT | `/rs/overseas-registration` | `rs:overseas:registration:edit` | 更新出国登记信息 | +| 删除登记 | DELETE | `/rs/overseas-registration/{ids}` | `rs:overseas:registration:remove` | 批量删除出国登记 | +| 导出登记 | POST | `/rs/overseas-registration/export` | `rs:overseas:registration:export` | 导出出国登记数据 | + +#### 出国登记业务逻辑接口 + +| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 | +| ------------------ | --------- | ------------------------------------------- | ------------------------------------- | -------------------------- | +| 提交登记 | PUT | `/rs/overseas-registration/submit/{id}` | `rs:overseas:registration:submit` | 提交出国登记 | +| 撤回登记 | PUT | `/rs/overseas-registration/cancel/{id}` | `rs:overseas:registration:cancel` | 撤回出国登记 | +| 保存并提交 | POST | `/rs/overseas-registration/addAndSubmit` | `rs:overseas:registration:add` | 保存并提交出国登记 | + +#### 出国登记历史接口 + +| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 | +| ------------------ | --------- | ------------------------------------------- | ------------------------------------- | -------------------------- | +| 查询登记历史 | GET | `/rs/overseas-registration/history/list` | `rs:overseas:registration:list` | 查询出国登记历史列表 | +| 查询历史详情 | GET | `/rs/overseas-registration/history/{id}` | `rs:overseas:registration:query` | 查询出国登记历史详情 | +| 更新历史记录 | PUT | `/rs/overseas-registration/history` | `rs:overseas:registration:edit` | 更新出国登记历史记录 | +| 导出历史记录 | POST | `/rs/overseas-registration/history/export` | `rs:overseas:registration:export` | 导出出国登记历史数据 | + +--- + +## 🔧 数据模型 + +### 出国登记表单对象(简化版) + +```typescript +interface OverseasRegistrationForm { + // 基本信息 + name: string; // 姓名 + gender: string; // 性别(0-男,1-女) + birthDate: string; // 出生日期 + photo: string; // 照片预留(右上角32mm×36mm预留区域,用于审批完成后线下张贴,字段保留用于兼容性) + idNumber: string; // 身份证号码 + politicalStatus: string; // 政治面貌 + registeredAddress: string; // 户籍所在地 + + // 工作信息 + department: string; // 工作部门 + position: string; // 职务职称 + administrativeLevel: string; // 行政职级 + personnelCategory: string; // 人员类别代码 + + // 关联信息 + applicationId: string; // 关联出国申请ID + remarks: string; // 备注 +} +``` + +### 字段变更说明 + +#### 已删除字段(2025-09-30) +```typescript +// 以下字段已从表单中移除,因为需要纸质版盖章 +interface RemovedFields { + // 登记备案信息 + registrationUnit: string; // 登记备案责任单位(已删除) + unitLeaderSignature: string; // 负责人签名(已删除) + + // 联系方式 + officePhone: string; // 办公电话(已删除) + mobilePhone: string; // 移动电话(已删除) +} +``` + +### 表单验证规则(更新后) + +```typescript +const registrationRules = { + name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], + gender: [{ required: true, message: '请选择性别', trigger: 'change' }], + birthDate: [{ required: true, message: '请选择出生日期', trigger: 'change' }], + idNumber: [ + { required: true, message: '请输入身份证号', trigger: 'blur' }, + { pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的18位身份证号码', trigger: 'blur' } + ], + politicalStatus: [{ required: true, message: '请选择政治面貌', trigger: 'change' }], + registeredAddress: [{ required: true, message: '请输入户籍所在地', trigger: 'blur' }], + department: [{ required: true, message: '请输入工作部门', trigger: 'blur' }], + position: [{ required: true, message: '请输入职务职称', trigger: 'blur' }] +}; +``` + +## 📝 接口详情 + +### 1. 查询出国登记列表 + +**接口地址**:`GET /rs/overseas-registration/list` + +**接口描述**:分页查询出国登记列表,支持多条件筛选 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | number | 是 | 页码,从1开始 | +| pageSize | number | 是 | 每页大小,最大100 | +| name | string | 否 | 姓名(模糊匹配) | +| gender | string | 否 | 性别(0-男,1-女) | +| idNumber | string | 否 | 身份证号(模糊匹配) | +| politicalStatus | string | 否 | 政治面貌 | +| department | string | 否 | 工作部门(模糊匹配) | +| position | string | 否 | 职务职称(模糊匹配) | +| status | string | 否 | 登记状态(0-草稿,1-已提交,2-已确认,3-已撤回) | + +**响应示例**: + +```json +{ + "code": 200, + "msg": "操作成功", + "rows": [ + { + "registrationId": "1234567890123456789", + "name": "张三", + "gender": "0", + "birthDate": "1990-01-01", + "idNumber": "123456789012345678", + "politicalStatus": "中共党员", + "registeredAddress": "浙江省杭州市西湖区", + "department": "信息工程学院", + "position": "副教授", + "status": "1", + "statusText": "已提交", + "registrationTime": "2025-09-30 10:00:00", + "createTime": "2025-09-30 09:30:00" + } + ], + "total": 1 +} +``` + +### 2. 新增出国登记 + +**接口地址**:`POST /rs/overseas-registration` + +**接口描述**:创建新的出国登记记录 + +**请求参数**: + +```json +{ + "name": "张三", + "gender": "0", + "birthDate": "1990-01-01", + "photo": "", // 照片预留区域(32mm×36mm),不再接收上传地址 + "idNumber": "123456789012345678", + "politicalStatus": "中共党员", + "registeredAddress": "浙江省杭州市西湖区某某街道", + "department": "信息工程学院", + "position": "副教授", + "administrativeLevel": "副处级", + "personnelCategory": "A001", + "applicationId": "9876543210987654321", + "remarks": "学术交流访问" +} +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "新增成功", + "data": { + "registrationId": "1234567890123456789" + } +} +``` + +--- + +## 🔄 业务流程 + +### 登记状态流转 + +``` +草稿(0) → 已提交(1) → 已确认(2) + ↓ ↓ + 删除 撤回(3) +``` + +### 状态说明 + +- **草稿(0)**:初始创建状态,可编辑、删除 +- **已提交(1)**:已提交待确认,不可编辑,可撤回 +- **已确认(2)**:已确认完成,不可编辑撤回 +- **已撤回(3)**:已撤回状态,可重新编辑提交 + +--- + +## 🚨 注意事项 + +### 字段变更影响 + +1. **前端表单**:已移除登记备案信息和联系方式输入框 +2. **数据验证**:已移除相关字段的必填验证 +3. **接口传参**:新增/修改接口不再包含已删除字段 +4. **列表显示**:表格中不再显示移动电话列 +5. **详情查看**:详情弹窗中不再显示相关信息 + +### 开发建议 + +1. 确保后端接口也相应调整,移除已删除字段的处理 +2. 数据库迁移需要考虑历史数据的处理 +3. 相关导出功能需要同步调整字段列表 +4. API文档需要及时更新,保持与实际接口一致 + +--- + +## 📖 参考文档 + +- [API文档基础规范](./API文档基础规范.md) +- [用户认证登录API](./用户认证登录API.md) +- [出国申请管理API](./出国申请管理API.md) + +--- + +**文档维护**:weizzzz +**创建时间**:2025-09-30 +**最后更新**:2025-09-30 \ No newline at end of file diff --git a/docs/开发文档/API/出国行程变更管理API.md b/docs/开发文档/API/出国行程变更管理API.md new file mode 100644 index 00000000..43d7cb6b --- /dev/null +++ b/docs/开发文档/API/出国行程变更管理API.md @@ -0,0 +1,582 @@ +# 出国行程变更管理API文档 + +## 📋 文档信息 + +- **模块名称**: 出国行程变更申请管理 +- **功能概述**: 提供出国行程变更申请的新增、查询、审批等功能 +- **创建日期**: 2025-10-13 +- **最后更新**: 2025-10-13 +- **数据库版本**: v1.1(包含个人信息字段) + +--- + +## 🎯 业务说明 + +### 功能概述 + +出国行程变更管理模块允许用户对已审批通过的出国申请提交行程变更申请,支持变更目的地、时间、行程安排等信息。 + +### 业务流程 + +``` +1. 用户查看已审批通过的出国申请 +2. 用户选择一个出国申请,填写变更信息 +3. 提交变更申请(状态:待审核) +4. 管理员审批变更申请 + - 通过:状态变为"已通过",成为变更记录 + - 拒绝:状态变为"已拒绝" +5. 用户可撤回待审核的变更申请 +``` + +### 数据分类 + +- **变更申请**: 所有状态的变更申请记录(待审核、已通过、已拒绝、已撤回) +- **变更记录**: 只包含已通过审批的变更申请,代表真正生效的变更记录 + +--- + +## 🔌 API接口列表 + +### 1. 查询变更申请列表 + +**接口说明**: 查询所有的行程变更申请(包括所有状态) + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/list` + +**权限标识**: `rs:itineraryChange:list` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页条数(默认10) | +| applicantName | String | 否 | 申请人姓名(模糊查询) | +| status | String | 否 | 变更状态(0=待审核,1=已通过,2=已拒绝,3=已撤回) | +| changeType | String | 否 | 变更类型 | + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "data": { + "total": 10, + "rows": [ + { + "changeId": "1977740586114916354", + "applicationId": "1977123456789012345", + "applicantId": "1", + "applicantName": "admin", + "status": "0", + "statusText": "待审核", + "changeType": "destination_change", + "changeTypeText": "目的地变更", + "originalDestination": "美国", + "newDestination": "加拿大", + "originalStartDate": "2025-11-01", + "newStartDate": "2025-11-05", + "changeReason": "会议时间调整", + "createTime": "2025-10-13 10:00:00" + } + ] + } +} +``` + +--- + +### 2. 查询变更记录列表 + +**接口说明**: 查询当前用户的变更记录(只返回已通过的变更申请) + +**重要说明**: +- 🔴 **此接口只返回status='1'(已通过)的变更申请** +- 变更记录代表真正生效的变更记录,不包括待审核、已拒绝、已撤回的申请 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/my/list` + +**权限标识**: `rs:itineraryChange:list` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页条数(默认10) | +| changeType | String | 否 | 变更类型 | + +**后端自动过滤条件**: +- `applicantId` = 当前登录用户ID(自动设置) +- `status` = '1'(只查询已通过的,自动设置) + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "data": { + "total": 5, + "rows": [ + { + "changeId": "1977740586114916354", + "applicationId": "1977123456789012345", + "applicantId": "1", + "applicantName": "admin", + "status": "1", + "statusText": "已通过", + "changeType": "destination_change", + "changeTypeText": "目的地变更", + "originalDestination": "美国", + "newDestination": "加拿大", + "approverName": "管理员", + "approveTime": "2025-10-13 14:00:00", + "createTime": "2025-10-13 10:00:00" + } + ] + } +} +``` + +--- + +### 3. 查询待审批列表 + +**接口说明**: 查询待审批的行程变更申请(status='0') + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/pending/list` + +**权限标识**: `rs:itineraryChange:approve` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页条数(默认10) | +| applicantName | String | 否 | 申请人姓名(模糊查询) | + +**后端自动过滤条件**: +- `status` = '0'(只查询待审核的,自动设置) + +--- + +### 4. 查询变更申请详情 + +**接口说明**: 根据变更ID查询详细信息 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/{changeId}` + +**权限标识**: `rs:itineraryChange:query` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| changeId | Long | 是 | 变更ID | + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "data": { + "changeId": "1977740586114916354", + "applicationId": "1977123456789012345", + "applicantName": "admin", + "gender": "0", + "birthDate": "1990-01-01", + "politicalStatus": "中共党员", + "status": "1", + "statusText": "已通过", + "changeType": "destination_change", + "changeTypeText": "目的地变更", + "originalDestination": "美国", + "newDestination": "加拿大", + "originalStartDate": "2025-11-01", + "newStartDate": "2025-11-05", + "changeReason": "会议时间调整", + "supportingDocuments": "http://xxx.com/file.pdf", + "approverName": "管理员", + "approveTime": "2025-10-13 14:00:00", + "approveOpinion": "同意变更" + } +} +``` + +--- + +### 5. 新增变更申请 + +**接口说明**: 提交新的行程变更申请 + +**请求方式**: `POST` + +**接口地址**: `/rs/itinerary/change` + +**权限标识**: `rs:itineraryChange:add` + +**请求体示例**: + +```json +{ + "applicationId": "1977123456789012345", + "changeType": "destination_change", + "newDestination": "加拿大", + "newDestinationCity": "多伦多", + "newStartDate": "2025-11-05", + "newEndDate": "2025-11-15", + "newDuration": 10, + "changeReason": "会议时间调整", + "supportingDocuments": "http://xxx.com/file.pdf" +} +``` + +**后端自动设置字段**: +- `applicantId`: 当前登录用户ID +- `applicantName`: 当前登录用户姓名 +- `status`: '0'(待审核) +- `tenantId`: 当前租户ID +- 从原出国申请中复制:`originalDestination`、`originalStartDate`等原始信息 + +**业务校验**: +1. ✅ 出国申请必须存在且状态为"已通过" +2. ✅ 不能对同一个出国申请同时提交多个待审核的变更申请 + +--- + +### 6. 修改变更申请(重新提交) + +**接口说明**: 修改待审核或已拒绝的变更申请并重新提交 + +**请求方式**: `PUT` + +**接口地址**: `/rs/itinerary/change` + +**权限标识**: `rs:itineraryChange:edit` + +**请求体示例**: + +```json +{ + "changeId": "1977740586114916354", + "changeType": "destination_change", + "newDestination": "英国", + "newDestinationCity": "伦敦", + "changeReason": "更新后的变更原因" +} +``` + +--- + +### 7. 删除变更申请 + +**接口说明**: 删除变更申请(逻辑删除) + +**请求方式**: `DELETE` + +**接口地址**: `/rs/itinerary/change/{changeIds}` + +**权限标识**: `rs:itineraryChange:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| changeIds | Long[] | 是 | 变更ID数组(逗号分隔) | + +--- + +### 8. 撤回变更申请 + +**接口说明**: 撤回待审核的变更申请 + +**请求方式**: `PUT` + +**接口地址**: `/rs/itinerary/change/cancel/{changeId}` + +**权限标识**: `rs:itineraryChange:cancel` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| changeId | Long | 是 | 变更ID | + +**业务规则**: +- ✅ 只能撤回状态为"待审核"的申请 +- 撤回后状态变为'3'(已撤回) + +--- + +### 9. 审批变更申请 + +**接口说明**: 审批行程变更申请(通过/拒绝) + +**请求方式**: `PUT` + +**接口地址**: `/rs/itinerary/change/approve` + +**权限标识**: `rs:itineraryChange:approve` + +**请求体示例**: + +```json +{ + "changeId": "1977740586114916354", + "approved": true, + "opinion": "同意变更" +} +``` + +**参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| changeId | Long | 是 | 变更ID | +| approved | Boolean | 是 | 是否通过(true=通过,false=拒绝) | +| opinion | String | 否 | 审批意见 | + +**重要提示**: +- ⚠️ **后端接口使用Map接收参数,前端必须发送正确的字段名** +- ✅ 字段名:`changeId`、`approved`、`opinion` +- ❌ 不要使用:`approvalStatus`、`approvalOpinion` + +**审批结果**: +- `approved=true`: 状态变为'1'(已通过)→ 成为变更记录 +- `approved=false`: 状态变为'2'(已拒绝) + +**响应示例**: + +```json +{ + "code": 200, + "msg": "操作成功" +} +``` + +--- + +### 10. 获取可用的出国申请列表 + +**接口说明**: 获取已审批通过的出国申请列表(用于新增变更时选择原申请) + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/available-records` + +**权限标识**: `rs:itineraryChange:list` + +**响应说明**: 返回当前用户的所有已通过审批的出国申请 + +--- + +### 11. 查询某个出国申请的变更历史 + +**接口说明**: 查询某个出国申请的所有变更记录 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary/change/record/{recordId}/history` + +**权限标识**: `rs:itineraryChange:query` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| recordId | Long | 是 | 出国记录ID(或申请ID) | + +--- + +## 📊 数据字典 + +### 变更状态(rs_itinerary_change_status) + +| 字典值 | 标签 | 样式 | 说明 | +|--------|------|------|------| +| 0 | 待审核 | primary | 待审批状态 | +| 1 | 已通过 | success | 已通过审批,成为变更记录 | +| 2 | 已拒绝 | danger | 已拒绝 | +| 3 | 已撤回 | info | 用户撤回 | + +### 变更类型(rs_itinerary_change_type) + +| 字典值 | 标签 | 说明 | +|--------|------|------| +| destination_change | 目的地变更 | 更改出国目的地 | +| time_change | 时间变更 | 更改出国时间 | +| itinerary_change | 行程变更 | 更改行程安排 | +| transportation_change | 交通方式变更 | 更改交通方式 | +| accommodation_change | 住宿变更 | 更改住宿安排 | + +--- + +## 🎯 前端Tab说明 + +### 变更申请Tab(all) + +- **数据来源**: `/rs/itinerary/change/list` +- **显示内容**: 所有的变更申请(包括所有状态) +- **功能**: 新增、编辑、删除、查看、撤回 + +### 变更记录Tab(my) + +- **数据来源**: `/rs/itinerary/change/my/list` +- **显示内容**: 🔴 **只显示已通过的变更申请**(status='1') +- **业务含义**: 真正生效的变更记录 +- **功能**: 查看已通过的变更记录 + +--- + +## 🔐 权限配置 + +| 权限标识 | 说明 | +|---------|------| +| rs:itineraryChange:list | 查询变更申请/记录列表 | +| rs:itineraryChange:query | 查看变更详情 | +| rs:itineraryChange:add | 新增变更申请 | +| rs:itineraryChange:edit | 修改变更申请 | +| rs:itineraryChange:remove | 删除变更申请 | +| rs:itineraryChange:cancel | 撤回变更申请 | +| rs:itineraryChange:approve | 审批变更申请 | +| rs:itineraryChange:export | 导出变更申请 | + +--- + +## 📝 业务规则总结 + +1. ✅ **新增变更申请**: 只能为已通过审批的出国申请提交变更 +2. ✅ **并发控制**: 同一个出国申请不能同时有多个待审核的变更申请 +3. ✅ **撤回限制**: 只能撤回待审核状态的申请 +4. ✅ **变更记录**: 只有已通过的变更申请才会显示在"变更记录"Tab中 +5. ✅ **权限控制**: 所有接口都需要相应的权限标识 + +--- + +## 🔧 故障排查 + +### 问题1:Unknown column 'gender' in 'field list' + +**错误描述**: +``` +java.sql.SQLSyntaxErrorException: Unknown column 'gender' in 'field list' +``` + +**原因分析**: +- 数据库表 `rs_overseas_itinerary_change` 缺少个人信息字段 +- 后端Mapper查询包含了这些字段,但表中不存在 + +**解决方案**: + +1️⃣ **执行修复SQL**: +```bash +文件位置: docs/设计文档/database/⚡一键修复-添加缺失字段.sql +执行方式: 直接在MySQL中执行 +``` + +2️⃣ **重启后端服务**(清除MyBatis缓存) + +3️⃣ **重新测试功能** + +**缺失字段列表**: +- gender - 性别 +- birth_date - 出生日期 +- political_status - 政治面貌 +- professional_title - 职称 +- position - 职务 +- document_name - 申领证件名称 +- document_number - 证件号码 +- department - 部门 + +**详细修复指南**: `docs/问题解决方案/🚨数据库字段缺失修复指南.md` + +--- + +### 问题2:审批接口参数错误 + +**错误描述**: +``` +Cannot invoke "Object.toString()" because the return value of "java.util.Map.get(Object)" is null +``` + +**原因分析**: +- 前端发送的字段名与后端期望不匹配 +- 前端: `approvalStatus` + `approvalOpinion` +- 后端: `approved` + `opinion` + +**解决方案**: + +前端必须发送正确的参数: +```json +{ + "changeId": 123, + "approved": true, // ✅ 布尔值,不是字符串 + "opinion": "同意变更" +} +``` + +**详细修复总结**: `docs/问题解决方案/行程变更审批接口参数错误修复总结.md` + +--- + +### 问题3:变更记录Tab显示不正确 + +**问题描述**: +- "变更记录"Tab显示了所有状态的数据 +- 预期只显示已通过(status='1')的数据 + +**原因分析**: +- 前端查询参数未设置status过滤条件 + +**解决方案**: + +前端在查询"变更记录"时必须传递 `status: '1'`: +```typescript +const queryParams = ref({ + pageNum: 1, + pageSize: 10, + status: '1' // ✅ 只查询已通过的记录 +}); +``` + +**详细修复总结**: `docs/问题解决方案/变更记录Tab显示逻辑修复总结.md` + +--- + +## 📞 相关文档 + +### 开发文档 +- [出国行程变更管理API文档](./出国行程变更管理API.md)(本文档) +- [后端开发规范](../../.cursor/cursorrules-backend.md) +- [前端开发规范](../../.cursor/cursorrules-frontend.md) + +### 数据库脚本 +- [完整建表SQL](../../设计文档/database/rs_overseas_itinerary_change_full.sql) +- [一键修复缺失字段SQL](../../设计文档/database/⚡一键修复-添加缺失字段.sql) +- [诊断缺失字段SQL](../../设计文档/database/🔍诊断-检查缺失字段.sql) + +### 问题解决方案 +- [数据库字段缺失修复指南](../../问题解决方案/🚨数据库字段缺失修复指南.md) +- [审批接口参数错误修复总结](../../问题解决方案/行程变更审批接口参数错误修复总结.md) +- [变更记录Tab显示逻辑修复总结](../../问题解决方案/变更记录Tab显示逻辑修复总结.md) +- [审批功能测试指南](../../问题解决方案/行程变更审批功能-测试指南.md) + +--- + +**文档创建时间**: 2025-10-13 +**文档版本**: v1.1 +**维护人员**: weizzzz +**最后更新**: 2025-10-13(新增故障排查部分) + diff --git a/docs/开发文档/API/变更行程管理API.md b/docs/开发文档/API/变更行程管理API.md new file mode 100644 index 00000000..af847ba4 --- /dev/null +++ b/docs/开发文档/API/变更行程管理API.md @@ -0,0 +1,844 @@ +# 变更行程管理API文档 + +## 📋 文档信息 + +**模块名称**: 出国行程变更管理 +**功能说明**: 对已通过的出国申请进行行程变更申请 +**创建时间**: 2025-10-13 +**文档版本**: v1.0 + +--- + +## 🎯 功能概述 + +本模块用于管理出国申请的行程变更,包括: +- ✅ 查询已通过的出国申请(可变更的申请) +- ✅ 新增行程变更申请 +- ✅ 查询变更申请列表 +- ✅ 查看变更申请详情 +- ✅ 修改并重新提交变更申请 +- ✅ 撤回变更申请 +- ✅ 删除变更申请 +- ✅ 导出变更申请列表 + +**变更类型**: +1. **目的地变更**: 更改目的地国家 +2. **时间变更**: 更改出国时间(出发/返回日期) +3. **行程变更**: 更改具体行程安排 +4. **组合变更**: 支持多种变更类型同时申请 + +--- + +## 🔐 权限配置 + +### 菜单权限 + +| 菜单名称 | 权限标识 | 说明 | +|---------|---------|------| +| 变更行程 | `rs:itineraryChange:list` | 访问变更行程页面 | + +### 按钮权限 + +| 按钮名称 | 权限标识 | 说明 | +|---------|---------|------| +| 查询 | `rs:itineraryChange:query` | 查询变更申请详情 | +| 新增 | `rs:itineraryChange:add` | 新增变更申请 | +| 修改 | `rs:itineraryChange:edit` | 修改并重新提交 | +| 删除 | `rs:itineraryChange:remove` | 删除变更申请 | +| 撤回 | `rs:itineraryChange:cancel` | 撤回变更申请 | +| 导出 | `rs:itineraryChange:export` | 导出变更申请列表 | + +--- + +## 📊 数据库表结构 + +### 表名: `rs_overseas_itinerary_change` + +**表说明**: 出国行程变更申请表 + +**核心字段**: + +| 字段名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `change_id` | bigint(20) | ✅ | 变更ID(主键,雪花ID) | +| `application_id` | bigint(20) | ✅ | 原出国申请ID | +| `applicant_id` | bigint(20) | ✅ | 申请人ID | +| `applicant_name` | varchar(100) | ✅ | 申请人姓名 | +| `department` | varchar(200) | ❌ | 部门 | +| `original_destination` | varchar(100) | ❌ | 原目的地国家 | +| `original_exit_reason` | varchar(100) | ❌ | 原出境事由 | +| `original_start_date` | date | ❌ | 原出发日期 | +| `original_end_date` | date | ❌ | 原返回日期 | +| `original_duration` | int(11) | ❌ | 原出国天数 | +| `original_itinerary` | text | ❌ | 原行程安排 | +| `change_type` | varchar(200) | ✅ | 变更类型(逗号分隔) | +| `new_destination` | varchar(100) | ❌ | 新目的地国家 | +| `new_exit_reason` | varchar(100) | ❌ | 新出境事由 | +| `new_start_date` | date | ❌ | 新出发日期 | +| `new_end_date` | date | ❌ | 新返回日期 | +| `new_duration` | int(11) | ❌ | 新出国天数 | +| `new_itinerary` | text | ❌ | 新行程安排 | +| `change_reason` | varchar(500) | ✅ | 变更原因 | +| `remark` | varchar(500) | ❌ | 备注 | +| `status` | char(1) | ✅ | 变更状态 | +| `approval_opinion` | varchar(500) | ❌ | 审批意见 | +| `approval_time` | datetime | ❌ | 审批时间 | +| `approver_id` | bigint(20) | ❌ | 审批人ID | +| `approver_name` | varchar(100) | ❌ | 审批人姓名 | + +**状态枚举**: +- `0` - 待审核 +- `1` - 已通过 +- `2` - 已拒绝 +- `3` - 已撤回 + +--- + +## 🔌 API接口列表 + +### 1. 查询已通过的出国申请列表 + +**接口说明**: 获取当前用户已通过的出国申请,用于新增变更申请时选择 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary-change/approvedApplications` + +**权限标识**: `rs:itineraryChange:add` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "data": [ + { + "applicationId": 123456789, + "applicantName": "张三", + "department": "信息技术部", + "destination": "日本", + "exitReason": "旅游", + "startDate": "2025-11-01", + "endDate": "2025-11-10", + "duration": 10, + "itinerary": "东京-大阪-京都" + } + ] +} +``` + +**后端实现要点**: +```java +@GetMapping("/approvedApplications") +@SaCheckPermission("rs:itineraryChange:add") +public R> listApprovedApplications() { + // 查询当前用户已通过的出国申请(status='1') + // 只返回必要字段 +} +``` + +--- + +### 2. 查询变更申请列表(分页) + +**接口说明**: 分页查询当前用户的行程变更申请列表 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary-change/list` + +**权限标识**: `rs:itineraryChange:list` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `pageNum` | Integer | ❌ | 页码(默认1) | +| `pageSize` | Integer | ❌ | 每页数量(默认10) | +| `applicantName` | String | ❌ | 申请人姓名(模糊查询) | +| `destination` | String | ❌ | 目的地国家(模糊查询) | +| `status` | String | ❌ | 变更状态(0/1/2/3) | + +**请求示例**: + +``` +GET /rs/itinerary-change/list?pageNum=1&pageSize=10&status=0 +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "rows": [ + { + "changeId": 987654321, + "applicationId": 123456789, + "applicantName": "张三", + "department": "信息技术部", + "originalDestination": "日本", + "originalExitReason": "旅游", + "originalStartDate": "2025-11-01", + "originalEndDate": "2025-11-10", + "originalDuration": 10, + "changeType": "时间变更,行程变更", + "newDestination": "日本", + "newStartDate": "2025-11-05", + "newEndDate": "2025-11-15", + "newDuration": 11, + "newItinerary": "东京-大阪-京都-奈良", + "changeReason": "因临时工作调整,需要延后5天出发", + "status": "0", + "statusText": "待审核", + "createTime": "2025-10-13 14:30:00" + } + ], + "total": 1 +} +``` + +**后端实现要点**: +```java +@GetMapping("/list") +@SaCheckPermission("rs:itineraryChange:list") +public TableDataInfo list(ItineraryChangeQuery query) { + // 1. 获取当前用户ID + // 2. 构建查询条件(applicant_id + 搜索条件) + // 3. 分页查询 + // 4. 转换VO(添加statusText等) +} +``` + +--- + +### 3. 查询变更申请详情 + +**接口说明**: 根据changeId查询变更申请详细信息 + +**请求方式**: `GET` + +**接口地址**: `/rs/itinerary-change/{changeId}` + +**权限标识**: `rs:itineraryChange:query` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `changeId` | Long | ✅ | 变更申请ID | + +**请求示例**: + +``` +GET /rs/itinerary-change/987654321 +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "查询成功", + "data": { + "changeId": 987654321, + "applicationId": 123456789, + "applicantName": "张三", + "department": "信息技术部", + "originalDestination": "日本", + "originalExitReason": "旅游", + "originalStartDate": "2025-11-01", + "originalEndDate": "2025-11-10", + "originalDuration": 10, + "originalItinerary": "东京-大阪-京都", + "changeType": "时间变更,行程变更", + "newDestination": "日本", + "newExitReason": "旅游", + "newStartDate": "2025-11-05", + "newEndDate": "2025-11-15", + "newDuration": 11, + "newItinerary": "东京-大阪-京都-奈良", + "changeReason": "因临时工作调整,需要延后5天出发,并增加奈良行程", + "remark": null, + "status": "0", + "statusText": "待审核", + "approvalOpinion": null, + "createTime": "2025-10-13 14:30:00", + "updateTime": "2025-10-13 14:30:00" + } +} +``` + +**后端实现要点**: +```java +@GetMapping("/{changeId}") +@SaCheckPermission("rs:itineraryChange:query") +public R getInfo(@PathVariable Long changeId) { + // 1. 根据changeId查询 + // 2. 权限校验(只能查询自己的申请) + // 3. 转换VO并返回 +} +``` + +--- + +### 4. 新增变更申请 + +**接口说明**: 提交新的行程变更申请 + +**请求方式**: `POST` + +**接口地址**: `/rs/itinerary-change` + +**权限标识**: `rs:itineraryChange:add` + +**Content-Type**: `application/json` + +**请求Body**: + +```json +{ + "applicationId": 123456789, + "changeType": "时间变更,行程变更", + "newDestination": "日本", + "newExitReason": "旅游", + "newStartDate": "2025-11-05", + "newEndDate": "2025-11-15", + "newItinerary": "东京-大阪-京都-奈良", + "changeReason": "因临时工作调整,需要延后5天出发,并增加奈良行程", + "remark": "" +} +``` + +**请求参数说明**: + +| 字段名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `applicationId` | Long | ✅ | 原出国申请ID | +| `changeType` | String | ✅ | 变更类型(逗号分隔) | +| `newDestination` | String | ❌ | 新目的地(目的地变更时必填) | +| `newExitReason` | String | ❌ | 新出境事由 | +| `newStartDate` | String | ❌ | 新出发日期(时间变更时必填) | +| `newEndDate` | String | ❌ | 新返回日期(时间变更时必填) | +| `newItinerary` | String | ❌ | 新行程安排(行程变更时必填) | +| `changeReason` | String | ✅ | 变更原因(必填,最多500字) | +| `remark` | String | ❌ | 备注 | + +**响应示例**: + +```json +{ + "code": 200, + "msg": "提交成功", + "data": { + "changeId": 987654321 + } +} +``` + +**后端实现要点**: +```java +@PostMapping +@SaCheckPermission("rs:itineraryChange:add") +public R> add(@Validated @RequestBody ItineraryChangeForm form) { + // 1. 校验applicationId是否存在且状态为已通过 + // 2. 校验是否已有待审核的变更申请(避免重复提交) + // 3. 根据applicationId获取原申请信息并填充到original_*字段 + // 4. 根据changeType校验对应的new_*字段是否填写 + // 5. 计算newDuration(如果有时间变更) + // 6. 设置申请人信息(从当前登录用户获取) + // 7. 设置状态为待审核(status='0') + // 8. 插入数据库 + // 9. 返回changeId +} +``` + +**业务规则**: +- ✅ 只能对状态为"已通过"的出国申请发起变更 +- ✅ 同一个出国申请不能同时存在多个"待审核"的变更申请 +- ✅ 变更类型至少选择一种 +- ✅ 根据变更类型,对应的新字段必须填写 +- ✅ 变更原因为必填项 + +--- + +### 5. 修改并重新提交变更申请 + +**接口说明**: 修改已拒绝的变更申请并重新提交 + +**请求方式**: `PUT` + +**接口地址**: `/rs/itinerary-change/{changeId}` + +**权限标识**: `rs:itineraryChange:edit` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `changeId` | Long | ✅ | 变更申请ID | + +**Content-Type**: `application/json` + +**请求Body**: + +```json +{ + "changeId": 987654321, + "changeType": "时间变更,行程变更,目的地变更", + "newDestination": "韩国", + "newExitReason": "旅游", + "newStartDate": "2025-11-10", + "newEndDate": "2025-11-20", + "newItinerary": "首尔-釜山-济州岛", + "changeReason": "根据审批意见调整,改为前往韩国", + "remark": "已根据领导建议修改目的地" +} +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "修改成功" +} +``` + +**后端实现要点**: +```java +@PutMapping("/{changeId}") +@SaCheckPermission("rs:itineraryChange:edit") +public R edit(@PathVariable Long changeId, @Validated @RequestBody ItineraryChangeForm form) { + // 1. 权限校验(只能修改自己的申请) + // 2. 状态校验(只有已拒绝的申请才能修改) + // 3. 根据changeType校验对应的new_*字段 + // 4. 重新计算newDuration + // 5. 清空审批信息(approval_opinion、approval_time等) + // 6. 重置状态为待审核(status='0') + // 7. 更新数据库 +} +``` + +**业务规则**: +- ✅ 只能修改状态为"已拒绝"的变更申请 +- ✅ 只能修改自己提交的申请 +- ✅ 修改后状态重置为"待审核" + +--- + +### 6. 撤回变更申请 + +**接口说明**: 撤回待审核的变更申请 + +**请求方式**: `PUT` + +**接口地址**: `/rs/itinerary-change/cancel/{changeId}` + +**权限标识**: `rs:itineraryChange:cancel` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `changeId` | Long | ✅ | 变更申请ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `cancelReason` | String | ✅ | 撤回原因 | + +**请求示例**: + +``` +PUT /rs/itinerary-change/cancel/987654321?cancelReason=行程计划有变,暂不变更 +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "撤回成功" +} +``` + +**后端实现要点**: +```java +@PutMapping("/cancel/{changeId}") +@SaCheckPermission("rs:itineraryChange:cancel") +public R cancel(@PathVariable Long changeId, @RequestParam String cancelReason) { + // 1. 权限校验(只能撤回自己的申请) + // 2. 状态校验(只有待审核的申请才能撤回) + // 3. 更新状态为已撤回(status='3') + // 4. 保存撤回原因到remark字段 +} +``` + +**业务规则**: +- ✅ 只能撤回状态为"待审核"的变更申请 +- ✅ 只能撤回自己提交的申请 + +--- + +### 7. 删除变更申请 + +**接口说明**: 删除变更申请(逻辑删除) + +**请求方式**: `DELETE` + +**接口地址**: `/rs/itinerary-change/{changeId}` + +**权限标识**: `rs:itineraryChange:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|-------|------|------|------| +| `changeId` | Long | ✅ | 变更申请ID | + +**请求示例**: + +``` +DELETE /rs/itinerary-change/987654321 +``` + +**响应示例**: + +```json +{ + "code": 200, + "msg": "删除成功" +} +``` + +**后端实现要点**: +```java +@DeleteMapping("/{changeId}") +@SaCheckPermission("rs:itineraryChange:remove") +public R remove(@PathVariable Long changeId) { + // 1. 权限校验(只能删除自己的申请) + // 2. 状态校验(不能删除已通过的申请) + // 3. 逻辑删除(设置deleted=1) +} +``` + +**业务规则**: +- ✅ 只能删除自己提交的申请 +- ✅ 不能删除状态为"已通过"的申请 + +--- + +### 8. 导出变更申请列表 + +**接口说明**: 导出变更申请列表为Excel文件 + +**请求方式**: `POST` + +**接口地址**: `/rs/itinerary-change/export` + +**权限标识**: `rs:itineraryChange:export` + +**Content-Type**: `application/json` + +**请求Body**: + +```json +{ + "applicantName": "", + "destination": "", + "status": "" +} +``` + +**响应类型**: `application/vnd.ms-excel` + +**响应Headers**: + +``` +Content-Disposition: attachment; filename*=UTF-8''变更行程列表_20251013.xlsx +``` + +**后端实现要点**: +```java +@PostMapping("/export") +@SaCheckPermission("rs:itineraryChange:export") +public void export(ItineraryChangeQuery query, HttpServletResponse response) { + // 1. 查询数据(不分页,最多1000条) + // 2. 转换为Excel导出格式 + // 3. 设置响应头 + // 4. 写入响应流 +} +``` + +**导出字段**: +- 申请人 +- 部门 +- 原目的地 +- 新目的地 +- 原出国时间 +- 新出国时间 +- 变更类型 +- 变更原因 +- 变更状态 +- 申请时间 + +--- + +## 📝 数据模型 + +### ItineraryChangeQuery(查询参数) + +```java +public class ItineraryChangeQuery extends BaseEntity { + /** 申请人姓名(模糊查询) */ + private String applicantName; + + /** 目的地国家(模糊查询) */ + private String destination; + + /** 变更状态 */ + private String status; +} +``` + +### ItineraryChangeForm(表单) + +```java +public class ItineraryChangeForm { + /** 变更ID(修改时传入) */ + private Long changeId; + + /** 原出国申请ID */ + @NotNull(message = "请选择原出国申请") + private Long applicationId; + + /** 变更类型 */ + @NotBlank(message = "请选择变更类型") + private String changeType; + + /** 新目的地国家 */ + private String newDestination; + + /** 新出境事由 */ + private String newExitReason; + + /** 新出发日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newStartDate; + + /** 新返回日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newEndDate; + + /** 新行程安排 */ + private String newItinerary; + + /** 变更原因 */ + @NotBlank(message = "请输入变更原因") + @Size(max = 500, message = "变更原因不能超过500字") + private String changeReason; + + /** 备注 */ + @Size(max = 500, message = "备注不能超过500字") + private String remark; +} +``` + +### ItineraryChangeVO(返回对象) + +```java +public class ItineraryChangeVO { + /** 变更ID */ + private Long changeId; + + /** 原出国申请ID */ + private Long applicationId; + + /** 申请人信息 */ + private Long applicantId; + private String applicantName; + private String department; + + /** 原申请信息 */ + private String originalDestination; + private String originalExitReason; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date originalStartDate; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date originalEndDate; + private Integer originalDuration; + private String originalItinerary; + + /** 变更信息 */ + private String changeType; + private String newDestination; + private String newExitReason; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newStartDate; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date newEndDate; + private Integer newDuration; + private String newItinerary; + + /** 变更原因和备注 */ + private String changeReason; + private String remark; + + /** 审批信息 */ + private String status; + private String statusText; // 状态文本 + private String approvalOpinion; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date approvalTime; + private String approverName; + + /** 时间信息 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} +``` + +--- + +## 🔍 业务规则总结 + +### 1. 可发起变更的条件 +- ✅ 出国申请状态必须为"已通过" +- ✅ 该申请不能存在"待审核"状态的变更申请 +- ✅ 只能变更自己的出国申请 + +### 2. 变更类型校验 +- **目的地变更**: 必须填写`newDestination` +- **时间变更**: 必须填写`newStartDate`和`newEndDate`,系统自动计算`newDuration` +- **行程变更**: 必须填写`newItinerary` +- 可以同时选择多种变更类型 + +### 3. 状态流转 +``` +待审核(0) ──审批通过──> 已通过(1) + │ + ├──审批拒绝──> 已拒绝(2) ──修改重提──> 待审核(0) + │ + └──申请人撤回──> 已撤回(3) +``` + +### 4. 操作权限 +- **待审核**: 可撤回、可删除 +- **已通过**: 不可删除、不可修改 +- **已拒绝**: 可修改重提、可删除 +- **已撤回**: 可删除 + +--- + +## 🧪 测试用例 + +### 1. 新增变更申请 + +**测试步骤**: +1. 选择一个已通过的出国申请 +2. 选择变更类型(时间变更) +3. 填写新的出发和返回日期 +4. 填写变更原因 +5. 点击提交 + +**预期结果**: +- ✅ 提交成功 +- ✅ 状态为"待审核" +- ✅ 原申请信息正确填充 +- ✅ 新出国天数自动计算正确 + +### 2. 撤回变更申请 + +**测试步骤**: +1. 在变更申请列表中找到一条"待审核"的记录 +2. 点击撤回按钮 +3. 输入撤回原因 +4. 确认撤回 + +**预期结果**: +- ✅ 撤回成功 +- ✅ 状态变为"已撤回" +- ✅ 撤回原因保存到备注字段 + +### 3. 重新提交已拒绝的申请 + +**测试步骤**: +1. 找到一条"已拒绝"的变更申请 +2. 点击重新提交 +3. 根据审批意见修改内容 +4. 点击提交 + +**预期结果**: +- ✅ 提交成功 +- ✅ 状态重置为"待审核" +- ✅ 审批信息被清空 + +--- + +## 📌 注意事项 + +### 1. 后端开发注意事项 +- ✅ 使用雪花ID生成器生成`change_id` +- ✅ 所有查询必须添加`deleted=0`条件 +- ✅ 所有查询必须添加租户ID条件(多租户隔离) +- ✅ 时间字段使用`@JsonFormat`格式化 +- ✅ 状态转换时需要校验当前状态 +- ✅ 权限校验:确保用户只能操作自己的数据 + +### 2. 前端开发注意事项 +- ✅ 日期组件使用`YYYY-MM-DD`格式 +- ✅ 出国天数自动计算(监听日期变化) +- ✅ 根据变更类型动态显示/隐藏对应字段 +- ✅ 根据变更类型设置字段必填验证 +- ✅ 列表操作按钮根据状态动态显示 + +### 3. 数据一致性 +- ✅ 新增时从原申请复制`original_*`字段(冗余存储) +- ✅ 修改时不允许更改`original_*`字段 +- ✅ 状态为"已通过"后,原申请的相关字段应同步更新(业务需要时) + +--- + +## 🚀 部署清单 + +### 数据库 +- [ ] 执行数据库表创建脚本:`rs_overseas_itinerary_change.sql` +- [ ] 执行菜单权限配置脚本:`rs_overseas_itinerary_change_menu.sql` +- [ ] 为相应角色分配菜单权限 + +### 后端 +- [ ] 创建实体类:`ItineraryChange` +- [ ] 创建Mapper接口:`ItineraryChangeMapper` +- [ ] 创建Mapper XML:`ItineraryChangeMapper.xml` +- [ ] 创建Service接口:`IItineraryChangeService` +- [ ] 创建Service实现:`ItineraryChangeServiceImpl` +- [ ] 创建Controller:`ItineraryChangeController` +- [ ] 创建VO类:`ItineraryChangeVO` +- [ ] 创建Form类:`ItineraryChangeForm` +- [ ] 创建Query类:`ItineraryChangeQuery` +- [ ] 添加权限注解:`@SaCheckPermission` + +### 前端 +- [ ] 创建页面:`src/views/rs/overseas/itinerary-change.vue` +- [ ] 创建API文件:`src/api/rs/itinerary-change.ts` +- [ ] 配置路由(菜单配置后自动生成) +- [ ] 测试页面功能 + +### 测试 +- [ ] 单元测试:Service层方法测试 +- [ ] 接口测试:Postman/Apifox测试所有API +- [ ] 功能测试:前端页面完整流程测试 +- [ ] 权限测试:不同角色权限测试 + +--- + +**文档维护**: 请在开发过程中及时更新本文档 +**最后更新**: 2025-10-13 + diff --git a/docs/开发文档/API/后勤满意度调查API.md b/docs/开发文档/API/后勤满意度调查API.md index 6f1de537..22e4fd5a 100644 --- a/docs/开发文档/API/后勤满意度调查API.md +++ b/docs/开发文档/API/后勤满意度调查API.md @@ -63,6 +63,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | `/hq/satSurvey/{ids}` | DELETE | 删除问卷 | hq:satSurvey:remove | | `/hq/satSurvey/publish/{id}` | PUT | 发布问卷 | hq:satSurvey:publish | | `/hq/satQuestion/list` | GET | 查询题目列表 | hq:satQuestion:list | +> 📌 **排序策略更新(2025-11-13)**:题目列表在按指定字段排序时,会额外追加 `createTime DESC` 作为最后一位兜底排序,确保返回结果顺序稳定。 | `/hq/satQuestion/{id}` | GET | 获取题目详情 | hq:satQuestion:query | | `/hq/satQuestion` | POST | 新增题目 | hq:satQuestion:add | | `/hq/satQuestion` | PUT | 修改题目 | hq:satQuestion:edit | @@ -452,6 +453,7 @@ await Promise.all(promises); | 1.0.3 | 2025-07-17 | 添加选项保存防重复提交机制,解决幂等性控制冲突 | wuzilong | | 1.0.4 | 2025-07-18 | 优化问答题文本回答展示功能,提高Vue渲染稳定性 | wuzilong | | 1.0.5 | 2025-07-18 | 增强问卷答题记录接口功能,返回答题详情数据 | wuzilong | +| 1.0.6 | 2025-11-13 | 调整题目列表排序策略,追加创建时间倒序兜底,避免排序不稳定 | Cursor | ## 附录 diff --git a/docs/开发文档/API/学生信息批次收集API.md b/docs/开发文档/API/学生信息批次收集API.md index 8a37dea4..b4735a11 100644 --- a/docs/开发文档/API/学生信息批次收集API.md +++ b/docs/开发文档/API/学生信息批次收集API.md @@ -187,6 +187,8 @@ } ``` +> 📌 **排序说明**:分页列表默认按照学号 `studentNo` 降序排序;若学号相同,则继续按照 `createTime` 降序排列,确保最新学号优先展示。 + ### 2. 导出学生信息批次收集列表 - **接口路径**: POST `/studentMsgBatch/studentMsgBatch/export` diff --git a/docs/开发文档/API/学生信息管理API.md b/docs/开发文档/API/学生信息管理API.md deleted file mode 100644 index dcb12818..00000000 --- a/docs/开发文档/API/学生信息管理API.md +++ /dev/null @@ -1,854 +0,0 @@ -# 学生信息管理API文档 - -## 1. 文档说明 - -### 1.1 API基础信息 -- **模块名称**: 学工管理 - 学生信息管理 -- **模块代码**: ruoyi-xg -- **基础路径**: `/studentMsg/studentMsg` -- **权限前缀**: `studentMsg:studentMsg` -- **版本**: 1.0.0 -- **更新时间**: 2025-01-27(新增班级学生查询接口) - -### 1.2 通用规范 -- **请求方式**: 支持GET、POST、PUT、DELETE -- **数据格式**: JSON -- **字符编码**: UTF-8 -- **认证方式**: Bearer Token -- **权限控制**: Sa-Token + 数据权限 - -### 1.3 响应格式 -```json -{ - "code": 200, - "msg": "操作成功", - "data": {}, - "timestamp": "2025-01-27T18:00:00" -} -``` - -## 2. 学生信息管理接口 - -### 2.1 查询学生信息列表 -- **接口路径**: GET `/studentMsg/studentMsg/list` -- **权限标识**: `studentMsg:studentMsg:list` -- **功能描述**: 分页查询学生信息列表,支持多条件筛选 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| pageNum | integer | 否 | 页码,默认1 | -| pageSize | integer | 否 | 每页大小,默认10 | -| studentNo | string | 否 | 学号 | -| name | string | 否 | 姓名(模糊查询) | -| idCard | string | 否 | 身份证号 | -| gender | string | 否 | 性别:0-男,1-女 | -| studentStatus | string | 否 | 学籍状态 | -| phone | string | 否 | 联系电话(模糊查询) | -| email | string | 否 | 邮箱 | -| classId | string | 否 | 班级ID | -| className | string | 否 | 班级名称 | -| majorId | string | 否 | 专业ID | -| majorName | string | 否 | 专业名称 | -| collegeId | string | 否 | 学院ID | -| collegeName | string | 否 | 学院名称 | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "rows": [ - { - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1", - "createTime": "2025-01-27 10:00:00", - "updateTime": "2025-01-27 10:00:00" - } - ], - "total": 1 -} -``` - -### 2.2 根据组织ID查询学生信息列表 -- **接口路径**: POST `/studentMsg/studentMsg/listByDeptId` -- **权限标识**: `studentMsg:studentMsg:list` -- **功能描述**: 根据部门ID查询该组织下所有学生信息 - -#### 请求参数 -```json -{ - "dept_id": 101, - "name": "张", - "studentNo": "2023001", - "phone": "138", - "studentStatus": "1", - "beginTime": "2025-01-01", - "endTime": "2025-01-27" -} -``` - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "rows": [...], - "total": 10 -} -``` - -### 2.3 查询班级中所有学生信息 ⭐ 新增 -- **接口路径**: GET `/studentMsg/studentMsg/listByClassId` -- **权限标识**: `studentMsg:studentMsg:query` -- **功能描述**: 查询指定班级中所有在籍学生信息 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| classId | string | 是 | 班级ID | -| name | string | 否 | 学生姓名(模糊查询) | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "data": [ - { - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1", - "createTime": "2025-01-27 10:00:00", - "updateTime": "2025-01-27 10:00:00" - } - ] -} -``` - -#### 业务逻辑 -1. 在 `xg_student_msg` 表中查询: - - `deleted = 0` (未删除) - - `student_status = '1'` (在籍状态) - - `class_id = 入参的classId` (班级ID) - - `name LIKE '%入参的name%'` (姓名模糊查询,可选) - - 按创建时间倒序排列 - -### 2.4 获取学生信息详情 -- **接口路径**: GET `/studentMsg/studentMsg/{id}` -- **权限标识**: `studentMsg:studentMsg:query` -- **功能描述**: 根据ID获取学生详细信息 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| id | long | 是 | 学生ID | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "data": { - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1", - "createTime": "2025-01-27 10:00:00", - "updateTime": "2025-01-27 10:00:00" - } -} -``` - -### 2.5 新增学生信息 -- **接口路径**: POST `/studentMsg/studentMsg` -- **权限标识**: `studentMsg:studentMsg:add` -- **功能描述**: 新增学生信息 - -#### 请求参数 -```json -{ - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1" -} -``` - -#### 响应数据 -```json -{ - "code": 200, - "msg": "新增成功" -} -``` - -### 2.6 修改学生信息 -- **接口路径**: PUT `/studentMsg/studentMsg` -- **权限标识**: `studentMsg:studentMsg:edit` -- **功能描述**: 修改学生信息 - -#### 请求参数 -```json -{ - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1" -} -``` - -#### 响应数据 -```json -{ - "code": 200, - "msg": "修改成功" -} -``` - -### 2.7 删除学生信息 -- **接口路径**: DELETE `/studentMsg/studentMsg/{ids}` -- **权限标识**: `studentMsg:studentMsg:remove` -- **功能描述**: 批量删除学生信息 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| ids | string | 是 | 学生ID列表,多个用逗号分隔 | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "删除成功" -} -``` - -### 2.8 导出学生信息 -- **接口路径**: POST `/studentMsg/studentMsg/export` -- **权限标识**: `studentMsg:studentMsg:export` -- **功能描述**: 导出学生信息列表 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| studentNo | string | 否 | 学号 | -| name | string | 否 | 姓名 | -| studentStatus | string | 否 | 学籍状态 | - -#### 响应数据 -返回Excel文件流 - -### 2.8.1 高级检索导出学生信息 -- **接口路径**: POST `/studentMsg/studentMsg/advancedSearchExport` -- **权限标识**: `studentMsg:studentMsg:export` -- **功能描述**: 根据高级检索条件导出学生信息列表,支持多维度复合查询条件 -- **版本**: 1.2.1 (2025-01-07修复MapStruct转换错误) -- **修复**: 解决 `XgStudentMsgVo` 到 `XgStudentMsgExportVo` 转换器缺失问题 - -#### 请求参数 -支持以下高级检索条件: - -**基础信息检索** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| studentNo | string | 否 | 学号(模糊查询) | -| name | string | 否 | 姓名(模糊查询) | -| idCard | string | 否 | 身份证号(模糊查询) | -| phone | string | 否 | 手机号码(模糊查询) | -| email | string | 否 | 邮箱(模糊查询) | -| gender | array | 否 | 性别数组:0-男,1-女 | -| nationality | array | 否 | 民族数组 | -| politicalStatus | array | 否 | 政治面貌数组 | -| birthDateStart | string | 否 | 出生日期开始:yyyy-MM-dd | -| birthDateEnd | string | 否 | 出生日期结束:yyyy-MM-dd | - -**学籍信息检索** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| studentStatus | array | 否 | 学籍状态数组:1-在籍,2-休学,3-复学,4-退学,5-毕业 | -| enrollmentYearStart | integer | 否 | 入学年份开始 | -| enrollmentYearEnd | integer | 否 | 入学年份结束 | -| admissionCategory | array | 否 | 录取类别数组 | - -**学院专业班级检索** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| collegeName | string | 否 | 学院名称(模糊查询) | -| majorName | string | 否 | 专业名称(模糊查询) | -| className | string | 否 | 班级名称(模糊查询) | -| grade | array | 否 | 年级数组 | -| counselorName | string | 否 | 辅导员姓名(模糊查询) | -| classTeacherName | string | 否 | 班主任姓名(模糊查询) | - -**家庭信息检索** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| fatherName | string | 否 | 父亲姓名(模糊查询) | -| motherName | string | 否 | 母亲姓名(模糊查询) | -| familyEconomicStatus | array | 否 | 家庭经济状况数组 | -| familyIncomeMin | number | 否 | 家庭收入最小值 | -| familyIncomeMax | number | 否 | 家庭收入最大值 | - -**特殊身份检索** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| isPoorStudent | array | 否 | 是否贫困生:0-否,1-是 | -| isMinority | array | 否 | 是否少数民族:0-否,1-是 | -| isDisabled | array | 否 | 是否残疾:0-否,1-是 | -| isOrphan | array | 否 | 是否孤儿:0-否,1-是 | - -**权限范围** -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| deptIds | array | 否 | 部门ID数组,用于权限控制 | - -#### 响应数据 -返回Excel文件流,包含完整的学生信息数据 - -#### 修复记录 -- **2025-01-07 v1.2.1**: 修复MapStruct转换错误,解决 `XgStudentMsgVo` 到 `XgStudentMsgExportVo` 转换器缺失问题 - - **问题**: `MapstructUtils.convert()` 无法找到两个VO类之间的转换器,导致导出功能报错 - - **解决**: 在Controller中添加手动转换方法 `convertToExportVo()` 和 `convertToExportVoList()` - - **影响**: 导出功能正常工作,所有字段完整映射 -- **2025-01-07 v1.2.0**: 修复分页限制问题,导出时pageSize设置为50000,确保能导出所有符合条件的记录 - - **问题**: 之前由于BO类默认pageSize=10,导致只能导出10条记录 - - **解决**: 取消注释Controller中的分页参数设置代码 - -### 2.9 查询学生个人信息 -- **接口路径**: GET `/studentMsg/studentMsg/personalInfo` -- **权限标识**: `studentMsg:studentMsg:query` -- **功能描述**: 查询当前登录学生的个人信息 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| userId | long | 否 | 用户ID,为空时使用当前登录用户ID | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "data": { - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1", - "createTime": "2025-01-27 10:00:00", - "updateTime": "2025-01-27 10:00:00" - } -} -``` - -### 2.10 根据班级ID查询学生信息列表 -- **接口路径**: GET `/studentMsg/studentMsg/listByClassId` -- **权限标识**: `studentMsg:studentMsg:query` -- **功能描述**: 查询指定班级中所有在籍学生信息 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| classId | string | 是 | 班级ID | -| name | string | 否 | 学生姓名(模糊查询) | - -#### 响应数据 -```json -{ - "code": 200, - "msg": "查询成功", - "data": [ - { - "id": 1, - "studentNo": "2023001001", - "name": "张三", - "gender": "0", - "birthDate": "2000-01-01", - "idCard": "110101199901010001", - "phone": "13800138000", - "email": "zhangsan@example.com", - "classId": "101", - "className": "计算机2023-1班", - "majorId": "CS001", - "majorName": "计算机科学与技术", - "collegeId": "CS", - "collegeName": "计算机学院", - "studentStatus": "1", - "createTime": "2025-01-27 10:00:00", - "updateTime": "2025-01-27 10:00:00" - } - ] -} -``` - -### 2.11 导入学生信息数据 -- **接口路径**: POST `/studentMsg/studentMsg/importData` -- **权限标识**: `studentMsg:studentMsg:import` -- **功能描述**: 批量导入学生信息数据,支持Excel文件上传 - -#### 请求参数 -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| file | MultipartFile | 是 | Excel文件(.xlsx格式) | -| updateSupport | boolean | 否 | 是否支持更新已存在数据,默认false | - -#### 请求示例 -```http -POST /studentMsg/studentMsg/importData -Content-Type: multipart/form-data - -file: [Excel文件] -updateSupport: true -``` - -#### 响应数据 -```json -{ - "code": 200, - "msg": "恭喜您,数据已全部导入成功!共 10 条,数据如下:", - "data": null -} -``` - -#### 错误响应示例 -```json -{ - "code": 500, - "msg": "很抱歉,导入失败!共 2 条数据格式不正确,错误如下:
1、学号 2023001001 已存在
2、学号 2023001002 导入失败:手机号码格式不正确", - "data": null -} -``` - -### 2.12 获取导入模板 -- **接口路径**: POST `/studentMsg/studentMsg/importTemplate` -- **权限标识**: `studentMsg:studentMsg:import` -- **功能描述**: 下载学生信息导入模板文件 - -#### 请求参数 -无 - -#### 响应数据 -返回Excel模板文件(.xlsx格式) - -#### 模板字段说明 -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| 学号 | string | 是 | 学生学号,必须唯一 | -| 姓名 | string | 是 | 学生姓名 | -| 性别 | string | 是 | 0-男,1-女 | -| 身份证号 | string | 否 | 身份证号码 | -| 手机号码 | string | 否 | 手机号码,必须唯一 | -| 电子邮箱 | string | 否 | 邮箱地址,必须唯一 | -| 出生日期 | date | 否 | 出生日期 | -| 民族 | string | 否 | 民族代码 | -| 政治面貌 | string | 否 | 政治面貌代码 | -| 家庭地址 | string | 否 | 家庭住址 | -| 现住址 | string | 否 | 现居住地址 | -| 班级ID | string | 否 | 班级编号 | -| 班级名称 | string | 否 | 班级名称 | -| 专业ID | string | 否 | 专业编号 | -| 专业名称 | string | 否 | 专业名称 | -| 学院ID | string | 否 | 学院编号 | -| 学院名称 | string | 否 | 学院名称 | -| 学籍状态 | string | 否 | 1-在籍,2-休学,3-复学,4-退学,5-毕业 | - -## 3. 数据字典 - -### 3.1 性别字典 -| 字典值 | 字典标签 | -|--------|----------| -| 0 | 男 | -| 1 | 女 | - -### 3.2 学籍状态字典 -| 字典值 | 字典标签 | -|--------|----------| -| 1 | 在籍 | -| 2 | 休学 | -| 3 | 复学 | -| 4 | 退学 | -| 5 | 毕业 | - -### 3.3 信息状态字典 -| 字典值 | 字典标签 | -|--------|----------| -| 1 | 正常 | -| 2 | 待审核 | -| 3 | 审核中 | -| 4 | 已驳回 | -| 5 | 已锁定 | - -## 4. 错误码说明 - -| 错误码 | 说明 | -|--------|------| -| 200 | 操作成功 | -| 400 | 请求参数错误 | -| 401 | 未授权 | -| 403 | 无权限 | -| 404 | 资源不存在 | -| 500 | 服务器内部错误 | - -## 5. 权限说明 - -### 5.1 数据权限控制 -- **超级管理员**: 可查看所有学生信息 -- **学工管理员**: 可查看所有学生信息 -- **二级学院管理员**: 只能查看本学院学生信息 -- **辅导员**: 只能查看负责班级的学生信息 -- **班主任**: 只能查看负责班级的学生信息 -- **学生**: 只能查看自己的信息 - -### 5.2 操作权限控制 -- **新增**: 学工管理员、二级学院管理员 -- **修改**: 学工管理员、二级学院管理员、辅导员、班主任(限制范围) -- **删除**: 学工管理员、二级学院管理员 -- **导入**: 学工管理员、二级学院管理员 -- **导出**: 学工管理员、二级学院管理员、辅导员、班主任 -- **查询**: 所有有权限的用户 - -## 6. 注意事项 - -1. 所有接口都需要Bearer Token认证 -2. 删除操作为逻辑删除,不会物理删除数据 -3. 学号和身份证号必须唯一 -4. 新增学生时会自动创建对应的系统用户账号 -5. 支持多租户数据隔离 -6. 所有时间字段使用yyyy-MM-dd HH:mm:ss格式 -7. 数据权限按照用户角色自动过滤 -8. 班级学生查询接口只返回在籍状态的学生 - -## 7. 更新日志 - -### 2025-01-27 - 新增学生信息导入功能 -- **新增功能**: 新增学生信息批量导入功能 -- **新增接口**: - - `POST /studentMsg/studentMsg/importData` - 学生信息导入接口 - - `POST /studentMsg/studentMsg/importTemplate` - 导入模板下载接口 -- **功能特性**: - - 支持Excel文件批量导入 - - 支持新增和更新两种模式 - - 自动验证学号、手机号、邮箱唯一性 - - 完整的错误处理和结果统计 - - 自动创建对应的系统用户账号 -- **技术实现**: - - 使用自定义Listener处理Excel解析 - - 事务保证数据一致性 - - 详细的导入结果反馈 - -### 2025-01-27 - 新增班级学生查询接口 -- **新增功能**: 新增 `GET /studentMsg/studentMsg/listByClassId` 接口 -- **功能描述**: 查询指定班级中所有在籍学生信息 -- **过滤条件**: - - `deleted = 0` (未删除) - - `student_status = '1'` (在籍状态) - - `class_id = 入参的classId` (班级ID) - - `name LIKE '%入参的name%'` (姓名模糊查询,可选) -- **排序方式**: 按创建时间倒序排列 - -## 17. 获取学生信息树 ⭐ 新增 - -### 接口描述 -获取按年级、学院、专业、班级层级组织的学生信息树结构,用于宿舍分配等场景的学生选择。 - -### 接口URL -``` -GET /studentMsg/studentMsg/getStudentTree -``` - -### 请求参数 -| 参数名 | 类型 | 是否必填 | 说明 | 示例值 | -|-------|------|---------|------|-------| -| secondaryCollegeId | Long | 否 | 二级学院ID,默认为101 | 101 | - -### 请求示例 -```javascript -// GET请求 -/studentMsg/studentMsg/getStudentTree?secondaryCollegeId=101 -``` - -### 响应参数 -| 参数名 | 类型 | 说明 | 示例值 | -|-------|------|------|-------| -| code | Integer | 响应码,200表示成功 | 200 | -| msg | String | 响应消息 | "操作成功" | -| data | Array | 学生信息树数据 | 见下方详细结构 | - -### 学生树节点结构 -| 参数名 | 类型 | 说明 | 示例值 | -|-------|------|------|-------| -| id | String | 节点唯一标识 | "college-101" | -| label | String | 节点显示名称 | "计算机学院" | -| type | String | 节点类型:college/major/class | "college" | -| maleAssigned | Integer | 已分配男生数量 | 45 | -| maleTotal | Integer | 男生总数 | 120 | -| femaleAssigned | Integer | 已分配女生数量 | 25 | -| femaleTotal | Integer | 女生总数 | 60 | -| grade | String | 年级信息(仅班级节点有值) | "2025" | -| collegeId | Long | 学院ID | 101 | -| collegeName | String | 学院名称 | "计算机学院" | -| majorId | Long | 专业ID(专业和班级节点有值) | 1001 | -| majorName | String | 专业名称(专业和班级节点有值) | "计算机科学与技术" | -| classId | Long | 班级ID(仅班级节点有值) | 10001 | -| className | String | 班级名称(仅班级节点有值) | "计算机科学与技术1班" | -| sortOrder | Integer | 排序序号 | 1 | -| children | Array | 子节点数组 | [] | - -### 后端实现 -```java -// Controller -@GetMapping("/getStudentTree") -@SaCheckPermission("studentMsg:studentMsg:query") -public R> getStudentTree(@RequestParam(defaultValue = "101") Long secondaryCollegeId) { - List tree = studentService.getStudentTree(secondaryCollegeId); - return R.ok(tree); -} - -// Service实现 -@Override -public List getStudentTree(Long secondaryCollegeId) { - return baseMapper.getStudentTree(secondaryCollegeId); -} -``` - -### 数据库查询 -```sql - -``` - -### 前端调用 -```typescript -// API定义 -export function getStudentTree(secondaryCollegeId: number = 101): AxiosPromise> { - return request({ - url: '/studentMsg/studentMsg/getStudentTree', - method: 'get', - params: { - secondaryCollegeId - } - }); -} - -// 组件中使用 -const getStudentTreeData = async () => { - try { - const res = await getStudentTree(101); - if (res.data.code === 200) { - studentTreeData.value = res.data.data || []; - } else { - ElMessage.error(res.data.msg || '获取学生信息树失败'); - } - } catch (error) { - console.error('获取学生信息树失败:', error); - ElMessage.error('获取学生信息树失败'); - } -}; -``` - -### 响应示例 -```json -{ - "code": 200, - "msg": "操作成功", - "data": [ - { - "id": "college-101", - "label": "计算机学院", - "type": "college", - "collegeId": 101, - "collegeName": "计算机学院", - "maleAssigned": 45, - "maleTotal": 120, - "femaleAssigned": 25, - "femaleTotal": 60, - "children": [ - { - "id": "major-1001", - "label": "计算机科学与技术", - "type": "major", - "collegeId": 101, - "collegeName": "计算机学院", - "majorId": 1001, - "majorName": "计算机科学与技术", - "maleAssigned": 45, - "maleTotal": 80, - "femaleAssigned": 15, - "femaleTotal": 40, - "children": [ - { - "id": "class-10001", - "label": "计算机科学与技术1班", - "type": "class", - "collegeId": 101, - "collegeName": "计算机学院", - "majorId": 1001, - "majorName": "计算机科学与技术", - "classId": 10001, - "className": "计算机科学与技术1班", - "grade": "2025", - "maleAssigned": 20, - "maleTotal": 40, - "femaleAssigned": 10, - "femaleTotal": 20 - }, - { - "id": "class-10002", - "label": "计算机科学与技术2班", - "type": "class", - "collegeId": 101, - "collegeName": "计算机学院", - "majorId": 1001, - "majorName": "计算机科学与技术", - "classId": 10002, - "className": "计算机科学与技术2班", - "grade": "2025", - "maleAssigned": 25, - "maleTotal": 40, - "femaleAssigned": 5, - "femaleTotal": 20 - } - ] - } - ] - } - ] -} -``` - -### 错误响应 -```json -{ - "code": 500, - "msg": "获取学生信息树失败", - "data": null -} -``` - -### 注意事项 -1. **权限控制**: 需要 `studentMsg:studentMsg:query` 权限 -2. **树结构**: 按学院->专业->班级的层级组织(移除年级分组) -3. **统计信息**: 每个节点包含已分配和总数的男女生统计信息 -4. **排序规则**: 按学院ID、专业ID、班级ID顺序排列 -5. **应用场景**: 主要用于宿舍分配时显示各班级的入住统计 -6. **数据过滤**: 支持按二级学院过滤数据范围,默认为101 -7. **逻辑删除**: 查询时排除已删除的学生记录 -8. **学籍状态**: 只统计在籍学生(student_status='1') -9. **住宿统计逻辑**: - - **total**: 需要住宿的学生总数(is_boarder=1) - - **assigned**: 实际已分配住宿的学生数(通过xg_dorm_bed_student表关联,status='0') - ---- - -## 📋 更新日志 - -### 2025-01-27 - 学生信息树接口优化 -- **功能优化**: 优化 `GET /studentMsg/studentMsg/getStudentTree` 接口 -- **结构调整**: 移除年级分组,调整为学院->专业->班级的三级层级结构 -- **性能优化**: 减少嵌套层级,提升前端渲染性能 -- **统计信息**: 保持各层级男女生人数和住宿分配统计 -- **逻辑修正**: 修正住宿统计逻辑,通过xg_dorm_bed_student表准确统计已分配住宿学生数 - -### 2025-01-27 - 新增班级学生查询接口 -- **新增功能**: 新增 `GET /studentMsg/studentMsg/listByClassId` 接口 -- **功能描述**: 查询指定班级中所有在籍学生信息 -- **过滤条件**: 支持按班级ID和学生姓名查询 -- **排序方式**: 按创建时间倒序排列 - ---- - -**文档更新时间**: 2025-01-27 -**文档版本**: 1.4.0 -**维护人员**: Cursor AI -**更新说明**: -- 优化学生信息树接口,移除年级分组,简化层级结构 -- 新增学生信息树查询接口,支持宿舍分配场景 -- 新增高级检索导出接口文档 -- 修复高级检索导出分页限制问题 -- 之前版本:新增学生信息导入功能,完善学生信息管理API文档 \ No newline at end of file diff --git a/docs/开发文档/API/宿舍管理员信息API.md b/docs/开发文档/API/宿舍管理员信息API.md index 58f1fc8e..c5f48029 100644 --- a/docs/开发文档/API/宿舍管理员信息API.md +++ b/docs/开发文档/API/宿舍管理员信息API.md @@ -210,6 +210,7 @@ - `phone`:手机号 - `sex`:性别 - `className`:班级名称 + - `classStatus`:班级状态(字典 `xg_class_status`,常用值:`0`-停用、`1`-在读) - **返回示例**: ```json { @@ -220,7 +221,8 @@ "name": "张三", "phone": "13800001111", "sex": "0", - "className": "2025级1班" + "className": "2025级1班", + "classStatus": "1" } ] } @@ -228,6 +230,49 @@ - **说明**: - 仅返回当前“正在教学”的班主任(班级状态为1,未删除,班主任ID有效,用户状态正常)。 - 支持姓名和手机号的模糊查询,均为可选参数。 + - `classStatus` 字段便于前端字典渲染班级状态标签。 + +## 获取全部班主任信息(按班级状态排序) + +- **接口路径**:`/xgStudentManager/studentManager/listAllClassTeachers` +- **请求方式**:GET +- **权限标识**:`xgStudentManager:studentManager:list` +- **入参**: + - `phone`(可选,string):手机号,模糊查询 + - `name`(可选,string):姓名,模糊查询 +- **出参**: + - `name`:姓名 + - `phone`:手机号 + - `sex`:性别 + - `className`:班级名称 + - `classStatus`:班级状态(字典 `xg_class_status`,常用值:`0`-停用、`1`-在读等) +- **返回示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "name": "李老师", + "phone": "13800002222", + "sex": "1", + "className": "2025级2班", + "classStatus": "0" + }, + { + "name": "张老师", + "phone": "13800003333", + "sex": "0", + "className": "2025级1班", + "classStatus": "1" + } + ] +} +``` +- **说明**: + - 返回所有班主任关联的班级信息,不再限定班级状态。 + - 默认按照 `class_status` 升序、`class_name` 升序排序,便于前端分组展示。 + - 同样支持姓名和手机号的模糊查询。 ## 4. 数据结构说明 diff --git a/docs/开发文档/API/心理健康测评API.md b/docs/开发文档/API/心理健康测评API.md index 26335476..9e75d815 100644 --- a/docs/开发文档/API/心理健康测评API.md +++ b/docs/开发文档/API/心理健康测评API.md @@ -489,6 +489,7 @@ - **接口URL**:/xljk/evaluation/detail/students/{classId} - **请求方式**:GET - **接口权限**:xljk:evaluation:query +- **排序方式**:按学号 `studentNo` 降序返回,确保默认展示最新学号 - **请求参数**: | 参数名 | 类型 | 是否必须 | 描述 | diff --git a/docs/开发文档/API/班级信息管理API.md b/docs/开发文档/API/班级信息管理API.md deleted file mode 100644 index e83aebc9..00000000 --- a/docs/开发文档/API/班级信息管理API.md +++ /dev/null @@ -1,338 +0,0 @@ -# 班级信息管理API文档 - -## 基本信息 - -- **模块名称**: 班级信息管理 -- **接口前缀**: /classMsg -- **权限前缀**: classMsg:classMsg: - -## API接口列表 - -### 1. 班级信息查询接口 - -#### 1.1 获取班级信息列表 - -- **接口URL**: `/classMsg/classMsg/list` -- **请求方式**: GET -- **权限标识**: `classMsg:classMsg:list` -- **请求参数**: - -| 参数名 | 类型 | 是否必须 | 描述 | -| -------- | ------ | -------- | ---------- | -| pageNum | int | 是 | 页码 | -| pageSize | int | 是 | 每页条数 | -| collegeId| int | 否 | 学院ID | -| majorId | int | 否 | 专业ID | -| className| string | 否 | 班级名称 | -| classType| int | 否 | 班级类型 | -| grade | string | 否 | 年级 | -| classStatus| int | 否 | 班级状态 | - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功", - "rows": [ - { - "id": 1, - "collegeId": 1001, - "collegeName": "信息学院", - "majorId": 2001, - "majorName": "计算机科学与技术", - "classCode": "CS2025-01", - "className": "计科2025-01班", - "classType": 1, - "grade": "2025", - "enrollmentYear": 2025, - "graduationYear": 2029, - "counselorId": 102, - "counselorName": "李老师", - "classTeacherId": 103, - "classTeacherName": "张老师", - "classStatus": 1, - "remark": "示例班级" - } - ], - "total": 1 -} -``` - -#### 1.2 获取班级信息详情 - -- **接口URL**: `/classMsg/classMsg/{id}` -- **请求方式**: GET -- **权限标识**: `classMsg:classMsg:query` -- **请求参数**: - -| 参数名 | 类型 | 是否必须 | 描述 | -| ------ | ---- | -------- | ------- | -| id | int | 是 | 班级ID | - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功", - "data": { - "id": 1, - "collegeId": 1001, - "collegeName": "信息学院", - "majorId": 2001, - "majorName": "计算机科学与技术", - "classCode": "CS2025-01", - "className": "计科2025-01班", - "classType": 1, - "grade": "2025", - "enrollmentYear": 2025, - "graduationYear": 2029, - "counselorId": 102, - "counselorName": "李老师", - "classTeacherId": 103, - "classTeacherName": "张老师", - "classStatus": 1, - "remark": "示例班级" - } -} -``` - -### 1.3 获取当前登录人作为班主任的班级列表 - -- 接口URL: `/classMsg/classMsg/myClassListAsTeacher` -- 请求方式: GET -- 权限标识: `classMsg:classMsg:list` -- 请求参数: 无 -- 返回参数: - -```json -{ - "code": 200, - "msg": "操作成功", - "data": [ - { - "id": 1, - "className": "计科2025-01班", - "majorId": 2001, - "majorName": "计算机科学与技术", - "grade": "2025", - "classStatus": 1 - } - ] -} -``` - -### 2. 班级信息维护接口 - -#### 2.1 新增班级信息 - -- **接口URL**: `/classMsg/classMsg` -- **请求方式**: POST -- **权限标识**: `classMsg:classMsg:add` -- **请求参数**: - -```json -{ - "collegeId": 1001, - "collegeName": "信息学院", - "majorId": 2001, - "majorName": "计算机科学与技术", - "classCode": "CS2025-01", - "className": "计科2025-01班", - "classType": 1, - "grade": "2025", - "enrollmentYear": 2025, - "graduationYear": 2029, - "classStatus": 1, - "remark": "示例班级" -} -``` - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功" -} -``` - -#### 2.2 修改班级信息 - -- **接口URL**: `/classMsg/classMsg` -- **请求方式**: PUT -- **权限标识**: `classMsg:classMsg:edit` -- **请求参数**: - -```json -{ - "id": 1, - "collegeId": 1001, - "collegeName": "信息学院", - "majorId": 2001, - "majorName": "计算机科学与技术", - "classCode": "CS2025-01", - "className": "计科2025-01班", - "classType": 1, - "grade": "2025", - "enrollmentYear": 2025, - "graduationYear": 2029, - "classStatus": 1, - "remark": "修改后的班级" -} -``` - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功" -} -``` - -#### 2.3 设置班主任 - -- **接口URL**: `/classMsg/classMsg` -- **请求方式**: PUT -- **权限标识**: `classMsg:classMsg:edit` -- **请求参数**: - -```json -{ - "id": 1, - "classTeacherId": 103, - "classTeacherName": "张老师" -} -``` - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功" -} -``` - -#### 2.4 设置辅导员 - -- **接口URL**: `/classMsg/classMsg` -- **请求方式**: PUT -- **权限标识**: `classMsg:classMsg:edit` -- **请求参数**: - -```json -{ - "id": 1, - "counselorId": 102, - "counselorName": "李老师" -} -``` - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功" -} -``` - -#### 2.5 删除班级信息 - -- **接口URL**: `/classMsg/classMsg/{ids}` -- **请求方式**: DELETE -- **权限标识**: `classMsg:classMsg:remove` -- **请求参数**: - -| 参数名 | 类型 | 是否必须 | 描述 | -| ------ | ------ | -------- | ------------------ | -| ids | string | 是 | 班级ID,多个用逗号分隔 | - -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功" -} -``` - -#### 2.6 导出班级信息 - -- **接口URL**: `/classMsg/classMsg/export` -- **请求方式**: GET -- **权限标识**: `classMsg:classMsg:export` -- **请求参数**: 与查询参数相同 -- **返回参数**: 文件流 - -### 3. 班级教师关联接口 - -#### 3.1 查询有效用户列表(用于班主任/辅导员选择) - -- **接口URL**: `/xg/sysUser/activeList` -- **请求方式**: GET -- **权限标识**: `xg:sysUser:list` -- **请求参数**: 无 -- **返回参数**: - -```json -{ - "code": 200, - "msg": "操作成功", - "data": [ - { - "userId": 100, - "userName": "teacher1", - "nickName": "张老师", - "email": "zhang@example.com", - "phonenumber": "13800138000", - "sex": "0", - "status": "0", - "deptId": 101, - "deptName": "计算机系" - }, - { - "userId": 101, - "userName": "teacher2", - "nickName": "李老师", - "email": "li@example.com", - "phonenumber": "13800138001", - "sex": "1", - "status": "0", - "deptId": 102, - "deptName": "机械系" - } - ] -} -``` - -## 数据字典 - -### 班级类型(classType) - -| 编码 | 名称 | -| ---- | ---------- | -| 1 | 中级工 | -| 2 | 高级工 | -| 3 | 预备技师 | -| 4 | 技师 | -| 5 | 中高职一体 | - -### 班级状态(classStatus) - -| 编码 | 名称 | -| ---- | ---- | -| 1 | 正常 | -| 2 | 暂停 | -| 3 | 毕业 | -| 4 | 解散 | - -### 是否已毕业(isGraduation) - -| 编码 | 名称 | -| ---- | ------ | -| 0 | 未毕业 | -| 1 | 已毕业 | \ No newline at end of file diff --git a/docs/开发文档/API/班级干部信息管理API.md b/docs/开发文档/API/班级干部信息管理API.md deleted file mode 100644 index 62bb016a..00000000 --- a/docs/开发文档/API/班级干部信息管理API.md +++ /dev/null @@ -1,415 +0,0 @@ -# 班级干部信息管理API文档 - -## 📋 文档信息 - -| 项目 | 信息 | -|------|------| -| **模块名称** | 班级干部信息管理 | -| **模块代码** | ruoyi-xg | -| **基础路径** | `/xgClassPosMsg/xgClassPosMsg` | -| **权限前缀** | `xgClassPosMsg:xgClassPosMsg` | -| **版本** | V1.0.0 | -| **创建时间** | 2025-01-27 | -| **更新时间** | 2025-01-27 | - -## 🎯 API概述 - -### 模块说明 -班级干部信息管理模块提供完整的班级干部信息管理功能,包括班级干部的任命、任期管理、状态跟踪等全生命周期管理,以及用户角色关联功能。 - -### 技术规范 -- **认证方式**: Bearer Token (Sa-Token) -- **数据格式**: JSON -- **响应格式**: 统一使用R类型或TableDataInfo分页格式 -- **权限控制**: 基于角色的权限管理(RBAC) - -### 核心特性 -- **基础管理**: 班级干部信息的增删改查、导入导出 -- **任期管理**: 任职日期、任期时长、到期提醒 -- **状态跟踪**: 在任、到期、辞职、免职、调任、休学等状态 -- **用户角色关联**: 自动建立学生与角色的关联关系 -- **数据权限**: 支持按部门查询班级干部信息 - -## 📊 数据模型 - -### 班级干部信息VO (XgClassPosMsgVo) - -```java -{ - "id": 1, // 主键ID(雪花ID) - "deleted": 0, // 删除标志(0代表存在 1代表删除) - "classId": 101, // 班级ID,关联xg_class_msg.id - "className": "计算机2023-1班", // 班级名称(联表查询获取) - "studentId": 1001, // 学生ID,关联xg_student_msg.id - "studentNo": "2023001", // 学号 - "studentName": "张三", // 学生姓名(冗余字段) - "positionCode": "monitor", // 职务编码,关联xg_class_pos_dict.position_code - "positionName": "班长", // 职务名称(联表查询获取) - "appointmentDate": "2024-09-01", // 任职日期 - "termStartDate": "2024-09-01", // 任期开始日期 - "termEndDate": "2025-07-01", // 任期结束日期 - "termDuration": 10, // 任期时长(月) - "isCurrent": 1, // 是否现任:0-否,1-是 - "cadreStatus": "1", // 学生职务任职状态:1-在任,2-到期,3-辞职,4-免职,5-调任,6-休学 - "appointmentReason": "综合表现优秀", // 任命原因 - "appointmentMethod": "appointment", // 学生职务任命方式:appointment-任命,election-选举,recommendation-推荐 - "dismissalDate": null, // 离任日期 - "dismissalReason": null, // 离任原因 - "remark": "备注信息", // 备注信息 - "createBy": 1, // 创建者 - "createTime": "2024-09-01 10:00:00", // 创建时间 - "updateBy": 1, // 更新者 - "updateTime": "2024-09-01 10:00:00" // 更新时间 -} -``` - -## 🔧 API接口列表 - -### 1. 基础管理接口 - -#### 1.1 查询班级干部信息列表 -- **接口路径**: GET `/xgClassPosMsg/xgClassPosMsg/list` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:list` -- **功能描述**: 分页查询班级干部信息列表,支持按部门查询 - -**请求参数**: -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| pageNum | integer | 否 | 页码,默认1 | -| pageSize | integer | 否 | 每页大小,默认10 | -| deptId | long | 否 | 部门ID,用于按部门查询班级干部信息 | -| classId | long | 否 | 班级ID | -| studentId | long | 否 | 学生ID | -| studentNo | string | 否 | 学号(模糊查询) | -| studentName | string | 否 | 学生姓名(模糊查询) | -| positionCode | string | 否 | 职务编码 | -| appointmentDate | string | 否 | 任职日期 | -| termStartDate | string | 否 | 任期开始日期 | -| termEndDate | string | 否 | 任期结束日期 | -| termDuration | integer | 否 | 任期时长(月) | -| isCurrent | integer | 否 | 是否现任:0-否,1-是 | -| cadreStatus | string | 否 | 学生职务任职状态 | -| appointmentReason | string | 否 | 任命原因 | -| appointmentMethod | string | 否 | 学生职务任命方式 | -| dismissalDate | string | 否 | 离任日期 | -| dismissalReason | string | 否 | 离任原因 | - -**响应数据**: -```json -{ - "code": 200, - "msg": "查询成功", - "rows": [ - { - "id": 1, - "classId": 101, - "className": "计算机2023-1班", - "studentId": 1001, - "studentNo": "2023001", - "studentName": "张三", - "positionCode": "monitor", - "positionName": "班长", - "appointmentDate": "2024-09-01", - "termStartDate": "2024-09-01", - "termEndDate": "2025-07-01", - "termDuration": 10, - "isCurrent": 1, - "cadreStatus": "1", - "appointmentReason": "综合表现优秀", - "appointmentMethod": "appointment", - "createTime": "2024-09-01 10:00:00" - } - ], - "total": 1 -} -``` - -#### 1.2 获取班级干部信息详情 -- **接口路径**: GET `/xgClassPosMsg/xgClassPosMsg/{id}` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:query` -- **功能描述**: 根据主键获取班级干部信息详细信息 - -**请求参数**: -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| id | long | 是 | 主键ID | - -**响应数据**: -```json -{ - "code": 200, - "msg": "查询成功", - "data": { - "id": 1, - "classId": 101, - "className": "计算机2023-1班", - "studentId": 1001, - "studentNo": "2023001", - "studentName": "张三", - "positionCode": "monitor", - "positionName": "班长", - "appointmentDate": "2024-09-01", - "termStartDate": "2024-09-01", - "termEndDate": "2025-07-01", - "termDuration": 10, - "isCurrent": 1, - "cadreStatus": "1", - "appointmentReason": "综合表现优秀", - "appointmentMethod": "appointment", - "dismissalDate": null, - "dismissalReason": null, - "remark": "备注信息", - "createTime": "2024-09-01 10:00:00", - "updateTime": "2024-09-01 10:00:00" - } -} -``` - -#### 1.3 新增班级干部信息 -- **接口路径**: POST `/xgClassPosMsg/xgClassPosMsg` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:add` -- **功能描述**: 新增班级干部信息,同时自动建立用户角色关联 - -**请求参数**: -```json -{ - "classId": 101, - "studentId": 1001, - "studentNo": "2023001", - "studentName": "张三", - "positionCode": "monitor", - "appointmentDate": "2024-09-01", - "termStartDate": "2024-09-01", - "termEndDate": "2025-07-01", - "termDuration": 10, - "isCurrent": 1, - "cadreStatus": "1", - "appointmentReason": "综合表现优秀", - "appointmentMethod": "appointment", - "remark": "备注信息" -} -``` - -**响应数据**: -```json -{ - "code": 200, - "msg": "新增成功" -} -``` - -**重要说明**: -- 新增班级干部信息时,系统会自动向 `sys_user_role` 表插入一条数据 -- `user_id` = 当前记录的 `studentId` -- `role_id` = 当前记录的 `id`(班级干部信息ID) -- 此操作在事务中进行,确保数据一致性 - -#### 1.4 修改班级干部信息 -- **接口路径**: PUT `/xgClassPosMsg/xgClassPosMsg` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:edit` -- **功能描述**: 修改班级干部信息 - -**请求参数**: -```json -{ - "id": 1, - "classId": 101, - "studentId": 1001, - "studentNo": "2023001", - "studentName": "张三", - "positionCode": "monitor", - "appointmentDate": "2024-09-01", - "termStartDate": "2024-09-01", - "termEndDate": "2025-07-01", - "termDuration": 10, - "isCurrent": 1, - "cadreStatus": "1", - "appointmentReason": "综合表现优秀", - "appointmentMethod": "appointment", - "remark": "修改后的备注信息" -} -``` - -**响应数据**: -```json -{ - "code": 200, - "msg": "修改成功" -} -``` - -#### 1.5 删除班级干部信息 -- **接口路径**: DELETE `/xgClassPosMsg/xgClassPosMsg/{ids}` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:remove` -- **功能描述**: 删除班级干部信息(支持批量删除),同时删除对应的用户角色关联 - -**请求参数**: -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| ids | string | 是 | 主键ID,多个用逗号分隔 | - -**响应数据**: -```json -{ - "code": 200, - "msg": "删除成功" -} -``` - -**重要说明**: -- 删除班级干部信息时,系统会自动删除 `sys_user_role` 表中对应的角色关联 -- 删除操作会先清理关联数据,再删除主数据 -- 此操作在事务中进行,确保数据一致性 - -#### 1.6 导出班级干部信息 -- **接口路径**: POST `/xgClassPosMsg/xgClassPosMsg/export` -- **权限标识**: `xgClassPosMsg:xgClassPosMsg:export` -- **功能描述**: 导出班级干部信息列表 - -**请求参数**: 同列表查询参数 - -**响应数据**: Excel文件下载 - -## 🔐 用户角色关联功能 - -### 功能说明 -班级干部信息管理模块集成了用户角色关联功能,确保班级干部信息与系统权限管理的一致性。 - -### 关联规则 -1. **新增关联**: 新增班级干部信息时,自动建立用户角色关联 - - `sys_user_role.user_id` = `xg_class_pos_msg.student_id` - - `sys_user_role.role_id` = `xg_class_pos_msg.id` - -2. **删除关联**: 删除班级干部信息时,自动删除对应的用户角色关联 - - 根据 `role_id` 删除 `sys_user_role` 表中的记录 - -### 事务处理 -- 所有用户角色关联操作都在事务中进行 -- 关联操作失败不会影响主业务流程,但会记录警告日志 -- 使用 `@Transactional(rollbackFor = Exception.class)` 确保数据一致性 - -### 异常处理 -- 用户角色关联操作异常会被捕获并记录日志 -- 不会抛出异常影响主业务流程 -- 通过日志记录操作结果和异常信息 - -## 📝 数据字典 - -### 学生职务任职状态 (cadre_status) -| 编码 | 名称 | 说明 | -|------|------|------| -| 1 | 在任 | 正常任职状态 | -| 2 | 到期 | 任期已结束 | -| 3 | 辞职 | 主动辞职 | -| 4 | 免职 | 被免职 | -| 5 | 调任 | 调任其他职务 | -| 6 | 休学 | 因休学暂停职务 | - -### 学生职务任命方式 (appointment_method) -| 编码 | 名称 | 说明 | -|------|------|------| -| appointment | 任命 | 直接任命 | -| election | 选举 | 通过选举产生 | -| recommendation | 推荐 | 通过推荐产生 | - -### 是否现任 (is_current) -| 编码 | 名称 | 说明 | -|------|------|------| -| 0 | 否 | 非现任职务 | -| 1 | 是 | 现任职务 | - -## 🔒 权限说明 - -### 权限标识列表 - -| 权限标识 | 权限名称 | 说明 | -|----------|----------|------| -| xgClassPosMsg:xgClassPosMsg:list | 班级干部信息列表 | 查看班级干部信息列表 | -| xgClassPosMsg:xgClassPosMsg:query | 班级干部信息查询 | 查询班级干部信息详情 | -| xgClassPosMsg:xgClassPosMsg:add | 班级干部信息新增 | 新增班级干部信息 | -| xgClassPosMsg:xgClassPosMsg:edit | 班级干部信息修改 | 修改班级干部信息 | -| xgClassPosMsg:xgClassPosMsg:remove | 班级干部信息删除 | 删除班级干部信息 | -| xgClassPosMsg:xgClassPosMsg:export | 班级干部信息导出 | 导出班级干部信息 | - -### 数据权限 -- 支持按部门查询班级干部信息 -- 通过 `deptId` 参数控制数据权限范围 -- 自动包含子部门的数据 - -## 🚨 错误码说明 - -| 错误码 | 错误信息 | 说明 | -|--------|----------|------| -| 40001 | 学生不存在 | 指定的学生ID不存在 | -| 40002 | 班级不存在 | 指定的班级ID不存在 | -| 40003 | 职务不存在 | 指定的职务编码不存在 | -| 40004 | 学生已担任该职务 | 学生已经担任该职务 | -| 40005 | 班级干部记录不存在 | 指定的班级干部记录不存在 | -| 40006 | 操作权限不足 | 当前用户没有操作权限 | -| 40007 | 数据校验失败 | 提交的数据不符合规则 | -| 40008 | 用户角色关联失败 | 用户角色关联操作失败 | - -## 📋 使用示例 - -### 1. 查询班级干部信息 -```javascript -// 查询指定部门的班级干部信息 -const response = await api.get('/xgClassPosMsg/xgClassPosMsg/list', { - params: { - deptId: 101, - pageNum: 1, - pageSize: 10 - } -}); -console.log(response.data); -``` - -### 2. 新增班级干部 -```javascript -// 新增班级干部信息 -const addData = { - classId: 101, - studentId: 1001, - studentNo: "2023001", - studentName: "张三", - positionCode: "monitor", - appointmentDate: "2024-09-01", - termStartDate: "2024-09-01", - termEndDate: "2025-07-01", - termDuration: 10, - isCurrent: 1, - cadreStatus: "1", - appointmentReason: "综合表现优秀", - appointmentMethod: "appointment" -}; -const response = await api.post('/xgClassPosMsg/xgClassPosMsg', addData); -``` - -### 3. 删除班级干部 -```javascript -// 删除班级干部信息 -const response = await api.delete('/xgClassPosMsg/xgClassPosMsg/1,2,3'); -``` - -## ⚠️ 注意事项 - -1. **数据一致性**: 班级干部信息与用户角色关联保持一致 -2. **权限控制**: 严格按照角色权限进行操作控制 -3. **事务管理**: 所有关联操作都在事务中进行 -4. **任期管理**: 注意任期时间的合理性和连续性 -5. **状态管理**: 正确维护班级干部的任职状态 -6. **并发控制**: 防止同时操作导致的数据冲突 -7. **日志记录**: 重要操作都会记录详细日志 - -## 📈 更新日志 - -| 版本 | 日期 | 更新内容 | -|------|------|----------| -| V1.0.0 | 2025-01-27 | 初始版本,完整的班级干部信息管理API | -| V1.0.1 | 2025-01-27 | 新增用户角色关联功能 | - ---- - -**文档维护**: Cursor AI 辅助 -**最后更新**: 2025-01-27 -**文档状态**: 已完成 \ No newline at end of file diff --git a/docs/开发文档/API/用户认证登录API.md b/docs/开发文档/API/用户认证登录API.md index eb32686b..9147c439 100644 --- a/docs/开发文档/API/用户认证登录API.md +++ b/docs/开发文档/API/用户认证登录API.md @@ -731,7 +731,7 @@ methods: { 1. 该接口需要用户登录后才能访问 2. 返回的用户信息已过滤敏感字段 3. 建议前端对结果进行缓存,避免频繁请求 -4. 返回结果按照用户ID升序排序 +4. 返回结果按照用户名(`user_name`)升序排序 ### 使用示例 ```typescript @@ -750,3 +750,66 @@ async function getAuditors() { } } ``` + +## 根据部门获取用户列表 + +### 基本信息 +- **接口URL**: `/system/user/list/dept/{deptId}` +- **请求方式**: GET +- **接口描述**: 根据部门ID获取该部门下所有启用状态的用户,用于部门人员选择器 +- **权限标识**: `system:user:list` + +### 路径参数 +| 参数名 | 类型 | 是否必须 | 说明 | 示例值 | +|--------|------|----------|------|--------| +| deptId | number | 是 | 部门ID | 1001 | + +### 响应参数 +```typescript +{ + code: number; + msg: string; + data: { + userId: number; + userName: string; + nickName: string; + phonenumber: string; + }[]; +} +``` + +### 注意事项 +1. 只返回未删除且状态为正常的用户 +2. 返回结果按照用户名(`user_name`)升序排序,便于快速定位 + +## 批量获取用户基础信息 + +### 基本信息 +- **接口URL**: `/system/user/optionselect` +- **请求方式**: GET +- **接口描述**: 根据用户ID集合(可选)和部门ID(可选)批量获取基础用户信息 +- **权限标识**: `system:user:query` + +### Query参数 +| 参数名 | 类型 | 是否必须 | 说明 | 示例值 | +|--------|------|----------|------|--------| +| userIds | string | 否 | 逗号分隔的用户ID集合 | 1,2,3 | +| deptId | number | 否 | 指定部门ID时仅返回该部门用户 | 1001 | + +### 响应参数 +```typescript +{ + code: number; + msg: string; + data: { + userId: number; + userName: string; + nickName: string; + phonenumber: string; + }[]; +} +``` + +### 注意事项 +1. 用户ID集合为空时返回有权限范围内的全部用户 +2. 返回结果默认按照用户名(`user_name`)升序排序,保持前端下拉展示一致性 diff --git a/docs/开发文档/API/预约限制功能API.md b/docs/开发文档/API/预约限制功能API.md index 56b575aa..f55c2bbd 100644 --- a/docs/开发文档/API/预约限制功能API.md +++ b/docs/开发文档/API/预约限制功能API.md @@ -57,6 +57,8 @@ } ``` +> 📌 **排序说明**:列表结果默认按照学号 `studentNo` 降序排列;当学号相同或为空时,继续按照未到访日期、创建时间倒序展示。 + ### 3.2 获取未到访记录详情 #### 接口信息 diff --git a/docs/开发文档/出国申请用户操作指引.md b/docs/开发文档/出国申请用户操作指引.md deleted file mode 100644 index eacec3bb..00000000 --- a/docs/开发文档/出国申请用户操作指引.md +++ /dev/null @@ -1,87 +0,0 @@ -# 出国申请用户操作指引 - -## 📋 **概述** -本指引将帮助您完成出国申请的完整流程,从新增申请到审批完成。 - ---- - -## 🔄 **完整业务流程** - -### **第一阶段:提交申请** -1. 访问:`http://localhost/rs/overseas` -2. 点击:**"新增申请"** 按钮 -3. 填写:申请表单信息 -4. 点击:**"确定"** 按钮 -5. 系统提示:**"申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。"** - -**📌 重要说明**:申请立即进入 **待审核状态**,无需额外操作! - -### **第二阶段:审批处理** -1. 访问审批页面:`http://localhost/rs/overseasApproval` -2. 有审批权限的用户可以在 **"待审批申请"** 选项卡中看到您的申请 -3. 申请状态显示:**"待审核"**(橙色标签) -4. 审批人进行审批操作 - -### **第三阶段:审批结果** -1. 审批完成后,申请状态会更新为:**"已通过"** 或 **"已拒绝"** -2. 您可以在申请列表中查看最新状态 -3. 如果通过,系统会自动创建出国记录 - ---- - -## 🎯 **状态说明** - -| 状态 | 显示文本 | 标签颜色 | 说明 | 可用操作 | -|------|----------|----------|------|----------| -| `0` | 待审核 | 橙色 | 申请已提交,等待审批 | 查看、取消、删除 | -| `1` | 已通过 | 绿色 | 申请审批通过 | 查看 | -| `2` | 已拒绝 | 红色 | 申请被拒绝 | 查看 | -| `3` | 已取消 | 灰色 | 申请被取消 | 查看 | -| `4` | 已完成 | 绿色 | 出国活动已完成 | 查看 | - ---- - -## ⚠️ **常见问题** - -### **Q1: 新增申请后多久能在审批页面看到?** -**A**: 新增申请后立即进入审批流程,有审批权限的用户可以立即在审批页面看到您的申请。 - -### **Q2: 如何知道申请是否提交成功?** -**A**: -- 系统提示:"申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。" -- 申请列表中显示"待审核"标签(橙色) - -### **Q3: 提交后可以修改申请吗?** -**A**: 申请提交后不能修改,只能取消申请。如需修改,请先取消当前申请,然后重新提交。 - -### **Q4: 谁可以审批我的申请?** -**A**: 拥有出国申请审批权限(`rs:overseas:application:approve`)的用户可以在审批页面看到并处理您的申请。 - ---- - -## 🛠️ **操作按钮说明** - -### **申请列表页面按钮** -- **👁️ 查看**:查看申请详情(所有状态都可用) -- **❌ 取消**:取消申请(仅待审核状态可用) -- **🗑️ 删除**:删除申请(待审核状态可用) - -### **审批页面按钮** -- **👁️ 查看**:查看申请详情 -- **✅ 通过**:审批通过 -- **❌ 拒绝**:审批拒绝 - ---- - -## 📞 **技术支持** - -如果您在使用过程中遇到问题,请联系系统管理员或查看相关技术文档: -- **API文档**: `docs/开发文档/API/出国申请管理API.md` -- **技术规范**: `.cursor/cursorrules-frontend.md` -- **变更记录**: `docs/变更管理/code-adjust-record-2025-08-03-weizzzz.md` - ---- - -**✅ 完成时间**: 2025-08-03 -**📝 版本**: v2.0 -**🔄 最后更新**: 简化业务流程,新增申请直接进入审批状态 \ No newline at end of file diff --git a/docs/开发文档/招聘门户系统最终完成报告.md b/docs/开发文档/招聘门户系统最终完成报告.md index 3a5dab9d..ba7a7316 100644 --- a/docs/开发文档/招聘门户系统最终完成报告.md +++ b/docs/开发文档/招聘门户系统最终完成报告.md @@ -97,7 +97,7 @@ rs_job # 招聘岗位表 ├── job_id # 岗位ID ├── title # 岗位名称 ├── department # 部门 -├── salary_range # 薪资范围 + ├── location # 工作地点 ├── description # 岗位描述 ├── requirements # 任职要求 diff --git a/docs/技术问题解决记录/撤销申请页面图片加载问题修复.md b/docs/技术问题解决记录/撤销申请页面图片加载问题修复.md deleted file mode 100644 index 34ff05db..00000000 --- a/docs/技术问题解决记录/撤销申请页面图片加载问题修复.md +++ /dev/null @@ -1,250 +0,0 @@ -# 撤销申请页面图片加载问题修复记录 - -## 问题描述 - -**现象**: 在已有撤销申请时,再次打开撤销申请界面(`revokeApplication.vue`),界面中的图片没有做加载显示,看控制台没有调用接口。 - -**影响范围**: 移动端撤销申请页面的图片附件显示功能 - -## 问题分析 - -### 原因分析 - -1. **缺少图片blob处理**: `revokeApplication.vue` 在 `handleExistingEvidenceData` 方法中只是简单地将 OSS 数据转换为附件列表,但没有像 `RevokeApplicationView.vue` 查看页面那样处理图片的 blob 数据获取。 - -2. **图片URL处理不完整**: 对于图片文件,需要特殊处理URL前缀并获取blob数据才能正确显示。 - -3. **缺少图片预加载机制**: 没有预加载机制确保图片能够正确显示。 - -### 参考实现 - -参考了 `RevokeApplicationView.vue` 中的图片处理机制: -- `processAllImageFiles()` 方法处理图片blob数据 -- `getImageBlobUrl()` 方法获取图片blob URL -- `preloadAllImages()` 方法预加载图片 - -## 解决方案 - -### 1. 增强附件数据处理 (`handleExistingEvidenceData`) - -```javascript -// 处理现有证明材料附件数据时 -const attachmentFiles = response.data.map((item, index) => { - // 使用fileSuffix字段判断文件类型 - const fileType = this.getFileTypeBySuffix(item.fileSuffix); - const isImage = this.isImageFile({ - name: item.originalName, - type: fileType, - }); - - // 处理图片URL - 确保有正确的API前缀 - let processedUrl = item.url; - if (isImage && item.url && item.url.startsWith('/resource/')) { - // 图片文件需要添加/api前缀 - processedUrl = `/api${item.url}`; - } - - // 构造完整的文件名(包含扩展名) - let fullFileName = item.originalName || `证明材料${index + 1}`; - if ( - item.fileSuffix && - !fullFileName - .toLowerCase() - .endsWith(`.${item.fileSuffix.toLowerCase()}`) - ) { - fullFileName = `${fullFileName}.${item.fileSuffix}`; - } - - return { - uid: `existing_${index}_${Date.now()}`, - name: fullFileName, - url: processedUrl, - ossId: item.ossId, - status: 'success', - type: fileType, - size: item.fileSize || 0, - }; -}); - -this.attachmentList = attachmentFiles; - -// 🚀 关键步骤:处理图片文件以确保正确显示 -await this.processAllImageFiles(); - -// 强制刷新组件显示 -this.$nextTick(() => { - // 预加载图片以确保能正确显示 - this.preloadAllImages(); -}); -``` - -### 2. 添加图片处理核心方法 - -#### `processAllImageFiles()` - 处理所有图片文件 - -```javascript -async processAllImageFiles() { - console.log('=== 处理所有图片文件 ==='); - - // 处理附件列表中的图片 - const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); - for (const file of imageFiles) { - if (file.url && !file.blobUrl) { - try { - console.log(`处理图片文件: ${file.name}`); - const blobUrl = await this.getImageBlobUrl(file.url); - if (blobUrl) { - file.blobUrl = blobUrl; - file.fileType = 'image'; - file.type = file.type && file.type.startsWith('image') ? file.type : 'image'; - file.isImage = true; - console.log(`图片文件处理成功: ${file.name} - ${blobUrl}`); - } - } catch (error) { - console.error(`处理图片文件失败: ${file.name}`, error); - } - } - } -} -``` - -#### `getImageBlobUrl()` - 获取图片blob数据 - -```javascript -async getImageBlobUrl(imageUrl) { - try { - console.log(`获取图片blob数据: ${imageUrl}`); - - // 处理图片URL路径 - let apiUrl = imageUrl; - if (imageUrl.startsWith('/api/')) { - // 移除/api前缀,因为request会自动添加baseURL - apiUrl = imageUrl.replace('/api', ''); - } else if (imageUrl.startsWith('/resource/')) { - // 直接使用相对路径 - apiUrl = imageUrl; - } else if (imageUrl.startsWith('http')) { - // 完整URL,需要特殊处理 - console.log('检测到完整URL,直接返回原URL'); - return imageUrl; - } - - console.log(`API请求URL: ${apiUrl}`); - - // 使用request方法获取图片数据 - const response = await request({ - url: apiUrl, - method: 'GET', - responseType: 'arraybuffer', // 设置响应类型为arraybuffer - header: { - isToken: true, // 图片请求可能需要token验证 - }, - }); - - // 将arraybuffer转换为blob - const blob = new Blob([response], { type: 'image/*' }); - const blobUrl = URL.createObjectURL(blob); - - console.log(`成功创建blob URL: ${blobUrl}`); - return blobUrl; - } catch (error) { - console.error(`获取图片blob数据失败: ${imageUrl}`, error); - // 如果请求失败,返回原始URL作为备用方案 - return imageUrl; - } -} -``` - -#### `preloadAllImages()` - 预加载图片 - -```javascript -async preloadAllImages() { - console.log('=== 预加载所有图片 ==='); - - // 预加载附件列表中的图片 - const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); - for (const file of imageFiles) { - if (file.url) { - try { - console.log(`预加载图片: ${file.name} - ${file.url}`); - const img = new Image(); - img.onload = () => { - console.log(`图片预加载成功: ${file.name}`); - }; - img.onerror = () => { - console.error(`图片预加载失败: ${file.name} - ${file.url}`); - }; - img.src = file.blobUrl || file.url; - } catch (error) { - console.error(`预加载图片出错: ${file.name}`, error); - } - } - } -} -``` - -### 3. 增强模板错误处理 - -在模板中为image组件添加错误处理: - -```vue - -``` - -```javascript -// 图片加载错误处理 -handleImageError(event) { - console.warn('图片加载失败:', event); -} -``` - -## 修复效果 - -### 修复前 -- 已有撤销申请记录时,图片附件无法显示 -- 控制台没有任何API调用记录 -- 图片区域显示空白或加载失败 - -### 修复后 -- 已有撤销申请记录时,图片附件正常显示 -- 控制台显示完整的图片处理流程日志 -- 图片正确加载并支持预览功能 - -## 技术要点 - -1. **blob数据处理**: 通过获取图片的arraybuffer数据并转换为blob URL,确保图片能正确显示 -2. **URL路径处理**: 正确处理不同格式的图片URL路径 -3. **异步处理**: 使用async/await确保图片处理的时序正确 -4. **错误处理**: 添加完善的错误处理和降级方案 -5. **内存管理**: 在页面卸载时清理blob URL,防止内存泄漏 - -## 测试建议 - -1. **基础功能测试**: - - 创建新的撤销申请(上传图片附件) - - 保存后重新进入页面,验证图片是否正常显示 - -2. **边界条件测试**: - - 测试不同格式的图片文件(JPG, PNG, GIF等) - - 测试混合附件(图片+文档) - - 测试网络异常情况下的降级处理 - -3. **性能测试**: - - 测试多张图片的加载性能 - - 检查内存泄漏情况 - -## 相关文件 - -- **主要修改文件**: `mobile/src/pages/xskh/wjjl/revokeApplication.vue` -- **参考实现文件**: `mobile/src/pages/xskh/wjjl/RevokeApplicationView.vue` -- **修复日期**: 2025-01-03 -- **修复人员**: AI Assistant - -## 总结 - -通过参考查看页面的成功实现,为撤销申请页面添加了完整的图片blob数据处理机制,确保了已有撤销申请记录的图片附件能够正确显示。修复方案具有良好的错误处理和性能优化特性。 \ No newline at end of file diff --git a/docs/技术问题解决记录/班主任考核申请计算公式修正记录.md b/docs/技术问题解决记录/班主任考核申请计算公式修正记录.md new file mode 100644 index 00000000..be79cdaa --- /dev/null +++ b/docs/技术问题解决记录/班主任考核申请计算公式修正记录.md @@ -0,0 +1,154 @@ +# 班主任考核申请计算公式修正记录 + +## 📋 问题描述 + +**问题标题**: 班主任考核申请(自评模式)月考核得分计算公式需要修正 +**发现时间**: 2025-01-27 +**问题类型**: 业务逻辑优化 +**影响范围**: 班主任考核申请模块 + +## 🔍 问题分析 + +### 原始公式 +``` +月考核得分 = 总系数(K) × (班主任常规工作自评分之和 - 按班级扣分50%计的分值 + 特色工作总分值) +``` + +### 问题原因 +原始公式在自评模式下,班级扣分会直接影响基础分值,可能导致计算结果不够合理。 + +### 修正需求 +需要在自评模式下,确保班级扣分项的满分值(50分)被加入到基础分值中,以平衡扣分的影响。 + +## ✅ 解决方案 + +### 修正后公式 +``` +月考核得分 = 总系数(K) × ((班主任常规工作自评分之和 + 班级扣分项满分值) - 按班级扣分50%计的分值 + 特色工作总分值) +``` + +### 关键变化 +1. **新增**: 班级扣分项满分值(固定50分) +2. **确保**: 基础分值不受扣分过度影响 +3. **提高**: 计算结果的合理性和公平性 + +## 🔧 技术实现 + +### 前端修改 + +#### 1. submitDialog.vue(班主任考核申请页面) +```javascript +/** 计算月考核得分 */ +const calculateMonthlyScore = () => { + // 总系数(K) + const totalCoefficient = assessmentDetail.value?.coefficient?.totalCoefficient || 0; + + // 班主任常规工作自评分之和 + const routineWorkTotal = routineWorkScores.value.reduce((sum, item) => sum + (item.selfScore || 0), 0); + + // 班级扣分项满分值(固定50分) + const classDeductionMaxScore = 50; + + // 按班级扣分50%计的分值 + const deductionHalf = managementEffectivenessForm.classDeductionHalf || 0; + + // 特色工作总分值 + const specialWorkTotal = specialWorkForm.totalScore || 0; + + // 修正后的月考核得分公式 + return totalCoefficient * ((routineWorkTotal + classDeductionMaxScore) - deductionHalf + specialWorkTotal); +}; +``` + +#### 2. viewDialog.vue(班主任考核查看页面) +- 修正自评模式下的计算逻辑 +- 保持审核模式计算不变 + +### 后端修改 + +#### AssessmentApprovalServiceImpl.java +```java +// 判断是否为自评模式 +boolean isSelfEvaluationMode = scoreDetail.getClassDeductionScore() != null && + scoreDetail.getClassDeductionScore().getSuperiorScore() == null; + +BigDecimal monthlyScore; +if (isSelfEvaluationMode) { + // 自评模式:修正后公式 + double classDeductionMaxScore = 50.0; + monthlyScore = totalCoefficient.multiply( + BigDecimal.valueOf((routineWorkTotal + classDeductionMaxScore) - deductionHalf + specialWorkTotal)); +} else { + // 审核模式:保持原公式 + monthlyScore = totalCoefficient.multiply( + BigDecimal.valueOf(routineWorkTotal - deductionHalf + specialWorkTotal)); +} +``` + +## 📊 影响分析 + +### 正面影响 +1. **计算合理性**: 提高自评模式下的得分计算合理性 +2. **公平性**: 确保班级扣分不会过度影响基础分值 +3. **一致性**: 自评和审核模式各有合理的计算逻辑 + +### 兼容性 +1. **审核模式**: 完全保持不变 +2. **历史数据**: 不影响已有的审核记录 +3. **系统稳定性**: 仅优化计算逻辑,不影响其他功能 + +## 📝 测试验证 + +### 测试场景 +1. **自评申请**: 验证修正后公式的计算准确性 +2. **审核流程**: 确认审核模式公式保持不变 +3. **数据一致性**: 验证前后端计算结果一致 +4. **边界情况**: 测试各种极值情况下的计算结果 + +### 验证结果 +- ✅ 自评模式计算公式修正成功 +- ✅ 审核模式保持原有逻辑 +- ✅ 前后端计算结果一致 +- ✅ 系统功能正常运行 + +## 📚 相关文档更新 + +1. **开发说明**: `班主任考核提交功能开发说明.md` +2. **API文档**: 待更新相关API文档说明 +3. **用户手册**: 待更新用户操作手册 + +## 🎯 结论 + +本次修正成功优化了班主任考核申请(自评模式)的月考核得分计算公式,通过添加班级扣分项满分值(50分)到基础分值中,确保了计算结果的合理性和公平性,同时保持了审核模式的原有逻辑不变。 + +## 📋 界面显示问题修正(2025-01-27 补充) + +### 问题描述 +修正计算公式后,发现月考核得分框在没有任何评分数据时,默认显示50分而不是期望的0分。 + +### 问题原因 +由于在计算公式中直接添加了班级扣分项满分值(50分),导致即使没有任何实际评分数据,界面也显示50分。 + +### 解决方案 +添加评分数据检查逻辑: +1. **前端检查**: 检查是否有实际评分数据(routineWorkTotal > 0 || deductionHalf > 0 || specialWorkTotal > 0) +2. **后端检查**: 同样的检查逻辑确保前后端一致 +3. **默认显示**: 没有评分数据或总系数为0时,显示0分 +4. **计算时机**: 只有在有实际评分数据时,才在内部计算中加上班级扣分项满分值 + +### 修正代码 +```javascript +// 检查是否有实际评分数据(避免空白状态显示50分) +const hasAnyScore = routineWorkTotal > 0 || deductionHalf > 0 || specialWorkTotal > 0; + +if (!hasAnyScore || totalCoefficient === 0) { + return 0; // 没有评分数据时显示0 +} + +// 班级扣分项满分值(固定50分)- 仅在有评分数据时计算 +const classDeductionMaxScore = 50; +``` + +**修正状态**: ✅ 已完成 +**验证状态**: ✅ 已验证 +**文档状态**: ✅ 已更新 \ No newline at end of file diff --git a/docs/数据库脚本/人事模块的表模型.sql b/docs/数据库脚本/人事模块的表模型.sql new file mode 100644 index 00000000..3b75c981 --- /dev/null +++ b/docs/数据库脚本/人事模块的表模型.sql @@ -0,0 +1,746 @@ +/* + Navicat Premium Dump SQL + + Source Server : 47.99.164.66 hn02le.34lkdLKD + Source Server Type : MySQL + Source Server Version : 50736 (5.7.36) + Source Host : 47.99.164.66:3306 + Source Schema : hzdyjs_xg + + Target Server Type : MySQL + Target Server Version : 50736 (5.7.36) + File Encoding : 65001 + + Date: 05/11/2025 15:56:55 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for rs_applicant +-- ---------------------------- +DROP TABLE IF EXISTS `rs_applicant`; +CREATE TABLE `rs_applicant` ( + `applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID', + `user_id` bigint(20) DEFAULT NULL COMMENT '关联用户ID', + `name` varchar(50) NOT NULL COMMENT '姓名', + `phone` varchar(20) NOT NULL COMMENT '手机号码', + `email` varchar(100) NOT NULL COMMENT '电子邮箱', + `resume_file_path` varchar(255) DEFAULT NULL COMMENT '简历文件路径', + `self_intro` text COMMENT '自我介绍', + `current_status` varchar(20) DEFAULT 'active' COMMENT '当前状态(active-可申请 hired-已录用 rejected-已拒绝)', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`applicant_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_phone` (`phone`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应聘者信息表'; + +-- ---------------------------- +-- Table structure for rs_application +-- ---------------------------- +DROP TABLE IF EXISTS `rs_application`; +CREATE TABLE `rs_application` ( + `application_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID', + `job_id` bigint(20) NOT NULL COMMENT '职位ID', + `applicant_id` bigint(20) DEFAULT NULL COMMENT '应聘者ID(NULL表示匿名用户)', + `apply_time` datetime NOT NULL COMMENT '申请时间', + `status` varchar(20) DEFAULT 'pending' COMMENT '状态(pending-待处理 interviewed-已面试 offered-已录用 rejected-已拒绝)', + `resume_path` varchar(255) DEFAULT NULL COMMENT '简历路径', + `self_description` text COMMENT '自我介绍', + `attachment_path` varchar(255) DEFAULT NULL COMMENT '附件路径', + `attachment_name` varchar(255) DEFAULT NULL COMMENT '附件名称', + `job_title` varchar(100) DEFAULT NULL COMMENT '职位名称', + `applicant_name` varchar(50) DEFAULT NULL COMMENT '应聘者姓名', + `gender` varchar(10) DEFAULT NULL COMMENT '性别', + `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话', + `contact_email` varchar(100) DEFAULT NULL COMMENT '联系邮箱', + `age` int(11) DEFAULT NULL COMMENT '年龄', + `education` text COMMENT '教育背景', + `education_background` text COMMENT '教育背景详情', + `experience` text COMMENT '工作经验', + `hr_notes` text COMMENT 'HR备注', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`application_id`), + KEY `idx_job_id` (`job_id`), + KEY `idx_applicant_id` (`applicant_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8mb4 COMMENT='应聘申请记录表'; + +-- ---------------------------- +-- Table structure for rs_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `rs_attachment`; +CREATE TABLE `rs_attachment` ( + `attachment_id` bigint(20) NOT NULL COMMENT '附件ID', + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `file_name` varchar(100) NOT NULL COMMENT '文件名', + `file_path` varchar(255) NOT NULL COMMENT '文件路径', + `file_type` varchar(50) DEFAULT NULL COMMENT '文件类型', + `file_size` bigint(20) DEFAULT NULL COMMENT '文件大小(字节)', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`attachment_id`), + KEY `idx_application_id` (`application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='申请附件表'; + +-- ---------------------------- +-- Table structure for rs_education +-- ---------------------------- +DROP TABLE IF EXISTS `rs_education`; +CREATE TABLE `rs_education` ( + `edu_id` bigint(20) NOT NULL COMMENT '教育经历ID', + `applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID', + `school_name` varchar(100) NOT NULL COMMENT '学校名称', + `major` varchar(100) DEFAULT NULL COMMENT '专业', + `degree` varchar(50) DEFAULT NULL COMMENT '学位', + `start_date` date DEFAULT NULL COMMENT '开始日期', + `end_date` date DEFAULT NULL COMMENT '结束日期', + `gpa` varchar(20) DEFAULT NULL COMMENT '绩点', + `description` text COMMENT '描述', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`edu_id`), + KEY `idx_applicant_id` (`applicant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教育经历表'; + +-- ---------------------------- +-- Table structure for rs_job +-- ---------------------------- +DROP TABLE IF EXISTS `rs_job`; +CREATE TABLE `rs_job` ( + `job_id` bigint(20) NOT NULL COMMENT '职位ID', + `title` varchar(100) NOT NULL COMMENT '职位名称', + `department` varchar(50) NOT NULL COMMENT '部门', + `salary_range` varchar(50) DEFAULT NULL COMMENT '薪资范围', + `location` varchar(100) DEFAULT NULL COMMENT '工作地点', + `description` text COMMENT '职位描述', + `requirements` text COMMENT '任职要求', + `is_active` char(1) DEFAULT '0' COMMENT '是否在招(0是 1否)', + `publish_time` datetime DEFAULT NULL COMMENT '发布时间', + `view_count` int(11) DEFAULT '0' COMMENT '浏览次数', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`job_id`), + KEY `idx_department` (`department`), + KEY `idx_is_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘职位表'; + +-- ---------------------------- +-- Table structure for rs_overseas_application +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_application`; +CREATE TABLE `rs_overseas_application` ( + `application_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID', + `serial_number` varchar(50) DEFAULT NULL COMMENT '编号(自动生成的登记编号)', + `applicant_id` bigint(20) NOT NULL COMMENT '申请人ID', + `applicant_name` varchar(50) NOT NULL COMMENT '申请人姓名', + `gender` char(1) DEFAULT NULL COMMENT '性别:0-男,1-女', + `birth_date` date DEFAULT NULL COMMENT '出生日期', + `photo` varchar(500) DEFAULT NULL COMMENT '照片地址(2寸免冠白色或淡蓝色底彩照)', + `id_number` varchar(18) DEFAULT NULL COMMENT '公民身份号码(18位身份证号)', + `political_status` varchar(50) DEFAULT NULL COMMENT '政治面貌', + `registered_address` varchar(200) DEFAULT NULL COMMENT '户籍所在地(填写至县市区)', + `department` varchar(200) DEFAULT NULL COMMENT '所属部门(工作部门/教学系部)', + `position` varchar(100) DEFAULT NULL COMMENT '职位', + `professional_title` varchar(100) DEFAULT NULL COMMENT '职称', + `administrative_level` varchar(20) DEFAULT NULL COMMENT '行政职级', + `personnel_category` varchar(10) DEFAULT NULL COMMENT '人员类别代码(根据报备人员范围中的情形项填写)', + `registration_unit` varchar(200) DEFAULT NULL COMMENT '登记备案责任单位', + `unit_leader_signature` varchar(100) DEFAULT NULL COMMENT '登记备案责任单位负责人签名', + `office_phone` varchar(20) DEFAULT NULL COMMENT '办公电话', + `mobile_phone` varchar(11) DEFAULT NULL COMMENT '移动电话', + `document_name` varchar(100) DEFAULT NULL COMMENT '申领证件名称', + `document_number` varchar(50) DEFAULT NULL COMMENT '证件号码', + `destination_country` varchar(100) DEFAULT NULL COMMENT '前往国家', + `exit_reason` varchar(50) DEFAULT NULL COMMENT '出境事由(旅游/探亲等)', + `visit_relationship` varchar(200) DEFAULT NULL COMMENT '探望对象姓名及与申请者关系', + `remarks` text COMMENT '备注', + `emergency_contact` varchar(200) DEFAULT NULL COMMENT '应急联系人及电话', + `signature` longtext COMMENT '手写签字(base64格式)', + `stamped_registration_file_path` varchar(500) DEFAULT NULL COMMENT '登记备案表盖章文件路径', + `stamped_registration_file_name` varchar(200) DEFAULT NULL COMMENT '登记备案表盖章文件名', + `stamped_file_upload_time` datetime DEFAULT NULL COMMENT '盖章文件上传时间', + `stamped_file_upload_status` char(1) DEFAULT '0' COMMENT '盖章文件上传状态:0-未上传,1-已上传', + `stamped_file_oss_id` bigint(20) DEFAULT NULL COMMENT '盖章文件OSS ID', + `party_retention_file_path` varchar(500) DEFAULT NULL COMMENT '党籍保留附件文件路径', + `party_retention_file_name` varchar(200) DEFAULT NULL COMMENT '党籍保留附件文件名', + `party_retention_upload_time` datetime DEFAULT NULL COMMENT '党籍保留附件上传时间', + `party_retention_upload_status` varchar(1) DEFAULT '0' COMMENT '党籍保留附件上传状态:0-未上传,1-已上传', + `party_retention_file_oss_id` bigint(20) DEFAULT NULL COMMENT '党籍保留附件OSS ID', + `attachment_count` int(11) DEFAULT '0' COMMENT '附件数量', + `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话(保留原字段兼容性)', + `contact_email` varchar(100) DEFAULT NULL COMMENT '联系邮箱', + `purpose` varchar(50) NOT NULL COMMENT '出国目的', + `purpose_detail` text NOT NULL COMMENT '目的详细说明', + `destination` varchar(100) NOT NULL COMMENT '目的地国家', + `destination_city` varchar(100) DEFAULT NULL COMMENT '目的地城市', + `start_date` date NOT NULL COMMENT '开始日期', + `end_date` date NOT NULL COMMENT '结束日期', + `duration` int(11) NOT NULL COMMENT '出国天数', + `invitation` text COMMENT '邀请方信息', + `itinerary` text NOT NULL COMMENT '行程安排', + `accommodation_plan` text COMMENT '住宿安排', + `transportation_plan` text COMMENT '交通安排', + `status` char(1) NOT NULL DEFAULT '0' COMMENT '申请状态:0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成', + `approval_status` varchar(20) DEFAULT 'draft' COMMENT '审批状态(工作流状态):draft-草稿,waiting-待审批,finish-已完成,cancel-已取消,back-已退回', + `current_approver` varchar(100) DEFAULT NULL COMMENT '当前审批人', + `current_approver_id` bigint(20) DEFAULT NULL COMMENT '当前审批人ID', + `approval_level` varchar(10) DEFAULT NULL COMMENT '审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记', + `apply_time` datetime NOT NULL COMMENT '申请时间', + `approval_time` datetime DEFAULT NULL COMMENT '审批时间', + `admin_remark` text COMMENT '管理员备注', + `applicant_remark` text COMMENT '申请人备注', + `attachment_ids` text COMMENT '附件OSS ID列表(逗号分隔)', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志:0-存在,1-删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`application_id`), + KEY `idx_applicant_id` (`applicant_id`), + KEY `idx_department` (`department`), + KEY `idx_status` (`status`), + KEY `idx_apply_time` (`apply_time`), + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`), + KEY `idx_stamped_file_oss_id` (`stamped_file_oss_id`), + KEY `idx_party_retention_file_oss_id` (`party_retention_file_oss_id`), + KEY `idx_approval_status` (`approval_status`), + KEY `idx_current_approver_id` (`current_approver_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COMMENT='出国申请表(含附件支持)'; + +-- ---------------------------- +-- Table structure for rs_overseas_application_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_application_attachment`; +CREATE TABLE `rs_overseas_application_attachment` ( + `attachment_id` bigint(20) NOT NULL COMMENT '附件ID(主键)', + `application_id` bigint(20) NOT NULL COMMENT '出国申请ID(关联rs_overseas_application)', + `attachment_title` varchar(200) NOT NULL COMMENT '附件标题', + `attachment_content` text COMMENT '附件内容说明', + `file_name` varchar(200) NOT NULL COMMENT '文件名称', + `file_original_name` varchar(200) NOT NULL COMMENT '文件原始名称', + `file_path` varchar(500) NOT NULL COMMENT '文件存储路径/OSS URL', + `file_size` bigint(20) DEFAULT NULL COMMENT '文件大小(字节)', + `file_type` varchar(50) DEFAULT NULL COMMENT '文件类型(如pdf、jpg、png、doc等)', + `mime_type` varchar(100) DEFAULT NULL COMMENT 'MIME类型', + `upload_time` datetime DEFAULT NULL COMMENT '上传时间', + `upload_user_id` bigint(20) DEFAULT NULL COMMENT '上传人ID', + `upload_user_name` varchar(50) DEFAULT NULL COMMENT '上传人姓名', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0-存在,1-删除)', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT 'æ›´æ–°æ—¶é—´', + PRIMARY KEY (`attachment_id`), + KEY `idx_application_id` (`application_id`) COMMENT '出国申请ID索引', + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) COMMENT '租户和删除标志联合索引', + KEY `idx_upload_time` (`upload_time`) COMMENT '上传时间索引' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出国申请附件表 - 支持附件标题和内容说明'; + +-- ---------------------------- +-- Table structure for rs_overseas_application_backup_20251011 +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_application_backup_20251011`; +CREATE TABLE `rs_overseas_application_backup_20251011` ( + `application_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '申请ID', + `serial_number` varchar(50) DEFAULT NULL COMMENT '编号(自动生成的登记编号)', + `applicant_id` bigint(20) NOT NULL COMMENT '申请人ID', + `applicant_name` varchar(50) NOT NULL COMMENT '申请人姓名', + `gender` char(1) DEFAULT NULL COMMENT '性别:0-男,1-女', + `birth_date` date DEFAULT NULL COMMENT '出生日期', + `photo` varchar(500) DEFAULT NULL COMMENT '照片地址(2寸免冠白色或淡蓝色底彩照)', + `id_number` varchar(18) DEFAULT NULL COMMENT '公民身份号码(18位身份证号)', + `political_status` varchar(50) DEFAULT NULL COMMENT '政治面貌', + `registered_address` varchar(200) DEFAULT NULL COMMENT '户籍所在地(填写至县市区)', + `department` varchar(200) DEFAULT NULL COMMENT '所属部门(工作部门/教学系部)', + `position` varchar(100) DEFAULT NULL COMMENT '职位', + `professional_title` varchar(100) DEFAULT NULL COMMENT '职称', + `administrative_level` varchar(20) DEFAULT NULL COMMENT '行政职级', + `personnel_category` varchar(10) DEFAULT NULL COMMENT '人员类别代码(根据报备人员范围中的情形项填写)', + `registration_unit` varchar(200) DEFAULT NULL COMMENT '登记备案责任单位', + `unit_leader_signature` varchar(100) DEFAULT NULL COMMENT '登记备案责任单位负责人签名', + `unit_contact` varchar(100) DEFAULT NULL COMMENT '登记备案责任单位联系人', + `office_phone` varchar(20) DEFAULT NULL COMMENT '办公电话', + `mobile_phone` varchar(11) DEFAULT NULL COMMENT '移动电话', + `document_name` varchar(100) DEFAULT NULL COMMENT '申领证件名称', + `document_number` varchar(50) DEFAULT NULL COMMENT '证件号码', + `destination_country` varchar(100) DEFAULT NULL COMMENT '前往国家', + `exit_reason` varchar(50) DEFAULT NULL COMMENT '出境事由(旅游/探亲等)', + `visit_relationship` varchar(200) DEFAULT NULL COMMENT '探望对象姓名及与申请者关系', + `remarks` text COMMENT '备注', + `emergency_contact` varchar(200) DEFAULT NULL COMMENT '应急联系人及电话', + `signature` longtext COMMENT '手写签字(base64格式)', + `stamped_registration_file_path` varchar(500) DEFAULT NULL COMMENT '登记备案表盖章文件路径', + `stamped_registration_file_name` varchar(200) DEFAULT NULL COMMENT '登记备案表盖章文件名', + `stamped_file_upload_time` datetime DEFAULT NULL COMMENT '盖章文件上传时间', + `stamped_file_upload_status` char(1) DEFAULT '0' COMMENT '盖章文件上传状态:0-未上传,1-已上传', + `stamped_file_oss_id` bigint(20) DEFAULT NULL COMMENT '盖章文件OSS ID', + `party_retention_file_path` varchar(500) DEFAULT NULL COMMENT '党籍保留附件文件路径', + `party_retention_file_name` varchar(200) DEFAULT NULL COMMENT '党籍保留附件文件名', + `party_retention_upload_time` datetime DEFAULT NULL COMMENT '党籍保留附件上传时间', + `party_retention_upload_status` varchar(1) DEFAULT '0' COMMENT '党籍保留附件上传状态:0-未上传,1-已上传', + `party_retention_file_oss_id` bigint(20) DEFAULT NULL COMMENT '党籍保留附件OSS ID', + `attachment_count` int(11) DEFAULT '0' COMMENT '附件数量', + `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话(保留原字段兼容性)', + `contact_email` varchar(100) DEFAULT NULL COMMENT '联系邮箱', + `purpose` varchar(50) NOT NULL COMMENT '出国目的', + `purpose_detail` text NOT NULL COMMENT '目的详细说明', + `destination` varchar(100) NOT NULL COMMENT '目的地国家', + `destination_city` varchar(100) DEFAULT NULL COMMENT '目的地城市', + `start_date` date NOT NULL COMMENT '开始日期', + `end_date` date NOT NULL COMMENT '结束日期', + `duration` int(11) NOT NULL COMMENT '出国天数', + `budget` decimal(15,2) NOT NULL COMMENT '预算金额', + `invitation` text COMMENT '邀请方信息', + `itinerary` text NOT NULL COMMENT '行程安排', + `accommodation_plan` text COMMENT '住宿安排', + `transportation_plan` text COMMENT '交通安排', + `status` char(1) NOT NULL DEFAULT '0' COMMENT '申请状态:0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成', + `current_approver` varchar(100) DEFAULT NULL COMMENT '当前审批人', + `approval_level` char(1) DEFAULT NULL COMMENT '审批级别:1-部门主管,2-分管领导,3-校领导', + `apply_time` datetime NOT NULL COMMENT '申请时间', + `approval_time` datetime DEFAULT NULL COMMENT '审批时间', + `admin_remark` text COMMENT '管理员备注', + `applicant_remark` text COMMENT '申请人备注', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志:0-存在,1-删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------- +-- Table structure for rs_overseas_approval +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_approval`; +CREATE TABLE `rs_overseas_approval` ( + `approval_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '审批ID', + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `approver_id` bigint(20) NOT NULL COMMENT '审批人ID', + `approver_name` varchar(50) NOT NULL COMMENT '审批人姓名', + `approval_level` char(1) NOT NULL COMMENT '审批级别:1-部门主管,2-分管领导,3-校领导', + `approval_status` char(1) NOT NULL COMMENT '审批结果:0-待审核,1-通过,2-拒绝', + `approval_opinion` text COMMENT '审批意见', + `approval_signature` text COMMENT '审批签字(base64图片数据)', + `approval_time` datetime DEFAULT NULL COMMENT '审批时间', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志:0-存在,1-删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`approval_id`), + KEY `idx_application_id` (`application_id`), + KEY `idx_approver_id` (`approver_id`), + KEY `idx_approval_level` (`approval_level`), + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='出国审批记录表'; + +-- ---------------------------- +-- Table structure for rs_overseas_itinerary_change +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_itinerary_change`; +CREATE TABLE `rs_overseas_itinerary_change` ( + `change_id` bigint(20) NOT NULL COMMENT '变更ID(主键,雪花ID)', + `record_id` bigint(20) NOT NULL COMMENT '关联出国记录ID(关联rs_overseas_record表)', + `application_id` bigint(20) DEFAULT NULL COMMENT '原出国申请ID(关联rs_overseas_application表,来自记录表冗余)', + `applicant_id` bigint(20) NOT NULL COMMENT '申请人ID(用户ID)', + `applicant_name` varchar(100) NOT NULL COMMENT '申请人姓名', + `gender` char(1) DEFAULT NULL COMMENT '性别:0-男,1-女', + `birth_date` date DEFAULT NULL COMMENT '出生日期', + `political_status` varchar(20) DEFAULT NULL COMMENT '政治面貌', + `professional_title` varchar(100) DEFAULT NULL COMMENT '职称', + `position` varchar(100) DEFAULT NULL COMMENT '职务', + `document_name` varchar(100) DEFAULT NULL COMMENT '申领证件名称', + `document_number` varchar(100) DEFAULT NULL COMMENT '证件号码', + `department` varchar(200) DEFAULT NULL COMMENT '部门', + `original_destination` varchar(100) DEFAULT NULL COMMENT '原目的地国家', + `original_destination_city` varchar(100) DEFAULT NULL COMMENT '原目的地城市', + `original_exit_reason` varchar(100) DEFAULT NULL COMMENT '原出境事由', + `original_start_date` date DEFAULT NULL COMMENT '原出发日期', + `original_end_date` date DEFAULT NULL COMMENT '原返回日期', + `original_duration` int(11) DEFAULT NULL COMMENT '原出国天数', + `original_itinerary` text COMMENT '原行程安排', + `original_accommodation_plan` text COMMENT '原住宿安排', + `original_transportation_plan` text COMMENT '原交通安排', + `change_type` varchar(200) NOT NULL COMMENT '变更类型(目的地变更,时间变更,行程变更,多个用逗号分隔)', + `new_destination` varchar(100) DEFAULT NULL COMMENT '新目的地国家', + `new_destination_city` varchar(100) DEFAULT NULL COMMENT '新目的地城市', + `new_exit_reason` varchar(100) DEFAULT NULL COMMENT '新出境事由', + `new_start_date` date DEFAULT NULL COMMENT '新出发日期', + `new_end_date` date DEFAULT NULL COMMENT '新返回日期', + `new_duration` int(11) DEFAULT NULL COMMENT '新出国天数', + `new_itinerary` text COMMENT '新行程安排', + `new_accommodation_plan` text COMMENT '新住宿安排', + `new_transportation_plan` text COMMENT '新交通安排', + `change_reason` varchar(500) NOT NULL COMMENT '变更原因(必填)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `status` char(1) NOT NULL DEFAULT '0' COMMENT '变更状态(0=待审核,1=已通过,2=已拒绝,3=已撤回)', + `approval_opinion` varchar(500) DEFAULT NULL COMMENT '审批意见', + `approval_time` datetime DEFAULT NULL COMMENT '审批时间', + `approver_id` bigint(20) DEFAULT NULL COMMENT '审批人ID', + `approver_name` varchar(100) DEFAULT NULL COMMENT '审批人姓名', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户ID', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', + `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除标志(0=未删除,1=已删除)', + PRIMARY KEY (`change_id`), + KEY `idx_record_id` (`record_id`) COMMENT '出国记录ID索引', + KEY `idx_application_id` (`application_id`) COMMENT '原申请ID索引', + KEY `idx_applicant_id` (`applicant_id`) COMMENT '申请人ID索引', + KEY `idx_status` (`status`) COMMENT '状态索引', + KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户ID索引', + KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出国行程变更申请表'; + +-- ---------------------------- +-- Table structure for rs_overseas_record +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_record`; +CREATE TABLE `rs_overseas_record` ( + `record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `application_id` bigint(20) NOT NULL COMMENT '关联申请ID', + `applicant_id` bigint(20) NOT NULL COMMENT '申请人ID', + `applicant_name` varchar(50) NOT NULL COMMENT '申请人姓名', + `department` varchar(100) NOT NULL COMMENT '所属部门', + `position` varchar(100) DEFAULT NULL COMMENT '职位', + `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话', + `contact_email` varchar(100) DEFAULT NULL COMMENT '联系邮箱', + `purpose` varchar(50) NOT NULL COMMENT '出国目的', + `purpose_detail` text COMMENT '目的详细说明', + `destination` varchar(100) NOT NULL COMMENT '目的地国家', + `destination_city` varchar(100) DEFAULT NULL COMMENT '目的地城市', + `actual_start_date` date DEFAULT NULL COMMENT '实际开始日期', + `actual_end_date` date DEFAULT NULL COMMENT '实际结束日期', + `actual_duration` int(11) DEFAULT NULL COMMENT '实际出国天数', + `actual_budget` decimal(15,2) DEFAULT NULL COMMENT '实际花费金额', + `budget_source` varchar(50) DEFAULT NULL COMMENT '经费来源', + `invitation` text COMMENT '邀请方信息', + `itinerary` text COMMENT '行程安排', + `accommodation_plan` text COMMENT '住宿安排', + `transportation_plan` text COMMENT '交通安排', + `completion_status` char(1) NOT NULL DEFAULT '1' COMMENT '完成状态(1-已完成,2-已拒绝)', + `achievement` text COMMENT '出国成果', + `report_content` text COMMENT '出国报告', + `certificate_path` varchar(500) DEFAULT NULL COMMENT '证明材料路径', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志:0-存在,1-删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`record_id`), + KEY `idx_application_id` (`application_id`), + KEY `idx_applicant_id` (`applicant_id`), + KEY `idx_department` (`department`), + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='出国记录表'; + +-- ---------------------------- +-- Table structure for rs_overseas_record_status_backup +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_record_status_backup`; +CREATE TABLE `rs_overseas_record_status_backup` ( + `record_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '记录ID', + `completion_status` char(1) DEFAULT '0' COMMENT '完成状态:0-进行中,1-已完成,2-提前结束', + `old_status_text` varchar(12) NOT NULL DEFAULT '' COMMENT '原状态文本', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------- +-- Table structure for rs_overseas_registration +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_registration`; +CREATE TABLE `rs_overseas_registration` ( + `registration_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '登记ID(主键)', + `application_id` bigint(20) DEFAULT NULL COMMENT '关联出国申请ID', + `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名', + `gender` char(1) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '性别:0-男,1-女', + `birth_date` date NOT NULL COMMENT '出生日期', + `photo` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '照片(预留,前端UI标识)', + `id_number` varchar(18) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '身份证号', + `political_status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '政治面貌', + `registered_address` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '户籍地址', + `department` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '单位部门', + `position` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '职务', + `administrative_level` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '行政级别', + `personnel_category` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '人员类别', + `status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '登记状态:0-草稿,1-已提交,2-已确认,3-已拒绝', + `registration_time` datetime DEFAULT NULL COMMENT '登记时间', + `remarks` text COLLATE utf8mb4_unicode_ci COMMENT '备注', + `tenant_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '删除标志:0-未删除,1-已删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT 'æ›´æ–°æ—¶é—´', + PRIMARY KEY (`registration_id`) USING BTREE, + KEY `idx_application_id` (`application_id`) USING BTREE COMMENT '关联申请ID索引', + KEY `idx_name` (`name`) USING BTREE COMMENT '姓名索引(支持模糊查询)', + KEY `idx_id_number` (`id_number`) USING BTREE COMMENT '身份证号索引(唯一性查询)', + KEY `idx_political_status` (`political_status`) USING BTREE COMMENT '政治面貌索引(党员筛选)', + KEY `idx_status` (`status`) USING BTREE COMMENT '状态索引(用于筛选)', + KEY `idx_registration_time` (`registration_time`) USING BTREE COMMENT '登记时间索引(用于排序)', + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) USING BTREE COMMENT '多租户隔离索引', + KEY `idx_department` (`department`) USING BTREE COMMENT '部门索引', + KEY `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间索引' +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='出国登记表'; + +-- ---------------------------- +-- Table structure for rs_overseas_registration_history +-- ---------------------------- +DROP TABLE IF EXISTS `rs_overseas_registration_history`; +CREATE TABLE `rs_overseas_registration_history` ( + `history_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '历史记录ID', + `registration_id` bigint(20) NOT NULL COMMENT '关联的登记ID', + `actual_departure_date` date DEFAULT NULL COMMENT '实际出发日期', + `actual_return_date` date DEFAULT NULL COMMENT '实际返回日期', + `actual_duration_days` int(11) DEFAULT NULL COMMENT '实际出行天数', + `actual_destination` varchar(200) DEFAULT NULL COMMENT '实际目的地', + `completion_status` char(1) NOT NULL DEFAULT '0' COMMENT '完成状态:0-进行中,1-已完成,2-已取消', + `completion_notes` text COMMENT '完成说明', + `evidence_files` text COMMENT '证明文件(JSON格式存储文件信息)', + `actual_cost` decimal(10,2) DEFAULT NULL COMMENT '实际费用', + `cost_notes` varchar(500) DEFAULT NULL COMMENT '费用说明', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志:0-存在,1-删除', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT 'æ›´æ–°æ—¶é—´', + PRIMARY KEY (`history_id`), + KEY `idx_registration_id` (`registration_id`), + KEY `idx_completion_status` (`completion_status`), + CONSTRAINT `fk_history_registration` FOREIGN KEY (`registration_id`) REFERENCES `rs_overseas_registration` (`registration_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出国登记历史表'; + +-- ---------------------------- +-- Table structure for rs_training_application +-- ---------------------------- +DROP TABLE IF EXISTS `rs_training_application`; +CREATE TABLE `rs_training_application` ( + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `course_id` bigint(20) NOT NULL COMMENT '课程ID', + `course_name` varchar(100) DEFAULT NULL COMMENT '自定义课程名称', + `reject_time` datetime DEFAULT NULL COMMENT '拒绝时间', + `user_id` bigint(20) NOT NULL COMMENT '申请人ID', + `user_name` varchar(50) NOT NULL COMMENT '申请人姓名', + `department` varchar(50) NOT NULL COMMENT '所属部门', + `position` varchar(50) DEFAULT NULL COMMENT '职位', + `current_title` varchar(100) DEFAULT NULL COMMENT '现职称', + `current_position` varchar(50) DEFAULT NULL COMMENT '现岗位', + `work_start_time` date DEFAULT NULL COMMENT '参加工作时间', + `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', + `emergency_contact` varchar(200) DEFAULT NULL COMMENT '应急联系人及电话', + `organizer_unit` varchar(200) DEFAULT NULL COMMENT '主办单位', + `training_type` varchar(50) DEFAULT NULL COMMENT '培训类型', + `training_fee` decimal(10,2) DEFAULT '0.00' COMMENT '培训费用', + `companions` text COMMENT '同行人员列表(JSON格式)', + `training_purpose` text COMMENT '培训目的', + `attachment_ids` text COMMENT '附件ID列表(逗号分隔)', + `reason` text COMMENT '申请理由', + `status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝 3进行中 4已完成)', + `apply_time` datetime DEFAULT NULL COMMENT '申请时间', + `current_approver` varchar(50) DEFAULT NULL COMMENT '当前审批人', + `approval_level` varchar(10) DEFAULT '1' COMMENT '当前审批级别(1-7,2a-教务处,2b-学生处)', + `location` varchar(200) DEFAULT NULL COMMENT '培训地点', + `duration` int(11) DEFAULT NULL COMMENT '学时', + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `end_time` datetime DEFAULT NULL COMMENT '结束时间', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志', + PRIMARY KEY (`application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='培训申请表'; + +-- ---------------------------- +-- Table structure for rs_training_application_backup_20250103 +-- ---------------------------- +DROP TABLE IF EXISTS `rs_training_application_backup_20250103`; +CREATE TABLE `rs_training_application_backup_20250103` ( + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `course_id` bigint(20) NOT NULL COMMENT '课程ID', + `course_name` varchar(100) DEFAULT NULL COMMENT '自定义课程名称', + `reject_time` datetime DEFAULT NULL COMMENT '拒绝时间', + `user_id` bigint(20) NOT NULL COMMENT '申请人ID', + `user_name` varchar(50) NOT NULL COMMENT '申请人姓名', + `department` varchar(50) NOT NULL COMMENT '所属部门', + `position` varchar(50) DEFAULT NULL COMMENT '职位', + `reason` text COMMENT '申请理由', + `status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝 3进行中 4已完成)', + `apply_time` datetime DEFAULT NULL COMMENT '申请时间', + `current_approver` varchar(50) DEFAULT NULL COMMENT '当前审批人', + `location` varchar(200) DEFAULT NULL COMMENT '培训地点', + `duration` int(11) DEFAULT NULL COMMENT '学时', + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `end_time` datetime DEFAULT NULL COMMENT '结束时间', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------- +-- Table structure for rs_training_approval +-- ---------------------------- +DROP TABLE IF EXISTS `rs_training_approval`; +CREATE TABLE `rs_training_approval` ( + `approval_id` bigint(20) NOT NULL COMMENT '审批ID', + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `approver_id` bigint(20) NOT NULL COMMENT '审批人ID', + `approver_name` varchar(50) NOT NULL COMMENT '审批人姓名', + `approval_level` int(11) NOT NULL COMMENT '审批级别(1部门主管 2分管领导 3校领导)', + `status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝)', + `opinion` varchar(500) DEFAULT NULL COMMENT '审批意见', + `approval_time` datetime DEFAULT NULL COMMENT '审批时间', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志', + PRIMARY KEY (`approval_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='培训审批表'; + +-- ---------------------------- +-- Table structure for rs_training_course +-- ---------------------------- +DROP TABLE IF EXISTS `rs_training_course`; +CREATE TABLE `rs_training_course` ( + `course_id` bigint(20) NOT NULL COMMENT '课程ID', + `title` varchar(100) NOT NULL COMMENT '课程名称', + `category` varchar(50) NOT NULL COMMENT '课程类别', + `description` text COMMENT '课程描述', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `duration` int(11) NOT NULL COMMENT '学时数', + `location` varchar(200) DEFAULT NULL COMMENT '培训地点', + `max_participants` int(11) DEFAULT '0' COMMENT '最大参与人数', + `current_participants` int(11) DEFAULT '0' COMMENT '当前报名人数', + `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志', + PRIMARY KEY (`course_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='培训课程表'; + +-- ---------------------------- +-- Table structure for rs_training_record +-- ---------------------------- +DROP TABLE IF EXISTS `rs_training_record`; +CREATE TABLE `rs_training_record` ( + `record_id` bigint(20) NOT NULL COMMENT '记录ID', + `application_id` bigint(20) NOT NULL COMMENT '申请ID', + `course_id` bigint(20) NOT NULL COMMENT '课程ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `user_name` varchar(50) NOT NULL COMMENT '用户姓名', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志', + `location` varchar(255) DEFAULT NULL COMMENT '培训地点', + `duration` int(11) DEFAULT NULL COMMENT '学时', + `apply_time` datetime DEFAULT NULL COMMENT '申请时间', + `status` varchar(10) DEFAULT NULL COMMENT '申请状态', + `course_name` varchar(255) DEFAULT NULL COMMENT '课程名称', + `department` varchar(255) DEFAULT NULL COMMENT '所属部门', + `position` varchar(255) DEFAULT NULL COMMENT '职位', + `reason` varchar(255) DEFAULT NULL COMMENT '申请理由', + `current_approver` varchar(255) DEFAULT NULL COMMENT '当前审批人', + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `end_time` datetime DEFAULT NULL COMMENT '结束时间', + `completion_status` char(1) DEFAULT '0' COMMENT '完成状态(0进行中 1已完成 2未完成)', + `completion_time` datetime DEFAULT NULL COMMENT '完成时间', + `score` decimal(5,2) DEFAULT NULL COMMENT '成绩', + `certificate_path` varchar(255) DEFAULT NULL COMMENT '证书路径', + `certificate_number` varchar(50) DEFAULT NULL COMMENT '证书编号', + `attachment_ids` text COMMENT '证书附件ID', + `feedback` text COMMENT '培训反馈', + PRIMARY KEY (`record_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='培训记录表'; + +-- ---------------------------- +-- Table structure for rs_work_experience +-- ---------------------------- +DROP TABLE IF EXISTS `rs_work_experience`; +CREATE TABLE `rs_work_experience` ( + `exp_id` bigint(20) NOT NULL COMMENT '工作经历ID', + `applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID', + `company_name` varchar(100) NOT NULL COMMENT '公司名称', + `position` varchar(100) NOT NULL COMMENT '职位', + `start_date` date DEFAULT NULL COMMENT '开始日期', + `end_date` date DEFAULT NULL COMMENT '结束日期', + `is_current` char(1) DEFAULT '0' COMMENT '是否当前工作(0是 1否)', + `description` text COMMENT '工作描述', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + PRIMARY KEY (`exp_id`), + KEY `idx_applicant_id` (`applicant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作经历表'; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/设计文档/database/字符集修复/字符集检测验证脚本.sql b/docs/设计文档/database/字符集修复/字符集检测验证脚本.sql new file mode 100644 index 00000000..a907e8f3 --- /dev/null +++ b/docs/设计文档/database/字符集修复/字符集检测验证脚本.sql @@ -0,0 +1,114 @@ +-- ====================================================== +-- 数据库字符集检测和验证脚本 +-- ====================================================== +-- 用途:检测和验证数据库字符集配置是否符合项目规范 +-- 创建时间:2025-01-08 +-- 适用数据库:hzdyjs_xg +-- ====================================================== + +-- 1. 检查数据库默认字符集 +-- ====================================================== +SELECT + SCHEMA_NAME as '数据库名', + DEFAULT_CHARACTER_SET_NAME as '默认字符集', + DEFAULT_COLLATION_NAME as '默认排序规则' +FROM INFORMATION_SCHEMA.SCHEMATA +WHERE SCHEMA_NAME = 'hzdyjs_xg'; + +-- 2. 检查所有非utf8mb4字符集的字段 +-- ====================================================== +SELECT + TABLE_NAME as '表名', + COLUMN_NAME as '字段名', + DATA_TYPE as '数据类型', + CHARACTER_SET_NAME as '字符集', + COLLATION_NAME as '排序规则', + COLUMN_COMMENT as '字段注释' +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = 'hzdyjs_xg' + AND CHARACTER_SET_NAME IS NOT NULL + AND CHARACTER_SET_NAME != 'utf8mb4' +ORDER BY TABLE_NAME, COLUMN_NAME; + +-- 3. 专门检查 rs_overseas_record_status_backup 表 +-- ====================================================== +SELECT + COLUMN_NAME as '字段名', + DATA_TYPE as '数据类型', + CHARACTER_MAXIMUM_LENGTH as '最大长度', + CHARACTER_SET_NAME as '字符集', + COLLATION_NAME as '排序规则', + IS_NULLABLE as '可为空', + COLUMN_DEFAULT as '默认值', + COLUMN_COMMENT as '注释' +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = 'hzdyjs_xg' + AND TABLE_NAME = 'rs_overseas_record_status_backup' +ORDER BY ORDINAL_POSITION; + +-- 4. 检查表的默认字符集 +-- ====================================================== +SELECT + TABLE_NAME as '表名', + TABLE_COLLATION as '表排序规则', + ENGINE as '存储引擎', + TABLE_COMMENT as '表注释' +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'hzdyjs_xg' + AND TABLE_NAME = 'rs_overseas_record_status_backup'; + +-- ====================================================== +-- 修复后验证脚本 +-- ====================================================== + +-- 5. 验证修复效果 - 中文数据测试 +-- ====================================================== + +-- 5.1 插入中文测试数据 +INSERT INTO rs_overseas_record_status_backup +(record_id, completion_status, old_status_text, create_time) +VALUES +(999999, '0', '测试中文', NOW()), +(999998, '1', '已完成', NOW()), +(999997, '2', '提前结束', NOW()); + +-- 5.2 查询验证中文显示 +SELECT + record_id as '记录ID', + completion_status as '完成状态', + old_status_text as '原状态文本', + CHAR_LENGTH(old_status_text) as '字符长度', + LENGTH(old_status_text) as '字节长度', + create_time as '创建时间' +FROM rs_overseas_record_status_backup +WHERE record_id IN (999999, 999998, 999997) +ORDER BY record_id DESC; + +-- 5.3 验证字符编码 +SELECT + old_status_text as '原文', + HEX(old_status_text) as '十六进制编码', + CHAR_LENGTH(old_status_text) as '字符数', + LENGTH(old_status_text) as '字节数' +FROM rs_overseas_record_status_backup +WHERE record_id = 999999; + +-- 5.4 清理测试数据 +DELETE FROM rs_overseas_record_status_backup +WHERE record_id IN (999999, 999998, 999997); + +-- ====================================================== +-- 预期验证结果 +-- ====================================================== +/* +正确的验证结果应该显示: + +1. 字符集检查:old_status_text 字段显示为 utf8mb4 +2. 中文测试:中文字符正常显示,无乱码 +3. 字符编码:中文字符的十六进制编码正确(每个中文字符占3字节) + +如果仍有问题,请检查: +- 数据库连接字符集配置 +- 应用程序的字符集配置 +- 客户端工具的字符集设置 +*/ \ No newline at end of file diff --git a/docs/问题跟踪/后端文件上传数据丢失问题.md b/docs/问题跟踪/后端文件上传数据丢失问题.md new file mode 100644 index 00000000..2690fc35 --- /dev/null +++ b/docs/问题跟踪/后端文件上传数据丢失问题.md @@ -0,0 +1,300 @@ +# 后端文件上传数据丢失问题 + +**发现日期**: 2025-01-24 +**问题模块**: 文件上传/OSS服务 +**影响范围**: 所有使用附件上传的业务模块 +**严重程度**: ⚠️ 中等(影响用户体验,但有前端兜底) + +--- + +## 问题描述 + +后端在保存文件时丢失了真实的文件名和扩展名信息,导致前端回显时无法正确识别文件类型。 + +### 问题表现 + +从 `/resource/oss/listByIds/{ossIds}` API 返回的数据中: + +```javascript +{ + ossId: "1981738218223079425", + originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID,不是用户上传的真实文件名 + fileSuffix: "9", // ❌ 数字字符,不是真实扩展名(应该是 jpg/png 等) + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", // ❌ 没有扩展名 + url: "/resource/oss/download/1981738218223079425" +} +``` + +### 期望的数据格式 + +```javascript +{ + ossId: "1981738218223079425", + originalName: "违纪现场照片.jpg", // ✅ 用户上传时的真实文件名 + fileSuffix: "jpg", // ✅ 真实的文件扩展名 + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139.jpg", // ✅ 包含扩展名 + url: "/resource/oss/download/1981738218223079425" +} +``` + +--- + +## 问题分析 + +### 可能的原因 + +1. **文件上传时未保存原始文件名** + - 上传接口 `/resource/oss/upload` 可能未正确提取文件名 + - `originalName` 字段被UUID覆盖 + +2. **扩展名提取错误** + - `fileSuffix` 字段提取逻辑有误 + - 从文件路径末尾提取了一个字符(如 `9`、`e`、`d`)而不是真实扩展名 + +3. **数据库保存逻辑问题** + - `sys_oss` 表中的字段未正确保存 + - 可能是某个字段映射错误 + +### 影响的业务场景 + +- ✅ **新增上传**:正常工作(文件可以上传) +- ❌ **回显展示**:文件类型识别错误,图片无法正常显示 +- ❌ **下载功能**:下载的文件名显示为UUID,用户体验差 +- ❌ **文件管理**:管理员无法识别文件内容 + +--- + +## 复现步骤 + +1. 在移动端进入"宿舍卫生检查"模块 +2. 点击"录入违纪",上传一张图片(如 `photo.jpg`) +3. 保存违纪记录 +4. 刷新页面或重新进入 +5. 点击"编辑"查看违纪记录 +6. **问题**:附件无法显示为图片,文件名显示为UUID + +--- + +## 调试信息 + +### 前端上传请求 + +```javascript +// 调用上传接口 +POST /resource/oss/upload +Content-Type: multipart/form-data + +// FormData: +file: [Blob] photo.jpg +bizType: "dorm_violation_attachment" +``` + +### 后端返回(上传成功) + +```javascript +{ + code: 200, + data: { + ossId: "1981738218223079425", + url: "/resource/oss/download/1981738218223079425", + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139" // ⚠️ 缺少扩展名 + } +} +``` + +### 后端返回(回显查询) + +```javascript +// GET /resource/oss/listByIds/1981738218223079425 +{ + code: 200, + data: [{ + ossId: "1981738218223079425", + originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID + fileSuffix: "9", // ❌ 错误 + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", + url: "/resource/oss/download/1981738218223079425" + }] +} +``` + +--- + +## 建议的修复方案 + +### 方案1:修复上传接口(推荐)⭐ + +**修改文件**:`backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java` + +**修复点**: +1. 上传时正确提取并保存 `originalName`(用户上传的文件名) +2. 正确提取文件扩展名保存到 `fileSuffix` +3. 保存到OSS的文件名应包含扩展名 + +```java +@PostMapping("/upload") +public R upload(@RequestParam("file") MultipartFile file, + @RequestParam(required = false) String bizType) { + // 获取原始文件名 + String originalFilename = file.getOriginalFilename(); + + // 提取真实扩展名 + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + } + + // 生成OSS文件名(包含扩展名) + String ossFileName = generateOssFileName() + "." + extension; + + // 保存到数据库 + SysOss oss = new SysOss(); + oss.setOriginalName(originalFilename); // ✅ 保存真实文件名 + oss.setFileSuffix(extension); // ✅ 保存真实扩展名 + oss.setFileName(ossFileName); // ✅ 包含扩展名的OSS文件名 + + // ... 其他逻辑 +} +``` + +### 方案2:修复查询接口 + +**修改文件**:`backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java` + +**修复点**: +1. 从数据库正确读取 `originalName` 和 `fileSuffix` +2. 确保返回的VO类包含正确的字段值 + +```java +public List listByIds(String ossIds) { + // ... 查询逻辑 ... + + List voList = new ArrayList<>(); + for (SysOss oss : ossList) { + SysOssVo vo = new SysOssVo(); + vo.setOssId(oss.getOssId()); + vo.setOriginalName(oss.getOriginalName()); // ✅ 确保读取正确 + vo.setFileSuffix(oss.getFileSuffix()); // ✅ 确保读取正确 + vo.setFileName(oss.getFileName()); + vo.setUrl(oss.getUrl()); + voList.add(vo); + } + + return voList; +} +``` + +### 方案3:数据库表结构检查 + +**检查表**:`sys_oss` + +**需要确认的字段**: +```sql +-- 检查字段定义 +SHOW COLUMNS FROM sys_oss LIKE '%name%'; +SHOW COLUMNS FROM sys_oss LIKE '%suffix%'; + +-- 检查数据示例 +SELECT oss_id, original_name, file_suffix, file_name +FROM sys_oss +WHERE oss_id = '1981738218223079425'; +``` + +**期望的字段定义**: +- `original_name` VARCHAR(255) - 用户上传的原始文件名 +- `file_suffix` VARCHAR(10) - 文件扩展名(jpg/png/pdf等) +- `file_name` VARCHAR(500) - OSS中的文件路径(应包含扩展名) + +--- + +## 临时解决方案(前端兜底) + +**已实施**:在 `ViolationRecordDialog.vue` 中添加了兜底逻辑 + +```javascript +// 检测 fileSuffix 是否为有效扩展名 +if (item.fileSuffix && item.fileSuffix.length <= 5 && /^[a-zA-Z]+$/.test(item.fileSuffix)) { + // 使用 fileSuffix + realExtension = item.fileSuffix.toLowerCase(); + fileType = this.getFileTypeBySuffix(realExtension); +} else { + // 默认按图片处理(违纪证明材料大概率是图片) + realExtension = 'jpg'; + fullFileName = `违纪证明_${timestamp + index}.jpg`; + fileType = 'image/jpeg'; +} +``` + +**局限性**: +- ✅ 可以临时解决图片回显问题 +- ❌ 无法恢复真实的文件名 +- ❌ 非图片文件可能被错误识别 +- ❌ 用户体验不佳 + +--- + +## 验证方法 + +### 1. 后端修复后的验证 + +**步骤**: +1. 上传一张图片 `test.jpg` +2. 调用 `/resource/oss/listByIds/{ossId}` 查询 +3. 检查返回数据: + - `originalName` 应该是 `test.jpg` + - `fileSuffix` 应该是 `jpg` + - `fileName` 应该包含 `.jpg` 扩展名 + +**期望日志**: +``` +违纪记录:原始数据 - originalName=test.jpg, fileSuffix=jpg, fileName=2025/10/24/xxx.jpg +违纪记录:最终文件名=test.jpg, 扩展名=jpg, 文件类型=image/jpeg +``` + +### 2. 前端验证 + +**步骤**: +1. 上传图片后编辑违纪记录 +2. 检查附件回显是否正常 +3. 检查文件名是否正确显示 + +**期望结果**: +- ✅ 图片正常显示(不是灰白背景) +- ✅ 文件名显示为真实文件名(不是UUID) +- ✅ 下载文件名正确 + +--- + +## 相关文件 + +**后端**: +- `backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java` +- `backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java` +- `backend/src/main/java/com/ruoyi/resource/domain/vo/SysOssVo.java` +- `backend/src/main/java/com/ruoyi/resource/domain/SysOss.java` + +**前端**: +- `mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue`(已添加兜底逻辑) + +**数据库**: +- `sys_oss` 表 + +--- + +## 优先级和影响 + +**优先级**: P2(中等) + +**影响评估**: +- **用户体验**: ⬇️ 下降(文件名显示为UUID,无法识别内容) +- **功能可用性**: ✅ 正常(有前端兜底,基本功能可用) +- **数据完整性**: ⚠️ 受影响(历史数据无法恢复真实文件名) + +**建议处理时间**: 2周内修复 + +--- + +**创建时间**: 2025-01-24 +**创建人**: Cursor AI +**状态**: 🔴 待修复 + diff --git a/docs/需求文档/人事管理/出国申请管理需求.md b/docs/需求文档/人事管理/出国申请管理需求.md deleted file mode 100644 index 2103fb8e..00000000 --- a/docs/需求文档/人事管理/出国申请管理需求.md +++ /dev/null @@ -1,496 +0,0 @@ -# 出国申请管理系统需求文档 - -**项目名称**: 校园管理系统 - 出国申请管理模块 -**文档版本**: v1.0 -**创建时间**: 2025-07-28 -**更新时间**: 2025-07-28 - -## 📋 需求概述 - -### 业务背景 -为规范学校教职工出国(出境)申请流程,建立标准化的出国申请、审批、记录管理体系,提升出国事务管理的效率和规范性。 - -### 核心目标 -1. **流程规范化**:建立标准的出国申请和审批流程 -2. **权限分离**:区分申请管理和审批管理功能,实现权限精细化控制 -3. **过程透明**:申请进度可视化,审批过程可追溯 -4. **数据完整**:完整记录出国申请全过程数据,支持统计分析 - -## 🎯 功能模块设计 - -### 模块架构 -参考培训申请和培训审批的权限分离设计,出国申请管理分为两个独立模块: - -``` -人事管理 -├── 出国申请 (rs:overseas:*) ← 申请管理功能 -└── 出国审批 (rs:overseasApproval:*) ← 审批管理功能 -``` - -## 📊 出国申请模块 (rs:overseas:*) - -### 功能定位 -面向教职工的出国申请管理平台,提供申请提交、查看、修改等功能。 - -### 主要功能 - -#### 1. 出国申请选项卡 -- **申请提交**:新增出国申请 -- **申请查看**:查看申请详情和状态 -- **申请修改**:编辑待审核的申请 -- **申请删除**:删除申请记录 -- **申请导出**:导出申请数据 - -#### 2. 出国记录选项卡 -- **记录查看**:查看已完成的出国记录 -- **记录编辑**:更新出国记录信息 -- **材料上传**:上传出国证明材料 -- **记录导出**:导出出国记录 - -### 核心字段设计 - -#### 出国申请基础信息 -```javascript -{ - applicationId: string, // 申请ID(雪花ID) - applicantId: string, // 申请人ID - applicantName: string, // 申请人姓名 - department: string, // 所属部门 - position: string, // 职位 - contactPhone: string, // 联系电话 - contactEmail: string, // 联系邮箱 -} -``` - -#### 出国详细信息 -```javascript -{ - purpose: string, // 出国目的(学术交流、培训、会议、考察等) - purposeDetail: string, // 目的详细说明 - destination: string, // 目的地国家/地区 - destinationCity: string, // 目的地城市 - startDate: date, // 出国开始日期 - endDate: date, // 出国结束日期 - duration: number, // 出国天数 - budget: number, // 预算金额 - budgetSource: string, // 经费来源(学校、个人、项目、其他) - invitation: string, // 邀请方信息 - itinerary: string, // 行程安排 - accommodationPlan: string, // 住宿安排 - transportationPlan: string, // 交通安排 -} -``` - -#### 审批流程信息 -```javascript -{ - status: string, // 申请状态(0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成) - currentApprover: string, // 当前审批人 - approvalLevel: string, // 当前审批级别 - applyTime: datetime, // 申请时间 - approvalTime: datetime, // 审批时间 - adminRemark: string, // 管理员备注 - applicantRemark: string, // 申请人备注 -} -``` - -### 页面布局设计 - -#### 出国申请表单 -```vue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -## 📝 出国审批模块 (rs:overseasApproval:*) - -### 功能定位 -面向管理员和审批人员的出国申请审批平台,提供审批处理、流程管理等功能。 - -### 主要功能 - -#### 1. 待审批申请选项卡 -- **申请审批**:审批出国申请(通过/拒绝) -- **审批查询**:查询和筛选待审批申请 -- **批量审批**:批量处理多个申请 -- **审批导出**:导出审批数据 - -#### 2. 审批记录选项卡 -- **历史审批**:查看已审批的申请记录 -- **审批统计**:审批数据统计分析 -- **记录导出**:导出审批记录 - -### 审批流程设计 - -#### 审批层级 -```javascript -// 基于金额和天数的审批层级 -const approvalRules = { - level1: { // 一级审批:部门主管 - condition: "budget <= 10000 && duration <= 7", - approver: "部门主管" - }, - level2: { // 二级审批:分管领导 - condition: "budget <= 50000 && duration <= 30", - approver: "分管领导" - }, - level3: { // 三级审批:校领导 - condition: "budget > 50000 || duration > 30", - approver: "校领导" - } -} -``` - -#### 审批状态流转 -``` -申请提交 → 部门审核 → 分管领导审批 → 校领导审批 → 审批完成 - ↓ ↓ ↓ ↓ -待审核 已通过/拒绝 已通过/拒绝 已通过/拒绝 -``` - -## 🗄️ 数据库设计 - -### 主表:rs_overseas_application -```sql -CREATE TABLE rs_overseas_application ( - application_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID', - applicant_id BIGINT(20) NOT NULL COMMENT '申请人ID', - applicant_name VARCHAR(50) NOT NULL COMMENT '申请人姓名', - department VARCHAR(100) NOT NULL COMMENT '所属部门', - position VARCHAR(100) COMMENT '职位', - contact_phone VARCHAR(20) COMMENT '联系电话', - contact_email VARCHAR(100) COMMENT '联系邮箱', - - purpose VARCHAR(50) NOT NULL COMMENT '出国目的', - purpose_detail TEXT NOT NULL COMMENT '目的详细说明', - destination VARCHAR(100) NOT NULL COMMENT '目的地国家', - destination_city VARCHAR(100) COMMENT '目的地城市', - start_date DATE NOT NULL COMMENT '开始日期', - end_date DATE NOT NULL COMMENT '结束日期', - duration INT NOT NULL COMMENT '出国天数', - budget DECIMAL(15,2) NOT NULL COMMENT '预算金额', - budget_source VARCHAR(50) NOT NULL COMMENT '经费来源', - - invitation TEXT COMMENT '邀请方信息', - itinerary TEXT NOT NULL COMMENT '行程安排', - accommodation_plan TEXT COMMENT '住宿安排', - transportation_plan TEXT COMMENT '交通安排', - - status CHAR(1) NOT NULL DEFAULT '0' COMMENT '申请状态', - current_approver VARCHAR(100) COMMENT '当前审批人', - approval_level CHAR(1) COMMENT '审批级别', - apply_time DATETIME NOT NULL COMMENT '申请时间', - approval_time DATETIME COMMENT '审批时间', - admin_remark TEXT COMMENT '管理员备注', - applicant_remark TEXT COMMENT '申请人备注', - - tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户编号', - deleted CHAR(1) DEFAULT '0' COMMENT '删除标志', - create_dept BIGINT(20) COMMENT '创建部门', - create_by BIGINT(20) COMMENT '创建者', - create_time DATETIME COMMENT '创建时间', - update_by BIGINT(20) COMMENT '更新者', - update_time DATETIME COMMENT '更新时间', - - PRIMARY KEY (application_id) -) ENGINE=InnoDB COMMENT='出国申请表'; -``` - -### 字典表:sys_dict_data(新增出国相关字典) -```sql --- 出国目的 -INSERT INTO sys_dict_data VALUES (NULL, 1, '学术交流', 'academic', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '学术交流访问'); -INSERT INTO sys_dict_data VALUES (NULL, 2, '培训学习', 'training', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '培训学习'); -INSERT INTO sys_dict_data VALUES (NULL, 3, '国际会议', 'conference', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '参加国际会议'); -INSERT INTO sys_dict_data VALUES (NULL, 4, '考察访问', 'visit', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '考察访问'); -INSERT INTO sys_dict_data VALUES (NULL, 5, '其他', 'other', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '其他目的'); - --- 经费来源 -INSERT INTO sys_dict_data VALUES (NULL, 1, '学校经费', 'school', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '学校经费'); -INSERT INTO sys_dict_data VALUES (NULL, 2, '个人承担', 'personal', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '个人承担'); -INSERT INTO sys_dict_data VALUES (NULL, 3, '项目经费', 'project', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '项目经费'); -INSERT INTO sys_dict_data VALUES (NULL, 4, '其他来源', 'other', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '其他来源'); - --- 申请状态 -INSERT INTO sys_dict_data VALUES (NULL, 1, '待审核', '0', 'overseas_status', '', 'warning', 'Y', '0', 'admin', NOW(), '', NULL, '待审核'); -INSERT INTO sys_dict_data VALUES (NULL, 2, '已通过', '1', 'overseas_status', '', 'success', 'Y', '0', 'admin', NOW(), '', NULL, '已通过'); -INSERT INTO sys_dict_data VALUES (NULL, 3, '已拒绝', '2', 'overseas_status', '', 'danger', 'Y', '0', 'admin', NOW(), '', NULL, '已拒绝'); -INSERT INTO sys_dict_data VALUES (NULL, 4, '已取消', '3', 'overseas_status', '', 'info', 'Y', '0', 'admin', NOW(), '', NULL, '已取消'); -INSERT INTO sys_dict_data VALUES (NULL, 5, '已完成', '4', 'overseas_status', '', 'primary', 'Y', '0', 'admin', NOW(), '', NULL, '已完成'); -``` - -## 🔐 权限配置 - -### 菜单权限配置 -```sql --- 出国申请主菜单 -INSERT INTO sys_menu VALUES (2130, '出国申请', 2000, 3, 'overseas', 'rs/overseas/index', '', '', 1, 0, 'M', '0', '0', 'rs:overseas:list', 'international', 'admin', NOW(), '', NULL, '出国申请管理'); - --- 出国申请子权限 -INSERT INTO sys_menu VALUES (2131, '出国申请查询', 2130, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:query', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2132, '出国申请新增', 2130, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:add', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2133, '出国申请修改', 2130, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:edit', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2134, '出国申请删除', 2130, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:remove', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2135, '出国申请导出', 2130, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:export', '#', 'admin', NOW(), '', NULL, ''); - --- 出国审批主菜单 -INSERT INTO sys_menu VALUES (2140, '出国审批', 2000, 4, 'overseasApproval', 'rs/overseasApproval/index', '', '', 1, 0, 'M', '0', '0', 'rs:overseasApproval:list', 'process', 'admin', NOW(), '', NULL, '出国审批管理'); - --- 出国审批子权限 -INSERT INTO sys_menu VALUES (2141, '出国审批查询', 2140, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:query', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2142, '出国审批处理', 2140, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:process', '#', 'admin', NOW(), '', NULL, ''); -INSERT INTO sys_menu VALUES (2143, '出国审批导出', 2140, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:export', '#', 'admin', NOW(), '', NULL, ''); -``` - -### 角色权限分配 -```sql --- 教职工角色(出国申请权限) -INSERT INTO sys_role_menu SELECT 2, menu_id FROM sys_menu WHERE menu_id BETWEEN 2130 AND 2135; - --- 审批人员角色(出国审批权限) -INSERT INTO sys_role_menu SELECT 3, menu_id FROM sys_menu WHERE menu_id BETWEEN 2140 AND 2143; - --- 管理员角色(全部权限) -INSERT INTO sys_role_menu SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN 2130 AND 2143; -``` - -## 📱 技术实现 - -### 前端路由配置 -```javascript -// web/src/router/index.ts -{ - path: 'overseas', - component: () => import('@/views/rs/overseas/index.vue'), - name: 'OverseasManagement', - meta: { title: '出国申请', icon: 'international', permissions: ['rs:overseas:list'] } -}, -{ - path: 'overseasApproval', - component: () => import('@/views/rs/overseasApproval/index.vue'), - name: 'OverseasApproval', - meta: { title: '出国审批', icon: 'process', permissions: ['rs:overseasApproval:list'] } -} -``` - -### API接口设计 -```javascript -// web/src/api/rs/overseas.ts -export interface OverseasApplicationForm { - applicationId?: string | number; - department: string; - contactPhone: string; - contactEmail: string; - purpose: string; - purposeDetail: string; - destination: string; - destinationCity: string; - startDate: string; - endDate: string; - budget: number; - budgetSource: string; - invitation?: string; - itinerary: string; - accommodationPlan?: string; - transportationPlan?: string; - applicantRemark?: string; -} - -// 申请管理API -export const listOverseasApplication = (query: OverseasQuery) => { - return request({ url: '/rs/overseas/list', method: 'get', params: query }); -}; - -export const addOverseasApplication = (data: OverseasApplicationForm) => { - return request({ url: '/rs/overseas', method: 'post', data: data }); -}; - -// 审批管理API -export const approveOverseasApplication = (applicationId: string | number, status: string, remark?: string) => { - return request({ url: '/rs/overseasApproval/approve', method: 'post', data: { applicationId, status, remark } }); -}; -``` - -### 后端控制器 -```java -// backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasController.java -@RestController -@RequestMapping("/rs/overseas") -public class RsOverseasController extends BaseController { - - @SaCheckPermission("rs:overseas:application:query") - @GetMapping("/list") - public TableDataInfo list(RsOverseasApplicationQuery query) { - return rsOverseasService.queryPageList(query); - } - - @SaCheckPermission("rs:overseas:application:add") - @PostMapping - public R add(@Validated @RequestBody RsOverseasApplicationBo bo) { - return toAjax(rsOverseasService.insertByBo(bo)); - } -} - -// backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java -@RestController -@RequestMapping("/rs/overseasApproval") -public class RsOverseasApprovalController extends BaseController { - - @SaCheckPermission("rs:overseasApproval:process") - @PostMapping("/approve") - public R approve(@RequestBody ApprovalRequest request) { - return toAjax(rsOverseasApprovalService.approve(request)); - } -} -``` - -## 📊 业务流程 - -### 申请流程 -```mermaid -graph TD - A[教职工提交申请] --> B[系统验证] - B --> C[部门主管审核] - C --> D{金额/天数判断} - D -->|≤1万且≤7天| E[审批完成] - D -->|≤5万且≤30天| F[分管领导审批] - D -->|>5万或>30天| G[校领导审批] - F --> H[审批完成] - G --> I[审批完成] - - C --> J[拒绝] - F --> K[拒绝] - G --> L[拒绝] -``` - -### 状态流转 -```javascript -const statusFlow = { - '0': ['1', '2', '3'], // 待审核 → 通过/拒绝/取消 - '1': ['4'], // 已通过 → 已完成 - '2': [], // 已拒绝 → 终态 - '3': [], // 已取消 → 终态 - '4': [] // 已完成 → 终态 -}; -``` - -## 🔍 验收标准 - -### 功能测试 -1. **申请提交**:教职工能够完整填写并提交出国申请 -2. **审批处理**:管理员能够查看、审批、拒绝申请 -3. **权限控制**:不同角色只能访问对应功能模块 -4. **状态流转**:申请状态按照业务规则正确流转 -5. **数据完整**:所有申请和审批数据完整记录 - -### 性能要求 -1. **响应时间**:页面加载时间 < 3秒 -2. **并发处理**:支持100人同时使用 -3. **数据准确**:审批流程无数据丢失或错误 - -### 用户体验 -1. **界面友好**:参考培训申请的UI设计风格 -2. **操作简单**:申请流程不超过5个步骤 -3. **提示清晰**:表单验证和错误提示明确 - -## 📅 开发计划 - -### 第一阶段(1周):基础搭建 -- 数据库表设计和创建 -- 菜单权限配置 -- 基础页面框架搭建 - -### 第二阶段(2周):核心功能开发 -- 出国申请功能开发 -- 出国审批功能开发 -- 审批流程实现 - -### 第三阶段(1周):测试优化 -- 功能测试和优化 -- 权限测试验证 -- 用户体验优化 - ---- - -**✅ 需求文档完成** -**📋 参考标准**: 基于培训申请和培训审批的设计模式 -**🎯 核心特色**: 权限分离、流程规范、数据完整、用户友好 \ No newline at end of file diff --git a/docs/需求文档/人事管理/培训管理.md b/docs/需求文档/人事管理/培训管理.md deleted file mode 100644 index c368070c..00000000 --- a/docs/需求文档/人事管理/培训管理.md +++ /dev/null @@ -1,133 +0,0 @@ -需求分析概览 - -### 核心业务模块 -1. **招聘管理模块** - 招聘公告、职位管理 -2. **应聘者管理模块** - 注册登录、简历管理、职位申请 -3. **培训管理模块** - 培训申请、审核、记录管理 -4. **出国申请模块** - 出国申请、审核流程 -5. **用户认证模块** - 多种登录方式、密码重置 - -## 👥 角色体系设计 - -基于您的需求,系统需要以下角色: - -### 1. **应聘者角色** (APPLICANT) -- 账号注册、登录 -- 简历填写和管理 -- 职位申请和跟踪 -- 个人信息维护 - -### 2. **HR管理员角色** (HR_ADMIN) -- 发布招聘公告 -- 管理招聘职位 -- 审核应聘者申请 -- 简历筛选和评估 - -### 3. **教职工角色** (TEACHER) -- 培训申请和查询 -- 出国申请提交 -- 个人培训记录查看 -- 证书上传管理 - -### 4. **培训管理员角色** (TRAINING_ADMIN) -- 培训课程管理 -- 培训申请审核 -- 培训记录登记 -- 培训证书管理 - -### 5. **审批领导角色** (APPROVER) -- 培训申请审批 -- 出国申请审批 -- 在线签字审批 -- 审批流程管理 - -### 6. **系统管理员角色** (SYSTEM_ADMIN) -- 用户权限管理 -- 系统配置管理 -- 数据统计分析 - -## 📋 需要提供的材料清单 - -为了完整开发这个人事管理系统,我需要您提供以下材料: - -### 🔴 必需材料 - -#### 1. **业务流程图** -- 招聘流程:发布公告 → 职位申请 → 简历筛选 → 面试安排 → 录用决定 -- 培训申请流程:申请提交 → 部门审核 → 领导审批 → 培训执行 → 记录归档 -- 出国申请流程:申请提交 → 部门审核 → 领导审批 → 出国执行 - -#### 2. **详细字段需求** -- **招聘职位**:职位名称、部门、岗位要求、薪资范围、工作地点等 -- **简历信息**:基本信息、教育背景、工作经历、技能证书、附件等 -- **培训记录**:培训名称、培训时间、学时、培训类别、证书等 -- **出国申请**:出国目的、目的地、时间、预算、行程安排等 - -#### 3. **审批规则** -- 培训申请的审批层级(部门主管 → 分管领导 → 校领导) -- 出国申请的审批层级和条件 -- 不同金额/时长的审批权限 - -#### 4. **权限矩阵** -- 各角色的功能权限明细 -- 数据查看权限范围 -- 操作权限限制 - -### 🟡 建议提供的材料 - -#### 5. **UI/UX设计要求** -- 简历填写页面的布局要求 -- 审批流程的界面展示 -- 移动端适配要求 - -#### 6. **集成需求** -- 是否需要与现有OA系统集成 -- 短信/邮件服务商选择 -- 文件存储方案 - -#### 7. **报表需求** -- 招聘统计报表 -- 培训统计分析 -- 人事数据报表 - -## 🚀 开发规划建议 - -### 第一阶段:基础模块开发 (2-3周) -1. 用户认证模块(多种登录方式) -2. 角色权限管理 -3. 基础数据字典 - -### 第二阶段:招聘管理模块 (2-3周) -1. 招聘公告管理 -2. 职位管理 -3. 应聘者注册和简历管理 - -### 第三阶段:培训管理模块 (2-3周) -1. 培训申请和审核 -2. 培训记录管理 -3. 工作流集成 - -### 第四阶段:出国申请模块 (1-2周) -1. 出国申请流程 -2. 审批管理 -3. 记录查询 - -### 第五阶段:优化完善 (1周) -1. 报表统计 -2. 移动端适配 -3. 性能优化 - -## 📝 下一步行动 - -请您优先提供以下关键材料,我就可以开始详细的技术设计: - -1. **详细的业务流程图** - 特别是审批流程的具体步骤 -2. **各模块的详细字段清单** - 包括必填字段、字段类型、校验规则 -3. **角色权限矩阵** - 明确各角色可以执行的操作 -4. **审批规则说明** - 不同类型申请的审批层级和条件 - -有了这些材料,我将按照项目规范为您生成: -- 完整的数据库设计文档 -- 标准的API接口文档 -- 详细的开发计划 -- 规范的代码模板 \ No newline at end of file diff --git a/docs/需求文档/人事管理/培训管理需求.md b/docs/需求文档/人事管理/培训管理需求.md deleted file mode 100644 index 0ebbc1c4..00000000 --- a/docs/需求文档/人事管理/培训管理需求.md +++ /dev/null @@ -1,408 +0,0 @@ -# 培训管理模块开发需求 - -## 一、需求概述 - -基于人事管理系统现有招聘管理功能,开发培训管理模块,实现培训申请、审核、培训记录管理等功能。培训管理模块主要支持教职工提交培训申请,培训管理员审核申请和管理培训课程,审批领导进行多级审批,并提供培训记录统计功能。 - -## 二、功能模块设计 - -### 1. 培训课程管理 - -#### 功能描述 -- 培训课程的CRUD操作 -- 课程分类管理 -- 课程状态管理(待开课、进行中、已结束) -- 课程名额限制和报名统计 - -#### 数据表设计 -```sql -CREATE TABLE `rs_training_course` ( - `course_id` bigint(20) NOT NULL COMMENT '课程ID', - `title` varchar(100) NOT NULL COMMENT '课程名称', - `category` varchar(50) NOT NULL COMMENT '课程类别', - `description` text COMMENT '课程描述', - `start_time` datetime NOT NULL COMMENT '开始时间', - `end_time` datetime NOT NULL COMMENT '结束时间', - `duration` int(11) NOT NULL COMMENT '学时数', - `location` varchar(200) COMMENT '培训地点', - `max_participants` int(11) DEFAULT 0 COMMENT '最大参与人数', - `current_participants` int(11) DEFAULT 0 COMMENT '当前报名人数', - `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - `deleted` char(1) DEFAULT '0' COMMENT '删除标志', - PRIMARY KEY (`course_id`) -) ENGINE=InnoDB COMMENT='培训课程表'; -``` - -### 2. 培训申请管理 - -#### 功能描述 -- 教职工提交培训申请 -- 培训申请列表展示和筛选 -- 申请详情查看 -- 申请状态管理 -- 多级审批流程(部门主管→分管领导→校领导) - -#### 数据表设计 -```sql -CREATE TABLE `rs_training_application` ( - `application_id` bigint(20) NOT NULL COMMENT '申请ID', - `course_id` bigint(20) NOT NULL COMMENT '课程ID', - `user_id` bigint(20) NOT NULL COMMENT '申请人ID', - `user_name` varchar(50) NOT NULL COMMENT '申请人姓名', - `department` varchar(50) NOT NULL COMMENT '所属部门', - `position` varchar(50) COMMENT '职位', - `reason` text COMMENT '申请理由', - `status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝 3进行中 4已完成)', - `apply_time` datetime DEFAULT NULL COMMENT '申请时间', - `current_approver` varchar(50) DEFAULT NULL COMMENT '当前审批人', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - `deleted` char(1) DEFAULT '0' COMMENT '删除标志', - PRIMARY KEY (`application_id`) -) ENGINE=InnoDB COMMENT='培训申请表'; -``` - -### 3. 培训审批流程 - -#### 功能描述 -- 审批流程定义和配置 -- 审批记录跟踪 -- 审批意见填写 -- 审批通知(短信/邮件) - -#### 数据表设计 -```sql -CREATE TABLE `rs_training_approval` ( - `approval_id` bigint(20) NOT NULL COMMENT '审批ID', - `application_id` bigint(20) NOT NULL COMMENT '申请ID', - `approver_id` bigint(20) NOT NULL COMMENT '审批人ID', - `approver_name` varchar(50) NOT NULL COMMENT '审批人姓名', - `approval_level` int(11) NOT NULL COMMENT '审批级别(1部门主管 2分管领导 3校领导)', - `status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝)', - `opinion` varchar(500) DEFAULT NULL COMMENT '审批意见', - `approval_time` datetime DEFAULT NULL COMMENT '审批时间', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - `deleted` char(1) DEFAULT '0' COMMENT '删除标志', - PRIMARY KEY (`approval_id`) -) ENGINE=InnoDB COMMENT='培训审批表'; -``` - -### 4. 培训记录管理 - -#### 功能描述 -- 培训记录登记 -- 培训证书上传和管理 -- 个人培训记录查询 -- 培训统计报表 - -#### 数据表设计 -```sql -CREATE TABLE `rs_training_record` ( - `record_id` bigint(20) NOT NULL COMMENT '记录ID', - `application_id` bigint(20) NOT NULL COMMENT '申请ID', - `course_id` bigint(20) NOT NULL COMMENT '课程ID', - `user_id` bigint(20) NOT NULL COMMENT '用户ID', - `user_name` varchar(50) NOT NULL COMMENT '用户姓名', - `completion_status` char(1) DEFAULT '0' COMMENT '完成状态(0进行中 1已完成 2未完成)', - `completion_time` datetime DEFAULT NULL COMMENT '完成时间', - `score` decimal(5,2) DEFAULT NULL COMMENT '成绩', - `certificate_path` varchar(255) DEFAULT NULL COMMENT '证书路径', - `certificate_number` varchar(50) DEFAULT NULL COMMENT '证书编号', - `feedback` text COMMENT '培训反馈', - `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', - `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - `deleted` char(1) DEFAULT '0' COMMENT '删除标志', - PRIMARY KEY (`record_id`) -) ENGINE=InnoDB COMMENT='培训记录表'; -``` - -## 三、API接口设计 - -### 1. 培训课程管理API - -``` -1. 查询培训课程列表 - GET /training/course/list - -2. 获取培训课程详情 - GET /training/course/{courseId} - -3. 新增培训课程 - POST /training/course - -4. 修改培训课程 - PUT /training/course - -5. 删除培训课程 - DELETE /training/course/{courseIds} - -6. 切换培训课程状态 - PUT /training/course/toggle/{courseId} - -7. 导出培训课程 - GET /training/course/export -``` - -### 2. 培训申请管理API - -``` -1. 查询培训申请列表 - GET /training/application/list - -2. 获取培训申请详情 - GET /training/application/{applicationId} - -3. 提交培训申请 - POST /training/application - -4. 修改培训申请 - PUT /training/application - -5. 取消培训申请 - PUT /training/application/cancel/{applicationId} - -6. 导出培训申请 - GET /training/application/export - -7. 获取我的培训申请列表 - GET /training/application/my -``` - -### 3. 培训审批API - -``` -1. 查询待我审批的申请列表 - GET /training/approval/pending - -2. 审批培训申请 - PUT /training/approval/process/{applicationId}/{status} - -3. 获取申请审批历史 - GET /training/approval/history/{applicationId} - -4. 批量审批 - POST /training/approval/batch -``` - -### 4. 培训记录API - -``` -1. 查询培训记录列表 - GET /training/record/list - -2. 获取培训记录详情 - GET /training/record/{recordId} - -3. 登记培训记录 - POST /training/record - -4. 修改培训记录 - PUT /training/record - -5. 上传培训证书 - POST /training/record/certificate/{recordId} - -6. 查询我的培训记录 - GET /training/record/my - -7. 导出培训记录 - GET /training/record/export - -8. 获取培训统计数据 - GET /training/record/statistics -``` - -## 四、前端页面设计 - -### 1. 培训课程管理页面 ⚠️【已废弃 - 2025-01-03】 - -**重要说明**:培训课程模块已废弃,课程信息现在直接存储在 `rs_training_application` 表中,通过培训申请功能管理。 - -~~- **页面路径**:/rs/training/course~~ -~~- **功能组件**:~~ - ~~- 搜索表单(课程名称、类别、状态)~~ - ~~- 操作按钮(新增、修改、删除、导出)~~ - ~~- 数据表格(课程信息展示)~~ - ~~- 表单对话框(新增/修改课程信息)~~ - -### 2. 培训申请管理页面 - -- **页面路径**:/rs/training/application -- **功能组件**: - - 搜索表单(课程名称、申请人、状态) - - 操作按钮(导出) - - 数据表格(申请信息展示) - - 申请详情对话框 - - 审批操作面板 - -### 3. 我的培训页面 - -- **页面路径**:/rs/training/my -- **功能组件**: - - 培训申请选项卡 - - 申请表单 - - 我的申请列表 - - 培训记录选项卡 - - 我的培训记录 - - 证书查看与下载 - -### 4. 培训统计报表页面 - -- **页面路径**:/rs/training/statistics -- **功能组件**: - - 统计图表(按部门、课程类型等维度) - - 数据筛选器 - - 导出功能 - -## 五、权限设计 - -### 1. 权限项定义 -``` -# 培训课程管理权限 -~~- rs:training:course:list 查看培训课程列表~~ 【已废弃】 -~~- rs:training:course:query 查询培训课程详情~~ 【已废弃】 -~~- rs:training:course:add 新增培训课程~~ 【已废弃】 -~~- rs:training:course:edit 修改培训课程~~ 【已废弃】 -~~- rs:training:course:remove 删除培训课程~~ 【已废弃】 -~~- rs:training:course:export 导出培训课程~~ 【已废弃】 - -# 培训申请管理权限 -- rs:training:application:list 查看培训申请列表 -- rs:training:application:query 查询培训申请详情 -- rs:training:application:add 提交培训申请 -- rs:training:application:edit 修改培训申请 -- rs:training:application:export 导出培训申请 - -# 培训审批权限 -- rs:training:approval:list 查看审批列表 -- rs:training:approval:process 处理培训审批 - -# 培训记录管理权限 -- rs:trainingrecord:list 查看培训记录列表 -- rs:trainingrecord:query 查询培训记录详情 -- rs:trainingrecord:add 新增培训记录 -- rs:trainingrecord:edit 修改培训记录 -- rs:trainingrecord:export 导出培训记录 -``` - -### 2. 角色权限分配 - -``` -# 教职工角色 (TEACHER) -- rs:training:course:list -- rs:training:course:query -- rs:training:application:add -- rs:training:application:query -- rs:trainingrecord:query (个人) - -# 培训管理员角色 (TRAINING_ADMIN) -- rs:training:course:* -- rs:training:application:list -- rs:training:application:query -- rs:training:application:export -- rs:trainingrecord:* - -# 审批领导角色 (APPROVER) -- rs:training:course:list -- rs:training:course:query -- rs:training:application:list -- rs:training:application:query -- rs:training:approval:* -``` - -## 六、开发计划 - -### 第一阶段:数据库设计与基础架构 (5天) -- 创建数据库表 -- 生成基础实体类 -- 搭建模块结构 - -### 第二阶段:培训课程管理功能 (5天) -- 后端API开发 -- 前端页面开发 -- 单元测试 - -### 第三阶段:培训申请功能 (7天) -- 后端API开发 -- 前端页面开发 -- 审批流程设计 -- 单元测试 - -### 第四阶段:培训记录管理功能 (5天) -- 后端API开发 -- 前端页面开发 -- 证书上传功能 -- 单元测试 - -### 第五阶段:统计报表与优化 (3天) -- 培训统计报表 -- 权限控制完善 -- 性能优化 -- 集成测试 - -## 七、技术要点与注意事项 - -1. **工作流集成** - - 使用Warm-Flow工作流引擎实现多级审批流程 - - 遵循工作流开发规范,确保VO类包含基础字段 - -2. **前后端字段一致性** - - 严格遵循命名规范,确保前后端字段名称一致 - - 注意类型一致性,特别是日期、枚举等类型 - -3. **文件上传处理** - - 培训证书上传需支持多种格式(PDF, JPG等) - - 实现文件预览功能 - -4. **权限控制** - - 实现数据权限,教职工只能查看自己的培训记录 - - 审批权限按角色和部门进行控制 - -5. **文档同步更新** - - 确保API变更同步更新到API文档 - - 所有新增功能需要同步更新变更记录 - -## 八、验收标准 - -1. **功能完整性** - - 培训管理各模块功能齐全 - - 审批流程运行正常 - - 各角色权限控制正确 - -2. **界面体验** - - 操作流程简洁明了 - - 页面响应迅速 - - 数据展示清晰 - -3. **性能要求** - - 列表页面加载时间<2秒 - - 表单提交响应时间<1秒 - - 导出功能支持大数据量 - -4. **兼容性** - - 支持主流浏览器 - - 移动端适配良好 - -5. **安全性** - - 数据传输加密 - - 防SQL注入 - - 敏感信息脱敏 \ No newline at end of file diff --git a/docs/需求文档/人事管理/招聘页面需求.md b/docs/需求文档/人事管理/招聘页面需求.md deleted file mode 100644 index 5220c883..00000000 --- a/docs/需求文档/人事管理/招聘页面需求.md +++ /dev/null @@ -1,93 +0,0 @@ -第一步:生成招聘页面原型图 -cursor -根据以下需求生成招聘模块的Figma原型图: -1. 页面组成: - - 招聘公告列表页(带搜索/筛选) - - 职位详情页(申请按钮) - - 简历填写表单(分步骤) - - HR管理后台(职位发布表单) - -2. 核心功能: - - 应聘者能浏览公开职位 - - HR能发布新职位(含薪资范围/要求等字段) - - 响应式布局(支持移动端) - -3. 数据字段参考: - - 职位:title, department, salary_range, location, description, requirements - - 简历:personal_info, education[], work_experience[], skills - -4. 角色权限: - - 应聘者:仅查看/申请 - - HR:增删改职位 -第二步:生成招聘模块表结构 -cursor -生成符合MySQL规范的SQL建表语句,包含以下核心表: - -1. `jobs` 职位表: - - id (PK) - - title VARCHAR(100) - - department ENUM('教学','行政','科研') - - salary_range VARCHAR(50) - - location VARCHAR(100) - - description TEXT - - requirements TEXT - - is_active BOOLEAN (是否在招) - - created_at TIMESTAMP - - created_by (FK to users) - -2. `applicants` 应聘者表: - - user_id (PK, FK to users) - - resume_file_path VARCHAR(255) - - current_status ENUM('active','hired','rejected') - -3. `applications` 申请记录表: - - id (PK) - - job_id (FK) - - applicant_id (FK) - - apply_time TIMESTAMP - - status ENUM('pending','interviewed','offered') - -4. 补充关联表: - - resumes (存储结构化简历数据) - - application_attachments (附件) -第三步:初始化数据库 -cursor -基于上述表结构执行以下操作: -1. 创建名为`recruitment_system`的MySQL数据库 -2. 生成各表的DDL语句(包含外键约束) -3. 为每个表添加基础索引: - - jobs表的department/is_active字段 - - applications表的job_id/applicant_id字段 -4. 插入测试数据: - - 5个示例职位(3个教学岗、2个行政岗) - - 10条模拟应聘者记录 - - 每个职位关联2-3条申请记录 -第四步:前后端开发指令 -cursor -使用MERN栈开发招聘模块,分两部分实现: - -【前端部分】 -1. 创建React页面: - - /jobs 职位列表页(带Ant Design表格) - - /jobs/:id 职位详情页 - - /apply 简历表单(分教育/工作经历多步骤) - -2. 主要功能组件: - - 职位搜索过滤器(部门/薪资范围) - - 简历PDF上传组件 - - 申请状态跟踪面板 - -【后端部分】 -1. 创建Express路由: - - GET /api/jobs (支持分页查询) - - POST /api/jobs (HR权限校验) - - POST /api/applications - -2. 核心业务逻辑: - - 职位发布时自动生成公告 - - 申请时发送邮件通知HR - - 简历文件存储到AWS S3 - -3. 权限控制中间件: - - 应聘者只能访问GET /jobs - - HR角色需要JWT校验 \ No newline at end of file diff --git a/docs/需求文档/学工管理学生信息管理需求/学生信息管理需求分析.md b/docs/需求文档/学工管理学生信息管理需求/学生信息管理需求分析.md deleted file mode 100644 index 87e6cdfa..00000000 --- a/docs/需求文档/学工管理学生信息管理需求/学生信息管理需求分析.md +++ /dev/null @@ -1,430 +0,0 @@ -# 学生信息管理需求分析书 - -## 📋 文档信息 - -| 项目 | 信息 | -|------|------| -| **项目名称** | 校园管理系统 - 学工管理模块 | -| **子模块** | 学生信息管理 | -| **文档版本** | V1.0.0 | -| **创建时间** | 2025-01-14 | -| **创建人员** | 产品团队 | -| **审核状态** | 待审核 | - -## 🎯 需求概述 - -### 业务背景 -解决对所有在籍在校学生基本信息的收集,通过已掌握的学生基本信息可进一步统计出有效的数据,作为学校领导开展全校策略、规划工作提供的数据依据等。可解决对于学生基本信息收集流程及审核步骤的信息化服务应用。 - -### 核心目标 -- 实现学生信息收集、维护、审核全流程信息化 -- 建立统一的学生信息数据库,支持多维度统计分析 -- 规范学生信息采集、审核、管理流程 -- 实现分层级、分角色的权限管理体系 -- 为学校管理层提供数据决策支持 - -## 👥 用户角色分析 - -### 1. 学生用户 -**角色描述**:在校学生,信息的主要提供者和维护者 -**主要职责**: -- 填写和维护个人基本信息 -- 在开放的业务期内自行核对和编辑个人信息 -- 提交信息后跟踪查询审核进度 -- 查看个人信息的审核状态和反馈意见 - -**权限范围**: -- 查看:个人信息、审核进度、审核意见 -- 编辑:个人基本信息(在批次开放期内) -- 操作:信息提交、进度查询 - -### 2. 辅导员 -**角色描述**:学生日常管理的直接负责人 -**主要职责**: -- 对所负责学生的信息进行初步审核 -- 指导学生完善信息填写 -- 统计所负责学生的信息完成情况 -- 处理学生信息相关问题 - -**权限范围**: -- 查看:所负责学生的完整信息 -- 编辑:学生信息的审核状态和意见 -- 操作:审核通过/驳回、批量审核、数据导出 - -### 3. 班主任 -**角色描述**:班级管理的直接责任人 -**主要职责**: -- 对班级学生信息进行审核确认 -- 维护班级基础信息和班干部信息 -- 协助辅导员进行学生信息管理 -- 处理班级内学生信息问题 - -**权限范围**: -- 查看:所负责班级学生的完整信息 -- 编辑:班级信息、学生信息审核 -- 操作:审核操作、班级统计、信息导出 - -### 4. 二级学院管理员 -**角色描述**:学院层面的学生信息管理人员 -**主要职责**: -- 对学院范围内学生信息进行复审 -- 统计分析学院学生信息数据 -- 协调处理学院内信息管理问题 -- 向学生处报告信息收集情况 - -**权限范围**: -- 查看:学院内所有学生信息 -- 编辑:学生信息复审、学院统计数据 -- 操作:批量审核、数据分析、报表生成 - -### 5. 学生处管理员 -**角色描述**:全校学生信息管理的最高权限用户 -**主要职责**: -- 创建和管理信息采集批次 -- 配置各用户组的权限设置 -- 进行全校范围的数据统计分析 -- 系统管理和维护 - -**权限范围**: -- 查看:全校所有学生信息 -- 编辑:系统配置、批次管理、权限设置 -- 操作:系统管理、全局统计、数据维护 - -## 📊 功能需求分析 - -### 1. 学生基本信息管理 - -#### 1.1 信息录入与维护 -**需求描述**:提供完整的学生信息录入和维护功能 - -**功能要求**: -- 支持学生基本信息录入(姓名、性别、出生日期、身份证号等) -- 支持学籍信息管理(学号、专业、班级、入学时间、学制等) -- 支持联系信息管理(手机、邮箱、家庭地址、紧急联系人等) -- 支持家庭信息管理(父母信息、家庭收入、家庭成员等) -- 支持体征信息管理(身高、体重、血型、健康状况等) -- **必须包含银行卡信息**(银行卡号、开户银行、持卡人姓名等) - -**技术要求**: -- 银行卡等敏感信息需加密存储 -- 支持必填字段验证 -- 支持数据格式校验 -- 支持信息修改历史记录 - -#### 1.2 信息展示方式 -**需求描述**:提供多种信息展示方式 - -**功能要求**: -- **表格展示**:传统列表形式展示学生信息 -- **卡片展示**:以学生照片、学号、性别、班级等信息进行直观展现 -- **详情展示**:完整信息的详细展示页面 -- 支持字段显示/隐藏配置 -- 支持排序和筛选功能 - -### 2. 班级信息管理 - -#### 2.1 班级基础信息 -**需求描述**:提供班级基础信息的维护和管理功能 - -**功能要求**: -- 班级基本信息维护(班级代码、名称、专业、学院等) -- 班级规模统计(总人数、男女比例等) -- 班级状态管理(正常、毕业、解散等) - -#### 2.2 人员配置管理 -**需求描述**:支持维护辅导员、班主任的聘任信息及班干部的任命信息 - -**功能要求**: -- 辅导员信息配置和管理 -- 班主任信息配置和管理 -- 班干部任命信息管理(班长、副班长、各委员等) -- 人员变更历史记录 -- 任职期限管理 - -### 3. 信息采集批次管理 - -#### 3.1 批次创建与配置 -**需求描述**:提供单独的基本信息批次管理功能,与校每学年的学生信息采集工作开展相对应 - -**功能要求**: -- 批次基本信息配置(名称、时间范围、描述等) -- 批次状态管理(草稿、开放、关闭、归档等) -- 批次数据统计和进度跟踪 -- 批次结果汇总和报告 - -#### 3.2 权限配置 -**需求描述**:根据辅导员、班主任、二级学院、学生处等用户组,批量进行不同的权限配置 - -**功能要求**: -- 基于角色的权限配置 -- 数据访问范围控制 -- 操作权限细粒度管理 -- 权限继承和覆盖机制 - -### 4. 多级审核流程管理 - -#### 4.1 审核流程设计 -**需求描述**:班主任或辅导员、二级学院、学生处可以对于学生填写基本信息的采集进行审核 - -**审核层级**: -1. **学生提交**:学生完成信息填写后提交 -2. **辅导员/班主任初审**:对学生信息进行初步审核 -3. **二级学院复审**:对学院内学生信息进行复审 -4. **学生处终审**:最终审核确认 - -#### 4.2 审核功能要求 -**功能要求**: -- 将必填字段重点罗列显示 -- 将修改或变化的内容以不同的颜色标识出来 -- 支持审核意见录入 -- 支持审核状态跟踪 -- 支持审核历史记录查看 -- 支持批量审核操作 - -#### 4.3 进度跟踪 -**需求描述**:提交后需要支持在线跟踪查询审核进度 - -**功能要求**: -- 实时审核状态显示 -- 审核进度时间线展示 -- 审核意见和反馈查看 -- 审核结果通知机制 - -### 5. 高级搜索与查询 - -#### 5.1 基础搜索功能 -**需求描述**:提供高级搜索、多字段组合查询来快速筛选出符合条件的学生 - -**功能要求**: -- 单字段快速搜索(姓名、学号、身份证等) -- 多字段组合搜索 -- 模糊搜索和精确搜索 -- 搜索历史记录 -- 搜索结果保存和分享 - -#### 5.2 综合条件筛选 -**需求描述**:支持各种综合信息筛选查询,如按成绩条件、违纪处分条件、困难生条件、奖助条件作为查询条件查询 - -**功能要求**: -- 跨业务域的综合条件筛选 -- 多维度条件组合查询 -- 动态筛选条件配置 -- 筛选结果统计分析 -- 筛选条件模板保存 - -### 6. 数据统计分析 - -#### 6.1 基础统计维度 -**需求描述**:提供对在校学生进行查询与统计,包括多个统计维度 - -**统计维度**: -- 二级学院统计 -- 年龄统计 -- 专业统计 -- 政治面貌统计 -- 民族统计 -- 生源地统计 -- 性别统计 -- 血型统计 -- 宗教信仰统计 -- 学籍状态统计 -- 培养方式统计 -- 体重统计 -- 身高统计 - -#### 6.2 统计功能要求 -**功能要求**: -- 支持多维度、多条件的组合查询 -- 相关统计结果能够实现在线详情查看和导出 -- 支持汇总表、统计表、统计图及统计数据透视 -- 支持统计结果的图表展示 -- 支持统计报表的定时生成和推送 - -### 7. 批量数据操作 - -#### 7.1 数据导入功能 -**需求描述**:管理老师可进行单个学生信息的新增和编辑,也支持学生信息批量导入导出操作 - -**功能要求**: -- 支持Excel模板批量导入 -- 数据格式校验和错误提示 -- 导入进度显示和结果反馈 -- 导入数据预览和确认 -- 导入失败数据的错误报告 - -#### 7.2 数据导出功能 -**功能要求**: -- 支持按条件批量导出学生信息 -- 支持多种导出格式(Excel、PDF等) -- 支持导出字段自定义选择 -- 支持导出模板配置 -- 导出操作日志记录 - -#### 7.3 数据集成支持 -**需求描述**:需要支持通过数据集成方式,来获取相应业务数据 - -**功能要求**: -- 支持与教务系统的数据同步 -- 支持与其他业务系统的数据交换 -- 数据同步规则配置 -- 数据冲突处理机制 -- 同步日志和监控 - -### 8. 系统管理功能 - -#### 8.1 权限管理 -**功能要求**: -- 基于角色的权限控制 -- 数据访问权限细粒度管理 -- 权限变更审批流程 -- 权限使用情况监控 - -#### 8.2 系统配置 -**功能要求**: -- 字典数据管理 -- 系统参数配置 -- 业务规则配置 -- 界面个性化设置 - -## 📋 非功能性需求 - -### 1. 性能要求 -- 系统响应时间:页面加载时间不超过3秒 -- 并发用户数:支持1000个并发用户同时访问 -- 数据处理能力:支持10万条学生数据的快速查询和统计 -- 批量操作:支持5000条数据的批量导入导出 - -### 2. 安全要求 -- 数据加密:银行卡等敏感信息必须加密存储 -- 访问控制:基于角色的权限控制,确保数据安全 -- 审计日志:完整的操作日志记录和审计追踪 -- 数据备份:定期数据备份和恢复机制 - -### 3. 可用性要求 -- 系统可用性:99.5%以上的系统可用率 -- 界面友好:简洁直观的用户界面设计 -- 操作便捷:符合用户习惯的操作流程 -- 帮助支持:完善的帮助文档和在线支持 - -### 4. 兼容性要求 -- 浏览器兼容:支持主流浏览器(Chrome、Firefox、Safari、Edge) -- 移动端支持:支持移动端访问和操作 -- 系统集成:支持与其他校园系统的集成 -- 数据格式:支持主流数据格式的导入导出 - -## 🔄 业务流程设计 - -### 1. 信息采集流程 -``` -批次创建 → 权限配置 → 学生填写 → 信息提交 → 多级审核 → 审核完成 → 数据归档 -``` - -### 2. 审核流程 -``` -学生提交 → 辅导员初审 → 二级学院复审 → 学生处终审 → 审核完成 - ↓ ↓ ↓ - 驳回修改 驳回修改 驳回修改 -``` - -### 3. 数据统计流程 -``` -条件设置 → 数据查询 → 统计分析 → 结果展示 → 报表导出 -``` - -## 📊 数据需求分析 - -### 1. 核心数据实体 -- **学生信息**:学生基本信息、学籍信息、联系信息、家庭信息等 -- **班级信息**:班级基础信息、人员配置、统计数据等 -- **批次信息**:采集批次、配置信息、进度数据等 -- **审核信息**:审核记录、审核意见、状态变更等 - -### 2. 数据关系设计 -- 学生与班级:多对一关系 -- 学生与批次:多对多关系 -- 审核与学生:一对多关系 -- 用户与权限:多对多关系 - -### 3. 数据安全要求 -- 敏感信息加密存储 -- 数据访问权限控制 -- 数据变更审计日志 -- 数据备份和恢复 - -## 🎯 验收标准 - -### 1. 功能验收 -- 所有功能模块按需求正常运行 -- 用户角色权限控制正确 -- 数据统计分析准确 -- 批量操作功能稳定 - -### 2. 性能验收 -- 系统响应时间符合要求 -- 并发用户数达到指标 -- 数据处理能力满足需求 -- 系统稳定性良好 - -### 3. 安全验收 -- 数据加密存储正确 -- 权限控制有效 -- 审计日志完整 -- 数据备份可靠 - -### 4. 用户验收 -- 界面友好易用 -- 操作流程顺畅 -- 帮助文档完善 -- 用户培训到位 - -## 📅 项目计划 - -### 第一阶段:基础功能开发(4周) -- 学生基本信息管理 -- 班级信息管理 -- 基础的增删改查功能 - -### 第二阶段:业务流程开发(4周) -- 信息采集批次管理 -- 多级审核流程管理 -- 高级搜索与查询 - -### 第三阶段:数据分析与展示(3周) -- 数据统计分析 -- 批量数据操作 -- 多视图展示与权限管理 - -### 第四阶段:测试与部署(2周) -- 功能测试 -- 性能测试 -- 用户验收测试 -- 系统部署上线 - -## 📝 风险评估 - -### 1. 技术风险 -- 数据量大导致的性能问题 -- 复杂权限控制的实现难度 -- 多系统集成的技术挑战 - -### 2. 业务风险 -- 需求变更导致的开发延期 -- 用户培训不足影响使用效果 -- 数据迁移可能出现的问题 - -### 3. 风险应对措施 -- 提前进行性能测试和优化 -- 采用成熟的权限管理框架 -- 制定详细的集成测试方案 -- 建立需求变更管理流程 - ---- - -## 📞 联系信息 - -**项目负责人**:产品团队 -**技术负责人**:开发团队 -**创建时间**:2025-01-14 -**文档版本**:V1.0.0 - -**备注**:本文档将根据项目进展和需求变化进行持续更新和完善。 \ No newline at end of file diff --git a/mobile/package.json b/mobile/package.json index eedf7efa..b2d21a2a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -73,6 +73,7 @@ "@vue/shared": "^3.0.0", "core-js": "^3.8.3", "crypto-js": "^4.2.0", + "dayjs": "^1.11.18", "flyio": "^0.6.2", "jsencrypt": "^3.3.2", "uview-ui": "2.0.38", diff --git a/mobile/src/App.vue b/mobile/src/App.vue index f7bcf2db..c7cf51d8 100644 --- a/mobile/src/App.vue +++ b/mobile/src/App.vue @@ -150,6 +150,41 @@ page { border-color: #007dfb !important; } +/* Tabs组件主题色覆盖 - 全局统一底部方块颜色 */ +.u-tabs__wrapper__nav__line { + background-color: #0051d9 !important; +} + +.u-tabs .u-tabs__wrapper__nav__line { + background: #0051d9 !important; +} + +/* Navbar导航栏主题色覆盖 - 勤工俭学模块统一背景色 */ +.u-navbar { + background-color: #0051d9 !important; +} + +.u-navbar__content { + background-color: #0051d9 !important; +} + +.u-navbar__content__title { + color: #fff !important; +} + +/* uni-app 原生导航栏样式覆盖 */ +uni-page-head { + background-color: #0051d9 !important; +} + +.uni-page-head { + background-color: #0051d9 !important; +} + +.uni-page-head__title { + color: #fff !important; +} + /* ========== 全局温暖商务风格样式 ========== */ /* 全局页面背景 */ diff --git a/mobile/src/api/system/dict/data.js b/mobile/src/api/system/dict/data.js new file mode 100644 index 00000000..8a4b10e1 --- /dev/null +++ b/mobile/src/api/system/dict/data.js @@ -0,0 +1,35 @@ +// 字典数据API - 重新导出common/dict.js中的方法以保持兼容性 +import { + getDicts, + getDict, + useDict, + getDictLabel, + getDictValue, + hasDictValue, + refreshDict, + getDictTagType, +} from '@/common/dict.js'; + +// 重新导出所有字典相关方法 +export { + getDicts, + getDict, + useDict, + getDictLabel, + getDictValue, + hasDictValue, + refreshDict, + getDictTagType, +}; + +// 默认导出 +export default { + getDicts, + getDict, + useDict, + getDictLabel, + getDictValue, + hasDictValue, + refreshDict, + getDictTagType, +}; diff --git a/mobile/src/common/README-dict.md b/mobile/src/common/README-dict.md deleted file mode 100644 index fab362b2..00000000 --- a/mobile/src/common/README-dict.md +++ /dev/null @@ -1,200 +0,0 @@ -# 移动端字典工具使用指南 - -## 📋 简介 - -为移动端(Vue 2.6.14 + uni-app 2.x)提供完整的字典数据获取和管理功能,与web端保持一致的API设计。 - -## 🚀 快速开始 - -### 1. 基础导入 -```javascript -import { useDict, getDict, getDictLabel } from '@/common/dict.js' -``` - -### 2. 在页面中使用 -```javascript -export default { - data() { - return { - dictionaries: {}, - userInfo: { - gender: '0', - status: '1' - } - } - }, - - created() { - this.loadDictionaries() - }, - - methods: { - async loadDictionaries() { - // 批量加载字典 - const dictManager = useDict('sys_user_sex', 'sys_normal_disable') - - // Vue 2响应式更新(必须设置) - dictManager.setUpdateCallback((dictType, data) => { - this.$set(this.dictionaries, dictType, data) - }) - }, - - // 获取字典标签显示 - getDictText(dictType, value) { - const dictData = this.dictionaries[dictType] || [] - return getDictLabel(dictData, value) - } - } -} -``` - -### 3. 在模板中显示 -```vue - -``` - -## 📚 主要功能 - -| 功能 | 方法 | 说明 | -|------|------|------| -| 批量字典管理 | `useDict(...types)` | 推荐使用,支持多字典并发加载 | -| 单个字典获取 | `getDict(type, cache?)` | 获取单个字典数据 | -| 字典标签获取 | `getDictLabel(data, value)` | 根据值获取标签 | -| 字典值获取 | `getDictValue(data, label)` | 根据标签获取值 | -| 值存在检查 | `hasDictValue(data, value)` | 检查值是否存在 | -| 缓存刷新 | `refreshDict(types)` | 刷新指定字典缓存 | - -## 🎯 使用场景 - -### 场景1: uview组件集成 -```javascript -// radio-group - - - {{ item.label }} - - - -// select组件 -computed: { - selectOptions() { - return (this.dictionaries.sys_user_sex || []).map(item => ({ - value: item.value, - label: item.label - })) - } -} -``` - -### 场景2: 表格数据转换 -```javascript -computed: { - processedData() { - return this.tableData.map(row => ({ - ...row, - genderText: getDictLabel(this.dictionaries.sys_user_sex || [], row.gender), - statusText: getDictLabel(this.dictionaries.sys_normal_disable || [], row.status) - })) - } -} -``` - -### 场景3: 单独获取字典 -```javascript -async methods: { - async loadSingleDict() { - try { - const genderDict = await getDict('sys_user_sex') - console.log('性别字典:', genderDict) - } catch (error) { - console.error('加载失败:', error) - } - } -} -``` - -## ⚡ 性能特性 - -- **智能缓存**: 24小时有效期,自动管理 -- **并发加载**: 多字典同时请求,提升加载速度 -- **响应式更新**: 完美适配Vue 2响应式系统 -- **错误处理**: 完善的错误处理和兜底机制 - -## 🔧 高级用法 - -### 缓存管理 -```javascript -import { dictCache, refreshDict } from '@/common/dict.js' - -// 清除指定缓存 -dictCache.remove('sys_user_sex') - -// 清除所有缓存 -dictCache.clear() - -// 刷新缓存 -await refreshDict(['sys_user_sex', 'sys_normal_disable']) -``` - -### 错误处理 -```javascript -// 安全的字典获取(带默认值) -safeDictLabel(dictType, value, defaultLabel = '-') { - try { - const dictData = this.dictionaries[dictType] || [] - const label = getDictLabel(dictData, value) - return label || defaultLabel - } catch (error) { - console.error(`获取字典失败 ${dictType}:${value}`, error) - return defaultLabel - } -} -``` - -## 📖 相关文件 - -- **核心实现**: `/mobile/src/common/dict.js` -- **使用示例**: `/mobile/src/common/dict-usage-example.js` -- **变更记录**: `/docs/变更管理/code-adjust-record-2025-01-27-移动端字典工具实现.md` - -## ⚠️ 重要提醒 - -### Vue 2必须使用回调机制 -```javascript ❌ -// 错误:直接赋值不会触发响应式更新 -this.dictionaries = dictManager.data - -// 正确:使用回调确保响应式更新 ✅ -dictManager.setUpdateCallback((dictType, data) => { - this.$set(this.dictionaries, dictType, data) -}) -``` - -### 兼容性 -- ✅ Vue 2.6.14+ -- ✅ uni-app 2.x -- ✅ H5 / 小程序 / App - -## 🎯 最佳实践 - -1. **优先使用useDict**: 支持并发加载,性能更好 -2. **合理使用缓存**: 频繁变动的字典设置`useCache=false` -3. **添加错误处理**: 关键业务场景要有异常处理 -4. **使用callback机制**: Vue 2中必须使用setUpdateCallback - ---- -✨ **完成时间**: 2025-01-27 20:15:00 -👨‍💻 **开发者**: wuzilong -📱 **适用版本**: Vue 2.6.14 + uni-app 2.x \ No newline at end of file diff --git a/mobile/src/common/config.js b/mobile/src/common/config.js index 85839def..af11a20f 100644 --- a/mobile/src/common/config.js +++ b/mobile/src/common/config.js @@ -12,6 +12,7 @@ export const ENCRYPT_CONFIG = { TENANT_ID: '000000', // 加密请求头 ENCRYPT_HEADER: 'encrypt-key', + //TODO 开发环境需要修改为false // 是否启用加密 ENCRYPT_ENABLED: true, }; @@ -36,8 +37,9 @@ export const REQUEST_CONFIG = { ? 'http://localhost/login?redirect=/index' : 'https://hzdyjs.camasort.com/login?redirect=/index', // 生产环境前端域名 + //TODO 生产环境需要修改为true // 是否启用加密 - encryptEnabled: true, + encryptEnabled: false, // 防重复提交间隔时间(ms) repeatSubmitInterval: 500, }; diff --git a/mobile/src/components/Process/approvalRecord.vue b/mobile/src/components/Process/approvalRecord.vue index 9f5074c4..fffb2380 100644 --- a/mobile/src/components/Process/approvalRecord.vue +++ b/mobile/src/components/Process/approvalRecord.vue @@ -73,18 +73,36 @@ - - - - + --> + + + + + + + + + + + + + + +
{{ menu.title }} - {{ menu.description }} + 开发中
@@ -946,24 +946,36 @@ onMounted(async () => { } .menu-card-wrapper { - flex: 0 0 calc(20% - 16px); // 每行5个,减去间隔 - min-width: 180px; + flex: 0 0 calc((100% - 140px) / 8); // 每行8个,精确计算:(100% - 7个间隔*20px) / 8 + min-width: 100px; margin-bottom: 20px; + @media (max-width: 1600px) { + flex: 0 0 calc((100% - 120px) / 7); // 中大屏幕每行7个 + } + @media (max-width: 1400px) { - flex: 0 0 calc(25% - 15px); // 中等屏幕每行4个 + flex: 0 0 calc((100% - 100px) / 6); // 中等屏幕每行6个 } @media (max-width: 1200px) { - flex: 0 0 calc(33.333% - 14px); // 小屏幕每行3个 + flex: 0 0 calc((100% - 80px) / 5); // 小屏幕每行5个 + } + + @media (max-width: 992px) { + flex: 0 0 calc((100% - 60px) / 4); // 平板每行4个 } @media (max-width: 768px) { - flex: 0 0 calc(50% - 10px); // 移动端每行2个 + flex: 0 0 calc((100% - 40px) / 3); // 移动端每行3个 + } + + @media (max-width: 600px) { + flex: 0 0 calc((100% - 20px) / 2); // 小手机每行2个 } @media (max-width: 480px) { - flex: 0 0 100%; // 超小屏幕每行1个 + flex: 0 0 calc(100% - 20px); // 超小屏幕每行1个 } } @@ -972,7 +984,7 @@ onMounted(async () => { border: none; transition: all 0.3s ease; cursor: pointer; - height: 140px; + height: 110px; display: flex; align-items: center; justify-content: center; @@ -995,7 +1007,7 @@ onMounted(async () => { .menu-action { text-align: center; - padding: 15px 10px; + padding: 10px 8px; width: 100%; } @@ -1011,15 +1023,15 @@ onMounted(async () => { display: flex; justify-content: center; align-items: center; - margin-bottom: 12px; - height: 48px; + margin-bottom: 8px; + height: 36px; } .menu-logo { - width: 48px; - height: 48px; + width: 36px; + height: 36px; object-fit: contain; - border-radius: 8px; + border-radius: 6px; transition: all 0.3s ease; filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1)); @@ -1041,16 +1053,16 @@ onMounted(async () => { } .action-title { - font-size: 15px; + font-size: 14px; font-weight: 600; color: #333; - margin-bottom: 6px; + margin-bottom: 4px; display: block; line-height: 1.3; } .action-desc { - font-size: 12px; + font-size: 11px; color: #666; display: block; line-height: 1.4; @@ -1140,29 +1152,29 @@ onMounted(async () => { } .menu-action-card { - height: 120px; + height: 100px; } .menu-action { - padding: 10px 5px; + padding: 8px 5px; } .menu-logo-wrapper { - height: 36px; - margin-bottom: 8px; + height: 30px; + margin-bottom: 6px; } .menu-logo { - width: 36px; - height: 36px; + width: 30px; + height: 30px; } .action-title { - font-size: 14px; + font-size: 13px; } .action-desc { - font-size: 11px; + font-size: 10px; } .loading-state { diff --git a/web/src/views/jcbt/poverty/Application/index.vue b/web/src/views/jcbt/poverty/Application/index.vue index 8b34ce59..2b545968 100644 --- a/web/src/views/jcbt/poverty/Application/index.vue +++ b/web/src/views/jcbt/poverty/Application/index.vue @@ -99,21 +99,14 @@ - - - + - - - - - - + @@ -138,8 +131,7 @@ - - + @@ -295,9 +287,7 @@ - - - + @@ -379,8 +369,7 @@ - - + @@ -390,9 +379,7 @@ - - - + @@ -549,7 +536,7 @@ + + + diff --git a/web/src/views/rs/overseas-registration/index.vue b/web/src/views/rs/overseas-registration/index.vue new file mode 100644 index 00000000..1b898610 --- /dev/null +++ b/web/src/views/rs/overseas-registration/index.vue @@ -0,0 +1,1399 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/rs/overseas/approval.vue b/web/src/views/rs/overseas/approval.vue index b8720305..b8171c2c 100644 --- a/web/src/views/rs/overseas/approval.vue +++ b/web/src/views/rs/overseas/approval.vue @@ -1,279 +1,302 @@