From 88c9fe5e0662ec995fe1e688766cd79a0fd0704b Mon Sep 17 00:00:00 2001 From: JiaoTianBo Date: Tue, 31 Mar 2026 15:45:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(open-api):=20=E6=96=B0=E5=A2=9E=E5=AF=B9?= =?UTF-8?q?=E5=A4=96=E5=BC=80=E6=94=BE=E6=8E=A5=E5=8F=A3=E5=8F=8A=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=97=A5=E6=8A=A5=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增项目日报表及其防重唯一索引,支持外部系统同步日报数据 - 添加项目日报实体类及对应 Mapper 和 XML 配置 - 新增对外开放接口控制器 OpenApiController,实现项目列表查询及日报同步接口 - 实现 OpenApiService 服务及其实现类,包含用户项目查询和日报防重同步逻辑 - 扩展 ProjectMapper,支持根据用户名查询用户关联项目列表 - 配置 SaToken 过滤白名单,放行 /api/open/** 路径无登录验证 - 引入 spring-boot-starter-validation 依赖,支持请求参数校验 - 创建数据传输对象 DailyReportSyncDTO,带参数校验注解 - 日志记录和异常处理增强,保证数据同步和查询的健壮性 --- docs/dev-ops/pgsql/sql/weform_run.sql | 28 +++- pom.xml | 6 + .../common/config/SaTokenConfig.java | 1 + .../controller/open/OpenApiController.java | 72 ++++++++++ .../domain/dto/DailyReportSyncDTO.java | 62 ++++++++ .../domain/entity/ProjectDailyReport.java | 83 +++++++++++ .../cn/yinlihupo/domain/vo/OpenProjectVO.java | 62 ++++++++ .../mapper/ProjectDailyReportMapper.java | 27 ++++ .../cn/yinlihupo/mapper/ProjectMapper.java | 9 ++ .../service/open/OpenApiService.java | 28 ++++ .../service/open/impl/OpenApiServiceImpl.java | 134 ++++++++++++++++++ .../mapper/ProjectDailyReportMapper.xml | 16 +++ src/main/resources/mapper/ProjectMapper.xml | 14 ++ 13 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cn/yinlihupo/controller/open/OpenApiController.java create mode 100644 src/main/java/cn/yinlihupo/domain/dto/DailyReportSyncDTO.java create mode 100644 src/main/java/cn/yinlihupo/domain/entity/ProjectDailyReport.java create mode 100644 src/main/java/cn/yinlihupo/domain/vo/OpenProjectVO.java create mode 100644 src/main/java/cn/yinlihupo/mapper/ProjectDailyReportMapper.java create mode 100644 src/main/java/cn/yinlihupo/service/open/OpenApiService.java create mode 100644 src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java create mode 100644 src/main/resources/mapper/ProjectDailyReportMapper.xml diff --git a/docs/dev-ops/pgsql/sql/weform_run.sql b/docs/dev-ops/pgsql/sql/weform_run.sql index c0b2e57..b4fa7ad 100644 --- a/docs/dev-ops/pgsql/sql/weform_run.sql +++ b/docs/dev-ops/pgsql/sql/weform_run.sql @@ -1337,4 +1337,30 @@ INSERT INTO sys_config (config_key, config_value, config_type, description) VALU ('ai.embedding.model', 'text-embedding-v4', 'ai', '向量嵌入模型'), ('ai.embedding.dimension', '1024', 'ai', '向量维度'), ('ai.rag.top_k', '5', 'ai', 'RAG检索返回数量'), - ('ai.rag.similarity_threshold', '0.7', 'ai', 'RAG相似度阈值'); + ('ai.rag.similarity_threshold', '0.7', 'ai', 'RAG相似度阈値'); + +-- ===================================================== +-- 项目日报表(供外部系统同步日报数据) +-- 防重键: (project_id, report_date, submitter_username) +-- ===================================================== +CREATE TABLE IF NOT EXISTS project_daily_report ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL, + submitter_username VARCHAR(100) NOT NULL, + submitter_id BIGINT, + report_date DATE NOT NULL, + work_content TEXT, + tomorrow_plan TEXT, + work_intensity INTEGER CHECK (work_intensity BETWEEN 1 AND 5), + need_help BOOLEAN DEFAULT FALSE, + help_content TEXT, + create_time TIMESTAMP DEFAULT NOW(), + update_time TIMESTAMP DEFAULT NOW(), + deleted INTEGER DEFAULT 0, + CONSTRAINT uq_daily_report UNIQUE (project_id, report_date, submitter_username) +); + +COMMENT ON TABLE project_daily_report IS '项目日报(外部同步)'; +COMMENT ON COLUMN project_daily_report.submitter_username IS '提交人用户名 (对应 sys_user.username)'; +COMMENT ON COLUMN project_daily_report.work_intensity IS '工作强度: 1-轻松 2-较轻 3-适中 4-繁忙 5-非常繁忙'; + diff --git a/pom.xml b/pom.xml index 4b5b396..e9c4956 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,12 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + org.projectlombok diff --git a/src/main/java/cn/yinlihupo/common/config/SaTokenConfig.java b/src/main/java/cn/yinlihupo/common/config/SaTokenConfig.java index 2bdc973..368fbea 100644 --- a/src/main/java/cn/yinlihupo/common/config/SaTokenConfig.java +++ b/src/main/java/cn/yinlihupo/common/config/SaTokenConfig.java @@ -31,6 +31,7 @@ public class SaTokenConfig implements WebMvcConfigurer { "/api/v1/auth/login", "/api/v1/auth/register", "/api/v1/auth/feishu/login", + "/api/open/**", "/error", "/swagger-ui/**", "/v3/api-docs/**" diff --git a/src/main/java/cn/yinlihupo/controller/open/OpenApiController.java b/src/main/java/cn/yinlihupo/controller/open/OpenApiController.java new file mode 100644 index 0000000..8de186e --- /dev/null +++ b/src/main/java/cn/yinlihupo/controller/open/OpenApiController.java @@ -0,0 +1,72 @@ +package cn.yinlihupo.controller.open; + +import cn.yinlihupo.common.core.BaseResponse; +import cn.yinlihupo.common.util.ResultUtils; +import cn.yinlihupo.domain.dto.DailyReportSyncDTO; +import cn.yinlihupo.domain.vo.OpenProjectVO; +import cn.yinlihupo.service.open.OpenApiService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 对外开放接口 + * 路径前缀 /api/open/** 已在 SaTokenConfig 中放行,无需登录鉴权 + */ +@Slf4j +@RestController +@RequestMapping("/api/open") +@RequiredArgsConstructor +public class OpenApiController { + + private final OpenApiService openApiService; + + /** + * 根据用户ID查询所在的项目列表 + *

+ * 请求示例:GET /api/open/projects/by-user?userId=zhangsan + * + * @param userId 用户标识(对应 sys_user.username) + * @return 项目列表 + */ + @GetMapping("/projects/by-user") + public BaseResponse> getProjectsByUser( + @RequestParam("userId") String userId) { + log.info("[OpenApi] 查询用户项目列表, userId={}", userId); + List projects = openApiService.getProjectsByUserId(userId); + return ResultUtils.success("查询成功", projects); + } + + /** + * 同步日报数据 + *

+ * 请求示例:POST /api/open/daily-report/sync + *

+     * {
+     *   "projectId": 123,
+     *   "userId": "zhangsan",
+     *   "reportDate": "2026-03-31",
+     *   "workContent": "完成了XXX功能开发",
+     *   "tomorrowPlan": "继续YYY模块",
+     *   "workIntensity": 3,
+     *   "needHelp": false,
+     *   "helpContent": null
+     * }
+     * 
+ * 防重规则:同一项目+同一日期+同一用户只能提交一次,重复提交返回错误提示 + * + * @param dto 日报数据 + * @return 操作结果 + */ + @PostMapping("/daily-report/sync") + public BaseResponse syncDailyReport( + @RequestBody @Valid DailyReportSyncDTO dto) { + log.info("[OpenApi] 日报同步请求, projectId={}, userId={}, reportDate={}", + dto.getProjectId(), dto.getUserId(), dto.getReportDate()); + String result = openApiService.syncDailyReport(dto); + return ResultUtils.success(result, null); + } +} diff --git a/src/main/java/cn/yinlihupo/domain/dto/DailyReportSyncDTO.java b/src/main/java/cn/yinlihupo/domain/dto/DailyReportSyncDTO.java new file mode 100644 index 0000000..bf13167 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/dto/DailyReportSyncDTO.java @@ -0,0 +1,62 @@ +package cn.yinlihupo.domain.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Data; + +import java.time.LocalDate; + +/** + * 日报数据同步 DTO(供外部系统调用) + */ +@Data +public class DailyReportSyncDTO { + + /** + * 项目ID(必填) + */ + @NotNull(message = "项目ID不能为空") + private Long projectId; + + /** + * 用户标识(对应 sys_user.username,必填) + */ + @NotBlank(message = "用户ID不能为空") + private String userId; + + /** + * 日报日期(必填) + */ + @NotNull(message = "日报日期不能为空") + private LocalDate reportDate; + + /** + * 工作内容(必填) + */ + @NotBlank(message = "工作内容不能为空") + private String workContent; + + /** + * 明日计划 + */ + private String tomorrowPlan; + + /** + * 工作强度 1-5 (1-轻松, 2-较轻, 3-适中, 4-繁忙, 5-非常繁忙) + */ + @Min(value = 1, message = "工作强度最小为1") + @Max(value = 5, message = "工作强度最大为5") + private Integer workIntensity; + + /** + * 是否需要协助 + */ + private Boolean needHelp; + + /** + * 协助内容(needHelp 为 true 时填写) + */ + private String helpContent; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/ProjectDailyReport.java b/src/main/java/cn/yinlihupo/domain/entity/ProjectDailyReport.java new file mode 100644 index 0000000..dbdda96 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/ProjectDailyReport.java @@ -0,0 +1,83 @@ +package cn.yinlihupo.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 项目日报实体类 + * 对应数据库表: project_daily_report + * 防重键: (project_id, report_date, submitter_username) + */ +@Data +@TableName("project_daily_report") +public class ProjectDailyReport { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 项目ID + */ + private Long projectId; + + /** + * 提交人用户名 (对应 sys_user.username) + */ + private String submitterUsername; + + /** + * 提交人ID (冗余,方便查询) + */ + private Long submitterId; + + /** + * 日报日期 + */ + private LocalDate reportDate; + + /** + * 工作内容 + */ + private String workContent; + + /** + * 明日计划 + */ + private String tomorrowPlan; + + /** + * 工作强度 1-5 (1-轻松, 2-较轻, 3-适中, 4-繁忙, 5-非常繁忙) + */ + private Integer workIntensity; + + /** + * 是否需要协助 + */ + private Boolean needHelp; + + /** + * 协助内容 + */ + private String helpContent; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** + * 删除标记 + */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/cn/yinlihupo/domain/vo/OpenProjectVO.java b/src/main/java/cn/yinlihupo/domain/vo/OpenProjectVO.java new file mode 100644 index 0000000..479dc56 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/vo/OpenProjectVO.java @@ -0,0 +1,62 @@ +package cn.yinlihupo.domain.vo; + +import lombok.Data; + +import java.time.LocalDate; + +/** + * 开放接口-项目列表 VO(对外简化字段,供外部系统使用) + */ +@Data +public class OpenProjectVO { + + /** + * 项目ID + */ + private Long id; + + /** + * 项目编号 + */ + private String projectCode; + + /** + * 项目名称 + */ + private String projectName; + + /** + * 项目类型 + */ + private String projectType; + + /** + * 项目状态: draft-草稿, planning-规划中, ongoing-进行中, paused-暂停, completed-已完成, cancelled-已取消 + */ + private String status; + + /** + * 优先级: critical-关键, high-高, medium-中, low-低 + */ + private String priority; + + /** + * 计划开始日期 + */ + private LocalDate planStartDate; + + /** + * 计划结束日期 + */ + private LocalDate planEndDate; + + /** + * 项目进度百分比 + */ + private Integer progress; + + /** + * 该用户在项目中的角色: manager-项目经理, leader-负责人, member-成员, observer-观察者 + */ + private String myRole; +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectDailyReportMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectDailyReportMapper.java new file mode 100644 index 0000000..50c0e7f --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectDailyReportMapper.java @@ -0,0 +1,27 @@ +package cn.yinlihupo.mapper; + +import cn.yinlihupo.domain.entity.ProjectDailyReport; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; + +/** + * 项目日报 Mapper 接口 + */ +@Mapper +public interface ProjectDailyReportMapper extends BaseMapper { + + /** + * 防重检查:查询同项目+同日期+同用户的日报数量 + * + * @param projectId 项目ID + * @param reportDate 日报日期 + * @param submitterUsername 提交人用户名 + * @return 记录数量,大于0表示已存在 + */ + int countByUniqueKey(@Param("projectId") Long projectId, + @Param("reportDate") LocalDate reportDate, + @Param("submitterUsername") String submitterUsername); +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java index 6f68a21..3b00a36 100644 --- a/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java +++ b/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java @@ -45,4 +45,13 @@ public interface ProjectMapper extends BaseMapper { * 查询即将超期的项目(N天内) */ List selectAboutToExpire(@Param("days") int days); + + /** + * 根据 sys_user.username 联查用户所在的项目列表 + * (包含作为项目经理或项目成员的项目) + * + * @param username 用户名 (对应 sys_user.username) + * @return 项目列表 + */ + List selectProjectsByUsername(@Param("username") String username); } diff --git a/src/main/java/cn/yinlihupo/service/open/OpenApiService.java b/src/main/java/cn/yinlihupo/service/open/OpenApiService.java new file mode 100644 index 0000000..c77a2cf --- /dev/null +++ b/src/main/java/cn/yinlihupo/service/open/OpenApiService.java @@ -0,0 +1,28 @@ +package cn.yinlihupo.service.open; + +import cn.yinlihupo.domain.dto.DailyReportSyncDTO; +import cn.yinlihupo.domain.vo.OpenProjectVO; + +import java.util.List; + +/** + * 开放接口服务(供外部系统调用,无需登录鉴权) + */ +public interface OpenApiService { + + /** + * 根据用户标识 (sys_user.username) 查询用户所在的项目列表 + * + * @param userId 用户标识(对应 sys_user.username) + * @return 项目列表 + */ + List getProjectsByUserId(String userId); + + /** + * 同步日报数据到库(带防重设计) + * + * @param dto 日报数据 + * @return 操作结果描述 + */ + String syncDailyReport(DailyReportSyncDTO dto); +} diff --git a/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java b/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java new file mode 100644 index 0000000..e92e873 --- /dev/null +++ b/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java @@ -0,0 +1,134 @@ +package cn.yinlihupo.service.open.impl; + +import cn.yinlihupo.common.exception.BusinessException; +import cn.yinlihupo.domain.dto.DailyReportSyncDTO; +import cn.yinlihupo.domain.entity.Project; +import cn.yinlihupo.domain.entity.ProjectDailyReport; +import cn.yinlihupo.domain.entity.SysUser; +import cn.yinlihupo.domain.vo.OpenProjectVO; +import cn.yinlihupo.mapper.ProjectDailyReportMapper; +import cn.yinlihupo.mapper.ProjectMapper; +import cn.yinlihupo.mapper.ProjectMemberMapper; +import cn.yinlihupo.mapper.SysUserMapper; +import cn.yinlihupo.service.open.OpenApiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 开放接口服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenApiServiceImpl implements OpenApiService { + + private final SysUserMapper sysUserMapper; + private final ProjectMapper projectMapper; + private final ProjectMemberMapper projectMemberMapper; + private final ProjectDailyReportMapper projectDailyReportMapper; + + /** + * 根据用户标识 (sys_user.username) 查询用户所在的项目列表 + */ + @Override + public List getProjectsByUserId(String userId) { + if (!StringUtils.hasText(userId)) { + return new ArrayList<>(); + } + + // 1. 先验证用户是否存在 + SysUser user = sysUserMapper.selectByUsername(userId); + if (user == null) { + log.warn("[OpenApi] 用户不存在, username={}", userId); + return new ArrayList<>(); + } + + // 2. 联查用户所在的项目列表(通过 username 关联 sys_user -> project/project_member) + List projects = projectMapper.selectProjectsByUsername(userId); + if (projects == null || projects.isEmpty()) { + return new ArrayList<>(); + } + + // 3. 转换为 OpenProjectVO 并填充用户角色 + List result = new ArrayList<>(); + for (Project project : projects) { + OpenProjectVO vo = new OpenProjectVO(); + vo.setId(project.getId()); + vo.setProjectCode(project.getProjectCode()); + vo.setProjectName(project.getProjectName()); + vo.setProjectType(project.getProjectType()); + vo.setStatus(project.getStatus()); + vo.setPriority(project.getPriority()); + vo.setPlanStartDate(project.getPlanStartDate()); + vo.setPlanEndDate(project.getPlanEndDate()); + vo.setProgress(project.getProgress()); + + // 4. 填充用户在该项目的角色 + if (project.getManagerId() != null && project.getManagerId().equals(user.getId())) { + vo.setMyRole("manager"); + } else { + String role = projectMemberMapper.selectRoleByUserAndProject(project.getId(), user.getId()); + vo.setMyRole(role); + } + result.add(vo); + } + + log.info("[OpenApi] 查询用户项目列表成功, username={}, 项目数={}", userId, result.size()); + return result; + } + + /** + * 同步日报数据到库(带防重设计) + */ + @Override + public String syncDailyReport(DailyReportSyncDTO dto) { + String username = dto.getUserId(); + + // 1. 校验用户是否存在 + SysUser user = sysUserMapper.selectByUsername(username); + if (user == null) { + log.warn("[OpenApi] 日报同步失败,用户不存在, username={}", username); + throw new BusinessException("用户不存在: " + username); + } + + // 2. 校验项目是否存在 + Project project = projectMapper.selectById(dto.getProjectId()); + if (project == null || project.getDeleted() == 1) { + log.warn("[OpenApi] 日报同步失败,项目不存在, projectId={}", dto.getProjectId()); + throw new BusinessException("项目不存在: " + dto.getProjectId()); + } + + // 3. 防重检查:同一用户同一天同一项目只能提交一条日报 + int count = projectDailyReportMapper.countByUniqueKey( + dto.getProjectId(), dto.getReportDate(), username); + if (count > 0) { + log.warn("[OpenApi] 日报重复提交, projectId={}, reportDate={}, username={}", + dto.getProjectId(), dto.getReportDate(), username); + throw new BusinessException("日报已提交,请勿重复提交(项目ID: " + + dto.getProjectId() + ",日期: " + dto.getReportDate() + ")"); + } + + // 4. 入库保存 + ProjectDailyReport report = new ProjectDailyReport(); + report.setProjectId(dto.getProjectId()); + report.setSubmitterUsername(username); + report.setSubmitterId(user.getId()); + report.setReportDate(dto.getReportDate()); + report.setWorkContent(dto.getWorkContent()); + report.setTomorrowPlan(dto.getTomorrowPlan()); + report.setWorkIntensity(dto.getWorkIntensity()); + report.setNeedHelp(dto.getNeedHelp()); + report.setHelpContent(dto.getHelpContent()); + + projectDailyReportMapper.insert(report); + + log.info("[OpenApi] 日报同步成功, projectId={}, reportDate={}, username={}", + dto.getProjectId(), dto.getReportDate(), username); + return "日报同步成功"; + } +} diff --git a/src/main/resources/mapper/ProjectDailyReportMapper.xml b/src/main/resources/mapper/ProjectDailyReportMapper.xml new file mode 100644 index 0000000..cb50d4f --- /dev/null +++ b/src/main/resources/mapper/ProjectDailyReportMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/mapper/ProjectMapper.xml b/src/main/resources/mapper/ProjectMapper.xml index ab05829..dd0df9c 100644 --- a/src/main/resources/mapper/ProjectMapper.xml +++ b/src/main/resources/mapper/ProjectMapper.xml @@ -107,4 +107,18 @@ ORDER BY plan_end_date ASC + + +