feat(m1): add project stakeholder CRUD

This commit is contained in:
2026-05-25 14:46:30 +08:00
parent 4bbf1f552f
commit e96383433d
9 changed files with 553 additions and 6 deletions
@@ -0,0 +1,98 @@
package cn.craftlabs.platform.api.persistence.project;
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 java.time.OffsetDateTime;
@TableName("platform_project_stakeholder")
public class PlatformProjectStakeholder {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("project_id")
private Long projectId;
@TableField("contact_name")
private String contactName;
@TableField("contact_role")
private String contactRole;
private String phone;
private String email;
@TableField("is_internal")
private Boolean isInternal;
@TableField("created_at")
private OffsetDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactRole() {
return contactRole;
}
public void setContactRole(String contactRole) {
this.contactRole = contactRole;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getIsInternal() {
return isInternal;
}
public void setIsInternal(Boolean isInternal) {
this.isInternal = isInternal;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,7 @@
package cn.craftlabs.platform.api.persistence.project;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlatformProjectStakeholderMapper extends BaseMapper<PlatformProjectStakeholder> {}
@@ -4,6 +4,8 @@ import cn.craftlabs.platform.api.service.ProjectService;
import cn.craftlabs.platform.api.web.dto.PageResponse;
import cn.craftlabs.platform.api.web.dto.ProjectRequest;
import cn.craftlabs.platform.api.web.dto.ProjectResponse;
import cn.craftlabs.platform.api.web.dto.StakeholderRequest;
import cn.craftlabs.platform.api.web.dto.StakeholderResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@@ -20,7 +22,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/** 项目 API。{@code DELETE /{id}} 为<strong>物理删除</strong>。 */
import java.util.List;
@RestController
@RequestMapping("/api/v1/projects")
@Validated
@@ -62,4 +65,33 @@ public class ProjectController {
public void delete(@PathVariable("id") long id) {
projectService.delete(id);
}
@GetMapping("/{projectId}/stakeholders")
public List<StakeholderResponse> listStakeholders(@PathVariable("projectId") long projectId) {
return projectService.listStakeholders(projectId);
}
@PostMapping("/{projectId}/stakeholders")
@ResponseStatus(HttpStatus.CREATED)
public StakeholderResponse addStakeholder(
@PathVariable("projectId") long projectId,
@Valid @RequestBody StakeholderRequest request) {
return projectService.addStakeholder(projectId, request);
}
@PutMapping("/{projectId}/stakeholders/{id}")
public StakeholderResponse updateStakeholder(
@PathVariable("projectId") long projectId,
@PathVariable("id") long id,
@Valid @RequestBody StakeholderRequest request) {
return projectService.updateStakeholder(id, request);
}
@DeleteMapping("/{projectId}/stakeholders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteStakeholder(
@PathVariable("projectId") long projectId,
@PathVariable("id") long id) {
projectService.deleteStakeholder(id);
}
}
@@ -2,9 +2,13 @@ package cn.craftlabs.platform.api.service;
import cn.craftlabs.platform.api.persistence.project.PlatformProject;
import cn.craftlabs.platform.api.persistence.project.PlatformProjectMapper;
import cn.craftlabs.platform.api.persistence.project.PlatformProjectStakeholder;
import cn.craftlabs.platform.api.persistence.project.PlatformProjectStakeholderMapper;
import cn.craftlabs.platform.api.web.dto.PageResponse;
import cn.craftlabs.platform.api.web.dto.ProjectRequest;
import cn.craftlabs.platform.api.web.dto.ProjectResponse;
import cn.craftlabs.platform.api.web.dto.StakeholderRequest;
import cn.craftlabs.platform.api.web.dto.StakeholderResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -26,10 +30,12 @@ public class ProjectService {
private static final String DEFAULT_PHASE = "PLANNING";
private final PlatformProjectMapper projectMapper;
private final PlatformProjectStakeholderMapper stakeholderMapper;
private final CustomerService customerService;
public ProjectService(PlatformProjectMapper projectMapper, CustomerService customerService) {
public ProjectService(PlatformProjectMapper projectMapper, PlatformProjectStakeholderMapper stakeholderMapper, CustomerService customerService) {
this.projectMapper = projectMapper;
this.stakeholderMapper = stakeholderMapper;
this.customerService = customerService;
}
@@ -101,6 +107,74 @@ public class ProjectService {
}
}
@Transactional(readOnly = true)
public List<StakeholderResponse> listStakeholders(long projectId) {
LambdaQueryWrapper<PlatformProjectStakeholder> q =
Wrappers.lambdaQuery(PlatformProjectStakeholder.class)
.eq(PlatformProjectStakeholder::getProjectId, projectId)
.orderByAsc(PlatformProjectStakeholder::getId);
return stakeholderMapper.selectList(q).stream()
.map(this::toStakeholderResponse)
.collect(Collectors.toList());
}
@Transactional
public StakeholderResponse addStakeholder(long projectId, StakeholderRequest request) {
requireProjectExists(projectId);
PlatformProjectStakeholder s = new PlatformProjectStakeholder();
s.setProjectId(projectId);
s.setContactName(request.getContactName().trim());
s.setContactRole(request.getContactRole());
s.setPhone(request.getPhone());
s.setEmail(request.getEmail());
s.setIsInternal(request.getIsInternal() != null && request.getIsInternal());
s.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC));
stakeholderMapper.insert(s);
return toStakeholderResponse(s);
}
@Transactional
public StakeholderResponse updateStakeholder(long stakeholderId, StakeholderRequest request) {
PlatformProjectStakeholder s = stakeholderMapper.selectById(stakeholderId);
if (s == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "stakeholder not found");
}
s.setContactName(request.getContactName().trim());
s.setContactRole(request.getContactRole());
s.setPhone(request.getPhone());
s.setEmail(request.getEmail());
s.setIsInternal(request.getIsInternal() != null && request.getIsInternal());
stakeholderMapper.updateById(s);
return toStakeholderResponse(s);
}
@Transactional
public void deleteStakeholder(long stakeholderId) {
int rows = stakeholderMapper.deleteById(stakeholderId);
if (rows == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "stakeholder not found");
}
}
private void requireProjectExists(long projectId) {
if (projectMapper.selectById(projectId) == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "project not found");
}
}
private StakeholderResponse toStakeholderResponse(PlatformProjectStakeholder s) {
StakeholderResponse r = new StakeholderResponse();
r.setId(s.getId());
r.setProjectId(s.getProjectId());
r.setContactName(s.getContactName());
r.setContactRole(s.getContactRole());
r.setPhone(s.getPhone());
r.setEmail(s.getEmail());
r.setIsInternal(s.getIsInternal());
r.setCreatedAt(s.getCreatedAt());
return r;
}
private String resolvePhase(String phase) {
return StringUtils.hasText(phase) ? phase.trim() : DEFAULT_PHASE;
}
@@ -0,0 +1,62 @@
package cn.craftlabs.platform.api.web.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class StakeholderRequest {
@NotBlank
@Size(max = 128)
private String contactName;
@Size(max = 64)
private String contactRole;
@Size(max = 32)
private String phone;
@Size(max = 128)
private String email;
private Boolean isInternal;
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactRole() {
return contactRole;
}
public void setContactRole(String contactRole) {
this.contactRole = contactRole;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getIsInternal() {
return isInternal;
}
public void setIsInternal(Boolean isInternal) {
this.isInternal = isInternal;
}
}
@@ -0,0 +1,79 @@
package cn.craftlabs.platform.api.web.dto;
import java.time.OffsetDateTime;
public class StakeholderResponse {
private Long id;
private Long projectId;
private String contactName;
private String contactRole;
private String phone;
private String email;
private Boolean isInternal;
private OffsetDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactRole() {
return contactRole;
}
public void setContactRole(String contactRole) {
this.contactRole = contactRole;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getIsInternal() {
return isInternal;
}
public void setIsInternal(Boolean isInternal) {
this.isInternal = isInternal;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,11 @@
CREATE TABLE platform_project_stakeholder (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES platform_project(id),
contact_name VARCHAR(128) NOT NULL,
contact_role VARCHAR(64),
phone VARCHAR(32),
email VARCHAR(128),
is_internal BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_stakeholder_project ON platform_project_stakeholder(project_id);