需求分解
This commit is contained in:
612
openspec/changes/add-project-management/design.md
Normal file
612
openspec/changes/add-project-management/design.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# Design: 项目管理模块
|
||||
|
||||
## 概述
|
||||
本文档描述项目管理模块的技术设计方案,包括系统架构、数据模型、API设计等。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 模块划分
|
||||
遵循若依框架的分层架构,新建 `ruoyi-dpc` 模块与若依框架代码分离,Controller 层也放在新建模块中:
|
||||
|
||||
```
|
||||
ruoyi-dpc/ (新建模块)
|
||||
├── pom.xml # 模块依赖配置
|
||||
├── src/main/java/com/ruoyi/dpc/
|
||||
│ ├── controller/
|
||||
│ │ └── DpcProjectController.java # 项目控制器
|
||||
│ ├── domain/
|
||||
│ │ ├── DpcProject.java # 项目实体
|
||||
│ │ ├── DpcProjectPerson.java # 项目人员关联
|
||||
│ │ ├── dto/
|
||||
│ │ │ ├── DpcProjectDTO.java # 项目数据传输对象
|
||||
│ │ │ ├── DpcProjectQueryDTO.java # 项目查询DTO
|
||||
│ │ │ └── DpcProjectImportDTO.java # 项目导入DTO
|
||||
│ │ └── vo/
|
||||
│ │ ├── DpcProjectVO.java # 项目视图对象
|
||||
│ │ └── DpcProjectQueryVO.java # 查询视图对象
|
||||
│ ├── mapper/
|
||||
│ │ ├── DpcProjectMapper.java
|
||||
│ │ └── DpcProjectPersonMapper.java
|
||||
│ └── service/
|
||||
│ ├── IDpcProjectService.java
|
||||
│ └── impl/
|
||||
│ └── DpcProjectServiceImpl.java
|
||||
└── src/main/resources/mapper/dpc/
|
||||
├── DpcProjectMapper.xml
|
||||
└── DpcProjectPersonMapper.xml
|
||||
|
||||
ruoyi-ui/src/
|
||||
├── api/
|
||||
│ └── dpcProject.js # API请求定义
|
||||
└── views/
|
||||
└── dpcProject/
|
||||
├── index.vue # 项目列表页
|
||||
├── add-or-edit.vue # 新增/编辑弹窗
|
||||
└── import-history.vue # 导入历史项目弹窗
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 数据库表设计
|
||||
|
||||
#### dpc_project (项目主表)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|-------|------|------|-----|
|
||||
| project_id | BIGINT | 项目ID | PK, AUTO_INCREMENT |
|
||||
| project_name | VARCHAR(100) | 项目名称 | NOT NULL |
|
||||
| description | VARCHAR(500) | 项目描述 | |
|
||||
| status | CHAR(1) | 状态 | NOT NULL, DEFAULT '0' |
|
||||
| target_count | INT | 目标人数 | NOT NULL, DEFAULT 0 |
|
||||
| warning_count | INT | 预警人数 | NOT NULL, DEFAULT 0 |
|
||||
| start_date | DATE | 开始日期 | |
|
||||
| end_date | DATE | 结束日期 | |
|
||||
| is_archived | CHAR(1) | 是否归档 | DEFAULT '0' |
|
||||
| archive_file_path | VARCHAR(255) | 归档文件路径 | |
|
||||
| create_by | VARCHAR(64) | 创建者 | |
|
||||
| create_time | DATETIME | 创建时间 | |
|
||||
| update_by | VARCHAR(64) | 更新者 | |
|
||||
| update_time | DATETIME | 更新时间 | |
|
||||
| remark | VARCHAR(500) | 备注 | |
|
||||
|
||||
**状态枚举值**:
|
||||
- `0`: 进行中
|
||||
- `1`: 已完成
|
||||
- `2`: 已归档
|
||||
|
||||
#### dpc_project_person (项目人员关联表)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|-------|------|------|-----|
|
||||
| id | BIGINT | 主键ID | PK, AUTO_INCREMENT |
|
||||
| project_id | BIGINT | 项目ID | FK -> dpc_project.project_id |
|
||||
| person_id | BIGINT | 人员ID | FK -> sys_user.user_id |
|
||||
| person_name | VARCHAR(30) | 人员姓名 | 冗余字段 |
|
||||
| person_dept_id | BIGINT | 部门ID | 冗余字段 |
|
||||
|
||||
**索引**:
|
||||
- `idx_project_id`: (project_id)
|
||||
- `uk_project_person`: (project_id, person_id) 唯一索引
|
||||
|
||||
### 实体类设计
|
||||
|
||||
#### DpcProject.java
|
||||
```java
|
||||
@Data
|
||||
@TableName("dpc_project")
|
||||
public class DpcProject {
|
||||
/** 项目ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long projectId;
|
||||
|
||||
/** 项目名称 */
|
||||
@NotBlank(message = "项目名称不能为空")
|
||||
@Size(max = 100, message = "项目名称不能超过100个字符")
|
||||
private String projectName;
|
||||
|
||||
/** 项目描述 */
|
||||
@Size(max = 500, message = "项目描述不能超过500个字符")
|
||||
private String description;
|
||||
|
||||
/** 状态(0进行中 1已完成 2已归档) */
|
||||
private String status;
|
||||
|
||||
/** 目标人数 */
|
||||
private Integer targetCount;
|
||||
|
||||
/** 预警人数 */
|
||||
private Integer warningCount;
|
||||
|
||||
/** 开始日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date startDate;
|
||||
|
||||
/** 结束日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date endDate;
|
||||
|
||||
/** 是否归档 */
|
||||
private String isArchived;
|
||||
|
||||
/** 归档文件路径 */
|
||||
private String archiveFilePath;
|
||||
|
||||
/** 创建者 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@Transient
|
||||
private List<Long> personIds; // 关联人员ID列表
|
||||
}
|
||||
```
|
||||
|
||||
#### 审计字段自动填充配置
|
||||
|
||||
```java
|
||||
/**
|
||||
* MyBatis Plus 审计字段自动填充处理器
|
||||
*/
|
||||
@Component
|
||||
public class DpcMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Resource
|
||||
private TokenService tokenService;
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名
|
||||
*/
|
||||
private String getUsername() {
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
return loginUser != null ? loginUser.getUsername() : "system";
|
||||
} catch (Exception e) {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 实体类审计字段注解
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("dpc_project")
|
||||
public class DpcProject {
|
||||
// ... 其他字段
|
||||
|
||||
/** 创建者 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
```
|
||||
|
||||
**配置说明**:
|
||||
- `@TableField(fill = FieldFill.INSERT)` - 仅在插入时自动填充
|
||||
- `@TableField(fill = FieldFill.INSERT_UPDATE)` - 插入和更新时都自动填充
|
||||
- `MetaObjectHandler` 处理器会自动从 Spring Security 上下文中获取当前登录用户
|
||||
|
||||
### DTO 设计
|
||||
|
||||
按照全局配置要求,接口传参使用单独的 DTO,不与 entity 混用。
|
||||
|
||||
#### DpcProjectDTO.java(新增/修改项目DTO)
|
||||
```java
|
||||
@Data
|
||||
public class DpcProjectDTO {
|
||||
/** 项目名称 */
|
||||
@NotBlank(message = "项目名称不能为空")
|
||||
@Size(max = 100, message = "项目名称不能超过100个字符")
|
||||
private String projectName;
|
||||
|
||||
/** 项目描述 */
|
||||
@Size(max = 500, message = "项目描述不能超过500个字符")
|
||||
private String description;
|
||||
|
||||
/** 开始日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date startDate;
|
||||
|
||||
/** 结束日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date endDate;
|
||||
|
||||
/** 关联人员ID列表 */
|
||||
@NotEmpty(message = "请至少选择一名参与人员")
|
||||
private List<Long> personIds;
|
||||
}
|
||||
```
|
||||
|
||||
#### DpcProjectQueryDTO.java(查询项目DTO)
|
||||
```java
|
||||
@Data
|
||||
public class DpcProjectQueryDTO {
|
||||
/** 项目名称(模糊搜索) */
|
||||
private String projectName;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 创建时间范围开始 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTimeStart;
|
||||
|
||||
/** 创建时间范围结束 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTimeEnd;
|
||||
}
|
||||
```
|
||||
|
||||
#### DpcProjectImportDTO.java(导入历史项目DTO)
|
||||
```java
|
||||
@Data
|
||||
public class DpcProjectImportDTO {
|
||||
/** 历史项目ID */
|
||||
@NotNull(message = "请选择要导入的历史项目")
|
||||
private Long historyProjectId;
|
||||
|
||||
/** 新项目名称 */
|
||||
@NotBlank(message = "新项目名称不能为空")
|
||||
@Size(max = 100, message = "项目名称不能超过100个字符")
|
||||
private String projectName;
|
||||
}
|
||||
```
|
||||
|
||||
#### DTO 与 Entity 转换
|
||||
```java
|
||||
/**
|
||||
* DTO 与 Entity 转换工具类
|
||||
*/
|
||||
public class DpcProjectConverter {
|
||||
|
||||
/**
|
||||
* DTO 转 Entity(新增/修改)
|
||||
*/
|
||||
public static DpcProject toEntity(DpcProjectDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
DpcProject entity = new DpcProject();
|
||||
entity.setProjectName(dto.getProjectName());
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setStartDate(dto.getStartDate());
|
||||
entity.setEndDate(dto.getEndDate());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity 转 VO
|
||||
*/
|
||||
public static DpcProjectVO toVO(DpcProject entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
DpcProjectVO vo = new DpcProjectVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity 列表转 VO 列表
|
||||
*/
|
||||
public static List<DpcProjectVO> toVOList(List<DpcProject> entityList) {
|
||||
if (entityList == null || entityList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return entityList.stream()
|
||||
.map(DpcProjectConverter::toVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Controller 使用 DTO
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/dpc/project")
|
||||
public class DpcProjectController {
|
||||
|
||||
@Resource
|
||||
private IDpcProjectService projectService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('dpc:project:add')")
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody DpcProjectDTO dto) {
|
||||
DpcProject project = DpcProjectConverter.toEntity(dto);
|
||||
return AjaxResult.success(projectService.insertProject(project));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('dpc:project:edit')")
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody DpcProjectDTO dto) {
|
||||
DpcProject project = DpcProjectConverter.toEntity(dto);
|
||||
return AjaxResult.success(projectService.updateProject(project));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('dpc:project:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(DpcProjectQueryDTO queryDTO) {
|
||||
DpcProject project = new DpcProject();
|
||||
// 将查询条件转换到实体
|
||||
BeanUtils.copyProperties(queryDTO, project);
|
||||
startPage();
|
||||
List<DpcProject> list = projectService.selectProjectList(project);
|
||||
List<DpcProjectVO> voList = DpcProjectConverter.toVOList(list);
|
||||
return getDataTable(voList);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API设计
|
||||
|
||||
### RESTful API规范
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|-----|------|------|-----|
|
||||
| GET | /project/list | 查询项目列表 | project:list |
|
||||
| GET | /project/{id} | 查询项目详情 | project:query |
|
||||
| POST | /project | 新增项目 | project:add |
|
||||
| PUT | /project | 修改项目 | project:edit |
|
||||
| DELETE | /project/{ids} | 删除项目 | project:remove |
|
||||
| GET | /project/history/{id} | 获取历史项目配置 | project:query |
|
||||
| POST | /project/import | 导入历史项目 | project:add |
|
||||
| POST | /project/archive/{id} | 归档项目 | project:archive |
|
||||
| POST | /project/reanalyze/{id} | 重新分析 | project:reanalyze |
|
||||
|
||||
### 请求/响应格式
|
||||
|
||||
#### 查询项目列表
|
||||
**请求**: `GET /project/list?projectName=xxx&status=0&pageNum=1&pageSize=10`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"total": 100,
|
||||
"rows": [
|
||||
{
|
||||
"projectId": 1,
|
||||
"projectName": "2026年Q1初核排查",
|
||||
"description": "季度常规排查",
|
||||
"status": "0",
|
||||
"targetCount": 100,
|
||||
"warningCount": 5,
|
||||
"createTime": "2026-01-01 10:00:00"
|
||||
}
|
||||
],
|
||||
"code": 200,
|
||||
"msg": "查询成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增项目
|
||||
**请求**: `POST /project`
|
||||
|
||||
```json
|
||||
{
|
||||
"projectName": "2026年Q1初核排查",
|
||||
"description": "季度常规排查",
|
||||
"startDate": "2026-01-01",
|
||||
"endDate": "2026-03-31",
|
||||
"personIds": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "新增成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 业务逻辑设计
|
||||
|
||||
### 项目状态流转
|
||||
|
||||
```
|
||||
[新建] → [进行中] → [已完成] → [已归档]
|
||||
↑
|
||||
|---- [重新分析]
|
||||
```
|
||||
|
||||
### 核心业务规则
|
||||
|
||||
1. **新建项目**
|
||||
- 项目名称必填
|
||||
- 至少选择一名人员
|
||||
- 默认状态为"进行中"
|
||||
|
||||
2. **导入历史项目**
|
||||
- 选择已完成的历史项目
|
||||
- 复制人员配置
|
||||
- 生成新项目(状态为"进行中")
|
||||
|
||||
3. **归档项目**
|
||||
- 只能归档"已完成"状态的项目
|
||||
- 生成PDF归档文件
|
||||
- 更新项目状态为"已归档"
|
||||
|
||||
4. **重新分析**
|
||||
- 只能对"已完成"项目执行
|
||||
- 异步执行风险模型
|
||||
- 更新预警人数
|
||||
|
||||
## 前端设计
|
||||
|
||||
### 页面组件结构
|
||||
|
||||
#### 项目列表页 (index.vue)
|
||||
```
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<el-form :inline="true">
|
||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" />
|
||||
<el-button @click="handleQuery">搜索</el-button>
|
||||
<el-button @click="handleAdd">新建项目</el-button>
|
||||
<el-button @click="handleImport">导入历史项目</el-button>
|
||||
</el-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="projectList">
|
||||
<el-table-column prop="projectName" label="项目名称" />
|
||||
<el-table-column prop="description" label="项目描述" />
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === '0'">进行中</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === '1'" type="success">已完成</el-tag>
|
||||
<el-tag v-else type="info">已归档</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="targetCount" label="目标人数" />
|
||||
<el-table-column prop="warningCount" label="预警人数" />
|
||||
<el-table-column label="操作" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status === '0'" @click="handleEnter(scope.row)">进入项目</el-button>
|
||||
<el-button v-if="scope.row.status === '1'" @click="handleViewResult(scope.row)">查看结果</el-button>
|
||||
<el-button v-if="scope.row.status === '1'" @click="handleReanalyze(scope.row)">重新分析</el-button>
|
||||
<el-button v-if="scope.row.status === '1'" @click="handleArchive(scope.row)">归档</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### API封装 (project.js)
|
||||
```javascript
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询项目列表
|
||||
export function listProject(query) {
|
||||
return request({
|
||||
url: '/dpc/project/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 新增项目
|
||||
export function addProject(data) {
|
||||
return request({
|
||||
url: '/dpc/project',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 归档项目
|
||||
export function archiveProject(projectId) {
|
||||
return request({
|
||||
url: '/dpc/project/archive/' + projectId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 重新分析
|
||||
export function reanalyzeProject(projectId) {
|
||||
return request({
|
||||
url: '/dpc/project/reanalyze/' + projectId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入历史项目
|
||||
export function importProject(data) {
|
||||
return request({
|
||||
url: '/dpc/project/import',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 技术约束
|
||||
|
||||
1. **后端**
|
||||
- Spring Boot 3.5.8
|
||||
- 使用 MyBatis Plus 3.5.10 进行数据访问(Spring Boot 3 适配版)
|
||||
- 使用 `@Resource` 注入依赖(替代 `@Autowired`)
|
||||
- 使用 `@Data` 注解简化实体类
|
||||
- 实体类不继承 BaseEntity,基础字段直接定义在实体类中
|
||||
- 新建 `ruoyi-dpc` 模块,与若依框架代码分离
|
||||
- Controller 层也要放在新建模块中(不在 `ruoyi-admin` 中)
|
||||
- 接口传参使用单独的 DTO,不与 entity 混用
|
||||
|
||||
2. **前端**
|
||||
- Vue 2.6.12
|
||||
- 使用 Element UI 2.15.14 组件库
|
||||
- 遵循现有视图组件风格
|
||||
- 在添加页面和组件后,注意与数据库中菜单表进行联动修改
|
||||
|
||||
3. **数据库**
|
||||
- MySQL 8.2.0
|
||||
- 表名使用 `dpc_` 前缀(项目英文名首字母集合)
|
||||
|
||||
4. **安全**
|
||||
- 所有API需要 `@PreAuthorize` 权限注解
|
||||
- 敏感操作记录操作日志
|
||||
|
||||
5. **文档**
|
||||
- 完成后端代码 controller 层代码生成测试后,在项目文件目录下生成 API 文档
|
||||
|
||||
6. **测试**
|
||||
- 测试方式为生成可执行的测试脚本
|
||||
- 在测试中启动的进程,在测试完成后立刻结束
|
||||
- `/login/test` 接口可传入 `username` 和 `password` 获取 token(测试账号:admin/admin123)
|
||||
- swagger-ui 地址:`/swagger-ui/index.html`
|
||||
|
||||
7. **审计字段自动填充**
|
||||
- 配置 MyBatis Plus `MetaObjectHandler` 实现审计字段自动填充
|
||||
- 插入操作自动填充:`create_by`、`create_time`
|
||||
- 更新操作自动填充:`update_by`、`update_time`
|
||||
|
||||
## 待确认事项
|
||||
|
||||
1. **PDF生成方案**:需确认使用 iText 或其他方案
|
||||
2. **异步任务实现**:重新分析是否需要集成若依的定时任务模块
|
||||
3. **菜单配置**:项目管理在系统菜单中的位置
|
||||
Reference in New Issue
Block a user