feat(server-go): 分子检测扩展(qPCR Ct 判读/SERS 光谱上传/光谱库,#19)
This commit is contained in:
@@ -35,6 +35,7 @@ func Init(cfg *config.Config) error {
|
|||||||
&model.LampTest{}, &model.LampTestStep{},
|
&model.LampTest{}, &model.LampTestStep{},
|
||||||
&model.Consumable{},
|
&model.Consumable{},
|
||||||
&model.Consultation{},
|
&model.Consultation{},
|
||||||
|
&model.SpectrumEntry{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
slog.Warn("自动迁移有警告(可忽略)", "err", err)
|
slog.Warn("自动迁移有警告(可忽略)", "err", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ func buildImageKey(filename string) (string, string, error) {
|
|||||||
return buildObjectKey("knowledge", filename)
|
return buildObjectKey("knowledge", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildDataKey 生成 <prefix>/<日期>/<随机><ext>(不校验扩展名,由调用方校验)
|
||||||
|
func buildDataKey(prefix, filename string) (string, string, error) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
date := time.Now().Format("20060102")
|
||||||
|
return fmt.Sprintf("%s/%s/%s%s", prefix, date, hex.EncodeToString(buf), ext), ext, nil
|
||||||
|
}
|
||||||
|
|
||||||
// uploadKnowledgeImage 上传知识库图片(multipart 字段名 file)
|
// uploadKnowledgeImage 上传知识库图片(multipart 字段名 file)
|
||||||
func uploadKnowledgeImage(s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
func uploadKnowledgeImage(s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -77,3 +77,16 @@ func TestBuildObjectKeyPrefix(t *testing.T) {
|
|||||||
t.Errorf("key/ext 应以 .jpg 结尾: %s / %s", key, ext)
|
t.Errorf("key/ext 应以 .jpg 结尾: %s / %s", key, ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildDataKey(t *testing.T) {
|
||||||
|
key, ext, err := buildDataKey("spectrum", "a.CSV")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildDataKey 失败: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(key, "spectrum/") || !strings.HasSuffix(key, ".csv") {
|
||||||
|
t.Errorf("key 不正确: %s", key)
|
||||||
|
}
|
||||||
|
if ext != ".csv" {
|
||||||
|
t.Errorf("ext = %s, want .csv", ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.PATCH("/lamp-tests/:id/steps/:stepNo", write, updateLampTestStep(db))
|
||||||
rg.POST("/lamp-tests/:id/result-image", write, uploadLampResultImage(db, s3, imageBucket))
|
rg.POST("/lamp-tests/:id/result-image", write, uploadLampResultImage(db, s3, imageBucket))
|
||||||
rg.GET("/lamp-tests/:id/cross-validation", read, getLampCrossValidation(db))
|
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 过滤)
|
// listLampTests 检测任务单列表(roomId/batchId/status 过滤)
|
||||||
@@ -58,6 +64,13 @@ func createLampTest(db *gorm.DB) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.ID = ""
|
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) {
|
if t.RoomID != nil && !isUUID(*t.RoomID) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||||
return
|
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
|
// updateLampTest 更新任务单;result 合法时自动置状态 resulted
|
||||||
func updateLampTest(db *gorm.DB) gin.HandlerFunc {
|
func updateLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
// LampTest LAMP 检测任务单
|
// LampTest LAMP 检测任务单
|
||||||
type LampTest struct {
|
type LampTest struct {
|
||||||
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
||||||
|
Method string `gorm:"size:32;default:lamp" json:"method"` // lamp/qpcr/sers/hyperspectral
|
||||||
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
|
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
|
||||||
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
|
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
|
||||||
Diseases json.RawMessage `gorm:"type:jsonb" json:"diseases,omitempty"`
|
Diseases json.RawMessage `gorm:"type:jsonb" json:"diseases,omitempty"`
|
||||||
@@ -19,6 +20,7 @@ type LampTest struct {
|
|||||||
ResultedAt *time.Time `gorm:"column:resulted_at;type:timestamptz" json:"resultedAt,omitempty"`
|
ResultedAt *time.Time `gorm:"column:resulted_at;type:timestamptz" json:"resultedAt,omitempty"`
|
||||||
CrossStatus string `gorm:"column:cross_status;size:16;default:pending" json:"crossStatus"` // pending/consistent/inconsistent
|
CrossStatus string `gorm:"column:cross_status;size:16;default:pending" json:"crossStatus"` // pending/consistent/inconsistent
|
||||||
CrossReason *string `gorm:"column:cross_reason;type:text" json:"crossReason,omitempty"`
|
CrossReason *string `gorm:"column:cross_reason;type:text" json:"crossReason,omitempty"`
|
||||||
|
ExtraData json.RawMessage `gorm:"column:extra_data;type:jsonb" json:"extraData,omitempty"`
|
||||||
Note *string `gorm:"type:text" json:"note,omitempty"`
|
Note *string `gorm:"type:text" json:"note,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
|
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
|
||||||
@@ -41,6 +43,18 @@ type LampTestStep struct {
|
|||||||
|
|
||||||
func (LampTestStep) TableName() string { return "lamp_test_steps" }
|
func (LampTestStep) TableName() string { return "lamp_test_steps" }
|
||||||
|
|
||||||
|
// SpectrumEntry 光谱库条目
|
||||||
|
type SpectrumEntry struct {
|
||||||
|
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
||||||
|
Disease string `gorm:"size:64" json:"disease"`
|
||||||
|
Source *string `gorm:"size:128" json:"source,omitempty"`
|
||||||
|
DataURL *string `gorm:"column:data_url;size:512" json:"dataUrl,omitempty"`
|
||||||
|
Note *string `gorm:"type:text" json:"note,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SpectrumEntry) TableName() string { return "spectrum_entries" }
|
||||||
|
|
||||||
// DefaultLampSteps LAMP 标准 5 步流程(规格书 3.3.2.1)
|
// DefaultLampSteps LAMP 标准 5 步流程(规格书 3.3.2.1)
|
||||||
func DefaultLampSteps() []string {
|
func DefaultLampSteps() []string {
|
||||||
return []string{"采样", "DNA提取", "反应体系配制", "恒温反应", "结果判读"}
|
return []string{"采样", "DNA提取", "反应体系配制", "恒温反应", "结果判读"}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxSpectrumSize = 5 << 20 // 5MB
|
||||||
|
|
||||||
|
// ValidDetectionMethod 检测方式枚举
|
||||||
|
func ValidDetectionMethod(method string) bool {
|
||||||
|
switch method {
|
||||||
|
case "lamp", "qpcr", "sers", "hyperspectral":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeQPCR qPCR Ct 值自动判读:任一 Ct < threshold 判阳性,无 Ct 判阴性
|
||||||
|
func JudgeQPCR(cts []float64, threshold float64) (string, string) {
|
||||||
|
if threshold <= 0 {
|
||||||
|
threshold = 35
|
||||||
|
}
|
||||||
|
if len(cts) == 0 {
|
||||||
|
return "negative", "未检出 Ct 值,判为阴性"
|
||||||
|
}
|
||||||
|
for _, ct := range cts {
|
||||||
|
if ct < threshold {
|
||||||
|
return "positive", "存在 Ct 值低于阈值,判为阳性"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "negative", "全部 Ct 值不低于阈值,判为阴性"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSpectrumFile 光谱数据文件校验(csv/txt/json,≤5MB)
|
||||||
|
func ValidateSpectrumFile(filename string, size int64) error {
|
||||||
|
if size <= 0 || size > maxSpectrumSize {
|
||||||
|
return errors.New("光谱文件大小需在 1B~5MB 之间")
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
switch ext {
|
||||||
|
case ".csv", ".txt", ".json":
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("仅支持 csv/txt/json 光谱数据文件")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestJudgeQPCR(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
cts []float64
|
||||||
|
threshold float64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"Ct32 阳性", []float64{32}, 35, "positive"},
|
||||||
|
{"Ct38 阴性", []float64{38}, 35, "negative"},
|
||||||
|
{"无Ct 阴性", nil, 35, "negative"},
|
||||||
|
{"多个Ct含阳性", []float64{33, 37}, 35, "positive"},
|
||||||
|
{"自定义阈值40", []float64{38}, 40, "positive"},
|
||||||
|
{"阈值为0用默认35", []float64{38}, 0, "negative"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got, reason := JudgeQPCR(c.cts, c.threshold)
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("%s: result=%s, want %s(原因 %s)", c.name, got, c.want, reason)
|
||||||
|
}
|
||||||
|
if reason == "" {
|
||||||
|
t.Errorf("%s: 缺少判读说明", c.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSpectrumFile(t *testing.T) {
|
||||||
|
for _, name := range []string{"a.csv", "b.txt", "c.json"} {
|
||||||
|
if err := ValidateSpectrumFile(name, 1024); err != nil {
|
||||||
|
t.Errorf("%s 应被允许: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ValidateSpectrumFile("a.jpg", 1024); err == nil {
|
||||||
|
t.Error("图片扩展名不应被允许")
|
||||||
|
}
|
||||||
|
if err := ValidateSpectrumFile("a.csv", 0); err == nil {
|
||||||
|
t.Error("空文件应被拒绝")
|
||||||
|
}
|
||||||
|
if err := ValidateSpectrumFile("a.csv", maxSpectrumSize+1); err == nil {
|
||||||
|
t.Error("超 5MB 应被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user