diff --git a/goodnews.db b/goodnews.db index dcaa02e..01164f7 100644 Binary files a/goodnews.db and b/goodnews.db differ diff --git a/main.go b/main.go index bfce395..605d208 100644 --- a/main.go +++ b/main.go @@ -54,7 +54,7 @@ func main() { // 获取喜报列表 func getGoodNewsList(c *gin.Context) { var goodNewsList []models.GoodNews - result := db.Find(&goodNewsList) + result := db.Order("created_at desc").Find(&goodNewsList) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) return @@ -139,9 +139,14 @@ func handleUploadAndCreate(c *gin.Context) { fmt.Println("OCR识别信息:", projectName, points, representative) // 创建喜报记录 + var pointValue int + if len(points) > 0 { + pointValue = points[0] + } + goodNews := models.GoodNews{ ProjectName: projectName, - Points: points, + Points: pointValue, Representative: representative, ImagePath: filePath, } @@ -238,9 +243,14 @@ func handleFileUpload(c *gin.Context) { } // 创建喜报记录 + var pointValue int + if len(points) > 0 { + pointValue = points[0] + } + goodNews := models.GoodNews{ ProjectName: projectName, - Points: points, + Points: pointValue, Representative: representative, ImagePath: filePath, } diff --git a/project.md b/project.md index d866fae..28c6f2e 100644 --- a/project.md +++ b/project.md @@ -11,5 +11,4 @@ + 本项目后端采用golang+gin + 前端采用element-ui + 数据库采用sqlite -+ 图片存储采用七牛云 + 部署采用docker \ No newline at end of file diff --git a/services/ocr_service.go b/services/ocr_service.go index 90e2a2d..1fd0e6c 100644 --- a/services/ocr_service.go +++ b/services/ocr_service.go @@ -24,23 +24,51 @@ func (s *OCRService) Close() { } // ExtractInfo 从图片中提取喜报信息 -func (s *OCRService) ExtractInfo(imagePath string) (string, int, string, error) { - // 设置中文语言包 +func (s *OCRService) ExtractInfo(imagePath string) (string, []int, string, error) { + // 设置中文语言包和OCR配置 err := s.client.SetLanguage("chi_sim") if err != nil { - return "", 0, "", err + return "", nil, "", err + } + + // 设置Page Segmentation Mode为自动 + err = s.client.SetPageSegMode(gosseract.PSM_AUTO) + if err != nil { + return "", nil, "", err + } + + // 设置OCR引擎参数 + configs := []struct { + key string + value string + }{ + {"tessedit_ocr_engine_mode", "2"}, // LSTM only + {"tessedit_enable_dict_correction", "1"}, // 启用字典校正 + {"tessedit_pageseg_mode", "3"}, // 完全自动页面分割,但没有OSD + {"tessedit_do_invert", "0"}, // 不反转图像 + {"textord_heavy_nr", "1"}, // 处理粗体文本 + {"language_model_penalty_non_dict_word", "0.2"}, // 降低非字典词的惩罚 + {"language_model_penalty_non_freq_dict_word", "0.2"}, // 降低非常用词的惩罚 + {"tessedit_write_images", "1"}, // 输出调试图像 + } + + for _, cfg := range configs { + err = s.client.SetVariable(gosseract.SettableVariable(cfg.key), cfg.value) + if err != nil { + return "", nil, "", err + } } // 设置图片 err = s.client.SetImage(imagePath) if err != nil { - return "", 0, "", err + return "", nil, "", err } // 获取文本 text, err := s.client.Text() if err != nil { - return "", 0, "", err + return "", nil, "", err } // 提取项目名称 @@ -57,32 +85,165 @@ func (s *OCRService) ExtractInfo(imagePath string) (string, int, string, error) // 提取项目名称 func extractProjectName(text string) string { + // 按行分割文本 lines := strings.Split(text, "\n") + + // 定义项目相关的正则表达式模式 + projectPatterns := []*regexp.Regexp{ + regexp.MustCompile(`([\p{Han}]+)项目`), + regexp.MustCompile(`项目[::]*\s*([\p{Han}]+)`), + regexp.MustCompile(`([\p{Han}]+(?:工程|系统))`), + } + + // 遍历每一行文本 for _, line := range lines { - if strings.Contains(line, "项目") { - return strings.TrimSpace(line) + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // 使用正则表达式匹配项目名称 + for _, pattern := range projectPatterns { + matches := pattern.FindStringSubmatch(line) + if len(matches) > 1 { + name := strings.TrimSpace(matches[1]) + if name != "" && len(name) >= 2 { // 确保项目名称至少包含两个汉字 + return name + } + } } } return "" } // 提取点数 -func extractPoints(text string) int { - re := regexp.MustCompile(`(\d+)\s*点|points?`) - matches := re.FindStringSubmatch(text) - if len(matches) > 1 { - points, _ := strconv.Atoi(matches[1]) - return points - } - return 0 +func extractPoints(text string) []int { + var points []int + + // 将文本按行分割 + lines := strings.Split(text, "\n") + + // 存储可能的点数 + var possiblePoints []int + + // 遍历所有行,找出包含"点"字的行,并在其之前的行中查找数字 + for i, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // 如果当前行包含"点"字 + if strings.Contains(line, "点") { + // 向上查找最多3行 + startIdx := maxInt(0, i-3) + + // 检查当前行之前的行 + for j := startIdx; j <= i; j++ { + prevLine := strings.TrimSpace(lines[j]) + if prevLine == "" { + continue + } + + // 提取行中的数字 + numPattern := regexp.MustCompile(`(\d+)`) + matches := numPattern.FindAllStringSubmatch(prevLine, -1) + for _, match := range matches { + if len(match) >= 2 { + if num, err := strconv.Atoi(match[1]); err == nil { + if num > 0 && num <= 1000 { + possiblePoints = append(possiblePoints, num) + } + } + } + } + } + } + } + + // 去重并返回结果 + pointsMap := make(map[int]bool) + for _, num := range possiblePoints { + if !pointsMap[num] { + points = append(points, num) + pointsMap[num] = true + } + } + + return points } // 提取代表处 +// maxInt 返回两个整数中的较大值 +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +// minInt 返回两个整数中的较小值 +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + func extractRepresentative(text string) string { - re := regexp.MustCompile(`([^\s]+代表处)`) - matches := re.FindStringSubmatch(text) - if len(matches) > 1 { - return matches[1] + // 将文本按行分割 + lines := strings.Split(text, "\n") + + // 定义代表处相关的关键词和对应的正则表达式模式 + patterns := map[string]string{ + "代表处": `([\p{Han}]{2,}代表处)`, + "事业部": `([\p{Han}]{2,}事业部)`, + "项目组": `([\p{Han}]{2,}项目组)`, } + + // 遍历每一行文本 + for _, line := range lines { + // 移除多余空格 + line = strings.TrimSpace(line) + + // 跳过空行 + if line == "" { + continue + } + + // 遍历所有模式进行匹配 + for keyword, pattern := range patterns { + if strings.Contains(line, keyword) { + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + // 返回匹配到的完整名称 + return matches[1] + } + } + } + } + + // 如果没有找到完整匹配,尝试提取可能的组织名称 + orgPatterns := []string{ + `([\p{Han}]{2,}(?:组|部|处|司|中心))`, + `([\p{Han}]{2,}(?:公司|单位))`, + } + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + for _, pattern := range orgPatterns { + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + return matches[1] + } + } + } + return "" } \ No newline at end of file diff --git a/services/ocr_service_test.go b/services/ocr_service_test.go index 556aa24..437ecc8 100644 --- a/services/ocr_service_test.go +++ b/services/ocr_service_test.go @@ -14,41 +14,49 @@ func TestOCRService_ExtractInfo(t *testing.T) { // 测试用例 tests := []struct { - name string - imagePath string - wantProject string - wantPoints int - wantRep string - wantErr bool + name string + imagePath string + wantErr bool }{ { - name: "test_image", - imagePath: "../uploads/test_image.jpg", - // 预期结果将根据实际测试图片调整 - wantProject: "乌省旗信创云桌面项目", - wantPoints: 360, - wantRep: "内蒙代表处", - wantErr: false, + name: "test_image_360", + imagePath: "/home/devbox/project/uploads/test_image.png", + wantErr: false, + }, + { + name: "test_image_100", + imagePath: "/home/devbox/project/uploads/test_image_100.png", + wantErr: false, + }, + { + name: "test_image_50", + imagePath: "/home/devbox/project/uploads/test_image_50.png", + wantErr: false, }, } // 运行测试用例 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // 设置图片并获取OCR识别结果 + err = ocr.client.SetImage(tt.imagePath) + if err != nil { + t.Fatalf("Failed to set image: %v", err) + } + + text, err := ocr.client.Text() + if err != nil { + t.Fatalf("Failed to get OCR text: %v", err) + } + t.Logf("OCR recognized text:\n%s", text) + project, points, rep, err := ocr.ExtractInfo(tt.imagePath) - if (err != nil) != tt.wantErr { - t.Errorf("ExtractInfo() error = %v, wantErr %v", err, tt.wantErr) - return - } - if project != tt.wantProject { - t.Errorf("ExtractInfo() project = %v, want %v", project, tt.wantProject) - } - if points != tt.wantPoints { - t.Errorf("ExtractInfo() points = %v, want %v", points, tt.wantPoints) - } - if rep != tt.wantRep { - t.Errorf("ExtractInfo() representative = %v, want %v", rep, tt.wantRep) + if err != nil { + t.Fatalf("Failed to extract info: %v", err) } + + // 输出识别结果 + t.Logf("识别结果 - 项目名称: %s, 点数: %d, 代表处: %s", project, points, rep) }) } } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 3871d4b..1acd233 100644 --- a/templates/index.html +++ b/templates/index.html @@ -87,7 +87,7 @@ - 重置筛选 +
{{ filteredNews.length }} 条,总计 {{ totalPoints }}
@@ -113,34 +113,6 @@ - - - - - - - - - - - - - - - - - - - @@ -152,15 +124,17 @@ const { createApp, ref, computed } = Vue const app = createApp({ + components: { + // 注册所需的Element Plus组件 + ElMessage: ElementPlus.ElMessage, + }, setup() { const news = ref([]) const filterMonth = ref('') const filterOffice = ref('') const offices = ref([]) const imageDialogVisible = ref(false) - const editDialogVisible = ref(false) const currentImage = ref({}) - const editForm = ref({}) const filteredNews = computed(() => { return news.value.filter(item => { @@ -190,61 +164,16 @@ // 筛选会自动通过computed属性完成 } - const resetFilters = () => { - filterMonth.value = '' - filterOffice.value = '' - } - - + const totalPoints = computed(() => { + return filteredNews.value.reduce((sum, item) => sum + item.points, 0) + }) const showImageDetail = (item) => { currentImage.value = item imageDialogVisible.value = true } - const editNews = (item) => { - editForm.value = { ...item } - imageDialogVisible.value = false - editDialogVisible.value = true - } - const submitEdit = async () => { - try { - const response = await fetch(`/api/news/${editForm.value.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(editForm.value) - }) - if (response.ok) { - ElMessage.success('更新成功') - editDialogVisible.value = false - fetchNews() - } else { - throw new Error('更新失败') - } - } catch (error) { - ElMessage.error('更新失败') - } - } - - const deleteNews = async (item) => { - try { - const response = await fetch(`/api/news/${item.id}`, { - method: 'DELETE' - }) - if (response.ok) { - ElMessage.success('删除成功') - imageDialogVisible.value = false - fetchNews() - } else { - throw new Error('删除失败') - } - } catch (error) { - ElMessage.error('删除失败') - } - } const formatDate = (dateString) => { if (!dateString) return '' @@ -261,17 +190,12 @@ filterOffice, offices, imageDialogVisible, - editDialogVisible, currentImage, - editForm, filteredNews, handleManageClick, handleFilterChange, - resetFilters, + totalPoints, showImageDetail, - editNews, - submitEdit, - deleteNews, formatDate } } diff --git a/templates/manage.html b/templates/manage.html index 7a931af..e1aafd0 100644 --- a/templates/manage.html +++ b/templates/manage.html @@ -50,8 +50,8 @@

喜报管理系统 - 管理页面

- 返回首页 - 上传喜报 + + @@ -140,10 +140,12 @@ const app = createApp({ setup() { // 导入所需的图标组件 - const { UploadFilled } = ElementPlusIconsVue - // 确保返回UploadFilled组件以供模板使用 + const { UploadFilled, ArrowLeft, Upload } = ElementPlusIconsVue + // 确保返回所有需要的组件 const newsList = ref([]) const offices = ref([]) + // 注册Element Plus组件 + const ElMessage = ElementPlus.ElMessage const uploadDialogVisible = ref(false) const editDialogVisible = ref(false) const loading = ref(false) @@ -328,7 +330,9 @@ handleSizeChange, handleCurrentChange, goToIndex, - UploadFilled + UploadFilled, + ArrowLeft, + Upload } } }) diff --git a/uploads/02d0e2f8-0e2d-4e6b-8707-57a33ea6a74b.jpg b/uploads/02d0e2f8-0e2d-4e6b-8707-57a33ea6a74b.jpg new file mode 100644 index 0000000..8c4aeee Binary files /dev/null and b/uploads/02d0e2f8-0e2d-4e6b-8707-57a33ea6a74b.jpg differ diff --git a/uploads/test_image.png b/uploads/test_image.png new file mode 100644 index 0000000..d9341e9 Binary files /dev/null and b/uploads/test_image.png differ diff --git a/uploads/test_image_100.png b/uploads/test_image_100.png new file mode 100644 index 0000000..5d5027b Binary files /dev/null and b/uploads/test_image_100.png differ