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
+
+
+