1.0.2
parent
1c2e2975bb
commit
d2f23a8381
BIN
goodnews.db
BIN
goodnews.db
Binary file not shown.
16
main.go
16
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,
|
||||
}
|
||||
|
|
|
@ -11,5 +11,4 @@
|
|||
+ 本项目后端采用golang+gin
|
||||
+ 前端采用element-ui
|
||||
+ 数据库采用sqlite
|
||||
+ 图片存储采用七牛云
|
||||
+ 部署采用docker
|
|
@ -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 ""
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
Loading…
Reference in New Issue