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) { func getGoodNewsList(c *gin.Context) {
var goodNewsList []models.GoodNews var goodNewsList []models.GoodNews
result := db.Find(&goodNewsList) result := db.Order("created_at desc").Find(&goodNewsList)
if result.Error != nil { if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return return
@ -139,9 +139,14 @@ func handleUploadAndCreate(c *gin.Context) {
fmt.Println("OCR识别信息:", projectName, points, representative) fmt.Println("OCR识别信息:", projectName, points, representative)
// 创建喜报记录 // 创建喜报记录
var pointValue int
if len(points) > 0 {
pointValue = points[0]
}
goodNews := models.GoodNews{ goodNews := models.GoodNews{
ProjectName: projectName, ProjectName: projectName,
Points: points, Points: pointValue,
Representative: representative, Representative: representative,
ImagePath: filePath, ImagePath: filePath,
} }
@ -238,9 +243,14 @@ func handleFileUpload(c *gin.Context) {
} }
// 创建喜报记录 // 创建喜报记录
var pointValue int
if len(points) > 0 {
pointValue = points[0]
}
goodNews := models.GoodNews{ goodNews := models.GoodNews{
ProjectName: projectName, ProjectName: projectName,
Points: points, Points: pointValue,
Representative: representative, Representative: representative,
ImagePath: filePath, ImagePath: filePath,
} }

View File

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

View File

@ -24,23 +24,51 @@ func (s *OCRService) Close() {
} }
// ExtractInfo 从图片中提取喜报信息 // 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") err := s.client.SetLanguage("chi_sim")
if err != nil { 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) err = s.client.SetImage(imagePath)
if err != nil { if err != nil {
return "", 0, "", err return "", nil, "", err
} }
// 获取文本 // 获取文本
text, err := s.client.Text() text, err := s.client.Text()
if err != nil { 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 { func extractProjectName(text string) string {
// 按行分割文本
lines := strings.Split(text, "\n") 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 { for _, line := range lines {
if strings.Contains(line, "项目") { line = strings.TrimSpace(line)
return 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 "" return ""
} }
// 提取点数 // 提取点数
func extractPoints(text string) int { func extractPoints(text string) []int {
re := regexp.MustCompile(`(\d+)\s*点|points?`) var points []int
matches := re.FindStringSubmatch(text)
if len(matches) > 1 { // 将文本按行分割
points, _ := strconv.Atoi(matches[1]) lines := strings.Split(text, "\n")
return points
} // 存储可能的点数
return 0 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 { func extractRepresentative(text string) string {
re := regexp.MustCompile(`([^\s]+代表处)`) // 将文本按行分割
matches := re.FindStringSubmatch(text) lines := strings.Split(text, "\n")
if len(matches) > 1 {
return matches[1] // 定义代表处相关的关键词和对应的正则表达式模式
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 "" return ""
} }

View File

@ -14,41 +14,49 @@ func TestOCRService_ExtractInfo(t *testing.T) {
// 测试用例 // 测试用例
tests := []struct { tests := []struct {
name string name string
imagePath string imagePath string
wantProject string wantErr bool
wantPoints int
wantRep string
wantErr bool
}{ }{
{ {
name: "test_image", name: "test_image_360",
imagePath: "../uploads/test_image.jpg", imagePath: "/home/devbox/project/uploads/test_image.png",
// 预期结果将根据实际测试图片调整 wantErr: false,
wantProject: "乌省旗信创云桌面项目", },
wantPoints: 360, {
wantRep: "内蒙代表处", name: "test_image_100",
wantErr: false, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) project, points, rep, err := ocr.ExtractInfo(tt.imagePath)
if (err != nil) != tt.wantErr { if err != nil {
t.Errorf("ExtractInfo() error = %v, wantErr %v", err, tt.wantErr) t.Fatalf("Failed to extract info: %v", err)
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)
} }
// 输出识别结果
t.Logf("识别结果 - 项目名称: %s, 点数: %d, 代表处: %s", project, points, rep)
}) })
} }
} }

View File

@ -87,7 +87,7 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="8" style="text-align: right;"> <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-col>
</el-row> </el-row>
</div> </div>
@ -113,34 +113,6 @@
<img :src="currentImage.image_url" style="width: 100%;"> <img :src="currentImage.image_url" style="width: 100%;">
<template #footer> <template #footer>
<el-button @click="imageDialogVisible = false">关闭</el-button> <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> </template>
</el-dialog> </el-dialog>
</div> </div>
@ -152,15 +124,17 @@
const { createApp, ref, computed } = Vue const { createApp, ref, computed } = Vue
const app = createApp({ const app = createApp({
components: {
// 注册所需的Element Plus组件
ElMessage: ElementPlus.ElMessage,
},
setup() { setup() {
const news = ref([]) const news = ref([])
const filterMonth = ref('') const filterMonth = ref('')
const filterOffice = ref('') const filterOffice = ref('')
const offices = ref([]) const offices = ref([])
const imageDialogVisible = ref(false) const imageDialogVisible = ref(false)
const editDialogVisible = ref(false)
const currentImage = ref({}) const currentImage = ref({})
const editForm = ref({})
const filteredNews = computed(() => { const filteredNews = computed(() => {
return news.value.filter(item => { return news.value.filter(item => {
@ -190,61 +164,16 @@
// 筛选会自动通过computed属性完成 // 筛选会自动通过computed属性完成
} }
const resetFilters = () => { const totalPoints = computed(() => {
filterMonth.value = '' return filteredNews.value.reduce((sum, item) => sum + item.points, 0)
filterOffice.value = '' })
}
const showImageDetail = (item) => { const showImageDetail = (item) => {
currentImage.value = item currentImage.value = item
imageDialogVisible.value = true 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) => { const formatDate = (dateString) => {
if (!dateString) return '' if (!dateString) return ''
@ -261,17 +190,12 @@
filterOffice, filterOffice,
offices, offices,
imageDialogVisible, imageDialogVisible,
editDialogVisible,
currentImage, currentImage,
editForm,
filteredNews, filteredNews,
handleManageClick, handleManageClick,
handleFilterChange, handleFilterChange,
resetFilters, totalPoints,
showImageDetail, showImageDetail,
editNews,
submitEdit,
deleteNews,
formatDate formatDate
} }
} }

View File

@ -50,8 +50,8 @@
<h1>喜报管理系统 - 管理页面</h1> <h1>喜报管理系统 - 管理页面</h1>
</el-col> </el-col>
<el-col :span="8" style="text-align: right;"> <el-col :span="8" style="text-align: right;">
<el-button @click="goToIndex" style="margin-right: 10px">返回首页</el-button> <el-button @click="goToIndex" style="margin-right: 10px" :icon="ArrowLeft" circle></el-button>
<el-button type="primary" @click="showUploadDialog">上传喜报</el-button> <el-button type="primary" @click="showUploadDialog" :icon="Upload" circle></el-button>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@ -140,10 +140,12 @@
const app = createApp({ const app = createApp({
setup() { setup() {
// 导入所需的图标组件 // 导入所需的图标组件
const { UploadFilled } = ElementPlusIconsVue const { UploadFilled, ArrowLeft, Upload } = ElementPlusIconsVue
// 确保返回UploadFilled组件以供模板使用 // 确保返回所有需要的组件
const newsList = ref([]) const newsList = ref([])
const offices = ref([]) const offices = ref([])
// 注册Element Plus组件
const ElMessage = ElementPlus.ElMessage
const uploadDialogVisible = ref(false) const uploadDialogVisible = ref(false)
const editDialogVisible = ref(false) const editDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@ -328,7 +330,9 @@
handleSizeChange, handleSizeChange,
handleCurrentChange, handleCurrentChange,
goToIndex, 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