feat(server-go): 分子检测扩展(qPCR Ct 判读/SERS 光谱上传/光谱库,#19)
This commit is contained in:
@@ -28,6 +28,12 @@ func RegisterLampRoutes(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Service,
|
||||
rg.PATCH("/lamp-tests/:id/steps/:stepNo", write, updateLampTestStep(db))
|
||||
rg.POST("/lamp-tests/:id/result-image", write, uploadLampResultImage(db, s3, imageBucket))
|
||||
rg.GET("/lamp-tests/:id/cross-validation", read, getLampCrossValidation(db))
|
||||
rg.POST("/lamp-tests/:id/judge-qpcr", write, judgeQPCR(db))
|
||||
rg.POST("/lamp-tests/:id/spectrum", write, uploadLampSpectrum(db, s3, imageBucket))
|
||||
rg.GET("/spectrum-entries", read, listSpectrumEntries(db))
|
||||
rg.POST("/spectrum-entries", write, createSpectrumEntry(db))
|
||||
rg.POST("/spectrum-entries/upload", write, uploadSpectrumEntryFile(s3, imageBucket))
|
||||
rg.DELETE("/spectrum-entries/:id", write, deleteSpectrumEntry(db))
|
||||
}
|
||||
|
||||
// listLampTests 检测任务单列表(roomId/batchId/status 过滤)
|
||||
@@ -58,6 +64,13 @@ func createLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
t.ID = ""
|
||||
if t.Method == "" {
|
||||
t.Method = "lamp"
|
||||
}
|
||||
if !model.ValidDetectionMethod(t.Method) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "method 仅支持 lamp/qpcr/sers/hyperspectral"})
|
||||
return
|
||||
}
|
||||
if t.RoomID != nil && !isUUID(*t.RoomID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||
return
|
||||
@@ -85,6 +98,183 @@ func createLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// judgeQPCR qPCR Ct 值录入并自动判读(#19)
|
||||
func judgeQPCR(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
CtValues []float64 `json:"ctValues"`
|
||||
Threshold float64 `json:"threshold"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
result, reason := model.JudgeQPCR(body.CtValues, body.Threshold)
|
||||
extra := map[string]interface{}{}
|
||||
if len(t.ExtraData) > 0 {
|
||||
_ = json.Unmarshal(t.ExtraData, &extra)
|
||||
}
|
||||
extra["ctValues"] = body.CtValues
|
||||
extra["judgeReason"] = reason
|
||||
raw, _ := json.Marshal(extra)
|
||||
now := time.Now()
|
||||
db.Model(&model.LampTest{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"extra_data": raw, "result": result, "status": "resulted", "resulted_at": now,
|
||||
})
|
||||
_ = runCrossValidation(db, id, result)
|
||||
db.Where("id = ?", id).First(&t)
|
||||
c.JSON(http.StatusOK, t)
|
||||
}
|
||||
}
|
||||
|
||||
// uploadLampSpectrum SERS 光谱数据上传(S3 spectrum/ 前缀)
|
||||
func uploadLampSpectrum(db *gorm.DB, s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
return
|
||||
}
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择光谱文件(字段名 file)"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if err := model.ValidateSpectrumFile(header.Filename, header.Size); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
key, ext, err := buildDataKey("spectrum", header.Filename)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s3.EnsureBucket(bucket); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不可用: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s3.UploadImage(bucket, key, bytes.NewReader(data), spectrumContentType(ext)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "上传失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
url := s3.Endpoint() + "/" + bucket + "/" + key
|
||||
extra := map[string]interface{}{}
|
||||
if len(t.ExtraData) > 0 {
|
||||
_ = json.Unmarshal(t.ExtraData, &extra)
|
||||
}
|
||||
extra["spectrumUrl"] = url
|
||||
raw, _ := json.Marshal(extra)
|
||||
db.Model(&model.LampTest{}).Where("id = ?", id).Update("extra_data", raw)
|
||||
c.JSON(http.StatusOK, gin.H{"url": url})
|
||||
}
|
||||
}
|
||||
|
||||
// spectrumContentType 光谱文件 Content-Type
|
||||
func spectrumContentType(ext string) string {
|
||||
switch ext {
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".csv":
|
||||
return "text/csv"
|
||||
default:
|
||||
return "text/plain"
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 光谱库 ----------
|
||||
|
||||
func listSpectrumEntries(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.SpectrumEntry{})
|
||||
if disease := c.Query("disease"); disease != "" {
|
||||
q = q.Where("disease = ?", disease)
|
||||
}
|
||||
var list []model.SpectrumEntry
|
||||
q.Order("created_at DESC").Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func createSpectrumEntry(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var e model.SpectrumEntry
|
||||
if err := c.ShouldBindJSON(&e); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
e.ID = ""
|
||||
if e.Disease == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&e).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, e)
|
||||
}
|
||||
}
|
||||
|
||||
func uploadSpectrumEntryFile(s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择光谱文件(字段名 file)"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if err := model.ValidateSpectrumFile(header.Filename, header.Size); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
key, ext, err := buildDataKey("spectrum", header.Filename)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s3.EnsureBucket(bucket); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不可用: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s3.UploadImage(bucket, key, bytes.NewReader(data), spectrumContentType(ext)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "上传失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": s3.Endpoint() + "/" + bucket + "/" + key})
|
||||
}
|
||||
}
|
||||
|
||||
func deleteSpectrumEntry(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var e model.SpectrumEntry
|
||||
if db.Where("id = ?", id).First(&e).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "spectrum entry not found"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", id).Delete(&model.SpectrumEntry{})
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
}
|
||||
|
||||
// updateLampTest 更新任务单;result 合法时自动置状态 resulted
|
||||
func updateLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user