feat(open-api): 新增对外开放接口及项目日报同步功能

- 新增项目日报表及其防重唯一索引,支持外部系统同步日报数据
- 添加项目日报实体类及对应 Mapper 和 XML 配置
- 新增对外开放接口控制器 OpenApiController,实现项目列表查询及日报同步接口
- 实现 OpenApiService 服务及其实现类,包含用户项目查询和日报防重同步逻辑
- 扩展 ProjectMapper,支持根据用户名查询用户关联项目列表
- 配置 SaToken 过滤白名单,放行 /api/open/** 路径无登录验证
- 引入 spring-boot-starter-validation 依赖,支持请求参数校验
- 创建数据传输对象 DailyReportSyncDTO,带参数校验注解
- 日志记录和异常处理增强,保证数据同步和查询的健壮性
This commit is contained in:
2026-03-31 15:45:36 +08:00
parent 135e723c64
commit 88c9fe5e06
13 changed files with 541 additions and 1 deletions

View File

@@ -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<OpenProjectVO> getProjectsByUserId(String userId);
/**
* 同步日报数据到库(带防重设计)
*
* @param dto 日报数据
* @return 操作结果描述
*/
String syncDailyReport(DailyReportSyncDTO dto);
}

View File

@@ -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<OpenProjectVO> 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<Project> projects = projectMapper.selectProjectsByUsername(userId);
if (projects == null || projects.isEmpty()) {
return new ArrayList<>();
}
// 3. 转换为 OpenProjectVO 并填充用户角色
List<OpenProjectVO> 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 "日报同步成功";
}
}