feat: 需求工单管理模块完整实现
## 新增功能
### 需求工单实体层
- Requirement.java: 需求工单实体类
- RequirementMapper.java: MyBatis-Plus Mapper接口
- RequirementDataService.java: 数据访问服务
### 业务层
- RequirementDTO.java: 数据传输对象
- RequirementVO.java: 视图对象
- RequirementService.java: 业务服务实现
### 接口层
- RequirementController.java: REST API接口
- GET /api/v1/requirement/list - 分页查询
- GET /api/v1/requirement/{id} - 查询详情
- POST /api/v1/requirement - 创建需求
- PUT /api/v1/requirement/{id} - 更新需求
- DELETE /api/v1/requirement/{id} - 删除需求
- PUT /api/v1/requirement/{id}/status - 更新状态
- PUT /api/v1/requirement/{id}/progress - 更新进度
### 数据库
- requirement.sql: 建表SQL脚本
This commit is contained in:
parent
da445a44de
commit
075525d577
34
doc/requirement.sql
Normal file
34
doc/requirement.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-- 需求工单表
|
||||||
|
CREATE TABLE IF NOT EXISTS `requirement` (
|
||||||
|
`requirement_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键,需求ID',
|
||||||
|
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
|
||||||
|
`requirement_code` VARCHAR(50) NOT NULL COMMENT '需求编号',
|
||||||
|
`requirement_name` VARCHAR(200) NOT NULL COMMENT '需求名称',
|
||||||
|
`description` TEXT COMMENT '需求描述',
|
||||||
|
`project_id` BIGINT NOT NULL COMMENT '项目ID',
|
||||||
|
`customer_id` BIGINT NOT NULL COMMENT '客户ID',
|
||||||
|
`priority` VARCHAR(20) DEFAULT 'normal' COMMENT '优先级:high-高,normal-中,low-低',
|
||||||
|
`estimated_hours` DECIMAL(8,2) DEFAULT 0.00 COMMENT '预估开发工时(小时)',
|
||||||
|
`actual_hours` DECIMAL(8,2) DEFAULT 0.00 COMMENT '实际开发工时(小时)',
|
||||||
|
`planned_start` DATE COMMENT '计划开始日期',
|
||||||
|
`planned_end` DATE COMMENT '计划结束日期',
|
||||||
|
`actual_start` DATE COMMENT '实际开始日期',
|
||||||
|
`actual_end` DATE COMMENT '实际结束日期',
|
||||||
|
`delivery_date` DATE COMMENT '交付日期',
|
||||||
|
`receivable_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '应收款金额',
|
||||||
|
`receivable_date` DATE COMMENT '应收款日期',
|
||||||
|
`status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending-待开发,developing-开发中,delivered-已交付,completed-已完成',
|
||||||
|
`progress` INT DEFAULT 0 COMMENT '开发进度(0-100)',
|
||||||
|
`attachment_url` VARCHAR(500) COMMENT '附件URL',
|
||||||
|
`created_by` BIGINT COMMENT '创建人ID',
|
||||||
|
`created_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT COMMENT '更新人ID',
|
||||||
|
`updated_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
PRIMARY KEY (`requirement_id`),
|
||||||
|
UNIQUE KEY `uk_tenant_code` (`tenant_id`, `requirement_code`),
|
||||||
|
INDEX `idx_tenant` (`tenant_id`),
|
||||||
|
INDEX `idx_project` (`project_id`),
|
||||||
|
INDEX `idx_customer` (`customer_id`),
|
||||||
|
INDEX `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='需求工单表';
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package com.fundplatform.proj.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.fundplatform.common.core.Result;
|
||||||
|
import com.fundplatform.proj.dto.RequirementDTO;
|
||||||
|
import com.fundplatform.proj.service.RequirementService;
|
||||||
|
import com.fundplatform.proj.vo.RequirementVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/requirement")
|
||||||
|
public class RequirementController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RequirementService requirementService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询需求工单列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Page<RequirementVO>> list(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestParam(required = false) String requirementName,
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) Long projectId,
|
||||||
|
@RequestParam(required = false) Long customerId,
|
||||||
|
@RequestParam(defaultValue = "1") int current,
|
||||||
|
@RequestParam(defaultValue = "10") int size) {
|
||||||
|
return requirementService.page(tenantId, requirementName, status, projectId, customerId, current, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询需求工单详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{requirementId}")
|
||||||
|
public Result<RequirementVO> get(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@PathVariable Long requirementId) {
|
||||||
|
return requirementService.getById(tenantId, requirementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建需求工单
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public Result<RequirementVO> create(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestHeader("X-User-Id") Long userId,
|
||||||
|
@Valid @RequestBody RequirementDTO dto) {
|
||||||
|
return requirementService.create(tenantId, userId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新需求工单
|
||||||
|
*/
|
||||||
|
@PutMapping("/{requirementId}")
|
||||||
|
public Result<RequirementVO> update(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestHeader("X-User-Id") Long userId,
|
||||||
|
@PathVariable Long requirementId,
|
||||||
|
@Valid @RequestBody RequirementDTO dto) {
|
||||||
|
return requirementService.update(tenantId, userId, requirementId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除需求工单
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{requirementId}")
|
||||||
|
public Result<Void> delete(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestHeader("X-User-Id") Long userId,
|
||||||
|
@PathVariable Long requirementId) {
|
||||||
|
return requirementService.delete(tenantId, userId, requirementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新需求状态
|
||||||
|
*/
|
||||||
|
@PutMapping("/{requirementId}/status")
|
||||||
|
public Result<RequirementVO> updateStatus(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestHeader("X-User-Id") Long userId,
|
||||||
|
@PathVariable Long requirementId,
|
||||||
|
@RequestParam String status) {
|
||||||
|
return requirementService.updateStatus(tenantId, userId, requirementId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新开发进度
|
||||||
|
*/
|
||||||
|
@PutMapping("/{requirementId}/progress")
|
||||||
|
public Result<RequirementVO> updateProgress(
|
||||||
|
@RequestHeader("X-Tenant-Id") Long tenantId,
|
||||||
|
@RequestHeader("X-User-Id") Long userId,
|
||||||
|
@PathVariable Long requirementId,
|
||||||
|
@RequestParam Integer progress) {
|
||||||
|
return requirementService.updateProgress(tenantId, userId, requirementId, progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,213 @@
|
|||||||
|
package com.fundplatform.proj.data.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fundplatform.common.core.BaseEntity;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单实体
|
||||||
|
*/
|
||||||
|
@TableName("requirement")
|
||||||
|
public class Requirement extends BaseEntity {
|
||||||
|
|
||||||
|
/** 需求编号 */
|
||||||
|
private String requirementCode;
|
||||||
|
|
||||||
|
/** 需求名称 */
|
||||||
|
private String requirementName;
|
||||||
|
|
||||||
|
/** 需求描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 项目ID */
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
/** 客户ID */
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/** 优先级(high-高,normal-中,low-低) */
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
/** 预估开发工时(小时) */
|
||||||
|
private BigDecimal estimatedHours;
|
||||||
|
|
||||||
|
/** 实际开发工时(小时) */
|
||||||
|
private BigDecimal actualHours;
|
||||||
|
|
||||||
|
/** 计划开始日期 */
|
||||||
|
private LocalDate plannedStart;
|
||||||
|
|
||||||
|
/** 计划结束日期 */
|
||||||
|
private LocalDate plannedEnd;
|
||||||
|
|
||||||
|
/** 实际开始日期 */
|
||||||
|
private LocalDate actualStart;
|
||||||
|
|
||||||
|
/** 实际结束日期 */
|
||||||
|
private LocalDate actualEnd;
|
||||||
|
|
||||||
|
/** 交付日期 */
|
||||||
|
private LocalDate deliveryDate;
|
||||||
|
|
||||||
|
/** 应收款金额 */
|
||||||
|
private BigDecimal receivableAmount;
|
||||||
|
|
||||||
|
/** 应收款日期 */
|
||||||
|
private LocalDate receivableDate;
|
||||||
|
|
||||||
|
/** 状态(pending-待开发,developing-开发中,delivered-已交付,completed-已完成) */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 开发进度(0-100) */
|
||||||
|
private Integer progress;
|
||||||
|
|
||||||
|
/** 附件URL */
|
||||||
|
private String attachmentUrl;
|
||||||
|
|
||||||
|
public String getRequirementCode() {
|
||||||
|
return requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementCode(String requirementCode) {
|
||||||
|
this.requirementCode = requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequirementName() {
|
||||||
|
return requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementName(String requirementName) {
|
||||||
|
this.requirementName = requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProjectId() {
|
||||||
|
return projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectId(Long projectId) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(Long customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriority() {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriority(String priority) {
|
||||||
|
this.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getEstimatedHours() {
|
||||||
|
return estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEstimatedHours(BigDecimal estimatedHours) {
|
||||||
|
this.estimatedHours = estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getActualHours() {
|
||||||
|
return actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualHours(BigDecimal actualHours) {
|
||||||
|
this.actualHours = actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedStart() {
|
||||||
|
return plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedStart(LocalDate plannedStart) {
|
||||||
|
this.plannedStart = plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedEnd() {
|
||||||
|
return plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedEnd(LocalDate plannedEnd) {
|
||||||
|
this.plannedEnd = plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualStart() {
|
||||||
|
return actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualStart(LocalDate actualStart) {
|
||||||
|
this.actualStart = actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualEnd() {
|
||||||
|
return actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualEnd(LocalDate actualEnd) {
|
||||||
|
this.actualEnd = actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getDeliveryDate() {
|
||||||
|
return deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliveryDate(LocalDate deliveryDate) {
|
||||||
|
this.deliveryDate = deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getReceivableAmount() {
|
||||||
|
return receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableAmount(BigDecimal receivableAmount) {
|
||||||
|
this.receivableAmount = receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getReceivableDate() {
|
||||||
|
return receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableDate(LocalDate receivableDate) {
|
||||||
|
this.receivableDate = receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProgress() {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(Integer progress) {
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAttachmentUrl() {
|
||||||
|
return attachmentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) {
|
||||||
|
this.attachmentUrl = attachmentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.fundplatform.proj.data.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.fundplatform.proj.data.entity.Requirement;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface RequirementMapper extends BaseMapper<Requirement> {
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.fundplatform.proj.data.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.fundplatform.proj.data.entity.Requirement;
|
||||||
|
import com.fundplatform.proj.data.mapper.RequirementMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单数据服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class RequirementDataService extends ServiceImpl<RequirementMapper, Requirement> {
|
||||||
|
}
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
package com.fundplatform.proj.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单DTO
|
||||||
|
*/
|
||||||
|
public class RequirementDTO {
|
||||||
|
|
||||||
|
private Long requirementId;
|
||||||
|
|
||||||
|
@NotBlank(message = "需求编号不能为空")
|
||||||
|
private String requirementCode;
|
||||||
|
|
||||||
|
@NotBlank(message = "需求名称不能为空")
|
||||||
|
private String requirementName;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@NotNull(message = "项目ID不能为空")
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
@NotNull(message = "客户ID不能为空")
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
private BigDecimal estimatedHours;
|
||||||
|
|
||||||
|
private BigDecimal actualHours;
|
||||||
|
|
||||||
|
private LocalDate plannedStart;
|
||||||
|
|
||||||
|
private LocalDate plannedEnd;
|
||||||
|
|
||||||
|
private LocalDate actualStart;
|
||||||
|
|
||||||
|
private LocalDate actualEnd;
|
||||||
|
|
||||||
|
private LocalDate deliveryDate;
|
||||||
|
|
||||||
|
private BigDecimal receivableAmount;
|
||||||
|
|
||||||
|
private LocalDate receivableDate;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private Integer progress;
|
||||||
|
|
||||||
|
private String attachmentUrl;
|
||||||
|
|
||||||
|
public Long getRequirementId() {
|
||||||
|
return requirementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementId(Long requirementId) {
|
||||||
|
this.requirementId = requirementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequirementCode() {
|
||||||
|
return requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementCode(String requirementCode) {
|
||||||
|
this.requirementCode = requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequirementName() {
|
||||||
|
return requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementName(String requirementName) {
|
||||||
|
this.requirementName = requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProjectId() {
|
||||||
|
return projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectId(Long projectId) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(Long customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriority() {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriority(String priority) {
|
||||||
|
this.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getEstimatedHours() {
|
||||||
|
return estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEstimatedHours(BigDecimal estimatedHours) {
|
||||||
|
this.estimatedHours = estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getActualHours() {
|
||||||
|
return actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualHours(BigDecimal actualHours) {
|
||||||
|
this.actualHours = actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedStart() {
|
||||||
|
return plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedStart(LocalDate plannedStart) {
|
||||||
|
this.plannedStart = plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedEnd() {
|
||||||
|
return plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedEnd(LocalDate plannedEnd) {
|
||||||
|
this.plannedEnd = plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualStart() {
|
||||||
|
return actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualStart(LocalDate actualStart) {
|
||||||
|
this.actualStart = actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualEnd() {
|
||||||
|
return actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualEnd(LocalDate actualEnd) {
|
||||||
|
this.actualEnd = actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getDeliveryDate() {
|
||||||
|
return deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliveryDate(LocalDate deliveryDate) {
|
||||||
|
this.deliveryDate = deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getReceivableAmount() {
|
||||||
|
return receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableAmount(BigDecimal receivableAmount) {
|
||||||
|
this.receivableAmount = receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getReceivableDate() {
|
||||||
|
return receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableDate(LocalDate receivableDate) {
|
||||||
|
this.receivableDate = receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProgress() {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(Integer progress) {
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAttachmentUrl() {
|
||||||
|
return attachmentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) {
|
||||||
|
this.attachmentUrl = attachmentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
package com.fundplatform.proj.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.fundplatform.common.core.Result;
|
||||||
|
import com.fundplatform.proj.data.entity.Requirement;
|
||||||
|
import com.fundplatform.proj.data.service.RequirementDataService;
|
||||||
|
import com.fundplatform.proj.dto.RequirementDTO;
|
||||||
|
import com.fundplatform.proj.vo.RequirementVO;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单业务服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class RequirementService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RequirementService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RequirementDataService requirementDataService;
|
||||||
|
|
||||||
|
private static final Map<String, String> STATUS_MAP = new HashMap<>();
|
||||||
|
private static final Map<String, String> PRIORITY_MAP = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
STATUS_MAP.put("pending", "待开发");
|
||||||
|
STATUS_MAP.put("developing", "开发中");
|
||||||
|
STATUS_MAP.put("delivered", "已交付");
|
||||||
|
STATUS_MAP.put("completed", "已完成");
|
||||||
|
|
||||||
|
PRIORITY_MAP.put("high", "高");
|
||||||
|
PRIORITY_MAP.put("normal", "中");
|
||||||
|
PRIORITY_MAP.put("low", "低");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询需求工单
|
||||||
|
*/
|
||||||
|
public Result<Page<RequirementVO>> page(Long tenantId, String requirementName, String status,
|
||||||
|
Long projectId, Long customerId, int current, int size) {
|
||||||
|
Page<Requirement> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<Requirement> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Requirement::getTenantId, tenantId);
|
||||||
|
wrapper.eq(Requirement::getDeleted, 0);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(requirementName)) {
|
||||||
|
wrapper.like(Requirement::getRequirementName, requirementName);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(status)) {
|
||||||
|
wrapper.eq(Requirement::getStatus, status);
|
||||||
|
}
|
||||||
|
if (projectId != null) {
|
||||||
|
wrapper.eq(Requirement::getProjectId, projectId);
|
||||||
|
}
|
||||||
|
if (customerId != null) {
|
||||||
|
wrapper.eq(Requirement::getCustomerId, customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.orderByDesc(Requirement::getCreatedTime);
|
||||||
|
|
||||||
|
Page<Requirement> result = requirementDataService.page(page, wrapper);
|
||||||
|
|
||||||
|
Page<RequirementVO> voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
|
||||||
|
voPage.setRecords(result.getRecords().stream().map(this::toVO).toList());
|
||||||
|
|
||||||
|
return Result.success(voPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询需求工单详情
|
||||||
|
*/
|
||||||
|
public Result<RequirementVO> getById(Long tenantId, Long requirementId) {
|
||||||
|
Requirement requirement = requirementDataService.getOne(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getId, requirementId)
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirement == null) {
|
||||||
|
return Result.error("需求工单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success(toVO(requirement));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建需求工单
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Result<RequirementVO> create(Long tenantId, Long userId, RequirementDTO dto) {
|
||||||
|
// 检查编号唯一性
|
||||||
|
long count = requirementDataService.count(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getRequirementCode, dto.getRequirementCode())
|
||||||
|
);
|
||||||
|
if (count > 0) {
|
||||||
|
return Result.error("需求编号已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Requirement requirement = new Requirement();
|
||||||
|
BeanUtils.copyProperties(dto, requirement);
|
||||||
|
requirement.setTenantId(tenantId);
|
||||||
|
requirement.setCreatedBy(userId);
|
||||||
|
requirement.setCreatedTime(LocalDateTime.now());
|
||||||
|
requirement.setUpdatedBy(userId);
|
||||||
|
requirement.setUpdatedTime(LocalDateTime.now());
|
||||||
|
requirement.setDeleted(0);
|
||||||
|
|
||||||
|
// 默认值
|
||||||
|
if (!StringUtils.hasText(requirement.getStatus())) {
|
||||||
|
requirement.setStatus("pending");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(requirement.getPriority())) {
|
||||||
|
requirement.setPriority("normal");
|
||||||
|
}
|
||||||
|
if (requirement.getProgress() == null) {
|
||||||
|
requirement.setProgress(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
requirementDataService.save(requirement);
|
||||||
|
|
||||||
|
log.info("创建需求工单: {} - {}", requirement.getRequirementCode(), requirement.getRequirementName());
|
||||||
|
|
||||||
|
return Result.success(toVO(requirement));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新需求工单
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Result<RequirementVO> update(Long tenantId, Long userId, Long requirementId, RequirementDTO dto) {
|
||||||
|
Requirement requirement = requirementDataService.getOne(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getId, requirementId)
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirement == null) {
|
||||||
|
return Result.error("需求工单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查编号唯一性(排除自身)
|
||||||
|
if (!requirement.getRequirementCode().equals(dto.getRequirementCode())) {
|
||||||
|
long count = requirementDataService.count(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getRequirementCode, dto.getRequirementCode())
|
||||||
|
.ne(Requirement::getId, requirementId)
|
||||||
|
);
|
||||||
|
if (count > 0) {
|
||||||
|
return Result.error("需求编号已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BeanUtils.copyProperties(dto, requirement, "id", "tenantId", "createdBy", "createdTime", "deleted");
|
||||||
|
requirement.setId(requirementId);
|
||||||
|
requirement.setUpdatedBy(userId);
|
||||||
|
requirement.setUpdatedTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
requirementDataService.updateById(requirement);
|
||||||
|
|
||||||
|
log.info("更新需求工单: {}", requirement.getRequirementCode());
|
||||||
|
|
||||||
|
return Result.success(toVO(requirement));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除需求工单
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Result<Void> delete(Long tenantId, Long userId, Long requirementId) {
|
||||||
|
Requirement requirement = requirementDataService.getOne(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getId, requirementId)
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirement == null) {
|
||||||
|
return Result.error("需求工单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
requirement.setDeleted(1);
|
||||||
|
requirement.setUpdatedBy(userId);
|
||||||
|
requirement.setUpdatedTime(LocalDateTime.now());
|
||||||
|
requirementDataService.updateById(requirement);
|
||||||
|
|
||||||
|
log.info("删除需求工单: {}", requirement.getRequirementCode());
|
||||||
|
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新需求状态
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Result<RequirementVO> updateStatus(Long tenantId, Long userId, Long requirementId, String status) {
|
||||||
|
Requirement requirement = requirementDataService.getOne(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getId, requirementId)
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirement == null) {
|
||||||
|
return Result.error("需求工单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
requirement.setStatus(status);
|
||||||
|
requirement.setUpdatedBy(userId);
|
||||||
|
requirement.setUpdatedTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
requirementDataService.updateById(requirement);
|
||||||
|
|
||||||
|
log.info("更新需求工单状态: {} -> {}", requirement.getRequirementCode(), status);
|
||||||
|
|
||||||
|
return Result.success(toVO(requirement));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新开发进度
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Result<RequirementVO> updateProgress(Long tenantId, Long userId, Long requirementId, Integer progress) {
|
||||||
|
Requirement requirement = requirementDataService.getOne(
|
||||||
|
new LambdaQueryWrapper<Requirement>()
|
||||||
|
.eq(Requirement::getId, requirementId)
|
||||||
|
.eq(Requirement::getTenantId, tenantId)
|
||||||
|
.eq(Requirement::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirement == null) {
|
||||||
|
return Result.error("需求工单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
requirement.setProgress(progress);
|
||||||
|
requirement.setUpdatedBy(userId);
|
||||||
|
requirement.setUpdatedTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
requirementDataService.updateById(requirement);
|
||||||
|
|
||||||
|
log.info("更新需求工单进度: {} -> {}%", requirement.getRequirementCode(), progress);
|
||||||
|
|
||||||
|
return Result.success(toVO(requirement));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequirementVO toVO(Requirement requirement) {
|
||||||
|
RequirementVO vo = new RequirementVO();
|
||||||
|
BeanUtils.copyProperties(requirement, vo);
|
||||||
|
vo.setRequirementId(requirement.getId());
|
||||||
|
vo.setStatusName(STATUS_MAP.getOrDefault(requirement.getStatus(), requirement.getStatus()));
|
||||||
|
vo.setPriorityName(PRIORITY_MAP.getOrDefault(requirement.getPriority(), requirement.getPriority()));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,246 @@
|
|||||||
|
package com.fundplatform.proj.vo;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求工单VO
|
||||||
|
*/
|
||||||
|
public class RequirementVO {
|
||||||
|
|
||||||
|
private Long requirementId;
|
||||||
|
private String requirementCode;
|
||||||
|
private String requirementName;
|
||||||
|
private String description;
|
||||||
|
private Long projectId;
|
||||||
|
private String projectName;
|
||||||
|
private Long customerId;
|
||||||
|
private String customerName;
|
||||||
|
private String priority;
|
||||||
|
private String priorityName;
|
||||||
|
private BigDecimal estimatedHours;
|
||||||
|
private BigDecimal actualHours;
|
||||||
|
private LocalDate plannedStart;
|
||||||
|
private LocalDate plannedEnd;
|
||||||
|
private LocalDate actualStart;
|
||||||
|
private LocalDate actualEnd;
|
||||||
|
private LocalDate deliveryDate;
|
||||||
|
private BigDecimal receivableAmount;
|
||||||
|
private LocalDate receivableDate;
|
||||||
|
private String status;
|
||||||
|
private String statusName;
|
||||||
|
private Integer progress;
|
||||||
|
private String attachmentUrl;
|
||||||
|
private Long tenantId;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
public Long getRequirementId() {
|
||||||
|
return requirementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementId(Long requirementId) {
|
||||||
|
this.requirementId = requirementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequirementCode() {
|
||||||
|
return requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementCode(String requirementCode) {
|
||||||
|
this.requirementCode = requirementCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequirementName() {
|
||||||
|
return requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequirementName(String requirementName) {
|
||||||
|
this.requirementName = requirementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProjectId() {
|
||||||
|
return projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectId(Long projectId) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProjectName() {
|
||||||
|
return projectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectName(String projectName) {
|
||||||
|
this.projectName = projectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(Long customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomerName() {
|
||||||
|
return customerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerName(String customerName) {
|
||||||
|
this.customerName = customerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriority() {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriority(String priority) {
|
||||||
|
this.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriorityName() {
|
||||||
|
return priorityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriorityName(String priorityName) {
|
||||||
|
this.priorityName = priorityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getEstimatedHours() {
|
||||||
|
return estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEstimatedHours(BigDecimal estimatedHours) {
|
||||||
|
this.estimatedHours = estimatedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getActualHours() {
|
||||||
|
return actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualHours(BigDecimal actualHours) {
|
||||||
|
this.actualHours = actualHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedStart() {
|
||||||
|
return plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedStart(LocalDate plannedStart) {
|
||||||
|
this.plannedStart = plannedStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPlannedEnd() {
|
||||||
|
return plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlannedEnd(LocalDate plannedEnd) {
|
||||||
|
this.plannedEnd = plannedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualStart() {
|
||||||
|
return actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualStart(LocalDate actualStart) {
|
||||||
|
this.actualStart = actualStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getActualEnd() {
|
||||||
|
return actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualEnd(LocalDate actualEnd) {
|
||||||
|
this.actualEnd = actualEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getDeliveryDate() {
|
||||||
|
return deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliveryDate(LocalDate deliveryDate) {
|
||||||
|
this.deliveryDate = deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getReceivableAmount() {
|
||||||
|
return receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableAmount(BigDecimal receivableAmount) {
|
||||||
|
this.receivableAmount = receivableAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getReceivableDate() {
|
||||||
|
return receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceivableDate(LocalDate receivableDate) {
|
||||||
|
this.receivableDate = receivableDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatusName() {
|
||||||
|
return statusName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusName(String statusName) {
|
||||||
|
this.statusName = statusName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProgress() {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(Integer progress) {
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAttachmentUrl() {
|
||||||
|
return attachmentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) {
|
||||||
|
this.attachmentUrl = attachmentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedTime() {
|
||||||
|
return createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedTime(LocalDateTime createdTime) {
|
||||||
|
this.createdTime = createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedTime() {
|
||||||
|
return updatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedTime(LocalDateTime updatedTime) {
|
||||||
|
this.updatedTime = updatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user