复诊管理功能旨在为患者和医生提供一个便捷的复诊预约和管理平台。该功能允许患者发起复诊请求,医生审核并安排复诊时间,双方可以查看和管理复诊记录。
根据项目数据库表设计规范(参见 docs/OLD/DevRule/07-数据库规范.md),创建复诊记录表:
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT(20) | 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID) |
| patient_user_id | BIGINT(20) | 患者用户ID,外键(可选) |
| doctor_user_id | BIGINT(20) | 医生用户ID,外键(可选) |
| appointment_time | DATETIME | 预约时间 |
| actual_time | DATETIME | 医生主动确认已完成的时间记录 |
| status | VARCHAR(20) | 复诊状态 (PENDING-待确认, CONFIRMED-已确认, CANCELLED-已取消, COMPLETED-已完成) |
| reason | TEXT | 复诊原因 |
| notes | TEXT | 备注 |
| version | INT(11) | 版本号(乐观锁) |
| create_user | BIGINT(20) | 创建者ID |
| create_time | DATETIME | 创建时间,默认值CURRENT_TIMESTAMP |
| update_user | BIGINT(20) | 更新者ID |
| update_time | DATETIME | 更新时间,默认值CURRENT_TIMESTAMP,更新时自动设置为当前时间 |
| remark | VARCHAR(255) | 备注 |
遵循的数据库规范要点:
BIGINT 主键并通过雪花算法生成(MyBatis-Plus ASSIGN_ID)。version、create_user、create_time、update_user、update_time)由 BaseEntity 与 CustomMetaObjectHandler 自动填充。V{version}__{description}.sql)。示例建表(参考):
CREATE TABLE `t_follow_up` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`patient_user_id` bigint(20) NOT NULL COMMENT '患者用户ID',
`doctor_user_id` bigint(20) NOT NULL COMMENT '医生用户ID',
`appointment_time` datetime NOT NULL COMMENT '预约时间',
`actual_time` datetime DEFAULT NULL COMMENT '医生主动确认已完成的时间记录',
`status` varchar(20) NOT NULL COMMENT '复诊状态 (PENDING-待确认, CONFIRMED-已确认, CANCELLED-已取消, COMPLETED-已完成)',
`reason` text COMMENT '复诊原因',
`notes` text COMMENT '备注',
`version` int(11) DEFAULT '0' COMMENT '版本号(乐观锁)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建者ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '更新者ID',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_patient_user_id` (`patient_user_id`),
KEY `idx_doctor_user_id` (`doctor_user_id`),
KEY `idx_appointment_time` (`appointment_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='复诊记录表';
备注:对外响应的 id 建议以字符串形式返回以避免前端 JS 精度丢失(参见 docs/OLD/DevRule/03-API设计规范.md 的长整型 ID 传输策略)。
``java package work.baiyun.chronicdiseaseapp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
/**
复诊状态枚举 */ public enum FollowUpStatus { PENDING("PENDING", "待确认"), CONFIRMED("CONFIRMED", "已确认"), CANCELLED("CANCELLED", "已取消"), COMPLETED("COMPLETED", "已完成");
@EnumValue private final String code; private final String description;
FollowUpStatus(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override public String toString() {
return description;
}
public static FollowUpStatus fromCode(String code) {
if (code == null) return null;
for (FollowUpStatus status : FollowUpStatus.values()) {
if (status.code.equals(code)) return status;
}
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"Unknown FollowUpStatus code: " + code);
} }
### 3.2 枚举类型处理器 (FollowUpStatusTypeHandler.java)
``java
package work.baiyun.chronicdiseaseapp.handler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import work.baiyun.chronicdiseaseapp.enums.FollowUpStatus;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(FollowUpStatus.class)
public class FollowUpStatusTypeHandler extends BaseTypeHandler<FollowUpStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, FollowUpStatus parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter.getCode());
}
@Override
public FollowUpStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
String code = rs.getString(columnName);
return code == null ? null : FollowUpStatus.fromCode(code);
}
@Override
public FollowUpStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String code = rs.getString(columnIndex);
return code == null ? null : FollowUpStatus.fromCode(code);
}
@Override
public FollowUpStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String code = cs.getString(columnIndex);
return code == null ? null : FollowUpStatus.fromCode(code);
}
}
``java 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 work.baiyun.chronicdiseaseapp.enums.FollowUpStatus;
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(value = "status", typeHandler = work.baiyun.chronicdiseaseapp.handler.FollowUpStatusTypeHandler.class)
private FollowUpStatus status;
@Schema(description = "复诊原因")
@TableField("reason")
private String reason;
@Schema(description = "备注")
@TableField("notes")
private String notes;
}
### 4.2 VO对象
#### 4.2.1 请求对象
``java
// 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;
@Schema(description = "复诊原因")
private String reason;
}
``java // 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 String patientUserId;
@Schema(description = "医生用户ID")
private String 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;
}
## 5. Mapper接口设计
``java
// 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> {
}
``java // 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);
}
### 6.2 实现类
``java
// 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);
}
}
``java // 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)))
})
@PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> update(@PathVariable Long id, @RequestBody UpdateFollowUpRequest req) {
try {
// 将路径ID赋值到请求对象,保证一致性
req.setId(id);
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(@RequestParam("patientUserId") 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)))
})
@DeleteMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> delete(@PathVariable Long id) {
try {
followUpService.deleteFollowUp(id);
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());
}
}
}
## 8. 请求/响应对象补充
``java
// 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 枚举中可能需要添加以下错误码:
``java // 在 ErrorCode.java 中添加 FOLLOW_UP_NOT_FOUND(6000, "复诊记录不存在"), FOLLOW_UP_ACCESS_DENIED(6001, "无权访问复诊记录"), FOLLOW_UP_STATUS_INVALID(6002, "复诊状态无效");
## 10. 接口调用示例
### 10.1 患者创建复诊请求
创建复诊请求时,会验证传入的医生ID是否有效(用户存在且角色为医生)。如果无效,将返回错误码6003。
POST /follow-up/create { "doctorUserId": 1988172854356631553, "appointmentTime": "2025-12-01T10:00:00", "reason": "血糖控制不佳,需要调整治疗方案" }
错误响应示例(医生ID无效):
json { "code": 6003, "message": "医生ID无效", "data": null, "timestamp": 1763570208009, "requestId": "xxx", "traceId": "xxx" }
### 10.2 医生确认复诊
POST /follow-up/update { "id": 1988502746686595073, "status": "CONFIRMED", "notes": "已确认预约" }
### 10.3 查询复诊记录
POST /follow-up/list { "pageNum": 1, "pageSize": 10, "startTime": "2025-11-01T00:00:00", "endTime": "2025-12-01T23:59:59" }
## 11. 权限设计
1. **患者**:
- 可以创建复诊请求
- 可以查看自己的复诊记录
- 可以删除自己的待确认复诊请求
- 可以修改自己的待确认复诊请求
- 可以更新复诊申请原因
2. **医生**:
- 可以查看分配给自己的复诊请求
- 可以确认、取消或完成复诊
- 可以查看患者的复诊历史
3. **系统管理员**:
- 具有医生的所有权限
- 可以查看所有复诊记录
## 12. 状态流转
PENDING(待确认) -> CONFIRMED(已确认) -> COMPLETED(已完成)
|
v
CANCELLED(已取消) ```
本项目安全实现应遵循 docs/OLD/DevRule/08-安全规范.md 中的已有约定,以下为与复诊管理功能相关的要点(需在实现时落地):
认证与授权:
Authorization: Bearer <token>,兼容 X-Token / token)并通过 AuthInterceptor 校验,将 currentUserId/currentUserRole 注入请求上下文。输入校验:
@NotNull、@Future 等)对请求参数进行校验。appointmentTime)增加业务校验:不可为过去时间、合法时间窗口、与已有预约冲突的检测。异常与统一错误响应:
@RestControllerAdvice/CustomExceptionHandler)统一映射 CustomException、校验异常、未知异常到 R.fail(code, message),并返回合适的 HTTP 状态码(如 400/403/404/500)。FOLLOW_UP_NOT_FOUND / FOLLOW_UP_ACCESS_DENIED)应使用明确错误码并在 ErrorCode 中定义。日志与敏感信息:
SQL 注入与 ORM 使用:
会话安全与 Token 管理:
t_user_token,有效期 72 小时,低于阈值自动延长)。部署/传输:
classpath:cert/ 并在启动时加载。最小权限原则:
审计与监控: