wuzhen 1 год назад
Родитель
Сommit
7173f86bdf
20 измененных файлов с 3322 добавлено и 2 удалено
  1. 24 1
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java
  2. 238 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipMonthlyController.java
  3. 7 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java
  4. 191 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipMonthly.java
  5. 147 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipMonthlyAddBo.java
  6. 61 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipMonthlyQueryBo.java
  7. 253 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/vo/ZsjyInternshipMonthlyVo.java
  8. 82 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipMonthlyMapper.java
  9. 7 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java
  10. 137 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipMonthlyService.java
  11. 49 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java
  12. 465 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipMonthlyServiceImpl.java
  13. 240 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml
  14. 11 0
      mobile/src/api/internship.js
  15. 1 1
      mobile/src/pages/index/index.vue
  16. 11 0
      web/src/api/zsjy/internship.ts
  17. 102 0
      web/src/api/zsjy/internshipMonthly.ts
  18. 318 0
      web/src/views/zsjy/internship/pmonthly/detail.vue
  19. 590 0
      web/src/views/zsjy/internship/pmonthly/form.vue
  20. 388 0
      web/src/views/zsjy/internship/pmonthly/index.vue

+ 24 - 1
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java

@@ -157,7 +157,7 @@ public class InternshipInfoController extends BaseController {
     }
 
     /**
-     * 修改实习信息
+     * 33
      */
     @Operation(summary = "修改实习信息", description = "修改学生实习信息")
     @SaCheckPermission("zsjy:internship:edit")
@@ -202,4 +202,27 @@ public class InternshipInfoController extends BaseController {
             return R.fail("删除实习信息失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 获取当前学生的实习信息
+     */
+    @Operation(summary = "获取当前学生实习信息", description = "获取当前登录学生的实习信息,用于月度总结等功能")
+    @SaCheckPermission("zsjy:internship:query")
+    @GetMapping("/current")
+    public R<InternshipInfoVO> getCurrentStudentInternship() {
+        try {
+            log.info("=== 获取当前学生实习信息接口被调用 ===");
+            InternshipInfoVO result = internshipInfoService.getCurrentStudentInternship();
+            if (result != null) {
+                log.info("成功获取当前学生实习信息,实习ID:{}", result.getInternshipId());
+                return R.ok(result);
+            } else {
+                log.info("当前学生没有实习信息");
+                return R.ok(); // 返回空数据,前端需要处理这种情况
+            }
+        } catch (Exception e) {
+            log.error("获取当前学生实习信息失败:{}", e.getMessage(), e);
+            return R.fail("获取实习信息失败:" + e.getMessage());
+        }
+    }
 }

+ 238 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipMonthlyController.java

@@ -0,0 +1,238 @@
+package org.dromara.zsjy.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+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.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyAddBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipMonthlyVo;
+import org.dromara.zsjy.service.IZsjyInternshipMonthlyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 学生月度实习总结Controller
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/zsjy/internship/monthly")
+@Tag(name = "学生月度实习总结", description = "学生月度实习总结填写管理")
+public class ZsjyInternshipMonthlyController extends BaseController {
+
+    private final IZsjyInternshipMonthlyService monthlyService;
+
+    /**
+     * 查询学生月度实习总结列表
+     */
+    @Operation(summary = "查询学生月度实习总结列表", description = "学生查看自己的月度总结列表")
+    @SaCheckPermission("zsjy:internshipmonthly:list")
+    @GetMapping("/list")
+    public TableDataInfo<ZsjyInternshipMonthlyVo> list(@Validated ZsjyInternshipMonthlyQueryBo queryBo, PageQuery pageQuery) {
+        log.info("=== Controller层调试 ===");
+        log.info("接口被调用: /zsjy/internship/monthly/list");
+        log.info("查询参数: {}", queryBo);
+        log.info("分页参数: {}", pageQuery);
+
+        return monthlyService.queryPageList(queryBo, pageQuery);
+    }
+
+    /**
+     * 查询学生月度实习总结详细信息
+     */
+    @Operation(summary = "查询学生月度实习总结详细信息", description = "根据ID查询月度总结详情")
+    @SaCheckPermission("zsjy:internshipmonthly:query")
+    @GetMapping("/{monthlyId}")
+    public R<ZsjyInternshipMonthlyVo> getInfo(@Parameter(description = "月度总结ID")
+                                             @PathVariable Long monthlyId) {
+        log.info("查询月度总结详情, ID: {}", monthlyId);
+        return R.ok(monthlyService.queryById(monthlyId));
+    }
+
+    /**
+     * 新增学生月度实习总结
+     */
+    @Operation(summary = "新增学生月度实习总结", description = "学生创建新的月度总结")
+    @SaCheckPermission("zsjy:internshipmonthly:add")
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody ZsjyInternshipMonthlyAddBo addBo) {
+        log.info("新增月度总结: {}", addBo);
+
+        if (!monthlyService.insertByBo(addBo)) {
+            return R.fail("新增月度总结失败");
+        }
+        return R.ok();
+    }
+
+    /**
+     * 修改学生月度实习总结
+     */
+    @Operation(summary = "修改学生月度实习总结", description = "学生编辑自己的月度总结")
+    @SaCheckPermission("zsjy:internshipmonthly:edit")
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ZsjyInternshipMonthlyAddBo addBo) {
+        log.info("修改月度总结: {}", addBo);
+
+        if (!monthlyService.updateByBo(addBo)) {
+            return R.fail("修改月度总结失败");
+        }
+        return R.ok();
+    }
+
+    /**
+     * 删除学生月度实习总结
+     */
+    @Operation(summary = "删除学生月度实习总结", description = "学生删除自己的草稿状态月度总结")
+    @SaCheckPermission("zsjy:internshipmonthly:remove")
+    @DeleteMapping("/{monthlyIds}")
+    public R<Void> remove(@Parameter(description = "月度总结ID串")
+                         @PathVariable Long[] monthlyIds) {
+        log.info("删除月度总结, IDs: {}", (Object) monthlyIds);
+
+        if (!monthlyService.deleteByIds(List.of(monthlyIds))) {
+            return R.fail("删除月度总结失败");
+        }
+        return R.ok();
+    }
+
+    /**
+     * 提交月度总结审批
+     */
+    @Operation(summary = "提交月度总结审批", description = "学生提交月度总结给班主任审批")
+    @SaCheckPermission("zsjy:internshipmonthly:submit")
+    @PutMapping("/submit/{monthlyId}")
+    public R<Void> submit(@Parameter(description = "月度总结ID")
+                         @PathVariable Long monthlyId) {
+        log.info("提交月度总结审批, ID: {}", monthlyId);
+
+        if (!monthlyService.submitForApproval(monthlyId)) {
+            return R.fail("提交月度总结失败");
+        }
+        return R.ok();
+    }
+
+    /**
+     * 自动保存草稿
+     */
+    @Operation(summary = "自动保存草稿", description = "学生编辑时自动保存草稿")
+    @SaCheckPermission("zsjy:internshipmonthly:autosave")
+    @PostMapping("/autosave")
+    public R<Void> autoSave(@RequestBody ZsjyInternshipMonthlyAddBo addBo) {
+        log.info("自动保存草稿: {}", addBo);
+
+        if (!monthlyService.autoSaveDraft(addBo)) {
+            return R.fail("自动保存失败");
+        }
+        return R.ok();
+    }
+
+    /**
+     * 检查月份是否存在总结
+     */
+    @Operation(summary = "检查月份是否存在总结", description = "检查学生某月是否已有总结记录")
+    @SaCheckPermission("zsjy:internshipmonthly:check")
+    @GetMapping("/check/{summaryMonth}")
+    public R<Boolean> checkMonth(@Parameter(description = "总结月份")
+                                @PathVariable String summaryMonth,
+                                @Parameter(description = "排除的总结ID")
+                                @RequestParam(required = false) Long monthlyId) {
+        log.info("检查月份总结, 月份: {}, 排除ID: {}", summaryMonth, monthlyId);
+
+        Boolean exists = monthlyService.checkMonthExists(null, summaryMonth, monthlyId);
+        return R.ok(exists);
+    }
+
+    /**
+     * 获取月度总结模板
+     */
+    @Operation(summary = "获取月度总结模板", description = "获取指定月份的总结模板信息")
+    @SaCheckPermission("zsjy:internshipmonthly:template")
+    @GetMapping("/template/{summaryMonth}")
+    public R<ZsjyInternshipMonthlyVo> getTemplate(@Parameter(description = "总结月份")
+                                                 @PathVariable String summaryMonth) {
+        log.info("获取月度总结模板, 月份: {}", summaryMonth);
+
+        ZsjyInternshipMonthlyVo template = monthlyService.getMonthlyTemplate(summaryMonth);
+        return R.ok(template);
+    }
+
+    /**
+     * 查询学生指定实习的月度总结列表
+     */
+    @Operation(summary = "查询学生指定实习的月度总结列表", description = "查询学生某个实习的所有月度总结")
+    @SaCheckPermission("zsjy:internshipmonthly:list")
+    @GetMapping("/internship/{internshipId}")
+    public R<List<ZsjyInternshipMonthlyVo>> listByInternship(@Parameter(description = "实习ID")
+                                                            @PathVariable Long internshipId) {
+        log.info("查询实习月度总结列表, 实习ID: {}", internshipId);
+
+        List<ZsjyInternshipMonthlyVo> list = monthlyService.queryByStudentAndInternship(null, internshipId);
+        return R.ok(list);
+    }
+
+    /**
+     * 统计学生各状态的总结数量
+     */
+    @Operation(summary = "统计学生各状态的总结数量", description = "统计学生草稿、待审核、已审核等状态的总结数量")
+    @SaCheckPermission("zsjy:internshipmonthly:statistics")
+    @GetMapping("/statistics")
+    public R<List<Object>> statistics() {
+        log.info("统计学生月度总结状态");
+
+        List<Object> statistics = monthlyService.countByStatus(null);
+        return R.ok(statistics);
+    }
+
+    /**
+     * 获取学生当前实习信息
+     */
+    @Operation(summary = "获取学生当前实习信息", description = "获取当前登录学生的进行中实习信息")
+    @SaCheckPermission("zsjy:internshipmonthly:add")
+    @GetMapping("/current-internship")
+    public R<Map<String, Object>> getCurrentInternship() {
+        log.info("获取学生当前实习信息");
+
+        Map<String, Object> internshipInfo = monthlyService.getCurrentInternshipInfo();
+        if (internshipInfo == null || internshipInfo.isEmpty()) {
+            return R.fail("您当前没有进行中的实习记录");
+        }
+
+        return R.ok(internshipInfo);
+    }
+
+    /**
+     * 导出学生月度实习总结
+     */
+    @Operation(summary = "导出学生月度实习总结", description = "导出学生的月度总结数据")
+    @SaCheckPermission("zsjy:internshipmonthly:export")
+    @Log(title = "学生月度实习总结", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(@RequestBody ZsjyInternshipMonthlyQueryBo queryBo, HttpServletResponse response) {
+        log.info("导出月度总结: {}", queryBo);
+
+        List<ZsjyInternshipMonthlyVo> list = monthlyService.queryList(queryBo);
+        ExcelUtil.exportExcel(list, "学生月度实习总结", ZsjyInternshipMonthlyVo.class, response);
+    }
+
+}

+ 7 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipInfo.java

@@ -1,6 +1,7 @@
 package org.dromara.zsjy.domain;
 
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -159,5 +160,11 @@ public class ZsjyInternshipInfo extends BaseEntity {
      * 实习状态(1进行中 2已完成 3中断)
      */
     private String internshipStatus;
+
+    /**
+     * 删除标志(0代表存在 1代表删除)
+     */
+    @TableLogic(value = "0", delval = "1")
+    private Long deleted;
 }
  

+ 191 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipMonthly.java

@@ -0,0 +1,191 @@
+package org.dromara.zsjy.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 学生月度实习总结实体对象 zsjy_internship_monthly
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("zsjy_internship_monthly")
+public class ZsjyInternshipMonthly extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 月度总结ID (对应数据库monthly_id字段)
+     */
+    @TableId(value = "monthly_id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * 租户ID
+     */
+    @TableField("tenant_id")
+    private String tenantId;
+
+    /**
+     * 实习ID
+     */
+    @TableField("internship_id")
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @TableField("student_id")
+    private Long studentId;
+
+    /**
+     * 总结月份 (格式: 2025-01)
+     */
+    @TableField("summary_month")
+    private String summaryMonth;
+
+    /**
+     * 月份序号 (1-12)
+     */
+    @TableField("month_number")
+    private Integer monthNumber;
+
+    /**
+     * 工作内容 (必填,500字以内)
+     */
+    @TableField("work_content")
+    private String workContent;
+
+    /**
+     * 主要成果
+     */
+    @TableField("achievements")
+    private String achievements;
+
+    /**
+     * 学习收获
+     */
+    @TableField("learning_gains")
+    private String learningGains;
+
+    /**
+     * 遇到的问题
+     */
+    @TableField("problems_encountered")
+    private String problemsEncountered;
+
+    /**
+     * 自我评价
+     */
+    @TableField("self_evaluation")
+    private String selfEvaluation;
+
+    /**
+     * 下月工作计划
+     */
+    @TableField("next_month_plan")
+    private String nextMonthPlan;
+
+    /**
+     * 专业技能自评 (1-10分)
+     */
+    @TableField("professional_skills")
+    private Integer professionalSkills;
+
+    /**
+     * 沟通技能自评 (1-10分)
+     */
+    @TableField("communication_skills")
+    private Integer communicationSkills;
+
+    /**
+     * 团队协作自评 (1-10分) - 修正字段映射
+     */
+    @TableField("teamwork_ability")
+    private Integer teamworkAbility;
+
+    /**
+     * 学习能力自评 (1-10分)
+     */
+    @TableField("learning_ability")
+    private Integer learningAbility;
+
+    /**
+     * 工作态度自评 (1-10分)
+     */
+    @TableField("work_attitude")
+    private Integer workAttitude;
+
+    /**
+     * 审核状态 (0:待审核 1:已审核 2:审核拒绝 3:草稿) - 修正字段映射
+     */
+    @TableField("review_status")
+    private Integer reviewStatus;
+
+    /**
+     * 审核人ID - 修正字段映射
+     */
+    @TableField("review_by")
+    private Long reviewBy;
+
+    /**
+     * 审核时间
+     */
+    @TableField("review_time")
+    private Date reviewTime;
+
+    /**
+     * 审核评分 (百分制)
+     */
+    @TableField("review_score")
+    private BigDecimal reviewScore;
+
+    /**
+     * 审核意见 - 修正字段映射
+     */
+    @TableField("review_comment")
+    private String reviewComment;
+
+    /**
+     * 提交时间
+     */
+    @TableField("submit_time")
+    private Date submitTime;
+
+    /**
+     * 字数统计
+     */
+    @TableField("word_count")
+    private Integer wordCount;
+
+    /**
+     * 是否逾期 (0:未逾期 1:已逾期)
+     */
+    @TableField("is_overdue")
+    private Integer isOverdue;
+
+    /**
+     * 提交截止日期 - 修正字段映射
+     */
+    @TableField("submit_deadline")
+    private Date submitDeadline;
+
+    /**
+     * 附件信息 (JSON格式存储)
+     */
+    @TableField("attachments")
+    private String attachments;
+
+}

+ 147 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipMonthlyAddBo.java

@@ -0,0 +1,147 @@
+package org.dromara.zsjy.domain.bo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+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 jakarta.validation.constraints.*;
+import java.util.Date;
+
+/**
+ * 学生月度实习总结新增/编辑对象 ZsjyInternshipMonthlyAddBo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "学生月度实习总结新增/编辑对象")
+public class ZsjyInternshipMonthlyAddBo extends BaseEntity {
+
+    /**
+     * 月度总结ID
+     */
+    @Schema(description = "月度总结ID")
+    @NotNull(message = "月度总结ID不能为空", groups = { EditGroup.class })
+    private Long monthlyId;
+
+    /**
+     * 实习ID
+     */
+    @Schema(description = "实习ID")
+    @NotNull(message = "实习ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Long internshipId;
+
+    /**
+     * 总结月份 (格式: 2025-01)
+     */
+    @Schema(description = "总结月份")
+    @NotBlank(message = "总结月份不能为空", groups = { AddGroup.class, EditGroup.class })
+    @Pattern(regexp = "^\\d{4}-\\d{2}$", message = "月份格式应为 YYYY-MM", groups = { AddGroup.class, EditGroup.class })
+    private String summaryMonth;
+
+    /**
+     * 工作内容 (必填,500字以内)
+     */
+    @Schema(description = "工作内容")
+    @NotBlank(message = "工作内容不能为空", groups = { AddGroup.class, EditGroup.class })
+    @Size(max = 500, message = "工作内容不能超过500字", groups = { AddGroup.class, EditGroup.class })
+    private String workContent;
+
+    /**
+     * 主要成果
+     */
+    @Schema(description = "主要成果")
+    @Size(max = 500, message = "主要成果不能超过500字", groups = { AddGroup.class, EditGroup.class })
+    private String achievements;
+
+    /**
+     * 学习收获
+     */
+    @Schema(description = "学习收获")
+    @Size(max = 500, message = "学习收获不能超过500字", groups = { AddGroup.class, EditGroup.class })
+    private String learningGains;
+
+    /**
+     * 遇到的问题(包含解决方案,按2-A方案合并)
+     */
+    @Schema(description = "遇到的问题")
+    @Size(max = 1000, message = "遇到的问题不能超过1000字", groups = { AddGroup.class, EditGroup.class })
+    private String problemsEncountered;
+
+    /**
+     * 自我评价
+     */
+    @Schema(description = "自我评价")
+    @Size(max = 500, message = "自我评价不能超过500字", groups = { AddGroup.class, EditGroup.class })
+    private String selfEvaluation;
+
+    /**
+     * 下月工作计划
+     */
+    @Schema(description = "下月工作计划")
+    @Size(max = 500, message = "下月工作计划不能超过500字", groups = { AddGroup.class, EditGroup.class })
+    private String nextMonthPlan;
+
+    /**
+     * 专业技能自评 (1-10分)
+     */
+    @Schema(description = "专业技能自评")
+    @Min(value = 1, message = "专业技能自评最小值为1", groups = { AddGroup.class, EditGroup.class })
+    @Max(value = 10, message = "专业技能自评最大值为10", groups = { AddGroup.class, EditGroup.class })
+    private Integer professionalSkills;
+
+    /**
+     * 沟通技能自评 (1-10分)
+     */
+    @Schema(description = "沟通技能自评")
+    @Min(value = 1, message = "沟通技能自评最小值为1", groups = { AddGroup.class, EditGroup.class })
+    @Max(value = 10, message = "沟通技能自评最大值为10", groups = { AddGroup.class, EditGroup.class })
+    private Integer communicationSkills;
+
+    /**
+     * 团队协作自评 (1-10分) - 修正字段映射
+     */
+    @Schema(description = "团队协作自评")
+    @Min(value = 1, message = "团队协作自评最小值为1", groups = { AddGroup.class, EditGroup.class })
+    @Max(value = 10, message = "团队协作自评最大值为10", groups = { AddGroup.class, EditGroup.class })
+    private Integer teamworkAbility;
+
+    /**
+     * 学习能力自评 (1-10分)
+     */
+    @Schema(description = "学习能力自评")
+    @Min(value = 1, message = "学习能力自评最小值为1", groups = { AddGroup.class, EditGroup.class })
+    @Max(value = 10, message = "学习能力自评最大值为10", groups = { AddGroup.class, EditGroup.class })
+    private Integer learningAbility;
+
+    /**
+     * 工作态度自评 (1-10分)
+     */
+    @Schema(description = "工作态度自评")
+    @Min(value = 1, message = "工作态度自评最小值为1", groups = { AddGroup.class, EditGroup.class })
+    @Max(value = 10, message = "工作态度自评最大值为10", groups = { AddGroup.class, EditGroup.class })
+    private Integer workAttitude;
+
+    /**
+     * 附件信息 (JSON格式存储)
+     */
+    @Schema(description = "附件信息")
+    private String attachments;
+
+    /**
+     * 审核状态 (0-待审核 1-已审核 2-审核拒绝 3-草稿)
+     */
+    @Schema(description = "审核状态")
+    private Integer reviewStatus;
+
+    /**
+     * 提交时间
+     */
+    @Schema(description = "提交时间")
+    private Date submitTime;
+
+}

+ 61 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipMonthlyQueryBo.java

@@ -0,0 +1,61 @@
+package org.dromara.zsjy.domain.bo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+/**
+ * 学生月度实习总结查询对象 ZsjyInternshipMonthlyQueryBo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "学生月度实习总结查询对象")
+public class ZsjyInternshipMonthlyQueryBo extends BaseEntity {
+
+    /**
+     * 学生ID
+     */
+    @Schema(description = "学生ID")
+    private Long studentId;
+
+    /**
+     * 总结月份 (格式: 2025-01)
+     */
+    @Schema(description = "总结月份")
+    private String summaryMonth;
+
+    /**
+     * 审核状态 (0:待审核 1:已审核 2:审核拒绝 3:草稿)
+     */
+    @Schema(description = "审核状态")
+    private Integer status;
+
+    /**
+     * 实习ID
+     */
+    @Schema(description = "实习ID")
+    private Long internshipId;
+
+    /**
+     * 是否逾期 (0:未逾期 1:已逾期)
+     */
+    @Schema(description = "是否逾期")
+    private Integer isOverdue;
+
+    /**
+     * 开始月份
+     */
+    @Schema(description = "开始月份")
+    private String startMonth;
+
+    /**
+     * 结束月份
+     */
+    @Schema(description = "结束月份")
+    private String endMonth;
+
+}

+ 253 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/vo/ZsjyInternshipMonthlyVo.java

@@ -0,0 +1,253 @@
+package org.dromara.zsjy.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 学生月度实习总结视图对象 ZsjyInternshipMonthlyVo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@Schema(description = "学生月度实习总结视图对象")
+public class ZsjyInternshipMonthlyVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 月度总结ID
+     */
+    @Schema(description = "月度总结ID")
+    private Long monthlyId;
+
+    /**
+     * 实习ID
+     */
+    @Schema(description = "实习ID")
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @Schema(description = "学生ID")
+    private Long studentId;
+
+    /**
+     * 学生姓名 - 通过关联查询获取
+     */
+    @Schema(description = "学生姓名")
+    private String studentName;
+
+    /**
+     * 学生学号 - 通过关联查询获取
+     */
+    @Schema(description = "学生学号")
+    private String studentNumber;
+
+    /**
+     * 总结月份 (格式: 2025-01)
+     */
+    @Schema(description = "总结月份")
+    private String summaryMonth;
+
+    /**
+     * 月份序号 (1-12)
+     */
+    @Schema(description = "月份序号")
+    private Integer monthNumber;
+
+    /**
+     * 工作内容
+     */
+    @Schema(description = "工作内容")
+    private String workContent;
+
+    /**
+     * 主要成果
+     */
+    @Schema(description = "主要成果")
+    private String achievements;
+
+    /**
+     * 学习收获
+     */
+    @Schema(description = "学习收获")
+    private String learningGains;
+
+    /**
+     * 遇到的问题(包含解决方案,按2-A方案合并)
+     */
+    @Schema(description = "遇到的问题")
+    private String problemsEncountered;
+
+    /**
+     * 自我评价
+     */
+    @Schema(description = "自我评价")
+    private String selfEvaluation;
+
+    /**
+     * 下月工作计划
+     */
+    @Schema(description = "下月工作计划")
+    private String nextMonthPlan;
+
+    /**
+     * 专业技能自评 (1-10分)
+     */
+    @Schema(description = "专业技能自评")
+    private Integer professionalSkills;
+
+    /**
+     * 沟通技能自评 (1-10分)
+     */
+    @Schema(description = "沟通技能自评")
+    private Integer communicationSkills;
+
+    /**
+     * 团队协作自评 (1-10分) - 修正字段映射
+     */
+    @Schema(description = "团队协作自评")
+    private Integer teamworkAbility;
+
+    /**
+     * 学习能力自评 (1-10分)
+     */
+    @Schema(description = "学习能力自评")
+    private Integer learningAbility;
+
+    /**
+     * 工作态度自评 (1-10分)
+     */
+    @Schema(description = "工作态度自评")
+    private Integer workAttitude;
+
+    /**
+     * 审核状态 (0:待审核 1:已审核 2:审核拒绝 3:草稿) - 修正字段映射
+     */
+    @Schema(description = "审核状态")
+    private Integer reviewStatus;
+
+    /**
+     * 状态文本描述
+     */
+    @Schema(description = "状态文本描述")
+    private String statusText;
+
+    /**
+     * 审核人ID - 修正字段映射
+     */
+    @Schema(description = "审核人ID")
+    private Long reviewBy;
+
+    /**
+     * 审核人姓名 - 通过关联查询获取
+     */
+    @Schema(description = "审核人姓名")
+    private String reviewerName;
+
+    /**
+     * 审核时间
+     */
+    @Schema(description = "审核时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date reviewTime;
+
+    /**
+     * 审核评分 (百分制)
+     */
+    @Schema(description = "审核评分")
+    private BigDecimal reviewScore;
+
+    /**
+     * 审核意见 - 修正字段映射
+     */
+    @Schema(description = "审核意见")
+    private String reviewComment;
+
+    /**
+     * 提交时间
+     */
+    @Schema(description = "提交时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date submitTime;
+
+    /**
+     * 字数统计
+     */
+    @Schema(description = "字数统计")
+    private Integer wordCount;
+
+    /**
+     * 是否逾期 (0:未逾期 1:已逾期)
+     */
+    @Schema(description = "是否逾期")
+    private Integer isOverdue;
+
+    /**
+     * 提交截止日期 - 修正字段映射
+     */
+    @Schema(description = "提交截止日期")
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date submitDeadline;
+
+    /**
+     * 附件信息
+     */
+    @Schema(description = "附件信息")
+    private String attachments;
+
+    /**
+     * 是否可编辑 (草稿状态才可编辑)
+     */
+    @Schema(description = "是否可编辑")
+    private Boolean editable;
+
+    /**
+     * 是否可删除 (草稿状态才可删除)
+     */
+    @Schema(description = "是否可删除")
+    private Boolean deletable;
+
+    /**
+     * 是否可提交 (草稿状态才可提交)
+     */
+    @Schema(description = "是否可提交")
+    private Boolean submittable;
+
+    // VO类基础字段规范 - 必须包含的四个字段
+    /**
+     * 创建者
+     */
+    @Schema(description = "创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新者
+     */
+    @Schema(description = "更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @Schema(description = "更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+}

+ 82 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipMonthlyMapper.java

@@ -0,0 +1,82 @@
+package org.dromara.zsjy.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.zsjy.domain.ZsjyInternshipMonthly;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipMonthlyVo;
+
+import java.util.List;
+
+/**
+ * 学生月度实习总结Mapper接口
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+public interface ZsjyInternshipMonthlyMapper extends BaseMapper<ZsjyInternshipMonthly> {
+
+    /**
+     * 查询学生月度实习总结
+     *
+     * @param monthlyId 月度总结ID
+     * @return 学生月度实习总结
+     */
+    ZsjyInternshipMonthlyVo selectVoById(Long monthlyId);
+
+    /**
+     * 查询学生月度实习总结列表
+     *
+     * @param queryBo 查询条件
+     * @return 学生月度实习总结集合
+     */
+    Page<ZsjyInternshipMonthlyVo> selectVoPage(Page<ZsjyInternshipMonthlyVo> page, @Param("bo") ZsjyInternshipMonthlyQueryBo queryBo);
+
+    /**
+     * 查询学生月度实习总结列表
+     *
+     * @param queryBo 查询条件
+     * @return 学生月度实习总结集合
+     */
+    List<ZsjyInternshipMonthlyVo> selectVoList(@Param("bo") ZsjyInternshipMonthlyQueryBo queryBo);
+
+    /**
+     * 检查学生某月是否已有总结
+     *
+     * @param studentId    学生ID
+     * @param summaryMonth 总结月份
+     * @param monthlyId    排除的总结ID(用于编辑时检查)
+     * @return 是否存在
+     */
+    boolean existsByStudentAndMonth(@Param("studentId") Long studentId,
+                                   @Param("summaryMonth") String summaryMonth,
+                                   @Param("monthlyId") Long monthlyId);
+
+    /**
+     * 批量删除学生月度实习总结
+     *
+     * @param monthlyIds 需要删除的数据ID
+     * @return 影响行数
+     */
+    int deleteByIds(@Param("monthlyIds") Long[] monthlyIds);
+
+    /**
+     * 查询学生指定实习的月度总结列表
+     *
+     * @param studentId    学生ID
+     * @param internshipId 实习ID
+     * @return 月度总结列表
+     */
+    List<ZsjyInternshipMonthlyVo> selectByStudentAndInternship(@Param("studentId") Long studentId,
+                                                              @Param("internshipId") Long internshipId);
+
+    /**
+     * 统计学生各状态的总结数量
+     *
+     * @param studentId 学生ID
+     * @return 状态统计结果
+     */
+    List<Object> countByStatus(@Param("studentId") Long studentId);
+
+}

+ 7 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IInternshipInfoService.java

@@ -98,4 +98,11 @@ public interface IInternshipInfoService {
      * @return 是否成功
      */
     Boolean deleteInternshipInfo(Long[] internshipIds);
+
+    /**
+     * 获取当前学生的实习信息
+     *
+     * @return 当前学生的实习信息,如果没有则返回null
+     */
+    InternshipInfoVO getCurrentStudentInternship();
 }

+ 137 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipMonthlyService.java

@@ -0,0 +1,137 @@
+package org.dromara.zsjy.service;
+
+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.zsjy.domain.bo.ZsjyInternshipMonthlyAddBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipMonthlyVo;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 学生月度实习总结Service接口
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+public interface IZsjyInternshipMonthlyService {
+
+    /**
+     * 查询学生月度实习总结
+     *
+     * @param monthlyId 月度总结ID
+     * @return 学生月度实习总结
+     */
+    ZsjyInternshipMonthlyVo queryById(Long monthlyId);
+
+    /**
+     * 查询学生月度实习总结列表
+     *
+     * @param queryBo   查询条件
+     * @param pageQuery 分页参数
+     * @return 学生月度实习总结分页结果
+     */
+    TableDataInfo<ZsjyInternshipMonthlyVo> queryPageList(ZsjyInternshipMonthlyQueryBo queryBo, PageQuery pageQuery);
+
+    /**
+     * 查询学生月度实习总结列表
+     *
+     * @param queryBo 查询条件
+     * @return 学生月度实习总结集合
+     */
+    List<ZsjyInternshipMonthlyVo> queryList(ZsjyInternshipMonthlyQueryBo queryBo);
+
+    /**
+     * 新增学生月度实习总结
+     *
+     * @param addBo 学生月度实习总结
+     * @return 新增结果
+     */
+    Boolean insertByBo(ZsjyInternshipMonthlyAddBo addBo);
+
+    /**
+     * 修改学生月度实习总结
+     *
+     * @param addBo 学生月度实习总结
+     * @return 修改结果
+     */
+    Boolean updateByBo(ZsjyInternshipMonthlyAddBo addBo);
+
+    /**
+     * 校验并批量删除学生月度实习总结信息
+     *
+     * @param ids 需要删除的学生月度实习总结ID
+     * @return 删除结果
+     */
+    Boolean deleteByIds(Collection<Long> ids);
+
+    /**
+     * 提交月度总结审批
+     *
+     * @param monthlyId 月度总结ID
+     * @return 提交结果
+     */
+    Boolean submitForApproval(Long monthlyId);
+
+    /**
+     * 自动保存草稿
+     *
+     * @param addBo 学生月度实习总结
+     * @return 保存结果
+     */
+    Boolean autoSaveDraft(ZsjyInternshipMonthlyAddBo addBo);
+
+    /**
+     * 检查学生某月是否已有总结
+     *
+     * @param studentId    学生ID
+     * @param summaryMonth 总结月份
+     * @param monthlyId    排除的总结ID(用于编辑时检查)
+     * @return 是否存在
+     */
+    Boolean checkMonthExists(Long studentId, String summaryMonth, Long monthlyId);
+
+    /**
+     * 获取月度总结模板信息
+     *
+     * @param summaryMonth 总结月份
+     * @return 模板信息
+     */
+    ZsjyInternshipMonthlyVo getMonthlyTemplate(String summaryMonth);
+
+    /**
+     * 查询学生指定实习的月度总结列表
+     *
+     * @param studentId    学生ID
+     * @param internshipId 实习ID
+     * @return 月度总结列表
+     */
+    List<ZsjyInternshipMonthlyVo> queryByStudentAndInternship(Long studentId, Long internshipId);
+
+    /**
+     * 统计学生各状态的总结数量
+     *
+     * @param studentId 学生ID
+     * @return 状态统计结果
+     */
+    List<Object> countByStatus(Long studentId);
+
+    /**
+     * 计算总结内容字数
+     *
+     * @param addBo 总结内容
+     * @return 字数统计
+     */
+    Integer calculateWordCount(ZsjyInternshipMonthlyAddBo addBo);
+
+    /**
+     * 获取学生当前实习信息
+     *
+     * @return 当前实习信息
+     */
+    Map<String, Object> getCurrentInternshipInfo();
+
+}

+ 49 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/InternshipInfoServiceImpl.java

@@ -459,4 +459,53 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService {
         }
         return studentInfo;
     }
+
+    /**
+     * 获取当前学生的实习信息
+     *
+     * @return 当前学生的实习信息,如果没有则返回null
+     */
+    @Override
+    public InternshipInfoVO getCurrentStudentInternship() {
+        log.info("开始获取当前学生的实习信息");
+        
+        // 获取当前登录用户ID
+        Long currentUserId = LoginHelper.getUserId();
+        log.info("当前登录用户ID:{}", currentUserId);
+        
+        if (currentUserId == null) {
+            log.error("当前用户未登录");
+            throw new ServiceException("用户未登录");
+        }
+        
+        // 根据用户ID查询实习信息
+        LambdaQueryWrapper<ZsjyInternshipInfo> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ZsjyInternshipInfo::getStudentId, currentUserId)
+               .eq(ZsjyInternshipInfo::getDeleted, 0)
+               .orderByDesc(ZsjyInternshipInfo::getCreateTime)
+               .last("LIMIT 1"); // 只取最新的一条记录
+        
+        ZsjyInternshipInfo internshipInfo = zsjyInternshipInfoMapper.selectOne(wrapper);
+        log.info("查询到的实习信息:{}", internshipInfo != null ? internshipInfo.getInternshipId() : "无");
+        
+        if (internshipInfo == null) {
+            log.info("当前学生没有实习信息");
+            return null;
+        }
+        
+        // 转换为VO
+        InternshipInfoVO vo = BeanUtil.toBean(internshipInfo, InternshipInfoVO.class);
+        
+        // 查询学生信息并设置到VO中
+        XgStudentMsgVo studentInfo = getStudentInfo(currentUserId);
+        vo.setStudentName(studentInfo.getName());
+        vo.setStudentNo(studentInfo.getStudentNo());
+        vo.setClassName(studentInfo.getClassName());
+        vo.setMajorName(studentInfo.getMajorName());
+        vo.setCollegeName(studentInfo.getCollegeName());
+        vo.setGrade(studentInfo.getGrade());
+        
+        log.info("成功获取当前学生实习信息,实习ID:{}", vo.getInternshipId());
+        return vo;
+    }
 }

+ 465 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipMonthlyServiceImpl.java

@@ -0,0 +1,465 @@
+package org.dromara.zsjy.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+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.zsjy.domain.ZsjyInternshipMonthly;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyAddBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipMonthlyQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipMonthlyVo;
+import org.dromara.zsjy.mapper.ZsjyInternshipMonthlyMapper;
+import org.dromara.zsjy.mapper.ZsjyInternshipInfoMapper;
+import org.dromara.zsjy.domain.ZsjyInternshipInfo;
+import org.dromara.zsjy.service.IZsjyInternshipMonthlyService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.YearMonth;
+import java.util.*;
+import java.time.ZoneId;
+
+/**
+ * 学生月度实习总结Service业务层处理
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ZsjyInternshipMonthlyServiceImpl implements IZsjyInternshipMonthlyService {
+
+    private final ZsjyInternshipMonthlyMapper baseMapper;
+    private final ZsjyInternshipInfoMapper internshipInfoMapper;
+
+    /**
+     * 查询学生月度实习总结
+     */
+    @Override
+    public ZsjyInternshipMonthlyVo queryById(Long monthlyId) {
+        ZsjyInternshipMonthlyVo vo = baseMapper.selectVoById(monthlyId);
+        if (ObjectUtil.isNull(vo)) {
+            throw new ServiceException("月度总结不存在");
+        }
+        return vo;
+    }
+
+    /**
+     * 查询学生月度实习总结分页列表
+     */
+    @Override
+    public TableDataInfo<ZsjyInternshipMonthlyVo> queryPageList(ZsjyInternshipMonthlyQueryBo queryBo, PageQuery pageQuery) {
+        // 如果没有指定学生ID,则使用当前登录用户ID(学生端)
+        if (ObjectUtil.isNull(queryBo.getStudentId())) {
+            queryBo.setStudentId(LoginHelper.getUserId());
+        }
+
+        Page<ZsjyInternshipMonthlyVo> result = baseMapper.selectVoPage(pageQuery.build(), queryBo);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询学生月度实习总结列表
+     */
+    @Override
+    public List<ZsjyInternshipMonthlyVo> queryList(ZsjyInternshipMonthlyQueryBo queryBo) {
+        // 如果没有指定学生ID,则使用当前登录用户ID(学生端)
+        if (ObjectUtil.isNull(queryBo.getStudentId())) {
+            queryBo.setStudentId(LoginHelper.getUserId());
+        }
+        return baseMapper.selectVoList(queryBo);
+    }
+
+    /**
+     * 新增学生月度实习总结
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean insertByBo(ZsjyInternshipMonthlyAddBo addBo) {
+        Long studentId = LoginHelper.getUserId();
+
+        // 如果前端没有传递实习ID,则自动获取当前学生的实习ID
+        if (ObjectUtil.isNull(addBo.getInternshipId())) {
+            ZsjyInternshipInfo internshipInfo = internshipInfoMapper.selectOne(
+                new LambdaQueryWrapper<ZsjyInternshipInfo>()
+                    .eq(ZsjyInternshipInfo::getStudentId, studentId)
+                    .eq(ZsjyInternshipInfo::getStatus, "1") // 查询进行中状态的实习
+                    .eq(ZsjyInternshipInfo::getDeleted, 0)
+                    .orderByDesc(ZsjyInternshipInfo::getCreateTime)
+                    .last("LIMIT 1")
+            );
+
+            if (ObjectUtil.isNull(internshipInfo)) {
+                throw new ServiceException("您当前没有进行中的实习记录,无法创建月度总结");
+            }
+
+            addBo.setInternshipId(internshipInfo.getInternshipId());
+            log.info("自动获取到学生实习ID:{}", internshipInfo.getInternshipId());
+        }
+
+        // 检查该学生该月份是否已有总结
+        if (checkMonthExists(studentId, addBo.getSummaryMonth(), null)) {
+            throw new ServiceException("该月份已存在总结记录,请勿重复创建");
+        }
+
+        // 手动转换BO到实体,解决字段映射问题
+        ZsjyInternshipMonthly add = new ZsjyInternshipMonthly();
+        add.setId(addBo.getMonthlyId()); // monthlyId -> id
+        add.setInternshipId(addBo.getInternshipId());
+        add.setSummaryMonth(addBo.getSummaryMonth());
+        add.setWorkContent(addBo.getWorkContent());
+        add.setAchievements(addBo.getAchievements());
+        add.setLearningGains(addBo.getLearningGains());
+        add.setProblemsEncountered(addBo.getProblemsEncountered());
+        add.setSelfEvaluation(addBo.getSelfEvaluation());
+        add.setNextMonthPlan(addBo.getNextMonthPlan());
+        add.setProfessionalSkills(addBo.getProfessionalSkills());
+        add.setCommunicationSkills(addBo.getCommunicationSkills());
+        add.setTeamworkAbility(addBo.getTeamworkAbility());
+        add.setLearningAbility(addBo.getLearningAbility());
+        add.setWorkAttitude(addBo.getWorkAttitude());
+        add.setAttachments(addBo.getAttachments());
+        
+        add.setStudentId(studentId);
+        add.setTenantId("000000"); // 默认租户
+
+        // 处理审核状态和提交时间
+        if (addBo.getReviewStatus() != null) {
+            add.setReviewStatus(addBo.getReviewStatus());
+            // 如果状态为待审核(0),设置提交时间
+            if (addBo.getReviewStatus() == 0) {
+                add.setSubmitTime(addBo.getSubmitTime() != null ? addBo.getSubmitTime() : new Date());
+            }
+        } else {
+            add.setReviewStatus(3); // 默认为草稿状态
+        }
+
+        // 解析月份序号
+        if (StringUtils.isNotBlank(addBo.getSummaryMonth())) {
+            String[] parts = addBo.getSummaryMonth().split("-");
+            if (parts.length == 2) {
+                add.setMonthNumber(Integer.parseInt(parts[1]));
+            }
+        }
+
+        // 计算字数
+        add.setWordCount(calculateWordCount(addBo));
+
+        // 设置默认截止日期(下月5号)
+        try {
+            YearMonth ym = YearMonth.parse(addBo.getSummaryMonth());
+            add.setSubmitDeadline(Date.from(ym.plusMonths(1).atDay(5).atStartOfDay(ZoneId.systemDefault()).toInstant()));
+        } catch (Exception e) {
+            log.warn("解析月份失败: {}", addBo.getSummaryMonth());
+        }
+
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            addBo.setMonthlyId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改学生月度实习总结
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateByBo(ZsjyInternshipMonthlyAddBo addBo) {
+        Long studentId = LoginHelper.getUserId();
+
+        // 查询原记录,验证权限和状态
+        ZsjyInternshipMonthly original = baseMapper.selectById(addBo.getMonthlyId());
+        if (ObjectUtil.isNull(original)) {
+            throw new ServiceException("月度总结不存在");
+        }
+
+        // 权限检查:只能修改自己的总结
+        if (!Objects.equals(original.getStudentId(), studentId)) {
+            throw new ServiceException("您只能修改自己的月度总结");
+        }
+
+        // 状态检查:只有草稿状态才能修改
+        if (!Objects.equals(original.getReviewStatus(), 3)) {
+            throw new ServiceException("只有草稿状态的总结才能修改");
+        }
+
+        // 如果前端没有传递实习ID,则使用原记录的实习ID
+        if (ObjectUtil.isNull(addBo.getInternshipId())) {
+            addBo.setInternshipId(original.getInternshipId());
+            log.info("使用原记录的实习ID:{}", original.getInternshipId());
+        }
+
+        // 检查月份是否被其他记录占用
+        if (!Objects.equals(original.getSummaryMonth(), addBo.getSummaryMonth())) {
+            if (checkMonthExists(studentId, addBo.getSummaryMonth(), addBo.getMonthlyId())) {
+                throw new ServiceException("该月份已存在其他总结记录");
+            }
+        }
+
+        // 手动转换BO到实体,解决字段映射问题
+        ZsjyInternshipMonthly update = new ZsjyInternshipMonthly();
+        update.setId(addBo.getMonthlyId()); // monthlyId -> id
+        update.setInternshipId(addBo.getInternshipId());
+        update.setSummaryMonth(addBo.getSummaryMonth());
+        update.setWorkContent(addBo.getWorkContent());
+        update.setAchievements(addBo.getAchievements());
+        update.setLearningGains(addBo.getLearningGains());
+        update.setProblemsEncountered(addBo.getProblemsEncountered());
+        update.setSelfEvaluation(addBo.getSelfEvaluation());
+        update.setNextMonthPlan(addBo.getNextMonthPlan());
+        update.setProfessionalSkills(addBo.getProfessionalSkills());
+        update.setCommunicationSkills(addBo.getCommunicationSkills());
+        update.setTeamworkAbility(addBo.getTeamworkAbility());
+        update.setLearningAbility(addBo.getLearningAbility());
+        update.setWorkAttitude(addBo.getWorkAttitude());
+        update.setAttachments(addBo.getAttachments());
+
+        // 处理审核状态和提交时间
+        if (addBo.getReviewStatus() != null) {
+            update.setReviewStatus(addBo.getReviewStatus());
+            // 如果状态从草稿(3)变为待审核(0),设置提交时间
+            if (addBo.getReviewStatus() == 0 && original.getReviewStatus() == 3) {
+                update.setSubmitTime(addBo.getSubmitTime() != null ? addBo.getSubmitTime() : new Date());
+            }
+            // 如果状态变回草稿(3),清空提交时间
+            else if (addBo.getReviewStatus() == 3) {
+                update.setSubmitTime(null);
+            }
+        }
+
+        // 解析月份序号
+        if (StringUtils.isNotBlank(addBo.getSummaryMonth())) {
+            String[] parts = addBo.getSummaryMonth().split("-");
+            if (parts.length == 2) {
+                update.setMonthNumber(Integer.parseInt(parts[1]));
+            }
+        }
+
+        // 重新计算字数
+        update.setWordCount(calculateWordCount(addBo));
+
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 批量删除学生月度实习总结
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteByIds(Collection<Long> ids) {
+        Long studentId = LoginHelper.getUserId();
+
+        // 验证权限:只能删除自己的草稿状态总结
+        for (Long id : ids) {
+            ZsjyInternshipMonthly monthly = baseMapper.selectById(id);
+            if (ObjectUtil.isNull(monthly)) {
+                continue;
+            }
+
+            if (!Objects.equals(monthly.getStudentId(), studentId)) {
+                throw new ServiceException("您只能删除自己的月度总结");
+            }
+
+            if (!Objects.equals(monthly.getReviewStatus(), 3)) {
+                throw new ServiceException("只有草稿状态的总结才能删除");
+            }
+        }
+
+        return baseMapper.deleteByIds(ids.toArray(new Long[0])) > 0;
+    }
+
+    /**
+     * 提交月度总结审批
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean submitForApproval(Long monthlyId) {
+        Long studentId = LoginHelper.getUserId();
+
+        ZsjyInternshipMonthly monthly = baseMapper.selectById(monthlyId);
+        if (ObjectUtil.isNull(monthly)) {
+            throw new ServiceException("月度总结不存在");
+        }
+
+        // 权限检查
+        if (!Objects.equals(monthly.getStudentId(), studentId)) {
+            throw new ServiceException("您只能提交自己的月度总结");
+        }
+
+        // 状态检查
+        if (!Objects.equals(monthly.getReviewStatus(), 3)) {
+            throw new ServiceException("只有草稿状态的总结才能提交");
+        }
+
+        // 必填字段检查
+        if (StringUtils.isBlank(monthly.getWorkContent())) {
+            throw new ServiceException("工作内容不能为空");
+        }
+
+        // 更新状态为待审核
+        ZsjyInternshipMonthly update = new ZsjyInternshipMonthly();
+        update.setId(monthlyId);
+        update.setReviewStatus(0); // 待审核
+        update.setSubmitTime(new Date());
+
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 自动保存草稿
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean autoSaveDraft(ZsjyInternshipMonthlyAddBo addBo) {
+        try {
+            if (ObjectUtil.isNull(addBo.getMonthlyId())) {
+                // 新增草稿
+                return insertByBo(addBo);
+            } else {
+                // 更新草稿
+                return updateByBo(addBo);
+            }
+        } catch (Exception e) {
+            log.warn("自动保存草稿失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 检查学生某月是否已有总结
+     */
+    @Override
+    public Boolean checkMonthExists(Long studentId, String summaryMonth, Long monthlyId) {
+        return baseMapper.existsByStudentAndMonth(studentId, summaryMonth, monthlyId);
+    }
+
+    /**
+     * 获取月度总结模板信息
+     */
+    @Override
+    public ZsjyInternshipMonthlyVo getMonthlyTemplate(String summaryMonth) {
+        ZsjyInternshipMonthlyVo template = new ZsjyInternshipMonthlyVo();
+        template.setSummaryMonth(summaryMonth);
+
+        // 解析月份序号
+        if (StringUtils.isNotBlank(summaryMonth)) {
+            String[] parts = summaryMonth.split("-");
+            if (parts.length == 2) {
+                template.setMonthNumber(Integer.parseInt(parts[1]));
+            }
+        }
+
+        // 设置默认截止日期(下月5号)
+        try {
+            YearMonth ym = YearMonth.parse(summaryMonth);
+            template.setSubmitDeadline(Date.from(ym.plusMonths(1).atDay(5).atStartOfDay(ZoneId.systemDefault()).toInstant()));
+        } catch (Exception e) {
+            log.warn("解析月份失败: {}", summaryMonth);
+        }
+
+        template.setReviewStatus(3); // 草稿
+        template.setStatusText("草稿");
+        template.setEditable(true);
+        template.setDeletable(true);
+        template.setSubmittable(true);
+
+        return template;
+    }
+
+    /**
+     * 查询学生指定实习的月度总结列表
+     */
+    @Override
+    public List<ZsjyInternshipMonthlyVo> queryByStudentAndInternship(Long studentId, Long internshipId) {
+        if (ObjectUtil.isNull(studentId)) {
+            studentId = LoginHelper.getUserId();
+        }
+        return baseMapper.selectByStudentAndInternship(studentId, internshipId);
+    }
+
+    /**
+     * 统计学生各状态的总结数量
+     */
+    @Override
+    public List<Object> countByStatus(Long studentId) {
+        if (ObjectUtil.isNull(studentId)) {
+            studentId = LoginHelper.getUserId();
+        }
+        return baseMapper.countByStatus(studentId);
+    }
+
+    /**
+     * 计算总结内容字数
+     */
+    @Override
+    public Integer calculateWordCount(ZsjyInternshipMonthlyAddBo addBo) {
+        int wordCount = 0;
+
+        if (StringUtils.isNotBlank(addBo.getWorkContent())) {
+            wordCount += addBo.getWorkContent().length();
+        }
+        if (StringUtils.isNotBlank(addBo.getAchievements())) {
+            wordCount += addBo.getAchievements().length();
+        }
+        if (StringUtils.isNotBlank(addBo.getLearningGains())) {
+            wordCount += addBo.getLearningGains().length();
+        }
+        if (StringUtils.isNotBlank(addBo.getProblemsEncountered())) {
+            wordCount += addBo.getProblemsEncountered().length();
+        }
+        if (StringUtils.isNotBlank(addBo.getSelfEvaluation())) {
+            wordCount += addBo.getSelfEvaluation().length();
+        }
+        if (StringUtils.isNotBlank(addBo.getNextMonthPlan())) {
+            wordCount += addBo.getNextMonthPlan().length();
+        }
+
+        return wordCount;
+    }
+
+        /**
+     * 获取学生当前实习信息
+     */
+    @Override
+    public Map<String, Object> getCurrentInternshipInfo() {
+        Long currentUserId = LoginHelper.getUserId();
+        log.info("获取学生当前实习信息, 用户ID: {}", currentUserId);
+
+        // 查询学生当前进行中的实习记录(与insertByBo方法保持一致)
+        LambdaQueryWrapper<ZsjyInternshipInfo> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ZsjyInternshipInfo::getStudentId, currentUserId)
+               .eq(ZsjyInternshipInfo::getStatus, "1") // 查询进行中状态的实习
+               .eq(ZsjyInternshipInfo::getDeleted, 0)
+               .orderByDesc(ZsjyInternshipInfo::getCreateTime)
+               .last("LIMIT 1");
+
+        ZsjyInternshipInfo internshipInfo = internshipInfoMapper.selectOne(wrapper);
+        
+        if (internshipInfo == null) {
+            log.warn("学生{}没有找到进行中的实习记录", currentUserId);
+            return new HashMap<>();
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("internshipId", internshipInfo.getInternshipId());
+        result.put("companyName", internshipInfo.getCompanyName());
+        result.put("position", internshipInfo.getPositionName());
+        result.put("startDate", internshipInfo.getStartDate());
+        result.put("endDate", internshipInfo.getEndDate());
+        
+        log.info("找到学生实习信息: {}", result);
+        return result;
+    }
+
+}

+ 240 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipMonthlyMapper.xml

@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.zsjy.mapper.ZsjyInternshipMonthlyMapper">
+
+    <resultMap type="org.dromara.zsjy.domain.ZsjyInternshipMonthly" id="ZsjyInternshipMonthlyResult">
+        <result property="id"                    column="monthly_id"           />
+        <result property="tenantId"              column="tenant_id"            />
+        <result property="internshipId"          column="internship_id"        />
+        <result property="studentId"             column="student_id"           />
+        <result property="summaryMonth"          column="summary_month"        />
+        <result property="monthNumber"           column="month_number"         />
+        <result property="workContent"           column="work_content"         />
+        <result property="achievements"          column="achievements"         />
+        <result property="learningGains"         column="learning_gains"       />
+        <result property="problemsEncountered"   column="problems_encountered" />
+        <result property="selfEvaluation"        column="self_evaluation"      />
+        <result property="nextMonthPlan"         column="next_month_plan"      />
+        <result property="professionalSkills"    column="professional_skills"  />
+        <result property="communicationSkills"   column="communication_skills" />
+        <result property="teamworkAbility"       column="teamwork_ability"     />
+        <result property="learningAbility"       column="learning_ability"     />
+        <result property="workAttitude"          column="work_attitude"        />
+        <result property="reviewStatus"          column="review_status"        />
+        <result property="reviewBy"              column="review_by"            />
+        <result property="reviewTime"            column="review_time"          />
+        <result property="reviewScore"           column="review_score"         />
+        <result property="reviewComments"        column="review_comment"       />
+        <result property="submitTime"            column="submit_time"          />
+        <result property="wordCount"             column="word_count"           />
+        <result property="isOverdue"             column="is_overdue"           />
+        <result property="submitDeadline"        column="submit_deadline"      />
+        <result property="attachments"           column="attachments"          />
+        <result property="createBy"              column="create_by"            />
+        <result property="createTime"            column="create_time"          />
+        <result property="updateBy"              column="update_by"            />
+        <result property="updateTime"            column="update_time"          />
+        <result property="deleted"               column="deleted"              />
+    </resultMap>
+
+    <resultMap type="org.dromara.zsjy.domain.vo.ZsjyInternshipMonthlyVo" id="ZsjyInternshipMonthlyVoResult">
+        <result property="monthlyId"             column="monthly_id"           />
+        <result property="internshipId"          column="internship_id"        />
+        <result property="studentId"             column="student_id"           />
+        <result property="studentName"           column="student_name"         />
+        <result property="studentNumber"         column="student_number"       />
+        <result property="summaryMonth"          column="summary_month"        />
+        <result property="monthNumber"           column="month_number"         />
+        <result property="workContent"           column="work_content"         />
+        <result property="achievements"          column="achievements"         />
+        <result property="learningGains"         column="learning_gains"       />
+        <result property="problemsEncountered"   column="problems_encountered" />
+        <result property="selfEvaluation"        column="self_evaluation"      />
+        <result property="nextMonthPlan"         column="next_month_plan"      />
+        <result property="professionalSkills"    column="professional_skills"  />
+        <result property="communicationSkills"   column="communication_skills" />
+        <result property="teamworkAbility"       column="teamwork_ability"     />
+        <result property="learningAbility"       column="learning_ability"     />
+        <result property="workAttitude"          column="work_attitude"        />
+        <result property="reviewStatus"          column="review_status"        />
+        <result property="statusText"            column="status_text"          />
+        <result property="reviewBy"              column="review_by"            />
+        <result property="reviewerName"          column="reviewer_name"        />
+        <result property="reviewTime"            column="review_time"          />
+        <result property="reviewScore"           column="review_score"         />
+        <result property="reviewComments"        column="review_comment"       />
+        <result property="submitTime"            column="submit_time"          />
+        <result property="wordCount"             column="word_count"           />
+        <result property="isOverdue"             column="is_overdue"           />
+        <result property="submitDeadline"        column="submit_deadline"      />
+        <result property="attachments"           column="attachments"          />
+        <result property="editable"              column="editable"             />
+        <result property="deletable"             column="deletable"            />
+        <result property="submittable"           column="submittable"          />
+        <result property="createBy"              column="create_by"            />
+        <result property="createTime"            column="create_time"          />
+        <result property="updateBy"              column="update_by"            />
+        <result property="updateTime"            column="update_time"          />
+    </resultMap>
+
+    <sql id="selectZsjyInternshipMonthlyVo">
+        SELECT
+            m.monthly_id,
+            m.internship_id,
+            m.student_id,
+            u.nick_name as student_name,
+            ui.student_no as student_number,
+            m.summary_month,
+            m.month_number,
+            m.work_content,
+            m.achievements,
+            m.learning_gains,
+            m.problems_encountered,
+            m.self_evaluation,
+            m.next_month_plan,
+            m.professional_skills,
+            m.communication_skills,
+            m.teamwork_ability,
+            m.learning_ability,
+            m.work_attitude,
+            m.review_status,
+            CASE m.review_status
+                WHEN 0 THEN '待审核'
+                WHEN 1 THEN '已审核'
+                WHEN 2 THEN '审核拒绝'
+                WHEN 3 THEN '草稿'
+                ELSE '未知状态'
+            END as status_text,
+            m.review_by,
+            r.nick_name as reviewer_name,
+            m.review_time,
+            m.review_score,
+            m.review_comment as review_comments,
+            m.submit_time,
+            m.word_count,
+            m.is_overdue,
+            m.submit_deadline,
+            m.attachments,
+            CASE WHEN m.review_status = 3 THEN 1 ELSE 0 END as editable,
+            CASE WHEN m.review_status = 3 THEN 1 ELSE 0 END as deletable,
+            CASE WHEN m.review_status = 3 THEN 1 ELSE 0 END as submittable,
+            m.create_by,
+            m.create_time,
+            m.update_by,
+            m.update_time
+        FROM zsjy_internship_monthly m
+        LEFT JOIN sys_user u ON u.user_id = m.student_id
+        LEFT JOIN zsjy_internship_info ui ON ui.internship_id = m.internship_id AND ui.student_id = m.student_id
+        LEFT JOIN sys_user r ON r.user_id = m.review_by
+        WHERE m.deleted = 0
+    </sql>
+
+    <select id="selectVoById" parameterType="Long" resultMap="ZsjyInternshipMonthlyVoResult">
+        <include refid="selectZsjyInternshipMonthlyVo"/>
+        AND m.monthly_id = #{monthlyId}
+    </select>
+
+    <select id="selectVoPage" resultMap="ZsjyInternshipMonthlyVoResult">
+        <include refid="selectZsjyInternshipMonthlyVo"/>
+        <if test="bo.studentId != null">
+            AND m.student_id = #{bo.studentId}
+        </if>
+        <if test="bo.internshipId != null">
+            AND m.internship_id = #{bo.internshipId}
+        </if>
+        <if test="bo.summaryMonth != null and bo.summaryMonth != ''">
+            AND m.summary_month = #{bo.summaryMonth}
+        </if>
+        <if test="bo.status != null">
+            AND m.review_status = #{bo.status}
+        </if>
+        <if test="bo.isOverdue != null">
+            AND m.is_overdue = #{bo.isOverdue}
+        </if>
+        <if test="bo.startMonth != null and bo.startMonth != ''">
+            AND m.summary_month &gt;= #{bo.startMonth}
+        </if>
+        <if test="bo.endMonth != null and bo.endMonth != ''">
+            AND m.summary_month &lt;= #{bo.endMonth}
+        </if>
+        ORDER BY m.summary_month DESC, m.create_time DESC
+    </select>
+
+    <select id="selectVoList" resultMap="ZsjyInternshipMonthlyVoResult">
+        <include refid="selectZsjyInternshipMonthlyVo"/>
+        <if test="bo.studentId != null">
+            AND m.student_id = #{bo.studentId}
+        </if>
+        <if test="bo.internshipId != null">
+            AND m.internship_id = #{bo.internshipId}
+        </if>
+        <if test="bo.summaryMonth != null and bo.summaryMonth != ''">
+            AND m.summary_month = #{bo.summaryMonth}
+        </if>
+        <if test="bo.status != null">
+            AND m.review_status = #{bo.status}
+        </if>
+        <if test="bo.isOverdue != null">
+            AND m.is_overdue = #{bo.isOverdue}
+        </if>
+        <if test="bo.startMonth != null and bo.startMonth != ''">
+            AND m.summary_month &gt;= #{bo.startMonth}
+        </if>
+        <if test="bo.endMonth != null and bo.endMonth != ''">
+            AND m.summary_month &lt;= #{bo.endMonth}
+        </if>
+        ORDER BY m.summary_month DESC, m.create_time DESC
+    </select>
+
+    <select id="existsByStudentAndMonth" resultType="boolean">
+        SELECT COUNT(1) > 0
+        FROM zsjy_internship_monthly
+        WHERE deleted = 0
+        AND student_id = #{studentId}
+        AND summary_month = #{summaryMonth}
+        <if test="monthlyId != null">
+            AND monthly_id != #{monthlyId}
+        </if>
+    </select>
+
+    <delete id="deleteByIds" parameterType="Long">
+        UPDATE zsjy_internship_monthly SET deleted = 1
+        WHERE monthly_id IN
+        <foreach item="monthlyId" collection="monthlyIds" open="(" separator="," close=")">
+            #{monthlyId}
+        </foreach>
+        AND review_status = 3
+    </delete>
+
+    <select id="selectByStudentAndInternship" resultMap="ZsjyInternshipMonthlyVoResult">
+        <include refid="selectZsjyInternshipMonthlyVo"/>
+        <if test="studentId != null">
+            AND m.student_id = #{studentId}
+        </if>
+        <if test="internshipId != null">
+            AND m.internship_id = #{internshipId}
+        </if>
+        ORDER BY m.summary_month DESC, m.create_time DESC
+    </select>
+
+    <select id="countByStatus" resultType="map">
+        SELECT
+            review_status,
+            CASE review_status
+                WHEN 0 THEN '待审核'
+                WHEN 1 THEN '已审核'
+                WHEN 2 THEN '审核拒绝'
+                WHEN 3 THEN '草稿'
+                ELSE '未知状态'
+            END as status_text,
+            COUNT(1) as count
+        FROM zsjy_internship_monthly
+        WHERE deleted = 0
+        AND student_id = #{studentId}
+        GROUP BY review_status
+        ORDER BY review_status
+    </select>
+
+</mapper>

+ 11 - 0
mobile/src/api/internship.js

@@ -28,6 +28,17 @@ export const getStudentInternshipInfo = () => {
     });
 };
 
+/**
+ * 获取当前学生实习信息(新版API)
+ * @returns Promise
+ */
+export const getCurrentStudentInternship = () => {
+    return request({
+        url: '/zsjy/internship/current',
+        method: 'GET'
+    });
+};
+
 /**
  * 学生签到打卡
  * @param {Object} data 签到数据

+ 1 - 1
mobile/src/pages/index/index.vue

@@ -170,7 +170,7 @@
                     </ServiceItem>
                     <ServiceItem
                         v-if="hasPermi(['student:visit:list'])"
-                        title="班主任走访1"
+                        title="班主任走访"
                         @click="gotoVisitManage"
                     >
                         <template #icon>

+ 11 - 0
web/src/api/zsjy/internship.ts

@@ -33,6 +33,17 @@ export const getInternshipInfo = (studentId: string | number): AxiosPromise<R<In
   });
 };
 
+/**
+ * 获取当前学生实习信息
+ * @returns 当前登录学生的实习信息
+ */
+export const getCurrentStudentInternship = (): AxiosPromise<R<InternshipInfoVO>> => {
+  return request({
+    url: '/zsjy/internship/current',
+    method: 'get'
+  });
+};
+
 /**
  * 导出实习信息列表
  * @param query 查询参数

+ 102 - 0
web/src/api/zsjy/internshipMonthly.ts

@@ -0,0 +1,102 @@
+import request from '@/utils/request'
+
+// 查询学生月度总结列表
+export function listInternshipMonthly(query: any) {
+  return request({
+    url: '/zsjy/internship/monthly/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 获取学生月度总结详细信息
+export function getInternshipMonthly(monthlyId: string | number) {
+  return request({
+    url: '/zsjy/internship/monthly/' + monthlyId,
+    method: 'get'
+  })
+}
+
+// 新增学生月度总结
+export function addInternshipMonthly(data: any) {
+  return request({
+    url: '/zsjy/internship/monthly',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改学生月度总结
+export function updateInternshipMonthly(data: any) {
+  return request({
+    url: '/zsjy/internship/monthly',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生月度总结
+export function delInternshipMonthly(monthlyIds: string | number | Array<string | number>) {
+  return request({
+    url: '/zsjy/internship/monthly/' + monthlyIds,
+    method: 'delete'
+  })
+}
+
+// 提交月度总结审批
+export function submitInternshipMonthly(monthlyId: string | number) {
+  return request({
+    url: '/zsjy/internship/monthly/submit/' + monthlyId,
+    method: 'put'
+  })
+}
+
+// 自动保存草稿
+export function autosaveInternshipMonthly(data: any) {
+  return request({
+    url: '/zsjy/internship/monthly/autosave',
+    method: 'post',
+    data: data
+  })
+}
+
+// 检查月份重复
+export function checkSummaryMonth(summaryMonth: string) {
+  return request({
+    url: '/zsjy/internship/monthly/check/' + summaryMonth,
+    method: 'get'
+  })
+}
+
+// 获取月度总结模板
+export function getMonthlyTemplate(summaryMonth: string) {
+  return request({
+    url: '/zsjy/internship/monthly/template/' + summaryMonth,
+    method: 'get'
+  })
+}
+
+// 获取状态统计
+export function getMonthlyStatistics() {
+  return request({
+    url: '/zsjy/internship/monthly/statistics',
+    method: 'get'
+  })
+}
+
+// 导出月度总结数据
+export function exportInternshipMonthly(query: any) {
+  return request({
+    url: '/zsjy/internship/monthly/export',
+    method: 'post',
+    params: query
+  })
+}
+
+// 获取学生当前实习信息
+export function getCurrentInternshipInfo() {
+  return request({
+    url: '/zsjy/internship/monthly/current-internship',
+    method: 'get'
+  })
+}

+ 318 - 0
web/src/views/zsjy/internship/pmonthly/detail.vue

@@ -0,0 +1,318 @@
+<template>
+  <div class="monthly-detail">
+    <el-row :gutter="20">
+      <!-- 左侧详情内容 -->
+      <el-col :span="16">
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <div class="flex justify-between items-center">
+              <span class="font-medium">基本信息</span>
+              <dict-tag :options="internship_monthly_status" :value="detail.reviewStatus" />
+            </div>
+          </template>
+
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="总结月份">
+              {{ detail.summaryMonth }}
+            </el-descriptions-item>
+            <el-descriptions-item label="技能评分" span="2">
+              <div class="flex gap-4 flex-wrap">
+                <el-tag v-if="detail.professionalSkills" :type="getScoreTagType(detail.professionalSkills)">
+                  专业技能: {{ detail.professionalSkills }}分
+                </el-tag>
+                <el-tag v-if="detail.communicationSkills" :type="getScoreTagType(detail.communicationSkills)">
+                  沟通技能: {{ detail.communicationSkills }}分
+                </el-tag>
+                <el-tag v-if="detail.teamworkAbility" :type="getScoreTagType(detail.teamworkAbility)">
+                  团队协作: {{ detail.teamworkAbility }}分
+                </el-tag>
+                <el-tag v-if="detail.learningAbility" :type="getScoreTagType(detail.learningAbility)">
+                  学习能力: {{ detail.learningAbility }}分
+                </el-tag>
+                <el-tag v-if="detail.workAttitude" :type="getScoreTagType(detail.workAttitude)">
+                  工作态度: {{ detail.workAttitude }}分
+                </el-tag>
+              </div>
+            </el-descriptions-item>
+            <el-descriptions-item label="总字数">
+              <el-tag :type="getWordCountTagType(detail.wordCount)">{{ detail.wordCount || 0 }}字</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="是否逾期">
+              <el-tag v-if="detail.isOverdue === 1" type="danger">逾期</el-tag>
+              <el-tag v-else type="success">正常</el-tag>
+            </el-descriptions-item>
+          </el-descriptions>
+        </el-card>
+
+        <el-card shadow="never">
+          <template #header>
+            <span class="font-medium">总结内容</span>
+          </template>
+
+          <div class="space-y-6">
+            <div v-if="detail.workContent">
+              <h4 class="text-base font-medium mb-2 text-gray-800">工作内容</h4>
+              <div class="bg-gray-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.workContent }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.achievements">
+              <h4 class="text-base font-medium mb-2 text-gray-800">成果成就</h4>
+              <div class="bg-blue-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.achievements }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.learningGains">
+              <h4 class="text-base font-medium mb-2 text-gray-800">学习收获</h4>
+              <div class="bg-green-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.learningGains }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.problemsEncountered">
+              <h4 class="text-base font-medium mb-2 text-gray-800">问题困难</h4>
+              <div class="bg-yellow-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.problemsEncountered }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.solutions">
+              <h4 class="text-base font-medium mb-2 text-gray-800">解决方案</h4>
+              <div class="bg-purple-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.solutions }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.nextMonthPlan">
+              <h4 class="text-base font-medium mb-2 text-gray-800">下月计划</h4>
+              <div class="bg-indigo-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.nextMonthPlan }}</p>
+              </div>
+            </div>
+
+            <div v-if="detail.remark">
+              <h4 class="text-base font-medium mb-2 text-gray-800">备注说明</h4>
+              <div class="bg-gray-50 p-4 rounded-md">
+                <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.remark }}</p>
+              </div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧信息面板 -->
+      <el-col :span="8">
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">审批信息</span>
+          </template>
+
+          <div class="space-y-4">
+            <div class="flex justify-between">
+              <span>审核状态:</span>
+              <dict-tag :options="internship_monthly_status" :value="detail.status" />
+            </div>
+
+            <div v-if="detail.submitTime" class="flex justify-between">
+              <span>提交时间:</span>
+              <span class="text-sm">{{ parseTime(detail.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+            </div>
+
+            <div v-if="detail.auditTime" class="flex justify-between">
+              <span>审核时间:</span>
+              <span class="text-sm">{{ parseTime(detail.auditTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+            </div>
+
+            <div v-if="detail.auditBy" class="flex justify-between">
+              <span>审核人:</span>
+              <span class="text-sm">{{ detail.auditByName || detail.auditBy }}</span>
+            </div>
+
+            <div v-if="detail.auditScore" class="flex justify-between">
+              <span>审核评分:</span>
+              <el-tag :type="getScoreTagType(detail.auditScore)">{{ detail.auditScore }}分</el-tag>
+            </div>
+          </div>
+        </el-card>
+
+        <el-card v-if="detail.auditComment" shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">审核意见</span>
+          </template>
+
+          <div class="bg-blue-50 p-3 rounded-md">
+            <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{{ detail.auditComment }}</p>
+          </div>
+        </el-card>
+
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">时间记录</span>
+          </template>
+
+          <div class="space-y-3">
+            <div class="flex justify-between">
+              <span>创建时间:</span>
+              <span class="text-sm">{{ parseTime(detail.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+            </div>
+
+            <div v-if="detail.updateTime" class="flex justify-between">
+              <span>最后更新:</span>
+              <span class="text-sm">{{ parseTime(detail.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+            </div>
+
+            <div v-if="detail.createByName" class="flex justify-between">
+              <span>创建人:</span>
+              <span class="text-sm">{{ detail.createByName }}</span>
+            </div>
+          </div>
+        </el-card>
+
+        <el-card shadow="never">
+          <template #header>
+            <span class="font-medium">统计数据</span>
+          </template>
+
+          <div class="space-y-3">
+            <div class="flex justify-between">
+              <span>总字数:</span>
+              <el-tag :type="getWordCountTagType(detail.wordCount)">{{ detail.wordCount || 0 }}字</el-tag>
+            </div>
+
+            <div class="flex justify-between">
+              <span>自评分数:</span>
+              <el-tag :type="getScoreTagType(detail.selfScore)">{{ detail.selfScore }}分</el-tag>
+            </div>
+
+            <div v-if="detail.auditScore" class="flex justify-between">
+              <span>审核评分:</span>
+              <el-tag :type="getScoreTagType(detail.auditScore)">{{ detail.auditScore }}分</el-tag>
+            </div>
+
+            <div class="flex justify-between">
+              <span>逾期状态:</span>
+              <el-tag v-if="detail.isOverdue === 1" type="danger">逾期</el-tag>
+              <el-tag v-else type="success">正常</el-tag>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 操作按钮 -->
+    <el-row class="mt-6">
+      <el-col :span="24" class="text-center">
+        <el-button @click="handleClose">关闭</el-button>
+        <el-button
+          v-if="detail.reviewStatus === 3"
+          type="primary"
+          @click="handleEdit"
+        >
+          编辑
+        </el-button>
+        <el-button
+          v-if="detail.reviewStatus === 3"
+          type="success"
+          @click="handleSubmit"
+        >
+          提交审核
+        </el-button>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getInternshipMonthly, submitInternshipMonthly } from '@/api/zsjy/internshipMonthly'
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { internship_monthly_status } = toRefs<any>(proxy?.useDict('internship_monthly_status'))
+
+const emit = defineEmits(['edit', 'close'])
+
+// 详情数据
+const detail = ref<any>({})
+const loading = ref(false)
+
+/** 重置表单 */
+const resetForm = async (monthlyId: string | number) => {
+  loading.value = true
+  try {
+    const res = await getInternshipMonthly(monthlyId)
+    detail.value = res.data || {}
+  } catch (error) {
+    console.error('获取月度总结详情失败:', error)
+    proxy?.$modal.msgError('获取详情失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 根据分数获取标签类型 */
+const getScoreTagType = (score: number) => {
+  if (!score) return 'info'
+  if (score >= 9) return 'success'
+  if (score >= 7) return 'warning'
+  if (score >= 5) return ''
+  return 'danger'
+}
+
+/** 获取字数标签类型 */
+const getWordCountTagType = (count: number) => {
+  if (!count) return 'info'
+  if (count < 100) return 'info'
+  if (count < 500) return 'warning'
+  if (count < 1000) return 'success'
+  return 'danger'
+}
+
+/** 编辑操作 */
+const handleEdit = () => {
+  emit('edit', detail.value.monthlyId)
+}
+
+/** 提交审核操作 */
+const handleSubmit = async () => {
+  try {
+    await proxy?.$modal.confirm('确认提交该月度总结?提交后将无法再次编辑')
+    await submitInternshipMonthly(detail.value.monthlyId)
+    proxy?.$modal.msgSuccess('提交成功')
+    // 重新加载详情
+    await resetForm(detail.value.monthlyId)
+  } catch {}
+}
+
+/** 关闭操作 */
+const handleClose = () => {
+  emit('close')
+}
+
+defineExpose({
+  resetForm
+})
+</script>
+
+<style scoped>
+.monthly-detail {
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+.space-y-6 > :not([hidden]) ~ :not([hidden]) {
+  margin-top: 1.5rem;
+}
+
+.space-y-4 > :not([hidden]) ~ :not([hidden]) {
+  margin-top: 1rem;
+}
+
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+  margin-top: 0.75rem;
+}
+
+.whitespace-pre-wrap {
+  white-space: pre-wrap;
+}
+</style>

+ 590 - 0
web/src/views/zsjy/internship/pmonthly/form.vue

@@ -0,0 +1,590 @@
+<template>
+  <el-form ref="monthlyFormRef" :model="form" :rules="rules" label-width="120px">
+    <el-row :gutter="20">
+      <!-- 左侧表单 -->
+      <el-col :span="16">
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <div class="flex justify-between items-center">
+              <span class="font-medium">基本信息</span>
+              <el-button
+                v-if="form.monthlyId && form.reviewStatus === 3"
+                type="primary"
+                size="small"
+                @click="handleAutoSave"
+                :loading="autoSaving"
+              >
+                {{ autoSaving ? '保存中...' : '保存草稿' }}
+              </el-button>
+            </div>
+          </template>
+
+          <el-form-item label="总结月份" prop="summaryMonth">
+            <el-date-picker
+              v-model="form.summaryMonth"
+              type="month"
+              value-format="YYYY-MM"
+              placeholder="请选择总结月份"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              style="width: 300px"
+              @change="checkMonthDuplicate"
+            />
+            <el-text class="ml-2" size="small" type="info">选择要总结的月份</el-text>
+          </el-form-item>
+        </el-card>
+
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">能力自评(1-10分)</span>
+          </template>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="专业技能" prop="professionalSkills">
+                <el-input-number
+                  v-model="form.professionalSkills"
+                  :min="1"
+                  :max="10"
+                  :precision="0"
+                  placeholder="1-10分"
+                  :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+                  style="width: 150px"
+                />
+                <el-text class="ml-2" size="small" type="info">专业技能掌握程度</el-text>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="沟通技能" prop="communicationSkills">
+                <el-input-number
+                  v-model="form.communicationSkills"
+                  :min="1"
+                  :max="10"
+                  :precision="0"
+                  placeholder="1-10分"
+                  :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+                  style="width: 150px"
+                />
+                <el-text class="ml-2" size="small" type="info">沟通表达能力</el-text>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="团队协作" prop="teamworkAbility">
+                <el-input-number
+                  v-model="form.teamworkAbility"
+                  :min="1"
+                  :max="10"
+                  :precision="0"
+                  placeholder="1-10分"
+                  :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+                  style="width: 150px"
+                />
+                <el-text class="ml-2" size="small" type="info">团队合作能力</el-text>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="学习能力" prop="learningAbility">
+                <el-input-number
+                  v-model="form.learningAbility"
+                  :min="1"
+                  :max="10"
+                  :precision="0"
+                  placeholder="1-10分"
+                  :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+                  style="width: 150px"
+                />
+                <el-text class="ml-2" size="small" type="info">学习新知识能力</el-text>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="工作态度" prop="workAttitude">
+                <el-input-number
+                  v-model="form.workAttitude"
+                  :min="1"
+                  :max="10"
+                  :precision="0"
+                  placeholder="1-10分"
+                  :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+                  style="width: 150px"
+                />
+                <el-text class="ml-2" size="small" type="info">工作认真负责程度</el-text>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <el-card shadow="never">
+          <template #header>
+            <span class="font-medium">工作总结内容</span>
+          </template>
+
+          <el-form-item label="工作内容" prop="workContent">
+            <el-input
+              v-model="form.workContent"
+              type="textarea"
+              :rows="6"
+              placeholder="请详细描述本月的主要工作内容和任务完成情况(必填,最多500字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="500"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="成果成就" prop="achievements">
+            <el-input
+              v-model="form.achievements"
+              type="textarea"
+              :rows="4"
+              placeholder="请描述本月取得的主要成果和成就(最多300字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="300"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="学习收获" prop="learningGains">
+            <el-input
+              v-model="form.learningGains"
+              type="textarea"
+              :rows="4"
+              placeholder="请分享本月的学习心得和收获(最多300字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="300"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="问题困难" prop="problemsEncountered">
+            <el-input
+              v-model="form.problemsEncountered"
+              type="textarea"
+              :rows="6"
+              placeholder="请描述遇到的问题、困难以及解决方案(包含问题和解决办法,最多1000字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="1000"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="自我评价" prop="selfEvaluation">
+            <el-input
+              v-model="form.selfEvaluation"
+              type="textarea"
+              :rows="4"
+              placeholder="请对本月的工作表现进行自我评价(最多300字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="300"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="下月计划" prop="nextMonthPlan">
+            <el-input
+              v-model="form.nextMonthPlan"
+              type="textarea"
+              :rows="4"
+              placeholder="请制定下个月的工作计划和目标(最多300字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="300"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+
+          <el-form-item label="备注说明" prop="remark">
+            <el-input
+              v-model="form.remark"
+              type="textarea"
+              :rows="3"
+              placeholder="其他需要说明的内容(最多200字)"
+              :disabled="form.reviewStatus !== undefined && form.reviewStatus !== 3"
+              show-word-limit
+              :maxlength="200"
+              @input="updateWordCount"
+            />
+          </el-form-item>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧信息面板 -->
+      <el-col :span="8">
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">统计信息</span>
+          </template>
+
+          <div class="space-y-3">
+            <div class="flex justify-between">
+              <span>总字数:</span>
+              <el-tag :type="getWordCountTagType(form.wordCount)">{{ form.wordCount || 0 }}字</el-tag>
+            </div>
+            <div class="flex justify-between">
+              <span>状态:</span>
+              <dict-tag :options="internship_monthly_status" :value="form.reviewStatus" />
+            </div>
+            <div v-if="form.submitTime" class="flex justify-between">
+              <span>提交时间:</span>
+              <span class="text-sm">{{ parseTime(form.submitTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+            </div>
+            <div v-if="form.createTime" class="flex justify-between">
+              <span>创建时间:</span>
+              <span class="text-sm">{{ parseTime(form.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+            </div>
+          </div>
+        </el-card>
+
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <span class="font-medium">填写提示</span>
+          </template>
+
+          <div class="text-sm text-gray-600 space-y-2">
+            <div>• 工作内容为必填项,最多500字</div>
+            <div>• 其他项目选填,建议填写完整</div>
+            <div>• 自评分数范围:1-10分</div>
+            <div>• 支持自动保存草稿功能</div>
+            <div>• 提交后将无法再次编辑</div>
+          </div>
+        </el-card>
+
+        <el-card v-if="monthDuplicateCheck.show" shadow="never">
+          <template #header>
+            <span class="font-medium">月份检查</span>
+          </template>
+
+          <el-alert
+            :type="monthDuplicateCheck.type"
+            :title="monthDuplicateCheck.message"
+            show-icon
+            :closable="false"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row class="mt-6">
+      <el-col :span="24" class="text-center">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button
+          v-if="form.reviewStatus === undefined || form.reviewStatus === 3"
+          type="primary"
+          @click="handleSubmit('draft')"
+          :loading="submitting"
+        >
+          保存草稿
+        </el-button>
+        <el-button
+          v-if="form.reviewStatus === undefined || form.reviewStatus === 3"
+          type="success"
+          @click="handleSubmit('submit')"
+          :loading="submitting"
+        >
+          提交审核
+        </el-button>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  getInternshipMonthly,
+  addInternshipMonthly,
+  updateInternshipMonthly,
+  autosaveInternshipMonthly,
+  checkSummaryMonth,
+  getMonthlyTemplate,
+  getCurrentInternshipInfo
+} from '@/api/zsjy/internshipMonthly'
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { internship_monthly_status } = toRefs<any>(proxy?.useDict('internship_monthly_status'))
+
+const emit = defineEmits(['success'])
+
+const monthlyFormRef = ref<ElFormInstance>()
+const submitting = ref(false)
+const autoSaving = ref(false)
+
+// 表单数据
+const initFormData = {
+  monthlyId: undefined,
+  internshipId: undefined,
+  summaryMonth: '',
+  workContent: '',
+  achievements: '',
+  learningGains: '',
+  problemsEncountered: '',
+  selfEvaluation: '',
+  nextMonthPlan: '',
+  professionalSkills: undefined,
+  communicationSkills: undefined,
+  teamworkAbility: undefined,
+  learningAbility: undefined,
+  workAttitude: undefined,
+  wordCount: 0,
+  remark: '',
+  reviewStatus: undefined,
+  submitTime: undefined,
+  createTime: undefined
+}
+
+const form = ref({ ...initFormData })
+
+// 表单验证规则
+const rules = ref({
+  summaryMonth: [
+    { required: true, message: '总结月份不能为空', trigger: 'change' }
+  ],
+  workContent: [
+    { required: true, message: '工作内容不能为空', trigger: 'blur' },
+    { min: 10, message: '工作内容至少需要10个字符', trigger: 'blur' },
+    { max: 500, message: '工作内容不能超过500个字符', trigger: 'blur' }
+  ],
+  professionalSkills: [
+    { required: true, message: '专业技能不能为空', trigger: 'blur' },
+    { type: 'number', min: 1, max: 10, message: '专业技能必须在1-10之间', trigger: 'blur' }
+  ],
+  communicationSkills: [
+    { required: true, message: '沟通技能不能为空', trigger: 'blur' },
+    { type: 'number', min: 1, max: 10, message: '沟通技能必须在1-10之间', trigger: 'blur' }
+  ],
+  teamworkAbility: [
+    { required: true, message: '团队协作不能为空', trigger: 'blur' },
+    { type: 'number', min: 1, max: 10, message: '团队协作必须在1-10之间', trigger: 'blur' }
+  ],
+  learningAbility: [
+    { required: true, message: '学习能力不能为空', trigger: 'blur' },
+    { type: 'number', min: 1, max: 10, message: '学习能力必须在1-10之间', trigger: 'blur' }
+  ],
+  workAttitude: [
+    { required: true, message: '工作态度不能为空', trigger: 'blur' },
+    { type: 'number', min: 1, max: 10, message: '工作态度必须在1-10之间', trigger: 'blur' }
+  ],
+  problemsEncountered: [
+    { required: true, message: '问题困难不能为空', trigger: 'blur' },
+    { min: 10, message: '问题困难至少需要10个字符', trigger: 'blur' },
+    { max: 1000, message: '问题困难不能超过1000个字符', trigger: 'blur' }
+  ],
+  selfEvaluation: [
+    { required: true, message: '自我评价不能为空', trigger: 'blur' },
+    { max: 300, message: '自我评价不能超过300个字符', trigger: 'blur' }
+  ],
+  nextMonthPlan: [
+    { max: 300, message: '下月计划不能超过300个字符', trigger: 'blur' }
+  ],
+  remark: [
+    { max: 200, message: '备注说明不能超过200个字符', trigger: 'blur' }
+  ]
+})
+
+// 月份重复检查
+const monthDuplicateCheck = ref({
+  show: false,
+  type: 'info',
+  message: ''
+})
+
+// 自动保存定时器
+let autoSaveTimer: NodeJS.Timeout | null = null
+
+/** 重置表单 */
+const resetForm = async (monthlyId?: string | number) => {
+  form.value = { ...initFormData }
+  monthDuplicateCheck.value.show = false
+
+  if (monthlyId) {
+    try {
+      const res = await getInternshipMonthly(monthlyId)
+      form.value = { ...res.data }
+      updateWordCount()
+    } catch (error) {
+      console.error('获取月度总结详情失败:', error)
+    }
+  } else {
+    // 新增时获取当前月份模板和学生实习信息
+    const currentMonth = new Date().toISOString().substring(0, 7)
+    form.value.summaryMonth = currentMonth
+    await loadMonthlyTemplate(currentMonth)
+    
+    // 获取学生当前实习信息
+    try {
+      const internshipRes = await getCurrentInternshipInfo()
+      if (internshipRes.code === 200 && internshipRes.data) {
+        form.value.internshipId = internshipRes.data.internshipId
+      } else {
+        // 如果没有进行中的实习记录,提示用户
+        proxy?.$modal.msgError('您当前没有进行中的实习记录,无法创建月度总结')
+        emit('success') // 关闭对话框
+        return
+      }
+    } catch (error) {
+      console.error('获取学生实习信息失败:', error)
+      proxy?.$modal.msgError('获取实习信息失败,请稍后重试')
+      emit('success') // 关闭对话框
+      return
+    }
+  }
+
+  nextTick(() => {
+    monthlyFormRef.value?.clearValidate()
+  })
+}
+
+/** 加载月份模板 */
+const loadMonthlyTemplate = async (month: string) => {
+  try {
+    const res = await getMonthlyTemplate(month)
+    if (res.data) {
+      // 如果有模板数据,可以预填充一些内容
+      // form.value.nextMonthPlan = res.data.suggestedPlan || ''
+    }
+  } catch (error) {
+    console.log('获取月份模板失败,将使用空模板')
+  }
+}
+
+/** 检查月份重复 */
+const checkMonthDuplicate = async () => {
+  if (!form.value.summaryMonth) return
+
+  try {
+    const res = await checkSummaryMonth(form.value.summaryMonth)
+    if (res.data.exists && (!form.value.monthlyId || res.data.monthlyId !== form.value.monthlyId)) {
+      monthDuplicateCheck.value = {
+        show: true,
+        type: 'warning',
+        message: '该月份已存在总结记录,请选择其他月份'
+      }
+    } else {
+      monthDuplicateCheck.value = {
+        show: true,
+        type: 'success',
+        message: '该月份可以创建总结'
+      }
+    }
+  } catch (error) {
+    monthDuplicateCheck.value = {
+      show: true,
+      type: 'error',
+      message: '检查月份失败,请重试'
+    }
+  }
+}
+
+/** 更新字数统计 */
+const updateWordCount = () => {
+  const content = [
+    form.value.workContent,
+    form.value.achievements,
+    form.value.learningGains,
+    form.value.problemsEncountered,
+    form.value.selfEvaluation,
+    form.value.nextMonthPlan,
+    form.value.remark
+  ].join('')
+
+  form.value.wordCount = content.length
+
+  // 设置自动保存定时器
+  if (form.value.monthlyId && form.value.reviewStatus === 3) {
+    if (autoSaveTimer) {
+      clearTimeout(autoSaveTimer)
+    }
+    autoSaveTimer = setTimeout(() => {
+      handleAutoSave()
+    }, 5000) // 5秒后自动保存
+  }
+}
+
+/** 获取字数标签类型 */
+const getWordCountTagType = (count: number) => {
+  if (count < 100) return 'info'
+  if (count < 500) return 'warning'
+  if (count < 1000) return 'success'
+  return 'danger'
+}
+
+/** 自动保存草稿 */
+const handleAutoSave = async () => {
+  if (!form.value.monthlyId || form.value.reviewStatus !== 3) return
+
+  autoSaving.value = true
+  try {
+    await autosaveInternshipMonthly(form.value)
+    proxy?.$message.success('草稿已自动保存')
+  } catch (error) {
+    console.error('自动保存失败:', error)
+  } finally {
+    autoSaving.value = false
+  }
+}
+
+/** 提交表单 */
+const handleSubmit = async (type: 'draft' | 'submit') => {
+  if (!monthlyFormRef.value) return
+
+  const valid = await monthlyFormRef.value.validate().catch(() => false)
+  if (!valid) return
+
+  submitting.value = true
+  try {
+    updateWordCount()
+
+    const formData = {
+      ...form.value,
+      reviewStatus: type === 'draft' ? 3 : 0, // 草稿=3,待审核=0
+      submitTime: type === 'submit' ? new Date() : undefined
+    }
+
+    if (form.value.monthlyId) {
+      await updateInternshipMonthly(formData)
+    } else {
+      await addInternshipMonthly(formData)
+    }
+
+    proxy?.$modal.msgSuccess(type === 'draft' ? '保存成功' : '提交成功')
+    emit('success')
+  } catch (error) {
+    console.error('提交失败:', error)
+  } finally {
+    submitting.value = false
+  }
+}
+
+/** 取消操作 */
+const handleCancel = () => {
+  emit('success')
+}
+
+// 清理定时器
+onUnmounted(() => {
+  if (autoSaveTimer) {
+    clearTimeout(autoSaveTimer)
+  }
+})
+
+defineExpose({
+  resetForm
+})
+</script>
+
+<style scoped>
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+  margin-top: 0.75rem;
+}
+
+.space-y-2 > :not([hidden]) ~ :not([hidden]) {
+  margin-top: 0.5rem;
+}
+</style>

+ 388 - 0
web/src/views/zsjy/internship/pmonthly/index.vue

@@ -0,0 +1,388 @@
+<template>
+  <div class="p-2 page-container">
+    <!-- 搜索区域 -->
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
+            <!-- 第一行:搜索条件 -->
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="总结月份" prop="summaryMonth">
+                  <el-date-picker
+                    v-model="queryParams.summaryMonth"
+                    type="month"
+                    value-format="YYYY-MM"
+                    placeholder="请选择总结月份"
+                    clearable
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="审核状态" prop="reviewStatus">
+                  <el-select v-model="queryParams.reviewStatus" placeholder="请选择审核状态" clearable style="width: 100%">
+                    <el-option label="草稿" value="3" />
+                    <el-option label="待审核" value="0" />
+                    <el-option label="已审核" value="1" />
+                    <el-option label="审核拒绝" value="2" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="专业技能" prop="professionalSkills">
+                  <el-input-number
+                    v-model="queryParams.professionalSkills"
+                    :min="1"
+                    :max="10"
+                    placeholder="请输入专业技能评分"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item>
+                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 操作按钮区域 -->
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['zsjy:internshipmonthly:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['zsjy:internshipmonthly:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['zsjy:internshipmonthly:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['zsjy:internshipmonthly:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <!-- 数据表格 -->
+      <el-table v-loading="loading" :data="monthlyList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="总结月份" align="center" prop="summaryMonth" width="120" />
+        <el-table-column label="工作内容" align="center" prop="workContent" :show-overflow-tooltip="true" min-width="200" />
+        <el-table-column label="综合评分" align="center" width="120">
+          <template #default="scope">
+            <el-tooltip content="专业/沟通/团队/学习/态度" placement="top">
+              <el-tag :type="getScoreTagType(getAverageScore(scope.row))">
+                {{ getAverageScore(scope.row).toFixed(1) }}分
+              </el-tag>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column label="字数统计" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.wordCount || 0 }}字
+          </template>
+        </el-table-column>
+        <el-table-column label="审核状态" align="center" prop="reviewStatus" width="100">
+          <template #default="scope">
+                          <dict-tag :options="internship_monthly_status" :value="scope.row.reviewStatus" />
+          </template>
+        </el-table-column>
+        <el-table-column label="是否逾期" align="center" prop="isOverdue" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.isOverdue === 1" type="danger">逾期</el-tag>
+            <el-tag v-else type="success">正常</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="提交时间" align="center" prop="submitTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
+          <template #default="scope">
+            <!-- 查看详情 -->
+            <el-tooltip content="查看详情" placement="top">
+              <el-button 
+                link 
+                type="primary" 
+                icon="View" 
+                @click="handleDetail(scope.row)" 
+                v-hasPermi="['zsjy:internshipmonthly:list']"
+              ></el-button>
+            </el-tooltip>
+
+            <!-- 编辑(只有草稿状态才能编辑) -->
+            <el-tooltip content="编辑" placement="top" v-if="scope.row.reviewStatus === 3">
+              <el-button
+                link
+                type="primary"
+                icon="Edit"
+                @click="handleUpdate(scope.row)"
+                v-hasPermi="['zsjy:internshipmonthly:edit']"
+              ></el-button>
+            </el-tooltip>
+
+            <!-- 提交(只有草稿状态才能提交) -->
+            <el-tooltip content="提交" placement="top" v-if="scope.row.reviewStatus === 3">
+              <el-button
+                link
+                type="success"
+                icon="Check"
+                @click="handleSubmit(scope.row)"
+                v-hasPermi="['zsjy:internshipmonthly:submit']"
+              ></el-button>
+            </el-tooltip>
+
+            <!-- 删除(只有草稿状态才能删除) -->
+            <el-tooltip content="删除" placement="top" v-if="scope.row.reviewStatus === 3">
+              <el-button
+                link
+                type="danger"
+                icon="Delete"
+                @click="handleDelete(scope.row)"
+                v-hasPermi="['zsjy:internshipmonthly:remove']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 添加或修改月度总结对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="1000px" append-to-body>
+      <MonthlyForm ref="monthlyFormRef" @success="getList" />
+    </el-dialog>
+
+    <!-- 月度总结详情对话框 -->
+    <el-dialog title="月度总结详情" v-model="detailDialog.visible" width="1000px" append-to-body>
+      <MonthlyDetail ref="monthlyDetailRef" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="InternshipMonthly" lang="ts">
+import {
+  listInternshipMonthly,
+  getInternshipMonthly,
+  delInternshipMonthly,
+  submitInternshipMonthly,
+  exportInternshipMonthly
+} from '@/api/zsjy/internshipMonthly'
+import MonthlyForm from './form.vue'
+import MonthlyDetail from './detail.vue'
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { internship_monthly_status } = toRefs<any>(proxy?.useDict('internship_monthly_status'))
+
+const monthlyList = ref<any[]>([])
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref<Array<string | number>>([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+})
+
+const detailDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+})
+
+const initFormData: any = {
+  pageNum: 1,
+  pageSize: 10,
+  summaryMonth: undefined,
+  reviewStatus: undefined,
+  professionalSkills: undefined
+}
+const data = reactive<PageData<any, any>>({
+  form: {},
+  queryParams: { ...initFormData },
+  rules: {}
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+const queryFormRef = ref<ElFormInstance>()
+const monthlyFormRef = ref<ComponentRef<typeof MonthlyForm>>()
+const monthlyDetailRef = ref<ComponentRef<typeof MonthlyDetail>>()
+
+/** 查询月度总结列表 */
+const getList = async () => {
+  loading.value = true
+  const res = await listInternshipMonthly(queryParams.value)
+  monthlyList.value = res.rows
+  total.value = res.total
+  loading.value = false
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any) => {
+  ids.value = selection.map((item: any) => item.monthlyId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  dialog.visible = true
+  dialog.title = '添加月度总结'
+  nextTick(() => {
+    monthlyFormRef.value?.resetForm()
+  })
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: any) => {
+  dialog.visible = true
+  dialog.title = '修改月度总结'
+  nextTick(() => {
+    const monthlyId = row?.monthlyId || ids.value[0]
+    monthlyFormRef.value?.resetForm(monthlyId)
+  })
+}
+
+/** 查看详情操作 */
+const handleDetail = async (row: any) => {
+  detailDialog.visible = true
+  nextTick(() => {
+    monthlyDetailRef.value?.resetForm(row.monthlyId)
+  })
+}
+
+/** 提交审批操作 */
+const handleSubmit = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm('确认提交该月度总结?提交后将无法再次编辑')
+    await submitInternshipMonthly(row.monthlyId)
+    await getList()
+    proxy?.$modal.msgSuccess('提交成功')
+  } catch {}
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: any) => {
+  const monthlyIds = row?.monthlyId || ids.value
+  try {
+    await proxy?.$modal.confirm('是否确认删除月度总结编号为"' + monthlyIds + '"的数据项?')
+    await delInternshipMonthly(monthlyIds)
+    await getList()
+    proxy?.$modal.msgSuccess('删除成功')
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.$download('zsjy/internship/monthly/export', {
+    ...queryParams.value
+  }, `月度总结_${new Date().getTime()}.xlsx`)
+}
+
+/** 计算五项技能的平均分 */
+const getAverageScore = (row: any) => {
+  const scores = [
+    row.professionalSkills || 0,
+    row.communicationSkills || 0,
+    row.teamworkAbility || 0,
+    row.learningAbility || 0,
+    row.workAttitude || 0
+  ].filter(score => score > 0)
+  
+  if (scores.length === 0) return 0
+  return scores.reduce((sum, score) => sum + score, 0) / scores.length
+}
+
+/** 根据分数获取标签类型 */
+const getScoreTagType = (score: number) => {
+  if (score >= 9) return 'success'
+  if (score >= 7) return 'warning'
+  if (score >= 5) return ''
+  return 'danger'
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped>
+/* 操作按钮样式优化 - 参考后勤管理页面 */
+:deep(.el-table__fixed-right) {
+  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-table__fixed-right .el-table__cell) {
+  background-color: #fff;
+}
+
+:deep(.el-table__row:hover .el-table__cell) {
+  background-color: #f5f7fa !important;
+}
+
+/* 操作列按钮间距优化 */
+.small-padding .el-button + .el-button {
+  margin-left: 8px;
+}
+
+/* 确保操作按钮在固定列中居中对齐 */
+.small-padding {
+  text-align: center;
+}
+
+.small-padding .el-tooltip {
+  display: inline-block;
+  margin-right: 8px;
+}
+
+.small-padding .el-tooltip:last-child {
+  margin-right: 0;
+}
+
+/* 分页容器样式 */
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 20px 0 0 !important;
+  background-color: #fff;
+  margin-top: 0px !important;
+}
+</style>