复诊管理功能旨在为患者和医生提供一个便捷的复诊预约和管理平台。该功能允许患者发起复诊请求,医生审核并安排复诊时间,双方可以查看和管理复诊记录。
根据项目数据库表设计规范,创建复诊记录表:
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT | 主键ID |
| patient_user_id | BIGINT | 患者用户ID |
| doctor_user_id | BIGINT | 医生用户ID |
| appointment_time | DATETIME | 预约时间 |
| actual_time | DATETIME | 实际就诊时间 |
| status | VARCHAR(20) | 复诊状态 (PENDING-待确认, CONFIRMED-已确认, CANCELLED-已取消, COMPLETED-已完成) |
| reason | TEXT | 复诊原因 |
| notes | TEXT | 备注 |
| create_user | BIGINT | 创建者ID |
| create_time | DATETIME | 创建时间 |
| update_user | BIGINT | 更新者ID |
| update_time | DATETIME | 更新时间 |
| version | INT | 版本号(乐观锁) |
| remark | VARCHAR(255) | 备注 |
package work.baiyun.chronicdiseaseapp.model.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Schema(description = "复诊记录表")
@TableName("t_follow_up")
@Data
@EqualsAndHashCode(callSuper = false)
public class FollowUp extends BaseEntity {
@Schema(description = "患者用户ID")
@TableField("patient_user_id")
private Long patientUserId;
@Schema(description = "医生用户ID")
@TableField("doctor_user_id")
private Long doctorUserId;
@Schema(description = "预约时间")
@TableField("appointment_time")
private LocalDateTime appointmentTime;
@Schema(description = "实际就诊时间")
@TableField("actual_time")
private LocalDateTime actualTime;
@Schema(description = "复诊状态 (PENDING-待确认, CONFIRMED-已确认, CANCELLED-已取消, COMPLETED-已完成)")
@TableField("status")
private String status;
@Schema(description = "复诊原因")
@TableField("reason")
private String reason;
@Schema(description = "备注")
@TableField("notes")
private String notes;
}
// CreateFollowUpRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
@Schema(description = "创建复诊请求")
@Data
public class CreateFollowUpRequest {
@Schema(description = "医生用户ID", required = true)
@NotNull(message = "医生ID不能为空")
private Long doctorUserId;
@Schema(description = "预约时间", required = true)
@NotNull(message = "预约时间不能为空")
private LocalDateTime appointmentTime;
@Schema(description = "复诊原因")
private String reason;
}
// UpdateFollowUpRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
@Schema(description = "更新复诊请求")
@Data
public class UpdateFollowUpRequest {
@Schema(description = "复诊记录ID", required = true)
@NotNull(message = "复诊记录ID不能为空")
private Long id;
@Schema(description = "预约时间")
private LocalDateTime appointmentTime;
@Schema(description = "复诊状态")
private String status;
@Schema(description = "备注")
private String notes;
}
// FollowUpResponse.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "复诊记录响应")
@Data
public class FollowUpResponse {
@Schema(description = "记录ID")
private String id;
@Schema(description = "患者用户ID")
private Long patientUserId;
@Schema(description = "医生用户ID")
private Long doctorUserId;
@Schema(description = "预约时间")
private LocalDateTime appointmentTime;
@Schema(description = "实际就诊时间")
private LocalDateTime actualTime;
@Schema(description = "复诊状态")
private String status;
@Schema(description = "复诊原因")
private String reason;
@Schema(description = "备注")
private String notes;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "患者昵称")
private String patientNickname;
@Schema(description = "医生昵称")
private String doctorNickname;
}
// FollowUpPageResponse.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "复诊记录分页响应")
@Data
public class FollowUpPageResponse {
@Schema(description = "数据列表")
private List<FollowUpResponse> records;
@Schema(description = "总数")
private long total;
@Schema(description = "每页大小")
private long size;
@Schema(description = "当前页码")
private long current;
@Schema(description = "总页数")
private long pages;
}
// FollowUpMapper.java
package work.baiyun.chronicdiseaseapp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import work.baiyun.chronicdiseaseapp.model.po.FollowUp;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FollowUpMapper extends BaseMapper<FollowUp> {
}
// FollowUpService.java
package work.baiyun.chronicdiseaseapp.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import work.baiyun.chronicdiseaseapp.model.vo.BaseQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.FollowUpResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateFollowUpRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateFollowUpRequest;
public interface FollowUpService {
/**
* 患者创建复诊请求
*/
void createFollowUp(CreateFollowUpRequest request);
/**
* 更新复诊记录(医生确认、取消或患者修改)
*/
void updateFollowUp(UpdateFollowUpRequest request);
/**
* 分页查询当前用户的复诊记录
*/
Page<FollowUpResponse> listFollowUps(BaseQueryRequest request);
/**
* 医生分页查询患者的复诊记录
*/
Page<FollowUpResponse> listFollowUpsByPatient(Long patientUserId, BaseQueryRequest request);
/**
* 删除复诊记录
*/
void deleteFollowUp(Long id);
}
// FollowUpServiceImpl.java
package work.baiyun.chronicdiseaseapp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import work.baiyun.chronicdiseaseapp.mapper.FollowUpMapper;
import work.baiyun.chronicdiseaseapp.mapper.UserInfoMapper;
import work.baiyun.chronicdiseaseapp.model.po.FollowUp;
import work.baiyun.chronicdiseaseapp.model.po.UserInfo;
import work.baiyun.chronicdiseaseapp.model.vo.BaseQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.FollowUpResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateFollowUpRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateFollowUpRequest;
import work.baiyun.chronicdiseaseapp.service.FollowUpService;
import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class FollowUpServiceImpl implements FollowUpService {
@Autowired
private FollowUpMapper followUpMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public void createFollowUp(CreateFollowUpRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
FollowUp followUp = new FollowUp();
BeanUtils.copyProperties(request, followUp);
followUp.setPatientUserId(userId);
followUp.setStatus("PENDING"); // 默认状态为待确认
followUpMapper.insert(followUp);
}
@Override
public void updateFollowUp(UpdateFollowUpRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
FollowUp followUp = followUpMapper.selectById(request.getId());
if (followUp == null) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
"复诊记录不存在");
}
// 权限检查:患者只能修改自己的记录,医生只能修改分配给自己的记录
work.baiyun.chronicdiseaseapp.enums.PermissionGroup role = SecurityUtils.getCurrentUserRole();
if (role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.PATIENT &&
!followUp.getPatientUserId().equals(userId)) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
"无权操作该复诊记录");
}
if ((role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.DOCTOR ||
role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.SYS_ADMIN) &&
!followUp.getDoctorUserId().equals(userId)) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
"无权操作该复诊记录");
}
// 更新字段
if (request.getAppointmentTime() != null) {
followUp.setAppointmentTime(request.getAppointmentTime());
}
if (request.getStatus() != null) {
followUp.setStatus(request.getStatus());
}
if (request.getNotes() != null) {
followUp.setNotes(request.getNotes());
}
// 如果状态更新为已完成,则设置实际就诊时间
if ("COMPLETED".equals(request.getStatus())) {
followUp.setActualTime(java.time.LocalDateTime.now());
}
followUpMapper.updateById(followUp);
}
@Override
public Page<FollowUpResponse> listFollowUps(BaseQueryRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
work.baiyun.chronicdiseaseapp.enums.PermissionGroup role = SecurityUtils.getCurrentUserRole();
Page<FollowUp> page = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<FollowUp> wrapper = new LambdaQueryWrapper<>();
// 根据用户角色查询不同的数据
if (role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.PATIENT) {
wrapper.eq(FollowUp::getPatientUserId, userId);
} else if (role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.DOCTOR ||
role == work.baiyun.chronicdiseaseapp.enums.PermissionGroup.SYS_ADMIN) {
wrapper.eq(FollowUp::getDoctorUserId, userId);
}
wrapper.ge(request.getStartTime() != null, FollowUp::getCreateTime, request.getStartTime())
.le(request.getEndTime() != null, FollowUp::getCreateTime, request.getEndTime())
.orderByDesc(FollowUp::getCreateTime);
Page<FollowUp> result = followUpMapper.selectPage(page, wrapper);
// 批量查询用户信息
Set<Long> userIds = result.getRecords().stream()
.flatMap(r -> java.util.stream.Stream.of(r.getPatientUserId(), r.getDoctorUserId()))
.filter(id -> id != null)
.collect(Collectors.toSet());
Map<Long, UserInfo> userInfoMap;
if (userIds.isEmpty()) {
userInfoMap = java.util.Collections.emptyMap();
} else {
List<UserInfo> userInfos = userInfoMapper.selectBatchIds(userIds);
userInfoMap = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, u -> u));
}
List<FollowUpResponse> responses = result.getRecords().stream().map(r -> {
FollowUpResponse resp = new FollowUpResponse();
BeanUtils.copyProperties(r, resp);
resp.setId(r.getId().toString());
// 设置用户昵称
UserInfo patient = userInfoMap.get(r.getPatientUserId());
if (patient != null) {
resp.setPatientNickname(patient.getNickname());
}
UserInfo doctor = userInfoMap.get(r.getDoctorUserId());
if (doctor != null) {
resp.setDoctorNickname(doctor.getNickname());
}
return resp;
}).collect(Collectors.toList());
Page<FollowUpResponse> responsePage = new Page<>();
responsePage.setRecords(responses);
responsePage.setCurrent(result.getCurrent());
responsePage.setSize(result.getSize());
responsePage.setTotal(result.getTotal());
responsePage.setPages(result.getPages());
return responsePage;
}
@Override
public Page<FollowUpResponse> listFollowUpsByPatient(Long patientUserId, BaseQueryRequest request) {
// 检查绑定关系
Long userId = SecurityUtils.getCurrentUserId();
work.baiyun.chronicdiseaseapp.enums.PermissionGroup role = SecurityUtils.getCurrentUserRole();
// 只有医生和管理员可以查询其他患者的复诊记录
if (role != work.baiyun.chronicdiseaseapp.enums.PermissionGroup.DOCTOR &&
role != work.baiyun.chronicdiseaseapp.enums.PermissionGroup.SYS_ADMIN) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
"无权查询其他患者的复诊记录");
}
Page<FollowUp> page = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<FollowUp> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FollowUp::getPatientUserId, patientUserId)
.ge(request.getStartTime() != null, FollowUp::getCreateTime, request.getStartTime())
.le(request.getEndTime() != null, FollowUp::getCreateTime, request.getEndTime())
.orderByDesc(FollowUp::getCreateTime);
Page<FollowUp> result = followUpMapper.selectPage(page, wrapper);
List<FollowUpResponse> responses = result.getRecords().stream().map(r -> {
FollowUpResponse resp = new FollowUpResponse();
BeanUtils.copyProperties(r, resp);
resp.setId(r.getId().toString());
return resp;
}).collect(Collectors.toList());
Page<FollowUpResponse> responsePage = new Page<>();
responsePage.setRecords(responses);
responsePage.setCurrent(result.getCurrent());
responsePage.setSize(result.getSize());
responsePage.setTotal(result.getTotal());
responsePage.setPages(result.getPages());
return responsePage;
}
@Override
public void deleteFollowUp(Long id) {
Long userId = SecurityUtils.getCurrentUserId();
FollowUp followUp = followUpMapper.selectById(id);
if (followUp == null) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
"复诊记录不存在");
}
// 只有患者本人才能删除自己的复诊记录
if (!followUp.getPatientUserId().equals(userId)) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
"无权删除该复诊记录");
}
followUpMapper.deleteById(id);
}
}
// FollowUpController.java
package work.baiyun.chronicdiseaseapp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import work.baiyun.chronicdiseaseapp.common.R;
import work.baiyun.chronicdiseaseapp.model.vo.BaseQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.FollowUpResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateFollowUpRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateFollowUpRequest;
import work.baiyun.chronicdiseaseapp.service.FollowUpService;
import work.baiyun.chronicdiseaseapp.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/follow-up")
@Tag(name = "复诊管理", description = "复诊预约与管理相关接口")
public class FollowUpController {
private static final Logger logger = LoggerFactory.getLogger(FollowUpController.class);
@Autowired
private FollowUpService followUpService;
@Operation(summary = "创建复诊请求", description = "患者创建复诊预约请求")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "复诊请求创建成功",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "500", description = "服务器内部错误",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> create(@RequestBody CreateFollowUpRequest req) {
try {
followUpService.createFollowUp(req);
return R.success(200, "复诊请求创建成功");
} catch (Exception e) {
logger.error("create follow up request failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
@Operation(summary = "更新复诊记录", description = "更新复诊记录(医生确认、取消或患者修改)")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "复诊记录更新成功",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "500", description = "服务器内部错误",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/update", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> update(@RequestBody UpdateFollowUpRequest req) {
try {
followUpService.updateFollowUp(req);
return R.success(200, "复诊记录更新成功");
} catch (Exception e) {
logger.error("update follow up request failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
@Operation(summary = "分页查询复诊记录", description = "根据时间范围和分页参数查询复诊记录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功查询复诊记录列表",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse.class))),
@ApiResponse(responseCode = "500", description = "服务器内部错误",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/list", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> list(@RequestBody BaseQueryRequest req) {
try {
Page<FollowUpResponse> page = followUpService.listFollowUps(req);
work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse();
vo.setRecords(page.getRecords());
vo.setTotal(page.getTotal());
vo.setSize(page.getSize());
vo.setCurrent(page.getCurrent());
vo.setPages(page.getPages());
return R.success(200, "ok", vo);
} catch (Exception e) {
logger.error("list follow up records failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
@Operation(summary = "医生分页查询患者复诊记录", description = "医生查询患者复诊记录(需有绑定关系或权限)")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功查询复诊记录列表",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse.class))),
@ApiResponse(responseCode = "403", description = "无权限访问",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/list-by-patient", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> listByPatient(Long patientUserId, @RequestBody BaseQueryRequest req) {
try {
Page<FollowUpResponse> page = followUpService.listFollowUpsByPatient(patientUserId, req);
work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.FollowUpPageResponse();
vo.setRecords(page.getRecords());
vo.setTotal(page.getTotal());
vo.setSize(page.getSize());
vo.setCurrent(page.getCurrent());
vo.setPages(page.getPages());
return R.success(200, "ok", vo);
} catch (Exception e) {
logger.error("list follow up records by patient failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
@Operation(summary = "删除复诊记录", description = "根据ID删除复诊记录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "复诊记录删除成功",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "500", description = "服务器内部错误",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> delete(@RequestBody work.baiyun.chronicdiseaseapp.model.vo.DeleteFollowUpRequest req) {
try {
followUpService.deleteFollowUp(req.getId());
return R.success(200, "复诊记录删除成功");
} catch (Exception e) {
logger.error("delete follow up record failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
}
// DeleteFollowUpRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "删除复诊记录请求")
@Data
public class DeleteFollowUpRequest {
@Schema(description = "复诊记录ID")
private Long id;
}
在 ErrorCode 枚举中可能需要添加以下错误码:
// 在 ErrorCode.java 中添加
FOLLOW_UP_NOT_FOUND(6000, "复诊记录不存在"),
FOLLOW_UP_ACCESS_DENIED(6001, "无权访问复诊记录"),
FOLLOW_UP_STATUS_INVALID(6002, "复诊状态无效");
POST /follow-up/create
{
"doctorUserId": 1988172854356631553,
"appointmentTime": "2025-12-01T10:00:00",
"reason": "血糖控制不佳,需要调整治疗方案"
}
POST /follow-up/update
{
"id": 1988502746686595073,
"status": "CONFIRMED",
"notes": "已确认预约"
}
POST /follow-up/list
{
"pageNum": 1,
"pageSize": 10,
"startTime": "2025-11-01T00:00:00",
"endTime": "2025-12-01T23:59:59"
}
患者:
医生:
系统管理员:
PENDING(待确认) -> CONFIRMED(已确认) -> COMPLETED(已完成)
|
v
CANCELLED(已取消)