电子发票解析工具-golang服务端开发案例详解

电子发票解析工具-golang服务端开发案例详解

1. 项目结构

本项目是上一篇【# 电子发票解析工具-c#桌面应用开发案例详解】的服务端开发案例详解,采用MVC架构模式设计,主要提供电子发票OCR识别、PDF/OFD文件预览等功能。项目结构清晰,各模块职责分明。

项目目录结构如下:

bash 复制代码
├── main.go              # 主程序入口文件
├── config/              # 配置相关目录
│   └── config.go        # 配置加载与解析
├── controller/          # 控制器层,处理请求
│   ├── api/             # API控制器
│   └── test/            # 测试控制器
├── datasource/          # 数据源连接
│   └── engine.go        # 数据库引擎初始化
├── inference/           # OCR模型和配置
├── logger/              # 日志系统
│   └── log.go           # 日志处理逻辑
├── model/               # 数据模型
├── service/             # 服务层,业务逻辑
├── static/              # 静态资源文件
│   ├── css/             # 样式文件
│   ├── js/              # JavaScript文件
│   └── views/           # HTML视图文件
└── utils/               # 工具函数

主要模块职责:

  • main.go:应用程序入口,初始化配置、中间件、路由注册等
  • config/:处理应用配置,包括数据库连接、服务器端口等
  • controller/:处理HTTP请求,调用相应的服务层逻辑
  • datasource/:负责数据库连接和会话管理
  • inference/:存放PaddleOCR模型文件和配置
  • logger/:实现日志记录功能,支持文件日志
  • static/:存放前端静态资源,包括Vue3相关文件和HTML视图

2. 项目应用技术架构及应用包介绍

2.1 技术栈概述

本项目采用现代化的技术栈构建,主要包括:

技术/框架 版本/来源 用途
Go - 主要开发语言
Iris v12 Web框架
PaddleOCR - 光学字符识别
Vue3 - 前端框架
XORM - ORM框架
MSSQL - 数据库
Zap - 日志库

2.2 项目包引用列表及核心技术包介绍

2.2.1 引用包列表

以下是项目中主要引用的包列表,按功能模块分类:

1. 基础功能包
go 复制代码
import (
    "encoding/base64"  // Base64编码解码处理
    "fmt"               // 格式化输出
    "io/ioutil"         // 文件读写操作
    "net/http"          // HTTP网络通信
    "os"                // 操作系统接口
    "os/exec"           // 执行外部命令
    "path/filepath"     // 文件路径处理
    "strconv"           // 字符串与基本数据类型转换
    "strings"           // 字符串处理函数
    "sync"              // 并发控制
    "time"              // 时间处理
    "io"                // 输入输出操作
    "unicode"           // Unicode字符处理
)
2. Web框架与中间件
go 复制代码
import (
    "github.com/kataras/iris/v12"               // 主Web框架
    "github.com/kataras/iris/v12/context"       // Iris上下文
    "github.com/kataras/iris/v12/middleware/logger" // 日志中间件
    "github.com/kataras/iris/v12/mvc"           // MVC架构支持
    "github.com/kataras/iris/v12/sessions"      // 会话管理
)
3. 日志系统
go 复制代码
import (
    "go.uber.org/zap"            // 高性能日志库
    mlog "invoiceServer/logger"   // 自定义日志模块
)
4. 数据库相关
go 复制代码
import (
    "github.com/denisenkom/go-mssqldb" // SQL Server数据库驱动
    "github.com/go-sql-driver/mysql"   // MySQL数据库驱动
    "github.com/go-xorm/xorm"          // ORM框架
    "xorm.io/core"                     // XORM核心库
    "invoiceServer/datasource"         // 自定义数据源模块
)
5. 工具类库
go 复制代码
import (
    "github.com/olekukonko/tablewriter" // 表格输出格式化
    "golang.org/x/exp/rand"            // 扩展随机数生成
    "golang.org/x/time/rate"           // 速率限制
)
6. 系统调用与C集成
go 复制代码
import (
    "syscall" // 系统调用(用于调用DLL)
    "unsafe"  // 不安全操作(用于DLL接口调用)
    "C"       // C语言集成
)
7. 项目内部包
go 复制代码
import (
    "invoiceServer/config"                 // 配置管理
    apiController "invoiceServer/controller/api" // API控制器
    testController "invoiceServer/controller/test" // 测试控制器
    testService "invoiceServer/service/test" // 测试服务
)

以上包列表基于项目中实际引用的依赖,主要用于实现Web服务、OCR识别、数据库操作、日志管理等核心功能。

2.2.2 Iris Web框架

Iris是一个高效、模块化、功能丰富的Go Web框架,提供了路由、中间件、视图模板等完整的Web开发功能。项目中使用Iris构建RESTful API,处理HTTP请求和响应。

go 复制代码
// Iris框架初始化示例
func newApp() *iris.Application {
    //新建iris框架app
    app := iris.New()
    //注册静态资源
    app.HandleDir(".", "./static")
    //注册视图文件
    tmpl := iris.HTML("./static/views", ".html")
    tmpl.Delims("{{", "}}")
    tmpl.Reload(true)
    app.RegisterView(tmpl)
    // 其他配置...
    return app
}
2.2.3 PaddleOCR

本项目通过调用PaddleOCR.dll动态库实现发票图像文字识别功能。

  • PaddleOCR.dll动态库是广州英田信息科技有限公司基于百度开源的PaddleOCR工具库封装的二次开发版本,支持Windows平台下的Go语言调用。
  • 广州英田信息科技有限公司提供专业的百度开源的PaddleOCR工具库封装二次开发服务,支持定制化需求。
  • 本项目中使用的是cpu免费版本的PaddleOCR,如需要定制化需求,可自行联系广州英田信息科技有限公司,购买加速版本。
arduino 复制代码
广州英田信息科技有限公司                  
https://www.yingtianit.com/               
QQ群:318860399 定制开发QQ:277784829         
go 复制代码
// PaddleOCR初始化示例
func init() {
    dll, _ := syscall.LoadDLL("PaddleOCR.dll")        //加载PaddleOCR.dll动态库文件
    Initjson, _ := dll.FindProc("Initializejson")     //查找动态库的接口函数
    detect, _ := dll.FindProc("Detect")                //查找动态库的接口函数
    // 读取配置文件
    jsonconfig, err := ioutil.ReadFile(root + "\\inference\\PaddleOCR.config.json")
    // 初始化PaddleOCR
    Initjson.Call(strPtr(root+"\\inference\\ch_PP-OCRv4_det_infer"),
        strPtr(root+"\\inference\\ch_ppocr_mobile_v2.0_cls_infer"),
        strPtr(root+"\\inference\\ch_PP-OCRv4_rec_infer"),
        strPtr(root+"\\inference\\ppocr_keys.txt"), strPtr(string(jsonconfig)))
}
2.2.4 XORM

XORM是一个简单而强大的Go语言ORM库,支持多种数据库。项目中使用XORM与MSSQL数据库进行交互。

go 复制代码
// XORM初始化示例
func NewMsSqlEngine() *xorm.Engine {
    //获取配置
    initConfig := config.InitConfig()
    database := initConfig.DbMsSql2008
    //构建连接字符串
    dataSourceName := "server=" + database.Host + ";database=" + database.Database + ";user id=" + database.User + ";password=" + database.Pwd + ";port=" + database.Port + ";encrypt=disable"
    //创建数据库引擎
    engine, err := xorm.NewEngine(database.Drive, dataSourceName)
    //配置连接池
    engine.SetMaxOpenConns(2000)
    engine.SetMaxIdleConns(1000)
    engine.SetConnMaxLifetime(60)
    return engine
}
2.2.5 Zap日志库

Zap是Uber开源的高性能日志库,提供结构化日志记录功能。项目中使用Zap记录系统运行日志。

go 复制代码
// Zap日志初始化示例
func InitLogger() {
    var err error
    Logger, err = zap.NewProduction()
    if err != nil {
        panic(err)
    }
}

3. 项目功能调用流程图

3.1 PDF/OFD文件在线预览流程

flowchart TD A[客户端请求PDF/OFD预览页面] --> B[服务器接收请求] B --> C[返回PDF/OFD预览HTML页面] C --> D[前端加载PDF.js/OFD.js库] D --> E[发送文件请求] E --> F[服务器返回文件内容] F --> G[前端渲染并显示文件]

3.2 接收图片并调用PaddleOCR解析流程

flowchart TD A[客户端上传图片/Base64数据] --> B[服务器接收请求] B --> C{数据格式} C -->|Base64| D[解码Base64数据] C -->|文件| E[保存临时文件] D --> F[创建临时文件] E --> G[读取文件内容] F --> G G --> H[调用PaddleOCR.dll进行识别] H --> I[处理识别结果] I --> J[返回JSON格式结果]

4. 项目运行效果图

这里是项目运行效果图

  1. ****:服务启动

2. 主页面:展示html中vue3.js文件引用实现用户信息查询界面,支持按ID、名称、邮箱搜索用户信息。

3. PDF/OFD预览页面:支持在线预览PDF和OFD格式的电子发票文件。

4. OCR识别功能:接收图片或Base64数据,调用PaddleOCR进行文字识别并返回结果。

5. 核心功能代码示例

5.1 PDF/OFD文件在线预览

服务端路由配置

go 复制代码
// PDF预览路由
app.Get("/pdfbrower", func(ctx iris.Context) {
    mlog.WriteLog(ctx, "pdfobject.html")
    ctx.View("pdfobject.html")
})

// OFD预览路由
app.Get("/ofdbrower", func(ctx iris.Context) {
    mlog.WriteLog(ctx, "ofd.html")
    ctx.View("ofd.html")
})

文件上传功能

go 复制代码
// 文件上传接口
app.Post("/uploadbrowerfile", func(ctx iris.Context) {
    // 获取上传的文件
    file, info, err := ctx.FormFile("file")
    if err != nil {
        ctx.StatusCode(iris.StatusBadRequest)
        ctx.JSON(iris.Map{"error": "获取文件失败: " + err.Error()})
        return
    }
    defer file.Close()

    // 获取文件扩展名
    ext := filepath.Ext(info.Filename)
    if ext == "" {
        ctx.StatusCode(iris.StatusBadRequest)
        ctx.JSON(iris.Map{"error": "文件没有扩展名"})
        return
    }

    // 构造目标文件路径
    targetFilename := ext[1:] + ext // 去掉开头的点,例如 "pdf" + ".pdf"
    targetPath := filepath.Join(tempDir, targetFilename)

    // 创建目标文件
    out, err := os.Create(targetPath)
    if err != nil {
        ctx.StatusCode(iris.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": "创建目标文件失败: " + err.Error()})
        return
    }
    defer out.Close()

    // 复制文件内容
    _, err = io.Copy(out, file)
    if err != nil {
        ctx.StatusCode(iris.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": "保存文件失败: " + err.Error()})
        return
    }

    // 返回成功信息
    ctx.JSON(iris.Map{
        "message": "文件上传成功",
        "file":    targetFilename,
        "path":    targetPath,
    })
})

HTML代码pdfobject.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>PDF Embed with Timestamp</title>
</head>

<body>
	<div style="width: 100%; height: 1024px;">
		<embed id="pdf-embed" type="application/pdf" width="100%" height="100%">
	</div>

	<script>
		const timestamp = new Date().getTime(); // 获取当前时间戳  
		const pdfUrl = `./tempData/pdf.pdf?t=${timestamp}`; // 构造带时间戳的 URL  
		document.getElementById('pdf-embed').src = pdfUrl; // 设置 embed 的 src  
	</script>
</body>

</html>

HTML代码ofd.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>一个简易的排序按钮</title>
    <script src="../js/ofd.umd.js"></script>
    <style>
        .hidden {
            display: none;
            /* 或者使用 visibility: hidden; */
        }
    </style>
</head>

<body>
    <input type="file" id="file-input" class="hidden" accept=".ofd" onchange="fileChanged(event)" />
    <div id="ofdContainer" style="width:100%; height:800px;"></div>

    <script>
        // 默认文件名  
        // const defaultFileName = './tempData/ofd.ofd';
        // 使用时间戳或随机数构造新的URL  
        const timestamp = new Date().getTime(); // 或者使用 Math.random()  
        const defaultFileName = `./tempData/ofd.ofd?t=${timestamp}`; // 添加时间戳  
        // 默认文件名
        // 默认文件名从 URL 查询参数中获取  
        //const urlParams = new URLSearchParams(window.location.search);  
        //const defaultFileName = urlParams.get('file');  // 默认值为 'ofd5.ofd'  

        // 在页面加载时自动加载默认的 OFD 文件  

        // 页面加载时自动加载默认的 OFD 文件  

        function fileChanged(e) {
            // 获取文件数据  
            console.log(e.target.files[0]);
            const file = e.target.files[0];
            // const file = fileName;

            if (!file) {
                console.error('没有选择文件');
                return;
            }

            // 转换 OFD 文档  
            ofd.parseOfdDocument({
                ofd: file,
                success: function (res) {
                    const screenWidth = 800;
                    const ofdRenderRes = ofd.renderOfd(screenWidth, res[0]);
                    let ofdContainerDiv = document.getElementById('ofdContainer');
                    ofdContainerDiv.innerHTML = ''; // 清空之前的内容  
                    for (const item of ofdRenderRes) {
                        ofdContainerDiv.appendChild(item);
                    }
                },
                fail: function (err) {
                    console.error('ofd文件渲染失败', err);
                }
            });
        }

        // 通过 JavaScript 读取默认文件并加入 input  
        function loadDefaultFile() {
            fetch(defaultFileName)
                .then(response => response.blob())
                .then(blob => {
                    const file = new File([blob], defaultFileName, { type: 'application/vnd.oasis.opendocument.text' });
                    const event = new Event('change'); // 创建change事件  
                    const input = document.getElementById('file-input');
                    const dataTransfer = new DataTransfer();
                    dataTransfer.items.add(file);
                    input.files = dataTransfer.files; // 将文件分配给文件输入  
                    input.dispatchEvent(event); // 手动触发change事件  
                })
                .catch(err => {
                    console.error('加载默认文件失败', err);
                });
        }

        // fileChanged(fileChanged);
        loadDefaultFile(); // 页面加载时自动调用该函数  
    </script>
</body>

</html>

5.2 PaddleOCR发票识别功能

OCR初始化代码

go 复制代码
var detect *syscall.Proc
var tempDir string

func init() {
    // 加载PaddleOCR.dll动态库
    dll, _ := syscall.LoadDLL("PaddleOCR.dll")
    // 查找初始化函数
    Initjson, _ := dll.FindProc("Initializejson")
    detect, _ := dll.FindProc("Detect")
    enableANSI, _ := dll.FindProc("EnableANSIResult")
    enableJson, _ := dll.FindProc("EnableJsonResult")
    // 获取当前目录
    root := getCurrentAbPathBywd()

    // 读取配置文件
    jsonconfig, err := ioutil.ReadFile(root + "\\inference\\PaddleOCR.config.json")
    if err != nil {
        fmt.Println(err)
    }

    // 调用初始化方法,传入模型路径和配置
    Initjson.Call(strPtr(root+"\\inference\\ch_PP-OCRv4_det_infer"),
        strPtr(root+"\\inference\\ch_ppocr_mobile_v2.0_cls_infer"),
        strPtr(root+"\\inference\\ch_PP-OCRv4_rec_infer"),
        strPtr(root+"\\inference\\ppocr_keys.txt"), strPtr(string(jsonconfig)))

    // 启用ANSI编码返回结果
    enableANSI.Call(1) // 0: Unicode编码, 1: ANSI编码
    enableJson.Call(0) // 0: 返回纯字符串结果, 1: 返回JSON字符串结果
}

OCR识别实现

go 复制代码
// OcrData 调用PaddleOCR进行识别
func OcrData(file *os.File) (string, error) {
    start := time.Now() // 记录开始时间
    // 调用PaddleOCR的Detect方法进行识别
    res, _, _ := detect.Call(strPtr(file.Name()))
    // 将C字符串转换为Go字符串
    p_result := (*C.char)(unsafe.Pointer(res))

    end := time.Now()         // 记录结束时间
    elapsed := end.Sub(start) // 计算执行时间

    ocrresult := C.GoString(p_result)
    str_res := fmt.Sprintf("%v", ocrresult)

    // 处理结果,替换换行符等
    str_res = strings.ReplaceAll(str_res, "\n", ";")
    str_res = strings.ReplaceAll(str_res, "\r", ";")
    str_res = strings.ReplaceAll(str_res, "\t", ";") // 替换制表符

    fmt.Printf("------------------------------------------------------【%s】解析时间为:【%s】\n", file.Name(), get_Time(elapsed))

    return str_res, nil
}

OCR API接口

go 复制代码
// 定义接收图片 Base64 数据的路由
app.Post("/ocr", func(ctx iris.Context) {
    // 从请求中解析 JSON 数据
    if err := ctx.ReadJSON(&jpgData); err != nil {
        ctx.StatusCode(http.StatusBadRequest)
        ctx.JSON(iris.Map{"error": "Failed to parse JSON data"})
        return
    }

    // 解码 Base64 数据
    decodedData, err := base64.StdEncoding.DecodeString(jpgData.Base64)
    if err != nil {
        ctx.StatusCode(http.StatusBadRequest)
        ctx.JSON(iris.Map{"error": "Failed to decode Base64 data"})
        return
    }

    // 创建临时文件保存图片
    tempFile, err := ioutil.TempFile("", "*.jpg")
    if err != nil {
        ctx.StatusCode(http.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": "Failed to create temporary file"})
        return
    }
    defer os.Remove(tempFile.Name())
    defer tempFile.Close()

    // 将解码后的数据写入临时文件
    if _, err := tempFile.Write(decodedData); err != nil {
        ctx.StatusCode(http.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": "Failed to write to temporary file"})
        return
    }

    // 调用 PaddleOCR 进行识别
    ocrText, err := OcrData(tempFile)
    if err != nil {
        ctx.StatusCode(http.StatusInternalServerError)
        ctx.JSON(iris.Map{"error": fmt.Sprintf("Failed to run PaddleOCR: %v", err)})
        return
    }
    // 处理 PaddleOCR 的输出
    resultText := strings.TrimSpace(string(ocrText))
    result := OCRResult{Text: resultText}
    tt := time.Now()
    mlog.WriteLog(ctx, fmt.Sprintf("解析时间:%s 解析内容:【%v】", tt.Format("2006-01-02 15:04:05"), resultText))
    // 返回 JSON 格式的结果
    ctx.JSON(result)
})

6. 主应用程序入口及中间件设置

6.1 应用程序入口

go 复制代码
func main() {
    // 确保 tempData 文件夹存在
    tempDir = "./tempData"
    if err := os.MkdirAll(tempDir, os.ModePerm); err != nil {
        panic(fmt.Sprintf("无法创建 tempData 文件夹: %v", err))
    }

    // 打印当前时间信息
    tt := time.Now()
    fmt.Printf("当前时间:%s\n", tt.Format("2006-01-02 15:04:05"))
    mlog.WriteNotCtxLog(fmt.Sprintf("MesServer API Server Start Time: %v", time.Now()))

    // 初始化服务器配置
    config := config.InitConfig()
    // 初始化日志模块
    mlog.InitLogger()
    // 在系统停止时记录日志
    defer func() {
        if err := mlog.Logger.Sync(); err != nil {
            mlog.WriteNotCtxLog(fmt.Sprintf("MesServer API Server Stop: %v", err))
        }
    }()

    // 创建Iris应用
    app := newApp()

    // 添加日志中间件
    app.Use(logger.New(
        logger.Config{
            Status: true,
            IP: true,
            Method: true,
            Path: true,
            Query: true,
            LogFunc: func(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) {
                // 日志记录逻辑
                // ...
            },
        }))

    // 自定义Recover中间件
    app.Use(recoverMiddleware)
    // 设置跨域
    app.Use(Cors)
    // 应用配置
    configation(app)
    // 路由设置
    mvcHandle(app)

    // 输出路由列表
    routes := app.GetRoutes()
    table := tablewriter.NewWriter(os.Stdout)
    table.SetHeader([]string{"路由访问方式", "访问地址", "执行函数"})
    for _, r := range routes {
        method := r.Method
        path := r.FormattedPath
        funcName := r.MainHandlerName
        row := []string{method, path, funcName}
        table.Append(row)
    }
    table.Render()

    // 获取配置中的IP地址和端口号
    addr := config.Host + ":" + config.Port
    mlog.WriteNotCtxLog(fmt.Sprintf("MesServer API Server Start: %v", addr))
    // 启动服务
    app.Run(
        iris.Addr(addr),
        iris.WithCharset("UTF-8"),
        iris.WithoutServerError(iris.ErrServerClosed),
        iris.WithOptimizations,
    )
}

6.2 中间件设置

Recover中间件:用于捕获panic并返回友好错误信息

go 复制代码
// 自定义Recover中间件
func recoverMiddleware(ctx iris.Context) {
    defer func() {
        if r := recover(); r != nil {
            // 记录崩溃日志
            mlog.Logger.Error("Recovered from panic", zap.Any("error", r))

            // 强制写入日志
            mlog.LogToFile(fmt.Sprintf("Recovered from panic: %v", r))

            // 返回用户友好的错误信息
            ctx.StatusCode(500)
            ctx.JSON(iris.Map{"error": "Internal Server Error"})
        }
    }()
    ctx.Next()
}

日志中间件:用于记录HTTP请求日志

go 复制代码
app.Use(logger.New(
    logger.Config{
        // 是否记录状态码
        Status: true,
        // 是否记录远程IP地址
        IP: true,
        // 是否呈现HTTP谓词
        Method: true,
        // 是否记录请求路径
        Path: true,
        // 是否开启查询追加
        Query: true,
        LogFunc: func(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) {
            msg := "<no message>" // 默认值
            if message != nil {
                msg = fmt.Sprintf("%v", message)
            }

            logMessage := fmt.Sprintf(
                "[%s] %s %s %s %s %d %s",
                endTime.Format(time.RFC3339),
                method,
                path,
                ip,
                status,
                latency,
                msg,
            )
            mlog.LogToFile(logMessage)
        },
    }))

7. Vue3与Go前后端开发设置

7.1 Vue3前端设置

项目中使用Vue3作为前端框架,通过Axios与Go后端API进行交互。以下是前端Vue3应用的关键设置:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"/> -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <!-- <meta http-equiv="X-UA-Compatible" content="chrome=1"> -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>myoa</title>
    <link rel="stylesheet" href="../css/bootstrap.min.css">
    <script src="../js/polyfill.min.js"></script>
    <script src="../js/browser.min.js"></script>
    <script src="../js/bluebird.js"></script>
    <script src="../js/vue.min.js"></script>
    <script src="../js/axios.min.js"></script>
    <script src="../js/IPConfig.js"></script>
    <script src="../js/html5shiv.js"></script>
    <style>
        a {
            margin-left: 15px;
            margin-top: -10px;
        }
    </style>
</head>

<body class="container-fluid">
    <div id="app">

        <div class="card border-info mb-3" style="margin-top: 20px;">
            <div class="card-header text-info">
                <b>用户信息</b>
            </div>
            <div class="card-body text-info form-inline">
                <div class="form-row">
                    <div class="form-group">
                        <div class="input-group mb-2 mr-sm-2">
                            <div class="input-group-prepend">
                                <div class="input-group-text text-info"> Id </div>
                            </div>
                            <input type="text" class="form-control" id="id" v-model="id" autocomplete="off">
                            <a class="btn btn-info" href="javascript:void(0)" id="id_query" role="button"
                                @click="search('id')">搜索</a>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="input-group mb-2 mr-sm-2">
                            <div class="input-group-prepend">
                                <div class="input-group-text text-info"> Name </div>
                            </div>
                            <input type="text" class="form-control" id="name" v-model="name" autocomplete="off">
                            <a class="btn btn-info" href="javascript:void(0)" id="name_query" role="button"
                                @click="search('name')">搜索</a>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="input-group mb-2 mr-sm-2">
                            <div class="input-group-prepend">
                                <div class="input-group-text text-info"> Email </div>
                            </div>
                            <input type="email" class="form-control" id="email" v-model="email" autocomplete="off">
                            <a class="btn btn-info" href="javascript:void(0)" id="email_query" role="button"
                                @click="search('email')">搜索</a>
                        </div>
                    </div>

                </div>
                <div class="form-row">
                    <div class="form-group">
                        <a class="btn btn-info" href="javascript:void(0)" role="button" @click="getList">全部</a>

                        <a class="btn btn-success" href="javascript:void(0)" role="button" @click="add">新增</a>


                    </div>
                </div>
            </div>
        </div>

        <table class="table table-striped table-bordered table-hover text-info">
            <thead class="thead-inverse">
                <tr>
                    <th>Id</th>
                    <th>Name</th>
                    <th>Email</th>
                    <th>Created On</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="item in users" :key="item.id">
                    <td scope="row">[item.id]</td>
                    <td>[item.name]</td>
                    <td>[item.email]</td>
                    <td>[item.createdTime]</td>
                </tr>
            </tbody>
        </table>

    </div>

    <script>


        // // request 请求拦截
        axios.interceptors.request.use(function (request) {
            // 对 request 进行拦截
            if (true) {
                console.log('跳转到登录页面')
                console.log('后台服务地址:', URL)
            }
            return request;
        }, function (error) {
            // 在错误请求时进行操作
            return Promise.reject(error);
        });

        // response 请求拦截
        axios.interceptors.response.use(function (response) {
            // 对 response 进行拦截
            switch (response.status) {
                case 200:
                    console.log('接口访问成功')
                    break
                case 400:
                    console.log('提示错误信息')
                    break
                case 401:
                    console.log('重定向到登录页面')
                    break
            }

            return response;
        }, function (error) {
            // 在错误请求时进行操作
            return Promise.reject(error);
        });
        //获取后台API服务全局IP地址、端口、接口组

        var URL = IPConfig.IP + ':' + IPConfig.HOST + '/api';
        axios.defaults.baseURL = URL;
        axios.defaults.xsrfHeaderName = 'X-CSRFToken'
        axios.defaults.xsrfCookieName = 'csrftoken'
        axios.defaults.withCredentials = false
        axios.defaults.timeout = '5000'

        axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
        var vm = new Vue({
            el: '#app',
            data: {
                id: '',
                name: '',
                email: '',
                queryBuilder: '',
                users: [],

            },

            created: function () {
                this.getList()
            },
            methods: {
                getList() {
                    axios.get('/users')
                        .then(response => {
                            this.users = response.data
                            // alert(this.users)
                        }).catch(error => {
                            console.log(error)
                        })
                },
                search(queryBuilder) {
                    //alert(queryBuilder)
                    this.queryBuilder = queryBuilder
                    axios.get('/user/query', {

                        params: {
                            id: this.id,
                            name: this.name,
                            email: this.email,
                            queryBuilder: this.queryBuilder,
                        }
                    }).then(response => {
                        this.users = response.data
                    }).catch(error => {
                        console.log(error)
                    })
                },
                add() {
                    axios.post('/user', {
                        // id:parseInt(this.id),
                        name: this.name,
                        email: this.email,
                    }).then(response => {
                        this.getList()
                        //this.users = response.data
                    }).catch(error => {
                        console.log(error)
                    })
                }
            },
            delimiters: ['[', "]"],
        });
        vm.prototype.$ajax = axios
    </script>


</body>

</html>

7.2 Go后端服务API实现

项目中使用Iris的MVC模式实现RESTful API,供Vue3前端调用:

go 复制代码
/**
 * main.go中 MVC 架构模式处理
 */
func mvcHandle(app *iris.Application) {
    //启用session,并设置session对象的过期时间为24小时
    sessManager := sessions.New(sessions.Config{
        Cookie:  "sessioncookie",
        Expires: 24 * time.Hour,
    })

    // 【vue api】功能模块
    // 例如:http://localhost:9000/api/users
    Api := mvc.New(app.Party("/api"))
    // 注册当前数据云服务类和启动对sessions.Session的访问操作
    Api.Register(
        engine,        // 数据库引擎
        dir,           // 当前目录
        sessManager.Start,
    )
    // 指定当前数据服务由那个控制器处理
    Api.Handle(new(apiController.ApiController))
}

7.3 结构体模板

go 复制代码
package model

import (
	"invoiceServer/utils"
	"time"
)

//TestUser is a struct
/**
 * 【TestUser】信息结构体,用于映射【test_user】数据表
 */
type TestUser struct {
	ID          int64     `xorm:"pk autoincr" json:"id"`      //主键ID
	Name        string    `xorm:"varchar(50)" json:"name"`    //用户姓名
	Email       string    `xorm:"varchar(100)" json:"email"`  //电子邮件
	CreatedTime time.Time `xorm:"created" json:"createdTime"` //创建时间
}

//TestUserToRespDesc is a function interface
/**
 * 将数据库查询出来的结果进行格式组装成request请求需要的json字段格式
 */
func (testUser *TestUser) TestUserToRespDesc() interface{} {
	respInfo := map[string]interface{}{
		"id":          testUser.ID,
		"name":        testUser.Name,
		"email":       testUser.Email,
		"createdTime": utils.FormatDatetime(testUser.CreatedTime),
	}
	return respInfo
}

7.4 控制器示例

go 复制代码
package api

import (
	"fmt"
	"invoiceServer/utils"
	"strconv"
	"strings"

	"github.com/go-xorm/xorm"
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/sessions"
)

//MainFrameController is a struct
/*
 *【主框架页】控制器结构体
 *根据客户端访问的API地址调用Service数据服务里的CURD接口处理数据,并返回给客户端
 */
type ApiController struct {
	//上下文对象
	Ctx    iris.Context
	Engine *xorm.Engine
	//【System_Session】 service
	// MainFrameService system.MainFrameService
	//session对象
	Path    string
	Session *sessions.Session
}

//GetMytable is a function
/*
 * 获取我的办公桌【功能后续开发中】
 * 请求类型:Get
 * 请求Url:/system/mytable
 * 函数名为请求类型+访问地址GetMytable(),访问地址首字母大写
 */
func (uc *ApiController) GetUsers() {
	//查询结果转map
	var osql = "select name,email,createdTime,ID as id from test_user"
	fmt.Printf("osql: %v\n", osql)
	//查询结果转MAP
	data, _ := utils.DoQueryForMap(uc.Engine, osql)
	//查询结果转JSON
	uc.Ctx.JSON(data)
}

type TestUser struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

func (uc *ApiController) PostUser() {
	// name := ctx.FormValue("name")
	// email := ctx.FormValue("email")
	// fmt.Printf("接收vue传入数据: Name:%s Email:%s\n", name, email)
	// ctx.WriteString("<h1><a>" + name + "</a><a>" + email + "</a></h1>")

	c := &TestUser{}
	if err := uc.Ctx.ReadJSON(c); err != nil {
		//Println("error")
		panic(err.Error())
	} else {
		//ctx.JSON(c)
		fmt.Printf("接收vue传入JSON数据: Name:%s Email:%s \n", c.Name, c.Email)
		//db, _ := util.GetDB()
		tsql := fmt.Sprintf("INSERT INTO test_user(name,email) VALUES('%s','%s') select ID = convert(bigint, SCOPE_IDENTITY());", c.Name, c.Email)
		var newId int64
		newId = utils.DoInsertRow(uc.Engine, tsql)
		fmt.Printf("新增数据ID为:【%d】\n", newId)

		uc.Ctx.JSON(c)
	}
}

func (uc *ApiController) GetUserQuery() {
	//id := ctx.FormValue("id")
	id, err := strconv.Atoi(uc.Ctx.FormValue("id"))
	if err != nil {
		id = 0
	}
	name := uc.Ctx.FormValue("name")
	email := uc.Ctx.FormValue("email")
	queryBuilder := uc.Ctx.FormValue("queryBuilder")
	fmt.Printf("接收vue传入数据: ID:%s Name:%s Email:%s queryBuilder:%s\n", id, name, email, queryBuilder)
	//查询结果转map
	//db, _ := util.GetDB()
	//查询结果转JSON
	var queryValue string = ""
	switch {
	case queryBuilder == "id":
		queryBuilder = strings.ToUpper(queryBuilder)
		queryValue = strconv.Itoa(id)
	case queryBuilder == "name":
		queryValue = "'" + name + "'"
	case queryBuilder == "email":
		queryValue = "'" + email + "'"
	default:
		queryValue = strconv.Itoa(id)
	}

	var osql = fmt.Sprintf("select name,email,createdTime,ID as id from test_user where %s=%s", queryBuilder, queryValue)
	fmt.Print(osql + "\n")
	//查询结果转MAP
	data, _ := utils.DoQueryForMap(uc.Engine, osql)
	//defer db.Close()
	uc.Ctx.JSON(data)
}

7.5 数据服务示例-采用通用数据服务类

go 复制代码
package utils

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	"github.com/go-xorm/xorm"
)

func DoInsertRow(engine *xorm.Engine, sqlInfo string) int64 {
	//插入数据 注:由于LastInsertId对自增加主键ID不支持,所以用另外一个方式解决插入返回新增ID
	// stmt, err := db.Prepare("INSERT 人员清单(班组,姓名) VALUES(?,?,ref SCOPE_IDENTITY)")
	// util.CheckError("insert: %s\n", err)
	// res, err := stmt.Exec("信息部","李GO")
	// util.CheckError("insert: %s\n", err)
	// id, err := res.LastInsertId()
	// util.CheckError("GetNewId: %s\n", err)
	// fmt.Printf("GetNewId:%s\n",id)
	//tsql=fmt.Sprintf("INSERT INTO 人员清单(班组,姓名) VALUES('%s','%s') select id = convert(bigint, SCOPE_IDENTITY());", "信息组","李GO")
	row := engine.DB().DB.QueryRow(sqlInfo)

	var id int64
	row.Scan(&id)
	//fmt.Printf("GetNewId:%d\n",id)
	return id
}

// CheckError is a function
// func CheckError(tag string, err error) {
// 	if err != nil {
// 		fmt.Printf(tag, err)
// 		return
// 	}
// }

// Count is a function
/**
 * 根据sql查询返回一行数据
 */
func Count(engine *xorm.Engine, sqlInfo string) (int, error) {
	fmt.Println(sqlInfo)
	// 执行SQL语句,并输出结果集内容
	rows, err := engine.DB().DB.Query(sqlInfo)
	CheckError("query: %s\n", err)
	defer rows.Close()
	var countId int
	for rows.Next() {
		rows.Scan(&countId)
		//fmt.Printf("data: %s \n",data)
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("数据集读取数据异常终止", err)
	}
	return countId, err
}

// FindRowData is a function
/**
 * 根据sql查询返回一行数据
 */
func FindRowData(engine *xorm.Engine, sqlInfo string) (data []map[string]string) {
	fmt.Println(sqlInfo)
	// 执行SQL语句,并输出结果集内容
	rows, err := engine.DB().DB.Query(sqlInfo)
	CheckError("query: %s\n", err)
	defer rows.Close()
	data = RowsToSliceMap(rows)
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("数据集读取数据异常终止", err)
	}
	return data
}

// RowsToSliceMap is a function
/**
 *将数据集直接转换为动态转成切片(字段任意拓展)
 *先把字段的值都当成字符串
 */
func RowsToSliceMap(rows *sql.Rows) (list []map[string]string) {
	//字段名称
	columns, _ := rows.Columns()
	//多少个字段
	length := len(columns)
	//每一行字段的值
	values := make([]sql.RawBytes, length)
	//保存的是values的内存地址
	pointer := make([]interface{}, length)
	//
	for i := 0; i < length; i++ {
		pointer[i] = &values[i]
	}
	//
	for rows.Next() {
		//把参数展开,把每一行的值存到指定的内存地址去,循环覆盖,values也就跟着被赋值了
		rows.Scan(pointer...)
		//每一行
		row := make(map[string]string)
		for i := 0; i < length; i++ {
			row[columns[i]] = string(values[i])
		}
		list = append(list, row)
	}
	//if err:= rows.Close(); err != nil {
	//	// but what should we do if there's an error?
	//	log.Println("获取返回值数据集读取数据异常终止",err)
	//}
	//
	return
}

// getExecProcParametersMap is a function
/**
 *根据存储过程名返回参数列表
 */
func getExecProcParametersMap(engine *xorm.Engine, procName string) (map[string]string, error) {
	osql := fmt.Sprintf("EXEC [dbo].[Proc_Parameters] @PROC_NAME='%s'", procName)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	defer rows.Close()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return nil, err
	}
	//获取结果集
	var list map[string]string //返回的切片
	list = make(map[string]string)
	for rows.Next() {
		var parameter string
		var parametertype string
		var length int
		var xscale int
		var parameter_output int
		var scale int
		rows.Scan(&parameter, &parametertype, &length, &xscale, &parameter_output, &scale)
		var item string
		//item=fmt.Sprintf("%s,%s,%s,%s,%s,%s",parameter, parametertype,strconv.Itoa(length),strconv.Itoa(xscale),strconv.Itoa(parameter_output),strconv.Itoa(scale))
		//int, err := strconv.Atoi(string) 将字符串转INT
		item = fmt.Sprintf("%v,%v,%v,%v,%v,%v", parameter, parametertype, length, xscale, parameter_output, scale)
		//fmt.Println(item)
		list[parameter] = item
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("获取返回值数据集读取数据异常终止", err)
	}
	return list, err
}

// getExecProcSql is a function
/**
 * 根据存储过程名和参数map数组,生成可以执行的【存储过程】sql语句,语句分为三个部分
 * 1、定义输出变量  DECLARE @return_value int,@msg varchar(100);
* 2、执行【存储过程】并配上输入参数和输出参数 EXEC @return_value = [dbo].[proc_aa] @dwUserID = 1,@nBaseEnsureType = 0,@msg=@msg output;
 * 3、执行【存储过程】后需要输出的结果和信息 SELECT 'ReturnValue' = @return_value,'ReturnMsg' =@msg
*/
func getExecProcSql(procName string, inParas map[string]string, inParasTypes map[string]string, bErrorDescribe bool) string {
	var sql string
	var paras string
	//inParasTypes是从后台数据库中根据存储过程名提取参数列表,下面是根据inParasTypes中的参数类型进行sql语句输入变量是否需要添加"'"
	//for key, value := range inParasTypes {
	//   fmt.Printf("inParasTypes[%d] =%s\n", key, value)
	//fmt.Printf("------->%s---[1]-->,%s\n", value, strings.Split(value, ",")[1])
	//}
	for key, value := range inParas {
		/*查看元素在集合中是否存在 */
		parName, ok := inParasTypes[key] /*如果确定是真实的,则存在,否则不存在 */
		if ok {
			parType := strings.Split(parName, ",")[1] //获取参数类型
			switch {
			case parType == "varchar", parType == "nvarchar":
				paras += key + " = '" + value + "'"
			case parType == "char", parType == "nchar":
				paras += key + " = '" + value + "'"
			case parType == "text", parType == "ntext":
				paras += key + " = '" + value + "'"
			case parType == "image":
				paras += key + " = '" + value + "'"
			case parType == "datetime":
				paras += key + " = '" + value + "'"
			case parType == "uniqueidentifier":
				paras += key + " = '" + value + "'"
			default:
				paras += key + " = " + value
			}
			//if parType=="varchar" || parType=="nvarchar"  || parType=="text"  || parType=="ntext" || parType=="char"   || parType=="nchar" || parType=="image" || parType=="datetime" {
			//	paras += key + " = '" + value+"'"
			//}else{
			//	paras += key + " = " + value
			//}
		} else {
			paras += key + " = " + value
		}

		paras += ","
	}

	paras = strings.TrimRight(paras, ",")
	paras += ",@msg=@msg output"
	paras += ";"

	if bErrorDescribe == true {
		sql = ""
	} else {
		sql = fmt.Sprintf("DECLARE @return_value int,@msg varchar(100); EXEC @return_value = [dbo].[%s] %s SELECT 'ReturnValue' = @return_value,'ReturnMsg'=@msg", procName, paras)
	}

	return sql
}

// ExecProcTest is a function
/**
 *这是一个测试存储过程的函数,根据存储过程名和参数返回结果集和输出存储过程的执行结果和信息提示
 *Go定义了一个特殊的错误常量,称为sql.ErrNoRows。当结果为空时,从QueryRow()返回的就是sql.ErrNoRows。在大多数情况下,这需要作为特殊情况来处理。空结果通常不会被应用程序代码视为错误,如果你不检查返回的Errro是否是这个特殊的常量,这将导致您的应用程序以您所不希望的方式失败。

 */
func ExecProcTest(engine *xorm.Engine, procName string, inParas map[string]string) error {
	parasTypesList, err := getExecProcParametersMap(engine, procName)
	if err != nil {
		fmt.Printf("err:%s\n", err)
	} else {
		fmt.Printf("parasTypesList:%s\n", parasTypesList)
	}
	osql := getExecProcSql(procName, inParas, parasTypesList, false)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}

	} else {
		//将数据集直接转换为动态转成切片(字段任意拓展)
		list := RowsToSliceMap(rows)
		for k, v := range list {
			fmt.Println(k)
			fmt.Println(v["id"], v["table_name"], v["file_data"], v["uploader"], v["upload_time"])
		}

		//获取结果集,rows.Nest()中一个只读向前游标,读取后光标到尾,不能重复读取,上面的代码和下面的代码只有一个有效
		for rows.Next() {
			var table_name string
			var uploader string
			var file_data string
			var id int
			var upload_time string
			rows.Scan(&id, &table_name, &file_data, &uploader, &upload_time)
			fmt.Printf("%v,%v,%v,%v,%v\n", id, table_name, file_data, uploader, upload_time)
		}
		//if err = rows.Close(); err != nil {
		//	// but what should we do if there's an error?
		//	log.Println("数据集读取数据异常终止",err)
		//}

		//获取返回值,多个结果集需要用rows.NextResultSet()对结果集进行切换并读取数据
		if rows.NextResultSet() {
			for rows.Next() {
				var returnValue int
				var returnMsg string
				rows.Scan(&returnValue, &returnMsg)
				fmt.Printf("ReturnValue %v ReturnMsg:%v\n", returnValue, returnMsg)
			}
			if err = rows.Close(); err != nil {
				// but what should we do if there's an error?
				log.Println("获取返回值数据集读取数据异常终止", err)
			}
		}
	}
	return err

}

// ExecProcNoParasNoResultSet is a function
/**
 *执行存储过程,无参数只返回存储过程的返回值和返回msg,无返回结果集
 */
func ExecProcNoParasNoResultSet(engine *xorm.Engine, procName string) (int, string, error) {

	osql := fmt.Sprintf("DECLARE @return_value int,@msg varchar(100); EXEC @return_value = [dbo].[%s] @msg=@msg output;SELECT 'ReturnValue' = @return_value,'ReturnMsg'=@msg", procName)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	defer rows.Close()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return 0, "", err
	}
	//获取返回值
	var returnValue int
	var returnMsg string
	//if rows.NextResultSet() {
	for rows.Next() {
		rows.Scan(&returnValue, &returnMsg)
		fmt.Printf("ReturnValue %v ReturnMsg:%v\n", returnValue, returnMsg)
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("获取返回值数据集读取数据异常终止", err)
	}
	//}
	return returnValue, returnMsg, err
}

// ExecProcNoResultSet is a function
/**
 *只返回存储过程的返回值和返回msg,无返回结果集
 */
func ExecProcNoResultSet(engine *xorm.Engine, procName string, inParas map[string]string) (int, string, error) {
	parasTypesList, err := getExecProcParametersMap(engine, procName)
	if err != nil {
		fmt.Printf("columns:%s\n", parasTypesList)
	}
	osql := getExecProcSql(procName, inParas, parasTypesList, false)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	defer rows.Close()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return 0, "", err
	}
	//获取返回值
	var returnValue int
	var returnMsg string
	//if rows.NextResultSet() {
	for rows.Next() {
		rows.Scan(&returnValue, &returnMsg)
		fmt.Printf("ReturnValue %v ReturnMsg:%v", returnValue, returnMsg)
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("获取返回值数据集读取数据异常终止", err)
	}
	//}
	return returnValue, returnMsg, err
}

// ExecProcResultSetForMap is a function
/**
 *返回结果集map,并且返回存储过程的返回值和返回msg
 */
func ExecProcResultSetForMap(engine *xorm.Engine, procName string, inParas map[string]string) ([]map[string]interface{}, int, string, error) {
	parasTypesList, err := getExecProcParametersMap(engine, procName)
	if err != nil {
		fmt.Printf("columns:%s\n", parasTypesList)
	}
	osql := getExecProcSql(procName, inParas, parasTypesList, false)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	defer rows.Close()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return nil, 0, "", err
	}
	columns, _ := rows.Columns()
	columnLength := len(columns)
	cache := make([]interface{}, columnLength) //临时存储每行数据
	for index, _ := range cache {
		//为每一列初始化一个指针
		var a interface{}
		cache[index] = &a
	}
	var list []map[string]interface{} //返回的切片
	for rows.Next() {
		_ = rows.Scan(cache...)
		item := make(map[string]interface{})
		for i, data := range cache {
			item[columns[i]] = *data.(*interface{}) //取实际类型
		}
		list = append(list, item)
	}
	//if err = rows.Close(); err != nil {
	//	// but what should we do if there's an error?
	//	log.Println("数据集读取数据异常终止",err)
	//}
	//获取返回值
	var returnValue int
	var returnMsg string
	if rows.NextResultSet() {
		for rows.Next() {
			rows.Scan(&returnValue, &returnMsg)
			fmt.Printf("ReturnValue %v ReturnMsg:%v\n", returnValue, returnMsg)
		}
		if err = rows.Close(); err != nil {
			// but what should we do if there's an error?
			log.Println("获取返回值数据集读取数据异常终止", err)
		}
	}
	fmt.Println(returnMsg)
	return list, returnValue, returnMsg, err
}

//遍历map
// for key, value := range list {
//     fmt.Printf("list[%d] =%s\n", key, value)
// }

// ExecProcResultSetForJSON is a function
/**
 *返回结果集json字符串,并且返回存储过程的返回值和返回msg
 *函数调用方法
 *	var jsondata string
 *	jsondata,_=util.ExecProcResultSetForJson(db,osql)
 *	fmt.Println(jsondata)
 */
func ExecProcResultSetForJSON(engine *xorm.Engine, procName string, inParas map[string]string) (string, int, string, error) {
	parasTypesList, err := getExecProcParametersMap(engine, procName)
	if err != nil {
		fmt.Printf("columns:%s\n", parasTypesList)
	}
	osql := getExecProcSql(procName, inParas, parasTypesList, false)
	fmt.Println(osql)
	rows, err := engine.DB().DB.Query(osql)
	if err != nil {
		return "", 0, "", err
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return "", 0, "", err
	}
	count := len(columns)
	tableData := make([]map[string]interface{}, 0)
	values := make([]interface{}, count)
	valuePtrs := make([]interface{}, count)
	for rows.Next() {
		for i := 0; i < count; i++ {
			valuePtrs[i] = &values[i]
		}
		rows.Scan(valuePtrs...)
		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}
			val := values[i]
			b, ok := val.([]byte)
			if ok {
				v = string(b)
			} else {
				v = val
			}
			entry[col] = v
		}
		tableData = append(tableData, entry)
	}
	//if err = rows.Close(); err != nil {
	//	// but what should we do if there's an error?
	//	log.Println("数据集读取数据异常终止",err)
	//}
	jsonData, err := json.Marshal(tableData)

	if err != nil {
		return "", 0, "", err
	}
	//fmt.Println(string(jsonData))
	//jsonstr:=
	var returnValue int
	var returnMsg string
	if rows.NextResultSet() {
		for rows.Next() {
			rows.Scan(&returnValue, &returnMsg)
			fmt.Printf("ReturnValue %v ReturnMsg:%v\n", returnValue, returnMsg)
		}
		if err = rows.Close(); err != nil {
			// but what should we do if there's an error?
			log.Println("获取返回值数据集读取数据异常终止", err)
		}
	}

	return string(jsonData), returnValue, returnMsg, err

}

// DoQueryForMap is a function
/*
 *根据传入的sql,从数据库中查询数据并将获得的结果集转换成map
 *调用方法
 	var osql string
	osql="select cast(id as varchar(20)) as id,姓名 AS name,班组 AS class from 人员清单 where 姓名 like '李GO%' AND 班组='信息组'"
	data,_:=util.DoQueryForMap(db,osql)
   //遍历map
    for key, value := range data {
		fmt.Printf("list[%d] =%s\n", key, value)
	}
	//将结果转为json
	if b,err:= json.Marshal(data);err==nil{
		str:=string(b[:])
		fmt.Println("main:",str)
	}
*/
func DoQueryForMap(engine *xorm.Engine, sqlInfo string) ([]map[string]interface{}, error) {
	//fmt.Println(sqlInfo)
	rows, err := engine.DB().DB.Query(sqlInfo)
	defer rows.Close()
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return nil, err
	}
	columns, _ := rows.Columns()

	columnLength := len(columns)
	cache := make([]interface{}, columnLength) //临时存储每行数据
	for index, _ := range cache {
		//为每一列初始化一个指针
		var a interface{}
		cache[index] = &a
	}
	var list []map[string]interface{} //返回的切片
	for rows.Next() {
		_ = rows.Scan(cache...)
		item := make(map[string]interface{})
		for i, data := range cache {
			item[columns[i]] = *data.(*interface{}) //取实际类型
		}
		list = append(list, item)
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("数据集读取数据异常终止", err)
	}
	//_ = rows.Close()
	//遍历map
	// for key, value := range list {
	//     fmt.Printf("list[%d] =%s\n", key, value)
	// }
	return list, nil
}

// DoQueryForJSON is a function
/*
 *根据传入的sql,从数据库中查询数据并将获得的结果集转换成
 *函数调用方法
	var jsondata string
	jsondata,_=util.DoQueryForJson(db,osql)
	fmt.Println(jsondata)
*/
func DoQueryForJSON(engine *xorm.Engine, sqlInfo string) (string, error) {
	//fmt.Println(sqlInfo)
	rows, err := engine.DB().DB.Query(sqlInfo)
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("没有获取到数据", err)
		} else {
			log.Fatal(err)
		}
		return "", err
	}
	defer rows.Close()
	columns, err := rows.Columns()
	if err != nil {
		return "", err
	}
	count := len(columns)
	tableData := make([]map[string]interface{}, 0)
	values := make([]interface{}, count)
	valuePtrs := make([]interface{}, count)
	for rows.Next() {
		for i := 0; i < count; i++ {
			valuePtrs[i] = &values[i]
		}
		rows.Scan(valuePtrs...)
		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}
			val := values[i]
			b, ok := val.([]byte)
			if ok {
				v = string(b)
			} else {
				v = val
			}
			entry[col] = v
		}
		tableData = append(tableData, entry)
	}
	if err = rows.Close(); err != nil {
		// but what should we do if there's an error?
		log.Println("数据集读取数据异常终止", err)
	}
	jsonData, err := json.Marshal(tableData)

	if err != nil {
		return "", err
	}
	//fmt.Println(string(jsonData))
	//jsonstr:=
	return string(jsonData), nil
}

// InterfaceSliceConvStringSlice is a function
/**
  * 将interface slice 转换成string slice
  * 根据YTPE返回interface slice中的各子项的类型,根据类型对子项进行转换,并保存到string slice
  * 函数调用代码:
	//InterfaceSliceToStringSlice函数,将Interface类型的切片Slice转换为String类型的Slice
	strArray:=utils.InterfaceSliceToStringSlice("redis",100,false,true,10.22,time.Now())
	//将字符型切片中的值用字符串连接在一起,以[","]作为分隔符
	res := strings.Join(strArray, ",")
	fmt.Println("InterfaceSliceConvStringSlice函数返回值:",res)
	//用反射包中的TypeOf函数返回数据类型
	day:=time.Now()
	fmt.Println(reflect.TypeOf(day))
*/
func InterfaceSliceToStringSlice(params ...interface{}) []string {
	var paramSlice []string
	for _, param := range params {
		switch v := param.(type) {
		case bool:
			paramSlice = append(paramSlice, strconv.FormatBool(v))
		case float64:
			paramSlice = append(paramSlice, strconv.FormatFloat(v, 'f', -1, 64))
		case string:
			paramSlice = append(paramSlice, v)
		case int:
			paramSlice = append(paramSlice, strconv.FormatInt(int64(v), 10))
		case time.Time:
			paramSlice = append(paramSlice, fmt.Sprintf("%d-%d-%d %d:%d:%d", v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), v.Second()))
		default:
			panic("params type not supported")
		}

	}
	//res := strings.Join(paramSlice, ",")
	//fmt.Println(res)
	return paramSlice
}

7.6 路由设置

go 复制代码
package router

import (
	"github.com/kataras/iris/v12"
)

const (
	apiPrefix              = "api"

)

8. 日志系统集成

项目使用Zap日志库实现高性能的日志记录功能,支持控制台输出和文件日志。

8.1 日志系统初始化

go 复制代码
var (
    Logger *zap.Logger
    mu     sync.Mutex // 用于文件日志的互斥锁
)

// 初始化日志系统
func InitLogger() {
    var err error
    Logger, err = zap.NewProduction()
    if err != nil {
        panic(err)
    }
}

8.2 日志写入文件

go 复制代码
// LogToFile 将日志写入文件,使用互斥锁保证并发安全
func LogToFile(logMessage string) {
    mu.Lock()
    defer mu.Unlock()

    file, err := os.OpenFile("./logs/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        Logger.Error("Failed to open log file", zap.Error(err))
        return
    }
    defer file.Close()

    _, err = file.WriteString(logMessage + "\n")
    if err != nil {
        Logger.Error("Failed to write to log file", zap.Error(err))
    }
}

8.3 日志记录方法

go 复制代码
// LogMessage 格式化并记录日志信息
func LogMessage(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}) {
    msg := "<no message>" // 默认值
    if message != nil {
        msg = fmt.Sprintf("%v", message)
    }

    logMessage := fmt.Sprintf(
        "[%s] %s %s %s %s %d %s",
        endTime.Format(time.RFC3339),
        method,
        path,
        ip,
        status,
        latency.Milliseconds(), // 以毫秒记录延迟
        msg,
    )
    LogToFile(logMessage)
}

// WriteLog 记录带上下文的日志
func WriteLog(ctx iris.Context, msg string) {
    start := time.Now()
    endTime := time.Now()
    latency := endTime.Sub(start)
    status := fmt.Sprintf("%d", ctx.GetStatusCode())
    ip := ctx.RemoteAddr()
    method := ctx.Method()
    path := ctx.Path()
    // 调用 LogMessage
    LogMessage(endTime, latency, status, ip, method, path, msg)
}

// WriteNotCtxLog 记录系统级日志(无请求上下文)
func WriteNotCtxLog(msg string) {
    start := time.Now()
    endTime := time.Now()
    latency := endTime.Sub(start)
    status := fmt.Sprintf("%d", 200)
    ip := "127.0.0.1"
    method := "System"
    path := "0.0.0.0:8888"
    // 调用 LogMessage
    LogMessage(endTime, latency, status, ip, method, path, msg)
}

9. 跨域设置

项目实现了CORS(跨域资源共享)功能,允许前端应用从不同域名访问后端API。

go 复制代码
// 设置iris跨域请求,以保证VUE项目可以正常请求数据
func Cors(ctx iris.Context) {
    ctx.Header("Access-Control-Allow-Origin", "*") // 允许所有来源
    if ctx.Request().Method == "OPTIONS" {
        ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
        ctx.Header("Access-Control-Max-Age", "86400") // 预检请求的有效期为24小时
        ctx.Header("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers,authorization,Authorization, X-Requested-With")
        ctx.StatusCode(204) // 无内容响应
        return
    }
    ctx.Next() // 继续处理请求
}

在应用程序中启用跨域中间件:

go 复制代码
// 在main函数中启用跨域中间件
app.Use(Cors)

10. 项目配置文件及加载功能

项目使用JSON格式的配置文件,包含应用程序配置、数据库配置等信息。

10.1 配置结构体定义

go 复制代码
// DbMysql MySQL数据库配置
type DbMysql struct {
    Drive    string `json:"drive"`
    Port     string `json:"port"`
    User     string `json:"user"`
    Pwd      string `json:"pwd"`
    Host     string `json:"host"`
    Database string `json:"database"`
}

// DbMssql2008 SQL Server 2008数据库配置
type DbMssql2008 struct {
    Drive    string `json:"drive"`
    Port     string `json:"port"`
    User     string `json:"user"`
    Pwd      string `json:"pwd"`
    Host     string `json:"host"`
    Database string `json:"database"`
}

// DbMssql2000 SQL Server 2000数据库配置
type DbMssql2000 struct {
    Drive    string `json:"drive"`
    Port     string `json:"port"`
    User     string `json:"user"`
    Pwd      string `json:"pwd"`
    Host     string `json:"host"`
    Database string `json:"database"`
    Provider string `json:"provider"`
}

// DbRedis Redis数据库配置
type DbRedis struct {
    NetWork  string `json:"net_work"`
    Addr     string `json:"addr"`
    Port     string `json:"port"`
    Password string `json:"password"`
    Prefix   string `json:"prefix"`
}

// AppConfig 应用程序配置
type AppConfig struct {
    AppName     string      `json:"app_name"`
    Host        string      `json:"host"`
    Port        string      `json:"port"`
    StaticPath  string      `json:"static_path"`
    Mode        string      `json:"mode"`
    DbMySql     DbMysql     `json:"dbmysql"`
    DbMsSql2000 DbMssql2000 `json:"dbmssql2000"`
    DbMsSql2008 DbMssql2008 `json:"dbmssql2008"`
    DbRedis     DbRedis     `json:"dbredis"`
}

10.2 config.json文件示例

json 复制代码
{
  "app_name": "invoiceServer",
  "host": "0.0.0.0",
  "port": "7777",
  "static_path": "/static",
  "mode": "dev",
  "dbmysql": {
    "drive": "mysql",
    "port": "3306",
    "user": "root",
    "pwd": "123456789",
    "host": "127.0.0.1",
    "database": "invoice"
  },
  "dbmssql2000": {
    "drive": "adodb",
    "port": "1433",
    "user": "sa",
    "pwd": "123456789",
    "host": "localhost",
    "database": "invoice",
    "provider": "SQLOLEDB"
  },
  "dbmssql2008": {
    "drive": "mssql",
    "port": "1888",
    "user": "sa",
    "pwd": "123456789",
    "host": "localhost",
    "database": "invoice"
  },
  "dbredis": {
    "net_work": "tcp",
    "addr": "127.0.0.1",
    "port": "6379",
    "password": "",
    "prefix": "invoice_"
  }
}

10.3 配置加载函数

go 复制代码
// InitConfig 初始化服务器配置
func InitConfig() *AppConfig {
    // 打开配置文件
    file, err := os.Open("./config.json")
    if err != nil {
        panic(err.Error())
    }
    // 创建JSON解码器
    decoder := json.NewDecoder(file)
    // 解码JSON数据到AppConfig结构体
    var config *AppConfig
    err = decoder.Decode(&config)
    if err != nil {
        panic(err.Error())
    }
    return config
}

10.4 配置使用示例

go 复制代码
// 在main函数中使用配置
config := config.InitConfig()
// 获取配置中的IP地址和端口号
addr := config.Host + ":" + config.Port
// 启动服务
app.Run(
    iris.Addr(addr),
    iris.WithCharset("UTF-8"),
    iris.WithoutServerError(iris.ErrServerClosed),
    iris.WithOptimizations,
)

11. MSSQL/XORM数据库连接配置与应用

项目使用XORM框架与MSSQL数据库进行交互,支持数据库连接池、SQL日志等功能。

11.1 数据库引擎初始化

go 复制代码
// NewMsSqlEngine 实例化MSSQL数据库引擎
func NewMsSqlEngine() *xorm.Engine {
    // 获取配置
    initConfig := config.InitConfig()
    if initConfig == nil {
        return nil
    }

    database := initConfig.DbMsSql2008
    // 构建MSSQL连接字符串
    dataSourceName := "server=" + database.Host + ";database=" + database.Database + ";user id=" + database.User + ";password=" + database.Pwd + ";port=" + database.Port + ";encrypt=disable"
    // 创建数据库引擎
    engine, err := xorm.NewEngine(database.Drive, dataSourceName)

    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    // 测试连接
    if err := engine.Ping(); err != nil {
        log.Fatal("建立数据库连接失败:", err)
    }

    // 设置字段映射规则
    engine.SetColumnMapper(core.GonicMapper{})
    // 同步数据库结构
    err = engine.Sync2(
        // 这里可以添加需要同步的模型结构体
        new(model.TestUser),

    )
    if err != nil {
        fmt.Printf("engine.Sync2:%s", err.Error())
    }

    // 设置日志级别
    engine.Logger().SetLevel(core.LOG_INFO)
    var logger *xorm.SimpleLogger = sqlLogger()
    logger.SetLevel(core.LOG_INFO)
    engine.SetLogger(logger)

    // 配置连接池
    engine.SetMaxOpenConns(2000)
    engine.SetMaxIdleConns(1000)
    engine.SetConnMaxLifetime(60) // 1分钟一次心跳

    return engine
}

11.2 SQL日志记录

go 复制代码
// sqlLogger 创建SQL日志记录器
func sqlLogger() *xorm.SimpleLogger {
    config := rollingwriter.Config{
        LogPath:                "./logs",                    // 日志路径
        TimeTagFormat:          "060102150405",              // 时间格式串
        FileName:               "ExecSql",                   // 日志文件名
        MaxRemain:              3,                           // 配置日志最大存留数
        RollingPolicy:          rollingwriter.VolumeRolling, // 配置滚动策略
        RollingVolumeSize:      "1M",                        // 配置截断文件下限大小
        WriterMode:             "none",
        BufferWriterThershould: 256,
        Compress: true, // 压缩日志文件
    }
    // 创建日志文件
    writer, err := rollingwriter.NewWriterFromConfig(&config)
    if err != nil {
        panic(err)
    }
    // 创建XORM日志记录器
    var logger *xorm.SimpleLogger = xorm.NewSimpleLogger(writer)
    return logger
}

11.3 数据库服务使用示例

go 复制代码
// MVC架构中的数据库服务使用
func mvcHandle(app *iris.Application) {
    // 启用session
    sessManager := sessions.New(sessions.Config{
        Cookie:  "sessioncookie",
        Expires: 24 * time.Hour,
    })

    // 【mssql数据库测试模块】
    mssqlDbToolsTestDataService := testService.NewMssqlDbToolsTestDataService(engine)
    mssqlDbToolsTest := mvc.New(app.Party("/test/mssqlDbToos"))
    // 注册服务和session
    mssqlDbToolsTest.Register(
        mssqlDbToolsTestDataService,
        dir,
        sessManager.Start,
    )
    // 指定控制器
    mssqlDbToolsTest.Handle(new(testController.MssqlDbToolsTestDataController))
}

12. 总结

本项目是一个功能完整的电子发票解析Go API服务端应用,结合了现代Web开发技术栈和OCR识别技术。项目具有以下特点:

  1. 结构清晰:采用MVC架构,各模块职责分明,便于维护和扩展。
  2. 技术先进:使用Go语言、Iris框架、PaddleOCR等先进技术,性能优良。
  3. 功能完善:支持发票OCR识别、PDF/OFD文件预览、数据库交互等功能。
  4. 前后端分离:采用Vue3作为前端框架,实现前后端分离开发。
  5. 安全可靠:实现了跨域设置、日志系统、错误处理等安全机制。

通过本文档的详细解析,希望能够帮助开发者快速理解项目架构和实现原理,为后续的开发和维护工作提供参考。

相关推荐
Mgx7 小时前
从“CPU 烧开水“到优雅暂停:Go 里 sync.Cond 的正确打开方式
go
GM_82818 小时前
从0开始在Go当中使用Apache Thrift框架(万字讲解+图文教程+详细代码)
rpc·go·apache·thrift
Kratos开源社区1 天前
别卷 LangChain 了!Blades AI 框架让 Go 开发者轻松打造智能体
go·agent·ai编程
Kratos开源社区1 天前
跟 Blades 学 Agent 设计 - 01 用“提示词链”让你的 AI 助手变身超级特工
llm·go·agent
百锦再1 天前
第10章 错误处理
java·git·ai·rust·go·错误·pathon
Mgx2 天前
从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
go
遥天棋子3 天前
实战PaddleOCR自动识别车位坐标并渲染可点击按钮
go
久违 °3 天前
【安全开发】Nuclei源码分析-任务执行流程(三)
安全·网络安全·go
喵个咪3 天前
开箱即用的GO后台管理系统 Kratos Admin - 数据脱敏和隐私保护
后端·go·protobuf