master
mula.liu 2025-04-07 03:18:38 +00:00
parent 1c2e2975bb
commit d2f23a8381
10 changed files with 244 additions and 138 deletions

Binary file not shown.

16
main.go
View File

@ -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,
}

View File

@ -11,5 +11,4 @@
+ 本项目后端采用golang+gin
+ 前端采用element-ui
+ 数据库采用sqlite
+ 图片存储采用七牛云
+ 部署采用docker

View File

@ -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 ""
}

View File

@ -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)
})
}
}

View File

@ -87,7 +87,7 @@
</el-select>
</el-col>
<el-col :span="8" style="text-align: right;">
<el-button @click="resetFilters">重置筛选</el-button>
<div><span style="color: red;">{{ filteredNews.length }}</span> 条,总计 <span style="color: red;">{{ totalPoints }}</span></div>
</el-col>
</el-row>
</div>
@ -113,34 +113,6 @@
<img :src="currentImage.image_url" style="width: 100%;">
<template #footer>
<el-button @click="imageDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="editNews(currentImage)">编辑</el-button>
<el-button type="danger" @click="deleteNews(currentImage)">删除</el-button>
</template>
</el-dialog>
<!-- 编辑对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑喜报信息" width="500px">
<el-form :model="editForm" label-width="100px">
<el-form-item label="项目名称">
<el-input v-model="editForm.project_name"></el-input>
</el-form-item>
<el-form-item label="签单点数">
<el-input-number v-model="editForm.points"></el-input-number>
</el-form-item>
<el-form-item label="代表处">
<el-select v-model="editForm.office">
<el-option
v-for="office in offices"
:key="office"
:label="office"
:value="office">
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
</div>
@ -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
}
}

View File

@ -50,8 +50,8 @@
<h1>喜报管理系统 - 管理页面</h1>
</el-col>
<el-col :span="8" style="text-align: right;">
<el-button @click="goToIndex" style="margin-right: 10px">返回首页</el-button>
<el-button type="primary" @click="showUploadDialog">上传喜报</el-button>
<el-button @click="goToIndex" style="margin-right: 10px" :icon="ArrowLeft" circle></el-button>
<el-button type="primary" @click="showUploadDialog" :icon="Upload" circle></el-button>
</el-col>
</el-row>
</div>
@ -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
}
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB