药品信息管理功能旨在为医生提供一个维护和管理常用药品信息的平台。该功能允许医生查看、添加、编辑和删除药品信息,包括药品名称、拼音首字母(用于快速搜索)以及条形码等基本信息。这些药品信息可用于处方开具、药品搜索等场景。
根据项目数据库表设计规范(参见 docs/OLD/DevRule/07-数据库规范.md),创建药品信息表:
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT(20) | 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID) |
| name | VARCHAR(100) | 药品名称 |
| pinyin_first_letters | VARCHAR(50) | 拼音首字母,用于快速搜索 |
| barcode | VARCHAR(50) | 条形码(可选) |
| 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_medicine` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`name` varchar(100) NOT NULL COMMENT '药品名称',
`pinyin_first_letters` varchar(50) NOT NULL COMMENT '拼音首字母,用于快速搜索',
`barcode` varchar(50) DEFAULT NULL 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`),
UNIQUE KEY `uk_name` (`name`),
KEY `idx_pinyin_first_letters` (`pinyin_first_letters`),
KEY `idx_barcode` (`barcode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='药品信息表';
备注:对外响应的 id 建议以字符串形式返回以避免前端 JS 精度丢失(参见 docs/OLD/DevRule/03-API设计规范.md 的长整型 ID 传输策略)。
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;
@Schema(description = "药品信息表")
@TableName("t_medicine")
@Data
@EqualsAndHashCode(callSuper = false)
public class Medicine extends BaseEntity {
@Schema(description = "药品名称")
@TableField("name")
private String name;
@Schema(description = "拼音首字母,用于快速搜索")
@TableField("pinyin_first_letters")
private String pinyinFirstLetters;
@Schema(description = "条形码")
@TableField("barcode")
private String barcode;
}
// CreateMedicineRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Schema(description = "创建药品请求")
@Data
public class CreateMedicineRequest {
@Schema(description = "药品名称", required = true)
@NotBlank(message = "药品名称不能为空")
@Size(max = 100, message = "药品名称长度不能超过100个字符")
private String name;
@Schema(description = "拼音首字母,用于快速搜索", required = true)
@NotBlank(message = "拼音首字母不能为空")
@Size(max = 50, message = "拼音首字母长度不能超过50个字符")
private String pinyinFirstLetters;
@Schema(description = "条形码")
@Size(max = 50, message = "条形码长度不能超过50个字符")
private String barcode;
}
// UpdateMedicineRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Schema(description = "更新药品请求")
@Data
public class UpdateMedicineRequest {
@Schema(description = "药品ID", required = true)
@NotNull(message = "药品ID不能为空")
private Long id;
@Schema(description = "药品名称", required = true)
@NotBlank(message = "药品名称不能为空")
@Size(max = 100, message = "药品名称长度不能超过100个字符")
private String name;
@Schema(description = "拼音首字母,用于快速搜索", required = true)
@NotBlank(message = "拼音首字母不能为空")
@Size(max = 50, message = "拼音首字母长度不能超过50个字符")
private String pinyinFirstLetters;
@Schema(description = "条形码")
@Size(max = 50, message = "条形码长度不能超过50个字符")
private String barcode;
}
// MedicineQueryRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "药品查询请求")
@Data
@EqualsAndHashCode(callSuper = true)
public class MedicineQueryRequest extends BaseQueryRequest {
@Schema(description = "搜索关键字(药品名称或拼音首字母)")
private String keyword;
}
// MedicineResponse.java
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "药品信息响应")
@Data
public class MedicineResponse {
@Schema(description = "药品ID")
private String id;
@Schema(description = "药品名称")
private String name;
@Schema(description = "拼音首字母,用于快速搜索")
private String pinyinFirstLetters;
@Schema(description = "条形码")
private String barcode;
@Schema(description = "创建时间")
private java.time.LocalDateTime createTime;
}
// MedicinePageResponse.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 MedicinePageResponse {
@Schema(description = "数据列表")
private List<MedicineResponse> records;
@Schema(description = "总数")
private long total;
@Schema(description = "每页大小")
private long size;
@Schema(description = "当前页码")
private long current;
@Schema(description = "总页数")
private long pages;
}
// MedicineMapper.java
package work.baiyun.chronicdiseaseapp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import work.baiyun.chronicdiseaseapp.model.po.Medicine;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MedicineMapper extends BaseMapper<Medicine> {
}
// MedicineService.java
package work.baiyun.chronicdiseaseapp.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import work.baiyun.chronicdiseaseapp.model.vo.MedicineQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.MedicineResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicineRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicineRequest;
public interface MedicineService {
/**
* 创建药品信息
*/
void createMedicine(CreateMedicineRequest request);
/**
* 更新药品信息
*/
void updateMedicine(UpdateMedicineRequest request);
/**
* 分页查询药品信息
*/
Page<MedicineResponse> listMedicines(MedicineQueryRequest request);
/**
* 根据ID删除药品信息
*/
void deleteMedicine(Long id);
/**
* 根据ID获取药品详情
*/
MedicineResponse getMedicineById(Long id);
}
// MedicineServiceImpl.java
package work.baiyun.chronicdiseaseapp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import work.baiyun.chronicdiseaseapp.mapper.MedicineMapper;
import work.baiyun.chronicdiseaseapp.model.po.Medicine;
import work.baiyun.chronicdiseaseapp.model.vo.MedicineQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.MedicineResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicineRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicineRequest;
import work.baiyun.chronicdiseaseapp.service.MedicineService;
import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MedicineServiceImpl implements MedicineService {
@Autowired
private MedicineMapper medicineMapper;
@Override
public void createMedicine(CreateMedicineRequest request) {
// 检查药品名称是否已存在
LambdaQueryWrapper<Medicine> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Medicine::getName, request.getName());
if (medicineMapper.selectCount(queryWrapper) > 0) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"药品名称已存在");
}
Medicine medicine = new Medicine();
BeanUtils.copyProperties(request, medicine);
medicine.setPinyinFirstLetters(request.getPinyinFirstLetters().toLowerCase());
medicineMapper.insert(medicine);
}
@Override
public void updateMedicine(UpdateMedicineRequest request) {
Medicine medicine = medicineMapper.selectById(request.getId());
if (medicine == null) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
"药品信息不存在");
}
// 检查药品名称是否已存在(排除自己)
LambdaQueryWrapper<Medicine> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Medicine::getName, request.getName())
.ne(Medicine::getId, request.getId());
if (medicineMapper.selectCount(queryWrapper) > 0) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"药品名称已存在");
}
BeanUtils.copyProperties(request, medicine);
medicine.setPinyinFirstLetters(request.getPinyinFirstLetters().toLowerCase());
medicineMapper.updateById(medicine);
}
@Override
public Page<MedicineResponse> listMedicines(MedicineQueryRequest request) {
Page<Medicine> page = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<Medicine> wrapper = new LambdaQueryWrapper<>();
// 根据关键字搜索(药品名称或拼音首字母)
if (request.getKeyword() != null && !request.getKeyword().trim().isEmpty()) {
String keyword = request.getKeyword().trim().toLowerCase();
wrapper.and(w -> w.like(Medicine::getName, keyword)
.or()
.like(Medicine::getPinyinFirstLetters, keyword));
}
// 时间范围筛选
wrapper.ge(request.getStartTime() != null, Medicine::getCreateTime, request.getStartTime())
.le(request.getEndTime() != null, Medicine::getCreateTime, request.getEndTime())
.orderByDesc(Medicine::getCreateTime);
Page<Medicine> result = medicineMapper.selectPage(page, wrapper);
List<MedicineResponse> responses = result.getRecords().stream().map(r -> {
MedicineResponse resp = new MedicineResponse();
BeanUtils.copyProperties(r, resp);
resp.setId(r.getId().toString());
return resp;
}).collect(Collectors.toList());
Page<MedicineResponse> 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 deleteMedicine(Long id) {
Medicine medicine = medicineMapper.selectById(id);
if (medicine == null) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
"药品信息不存在");
}
medicineMapper.deleteById(id);
}
@Override
public MedicineResponse getMedicineById(Long id) {
Medicine medicine = medicineMapper.selectById(id);
if (medicine == null) {
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
"药品信息不存在");
}
MedicineResponse response = new MedicineResponse();
BeanUtils.copyProperties(medicine, response);
response.setId(medicine.getId().toString());
return response;
}
}
// MedicineController.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.MedicineQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.MedicineResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicineRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicineRequest;
import work.baiyun.chronicdiseaseapp.service.MedicineService;
import work.baiyun.chronicdiseaseapp.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/medicine")
@Tag(name = "药品信息管理", description = "药品信息管理相关接口")
public class MedicineController {
private static final Logger logger = LoggerFactory.getLogger(MedicineController.class);
@Autowired
private MedicineService medicineService;
@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 CreateMedicineRequest req) {
try {
medicineService.createMedicine(req);
return R.success(200, "药品信息创建成功");
} catch (Exception e) {
logger.error("create medicine 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 UpdateMedicineRequest req) {
try {
// 将路径ID赋值到请求对象,保证一致性
req.setId(id);
medicineService.updateMedicine(req);
return R.success(200, "药品信息更新成功");
} catch (Exception e) {
logger.error("update medicine 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.MedicinePageResponse.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 MedicineQueryRequest req) {
try {
Page<MedicineResponse> page = medicineService.listMedicines(req);
work.baiyun.chronicdiseaseapp.model.vo.MedicinePageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.MedicinePageResponse();
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 medicines 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 {
medicineService.deleteMedicine(id);
return R.success(200, "药品信息删除成功");
} catch (Exception e) {
logger.error("delete medicine 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 = work.baiyun.chronicdiseaseapp.model.vo.MedicineResponse.class))),
@ApiResponse(responseCode = "500", description = "服务器内部错误",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> get(@PathVariable Long id) {
try {
MedicineResponse medicine = medicineService.getMedicineById(id);
return R.success(200, "ok", medicine);
} catch (Exception e) {
logger.error("get medicine failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
}
在 ErrorCode 枚举中可能需要添加以下错误码:
// 在 ErrorCode.java 中添加
MEDICINE_NOT_FOUND(7000, "药品信息不存在"),
MEDICINE_NAME_EXISTS(7001, "药品名称已存在");
POST /medicine/create
Content-Type: application/json
{
"name": "阿司匹林",
"pinyinFirstLetters": "asp",
"barcode": "6920224840072"
}
PUT /medicine/1
Content-Type: application/json
{
"id": 1,
"name": "阿司匹林肠溶片",
"pinyinFirstLetters": "asplr",
"barcode": "6920224840073"
}
POST /medicine/list
Content-Type: application/json
{
"pageNum": 1,
"pageSize": 10,
"keyword": "asp"
}
DELETE /medicine/1
GET /medicine/1
医生:
系统管理员:
本项目安全实现应遵循 docs/OLD/DevRule/08-安全规范.md 中的已有约定,以下为与药品信息管理功能相关的要点(需在实现时落地):
认证与授权:
Authorization: Bearer <token>,兼容 X-Token / token)并通过 AuthInterceptor 校验,将 currentUserId/currentUserRole 注入请求上下文。输入校验:
@NotBlank、@Size 等)对请求参数进行校验。异常与统一错误响应:
@RestControllerAdvice/CustomExceptionHandler)统一映射 CustomException、校验异常、未知异常到 R.fail(code, message),并返回合适的 HTTP 状态码(如 400/403/404/500)。MEDICINE_NOT_FOUND / MEDICINE_NAME_EXISTS)应使用明确错误码并在 ErrorCode 中定义。日志与敏感信息:
SQL 注入与 ORM 使用:
会话安全与 Token 管理:
t_user_token,有效期 72 小时,低于阈值自动延长)。部署/传输:
classpath:cert/ 并在启动时加载。最小权限原则:
审计与监控: