This commit is contained in:
lychang
2026-05-20 03:25:35 +08:00
parent 5e3dac3020
commit 7f5350db62
30 changed files with 3370 additions and 1752 deletions

28
build_wasm.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
REM 编译Go代码为WASM的批处理脚本
echo 正在编译Go代码为WASM...
REM 检查是否安装了Go
go version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到Go。请先安装Go并将其添加到PATH。
pause
exit /b 1
)
REM 设置目标目录
set TARGET_DIR=E:\code\go\wasm-go\web
REM 检查目标目录是否存在,不存在则创建
if not exist "%TARGET_DIR%" (
mkdir "%TARGET_DIR%"
)
REM 编译Go代码为WASM
echo 编译WASM...
set GOOS=js
set GOARCH=wasm
go build -o "%TARGET_DIR%\main.wasm" E:\code\go\wasm-go\cmd\wasm\main.go
pause

188
cmd/main.go Normal file
View File

@@ -0,0 +1,188 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"wasm-go/internal/api"
"wasm-go/internal/excel"
)
func main() {
fmt.Println("========== Excel转JSON ==========")
excelFile := "yst郑州惠兴财务报表2021(3).xlsx"
// 检查Excel文件是否存在
if _, err := os.Stat(excelFile); os.IsNotExist(err) {
fmt.Printf("❌ Excel文件不存在: %s\n", excelFile)
return
}
// 读取Excel文件为bytes
excelBytes, err := os.ReadFile(excelFile)
if err != nil {
fmt.Printf("❌ 读取Excel文件失败: %v\n", err)
return
}
fmt.Println("✓ 读取Excel文件为字节成功")
// 使用NewXLSXReaderFromBytes从bytes创建读取器
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
if err != nil {
fmt.Printf("❌ 创建XLSX读取器失败: %v\n", err)
return
}
fmt.Println("✓ 创建XLSX读取器成功")
// 读取所有的sheets并转换为markdown格式
var allMarkdownTables strings.Builder
allMarkdownTables.WriteString("以下是Excel表格的数据:\n\n")
sheets, err := reader.GetSheetNames()
if err != nil {
fmt.Printf("❌ 获取工作表名称失败: %v\n", err)
return
}
for _, sheetName := range sheets {
fmt.Printf("\n========== 工作表: %s ==========\n", sheetName)
data, err := reader.GetSheetData(sheetName)
if err != nil {
fmt.Printf("❌ 读取工作表失败: %v\n", err)
continue
}
if len(data) == 0 {
fmt.Println("⚠️ 工作表为空")
continue
}
fmt.Printf("找到 %d 行数据\n", len(data))
// 转换为markdown格式
markdown := excel.ConvertToMarkdown(data)
allMarkdownTables.WriteString(fmt.Sprintf("## 工作表: %s\n", sheetName))
allMarkdownTables.WriteString(markdown)
allMarkdownTables.WriteString("\n\n")
}
fmt.Println("\n✅ 所有工作表已转换为Markdown格式!")
// 读取quarterly1.txt的prompt
promptBytes, err := os.ReadFile("./quarterly1.txt")
if err != nil {
fmt.Printf("❌ 读取quarterly1.txt失败: %v\n", err)
return
}
prompt := string(promptBytes)
// 合并prompt和markdown表格
finalPrompt := fmt.Sprintf("%s\n\n%s", prompt, allMarkdownTables.String())
// 尝试连接到不同的LLM提供商
var response *api.ChatResponse
var llmErr error
// 首先尝试Ollama
fmt.Println("\n🔄 尝试连接到Ollama服务...")
if ollamaClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b"); ollamaClient != nil {
response, llmErr = ollamaClient.Chat([]api.ChatMessage{
{Role: "user", Content: finalPrompt},
})
if llmErr == nil {
fmt.Println("✓ 成功使用Ollama生成响应")
} else {
fmt.Printf("⚠ Ollama调用失败: %v\n", llmErr)
// 检查是否是连接问题
if strings.Contains(llmErr.Error(), "connection refused") || strings.Contains(llmErr.Error(), "404") {
fmt.Println("💡 提示:Ollama服务可能未启动,或模型未安装")
}
}
}
fmt.Println("\n========== LLM生成的JSON ==========")
// 解析LLM返回的响应,提取```json{内容}```中的JSON部分
parsedJSON := extractJSONFromResponse(response.Choices[0].Message.Content)
fmt.Println(parsedJSON)
fmt.Println("==================================")
// 使用解析出的JSON调用税务健康API
fmt.Println("\n🔄 正在使用解析的JSON调用税务健康API...")
// 创建税务健康服务客户端
// 注意:这里需要替换为实际的用户ID和私钥
taxHealthService := api.NewTaxHealthService("1560142251348287489", "")
// 将解析的JSON解析为map结构
var companyData map[string]interface{}
if err := json.Unmarshal([]byte(parsedJSON), &companyData); err != nil {
fmt.Printf("❌ 解析JSON数据失败: %v\n", err)
return
}
// 构建税务评估请求
request := api.TaxEvalRequest{
ReqNo: fmt.Sprintf("REQ%v", time.Now().Unix()), // 使用时间戳生成请求号
Detail: api.TaxEvalDetail{
CompanyName: "企业名称", // 可以从数据中提取公司名称
Data: companyData,
},
}
// 调用税务健康API
taxResponse, err := taxHealthService.EvaluateTaxHealth(request)
if err != nil {
fmt.Printf("❌ 调用税务健康API失败: %v\n", err)
return
}
fmt.Println("\n========== 税务健康API反馈结果 ==========")
fmt.Printf("API响应: %+v\n", taxResponse)
fmt.Println("======================================")
}
// extractJSONFromResponse 从LLM响应中提取JSON内容
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
func extractJSONFromResponse(response string) string {
// 首先尝试匹配 ```json 开头的代码块
start := strings.Index(response, "```json")
if start != -1 {
start += 7 // 跳过 "```json"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
return jsonContent
}
}
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
start = strings.Index(response, "```")
if start != -1 {
start += 3 // 跳过 "```"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
// 检查是否看起来像JSON
jsonContent = strings.TrimSpace(jsonContent)
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
return jsonContent
}
}
}
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
response = strings.TrimSpace(response)
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
return response
}
// 如果以上都失败,返回原内容
return response
}

55
cmd/test/main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"fmt"
"log"
"wasm-go/internal/api"
)
func main() {
fmt.Println("=== 企业行业分类示例 ===")
// 创建LLM客户端 - 这里使用Ollama作为示例
// 如果Ollama不可用,也可以使用OpenAI
var llmClient *api.LLMClient
// 尝试连接到Ollama
fmt.Println("尝试连接到Ollama服务...")
llmClient = api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
// 如果需要使用OpenAI,取消下面的注释并替换API KEY
/*
llmClient = api.NewOpenAIClient("your-openai-api-key", "gpt-3.5-turbo")
*/
// 创建企业行业分类器
classifier := api.NewCompanyIndustryClassifier(llmClient)
// 示例公司名称 - 可以从命令行参数获取
companyName := "深圳市致远速联信息技术有限公司"
fmt.Printf("正在分析公司: %s\n", companyName)
// 分析企业行业
result, err := classifier.ClassifyCompany(companyName)
if err != nil {
log.Printf("分析失败: %v", err)
return
}
// 输出结果
fmt.Printf("\n✅ 企业行业分析完成!\n")
fmt.Printf("公司名称: %s\n", result.CompanyName)
fmt.Printf("主要行业: %s - %s\n", result.Primary.Code, result.Primary.Name)
fmt.Printf("判断依据: %s\n", result.Primary.Reason)
if len(result.Secondary) > 0 {
fmt.Printf("次要行业:\n")
for _, sec := range result.Secondary {
fmt.Printf(" %s - %s\n", sec.Code, sec.Name)
fmt.Printf(" 判断依据: %s\n", sec.Reason)
}
} else {
fmt.Printf("次要行业: 无\n")
}
}

334
cmd/wasm/main.go Normal file
View File

@@ -0,0 +1,334 @@
package main
import (
"encoding/json"
"fmt"
"syscall/js"
"wasm-go/internal/api"
"wasm-go/internal/excel"
)
func main() {
// 注册各种功能函数
js.Global().Set("analyzeCompanyIndustryAsync", js.FuncOf(analyzeCompanyIndustryAsync))
js.Global().Set("parseExcelAsync", js.FuncOf(parseExcelAsync))
js.Global().Set("callLLMAsync", js.FuncOf(callLLMAsync))
js.Global().Set("callTaxHealthAsync", js.FuncOf(callTaxHealthAsync))
// 保持程序运行
select {} // 无限循环,保持程序运行
}
// analyzeCompanyIndustryAsync 异步版本的函数,返回Promise
func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
// 获取公司名称参数
companyName := args[0].String()
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1) // 使用error类型而不是interface{}
// 在goroutine中执行异步操作
go func() {
// 创建LLM客户端
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
// 创建企业行业分类器
classifier := api.NewCompanyIndustryClassifier(llmClient)
// 分析企业行业
result, err := classifier.ClassifyCompany(companyName)
if err != nil {
// 发送错误到错误通道
errorChan <- err
} else {
// 发送结果到结果通道
resultChan <- result
}
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": err.Error(),
}))
}
}()
return nil
}))
return promise
}
// parseExcelAsync 解析Excel文件的异步函数
func parseExcelAsync(this js.Value, args []js.Value) interface{} {
// 获取Excel文件的ArrayBuffer
arrayBuffer := args[0]
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
// 创建字节切片
length := uint8Array.Get("length").Int()
excelBytes := make([]byte, length)
// 将数据复制到Go字节切片
for i := 0; i < length; i++ {
excelBytes[i] = byte(uint8Array.Index(i).Int())
}
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 创建Excel读取器
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
if err != nil {
errorChan <- err
return
}
// 读取所有工作表
sheetNames, err := reader.GetSheetNames()
if err != nil {
errorChan <- err
return
}
// 创建一个map来存储所有工作表的数据
sheets := make(map[string][][]string)
// 读取每个工作表的数据
for _, sheetName := range sheetNames {
sheetData, err := reader.GetSheetData(sheetName)
if err != nil {
// 记录错误但继续处理其他工作表
continue
}
sheets[sheetName] = sheetData
}
// 将结果转换为适当的格式
result := map[string]interface{}{
"sheets": sheets,
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": err.Error(),
}))
}
}()
return nil
}))
return promise
}
// callLLMAsync 调用大模型的异步函数
func callLLMAsync(this js.Value, args []js.Value) interface{} {
// 获取提示词参数
prompt := args[0].String()
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 创建LLM客户端(可以根据需要选择不同的模型)
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
// 调用大模型
response, err := llmClient.SimpleChat(prompt)
if err != nil {
errorChan <- err
return
}
result := map[string]string{
"response": response,
}
resultChan <- result
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": err.Error(),
}))
}
}()
return nil
}))
return promise
}
// callTaxHealthAsync 调用税务健康API的异步函数
// 注意:由于WASM环境的CORS限制,我们在这里提供模拟实现
func callTaxHealthAsync(this js.Value, args []js.Value) interface{} {
// 获取公司信息参数
companyInfoJSON := args[0].String()
// 创建一个通道来处理结果
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
// 在goroutine中执行异步操作
go func() {
// 由于WASM环境无法直接调用外部API(CORS限制)
// 我们返回一个模拟的响应
simulatedResponse := map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"req_no": "REQ" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
"order_no": "ORDER" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
},
"message": "模拟的税务健康评估结果",
}
// 解析输入的JSON以获取一些信息用于模拟
var companyInfo map[string]interface{}
err := json.Unmarshal([]byte(companyInfoJSON), &companyInfo)
if err != nil {
errorChan <- err
return
}
// 根据输入数据添加一些模拟的评估结果
simulatedResponse["company_data"] = companyInfo
resultChan <- simulatedResponse
}()
// 创建Promise
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
if len(promiseArgs) < 2 {
// 如果参数不足,返回一个立即拒绝的Promise
return nil
}
resolve := promiseArgs[0]
reject := promiseArgs[1]
// 启动一个goroutine来处理结果
go func() {
select {
case result := <-resultChan:
// 将复杂结构体序列化为JSON
jsonResult, err := json.Marshal(result)
if err != nil {
// 序列化失败,返回错误
reject.Invoke(js.ValueOf(map[string]string{
"error": "JSON序列化失败: " + err.Error(),
}))
return
}
// 将JSON字符串解析为JavaScript对象
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
resolve.Invoke(jsResult)
case err := <-errorChan:
// 将错误转换为JavaScript值
reject.Invoke(js.ValueOf(map[string]string{
"error": err.Error(),
}))
}
}()
return nil
}))
return promise
}

15
go.mod
View File

@@ -1,18 +1,7 @@
module tax-health-api
module wasm-go
go 1.22
require (
github.com/bytedance/sonic v1.15.1
github.com/tjfoc/gmsm v1.4.1
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/sys v0.22.0 // indirect
)
)

34
go.sum
View File

@@ -1,19 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -32,26 +21,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -73,8 +45,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -97,9 +67,5 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

309
internal/api/classifier.go Normal file
View File

@@ -0,0 +1,309 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// CompanyIndustryClassifier 企业行业分类器
type CompanyIndustryClassifier struct {
httpClient *http.Client
llmClient *LLMClient
}
// CompanyData 企业数据
type CompanyData struct {
Description string `json:"description"`
Business string `json:"business"`
BiddingTitles []string `json:"bidding_titles"`
}
// IndustryClassification 行业分类结果
type IndustryClassification struct {
CompanyName string `json:"company_name"`
Primary PrimaryInfo `json:"primary"`
Secondary []Secondary `json:"secondary"`
}
// PrimaryInfo 主要行业信息
type PrimaryInfo struct {
Code string `json:"code"`
Name string `json:"name"`
Reason string `json:"reason"`
}
// Secondary 次要行业信息
type Secondary struct {
Code string `json:"code"`
Name string `json:"name"`
Reason string `json:"reason"`
}
// NewCompanyIndustryClassifier 创建企业行业分类器
func NewCompanyIndustryClassifier(llmClient *LLMClient) *CompanyIndustryClassifier {
client := &http.Client{
Timeout: 30 * time.Second,
}
return &CompanyIndustryClassifier{
httpClient: client,
llmClient: llmClient,
}
}
// getDescriptionText 获取简介文本
func getDescriptionText(description string) string {
if description == "" {
return "(无简介)"
}
return description
}
// getBusinessText 获取经营范围文本
func getBusinessText(business string) string {
if business == "" {
return "(无经营范围)"
}
return business
}
// FetchCompanyData 从cmdp API获取企业工商和招投标数据
func (c *CompanyIndustryClassifier) FetchCompanyData(companyName string) (map[string]interface{}, error) {
apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom"
params := url.Values{}
params.Set("name", companyName)
params.Set("api", "p_info,p_biddings_all")
fullURL := apiURL + "?" + params.Encode()
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("解析响应JSON失败: %v", err)
}
return result, nil
}
// ExtractFields 从API响应中提取关键字段
func (c *CompanyIndustryClassifier) ExtractFields(data map[string]interface{}) (string, string, []string) {
description := ""
business := ""
var biddingTitles []string
// 兼容不同的响应结构
payload := data
if data["data"] != nil {
if payloadData, ok := data["data"].(map[string]interface{}); ok {
payload = payloadData
}
}
// p_info -> description, business
if pInfo := payload["p_info"]; pInfo != nil {
var pInfoObj map[string]interface{}
switch v := pInfo.(type) {
case []interface{}:
if len(v) > 0 {
if item, ok := v[0].(map[string]interface{}); ok {
pInfoObj = item
}
}
case map[string]interface{}:
pInfoObj = v
}
if pInfoObj != nil {
if desc, ok := pInfoObj["description"].(string); ok {
description = desc
}
if bus, ok := pInfoObj["business"].(string); ok {
business = bus
}
}
}
// p_biddings_all -> title list
if biddingsRaw := payload["p_biddings_all"]; biddingsRaw != nil {
var biddingsList []interface{}
switch v := biddingsRaw.(type) {
case []interface{}:
biddingsList = v
case map[string]interface{}:
// 尝试从data或list字段获取
if dataField, ok := v["data"].([]interface{}); ok {
biddingsList = dataField
} else if listField, ok := v["list"].([]interface{}); ok {
biddingsList = listField
}
}
for _, item := range biddingsList {
if itemMap, ok := item.(map[string]interface{}); ok {
if title, ok := itemMap["title"].(string); ok && strings.TrimSpace(title) != "" {
biddingTitles = append(biddingTitles, strings.TrimSpace(title))
}
}
}
}
return description, business, biddingTitles
}
// BuildPrompt 构建提示词
func (c *CompanyIndustryClassifier) BuildPrompt(companyName, description, business string, biddingTitles []string) string {
titlesText := "(无招投标记录)"
if len(biddingTitles) > 0 {
var sb strings.Builder
for i, title := range biddingTitles {
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, title))
}
titlesText = sb.String()
}
prompt := fmt.Sprintf(`你是国民经济行业分类专家,请根据以下企业信息,依据《国民经济行业分类》(GB/T 4754-2017)进行行业大类判断。
## 企业名称
%s
## 企业简介(description)
%s
## 经营范围(business)
%s
## 招投标项目标题(bidding titles)
%s
## 任务要求
1. 判断该企业**主要**所属的行业大类(两位数字编号 + 名称)
2. 如有次要业务涉及其他大类,也一并列出(最多2个)
3. 给出简要判断依据(不超过100字)
## 输出格式(严格 JSON,不要 Markdown 代码块)
{
"company_name": "%s",
"primary": {
"code": "65",
"name": "软件和信息技术服务业",
"reason": "判断依据..."
},
"secondary": [
{
"code": "72",
"name": "商务服务业",
"reason": "轻微涉及原因..."
}
]
}
注意:secondary 可以为空列表 [];只输出 JSON,不要任何额外说明。`,
companyName,
getDescriptionText(description),
getBusinessText(business),
titlesText,
companyName)
return prompt
}
// ClassifyCompany 完整流程:获取数据 → 提取字段 → 构建提示词 → 调用 LLM → 返回 JSON
func (c *CompanyIndustryClassifier) ClassifyCompany(companyName string) (*IndustryClassification, error) {
// 1. 获取企业数据
rawData, err := c.FetchCompanyData(companyName)
if err != nil {
return nil, fmt.Errorf("获取企业数据失败: %v", err)
}
// 2. 提取关键字段
description, business, biddingTitles := c.ExtractFields(rawData)
// 3. 构建提示词
prompt := c.BuildPrompt(companyName, description, business, biddingTitles)
// 4. 调用LLM并解析JSON
response, err := c.llmClient.Chat([]ChatMessage{
{Role: "user", Content: prompt},
})
if err != nil {
return nil, fmt.Errorf("调用LLM失败: %v", err)
}
// 提取JSON部分(如果LLM返回了markdown代码块)
jsonStr := extractJSONFromResponse(response.Choices[0].Message.Content)
var result IndustryClassification
err = json.Unmarshal([]byte(jsonStr), &result)
if err != nil {
return nil, fmt.Errorf("解析LLM返回的JSON失败: %v", err)
}
return &result, nil
}
// extractJSONFromResponse 从LLM响应中提取JSON内容
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
func extractJSONFromResponse(response string) string {
// 首先尝试匹配 ```json 开头的代码块
start := strings.Index(response, "```json")
if start != -1 {
start += 7 // 跳过 "```json"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
return jsonContent
}
}
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
start = strings.Index(response, "```")
if start != -1 {
start += 3 // 跳过 "```"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
// 检查是否看起来像JSON
jsonContent = strings.TrimSpace(jsonContent)
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
return jsonContent
}
}
}
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
response = strings.TrimSpace(response)
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
return response
}
// 如果以上都失败,返回原内容
return response
}

389
internal/api/client.go Normal file
View File

@@ -0,0 +1,389 @@
package api
import (
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"net/http"
"net/url"
"strconv"
"time"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/sm3"
)
// CompanyInfoService 公司信息查询服务
type CompanyInfoService struct {
httpClient *HTTPClient
userID string
privateKey string
}
// TaxHealthService 税务健康测评服务
type TaxHealthService struct {
httpClient *HTTPClient
userID string
privateKey string
}
// CompanyInfoRequest 公司信息查询请求
type CompanyInfoRequest struct {
Name string `json:"name"`
Apis []string `json:"apis"`
}
// CompanyInfoResponse 公司信息查询响应
type CompanyInfoResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
// TaxEvalRequest 税务测评请求
type TaxEvalRequest struct {
ReqNo string `json:"req_no"`
Callback string `json:"callback,omitempty"`
Detail TaxEvalDetail `json:"detail"`
}
// TaxEvalDetail 税务测评详情
type TaxEvalDetail struct {
FileURL string `json:"file_url,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
CompanyName string `json:"company_name"`
Period string `json:"period"`
Year string `json:"year"`
IsVerify bool `json:"is_verify,omitempty"`
CIT float64 `json:"cit"`
VAT float64 `json:"vat"`
InputVAT float64 `json:"input_vat"`
OutputVAT float64 `json:"output_vat"`
PreCIT float64 `json:"pre_cit"`
PreVAT float64 `json:"pre_vat"`
PreInputVAT float64 `json:"pre_input_vat"`
PreOutputVAT float64 `json:"pre_output_vat"`
}
// TaxEvalResponse 税务测评响应
type TaxEvalResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Data struct {
ReqNo string `json:"req_no"`
OrderNo string `json:"order_no"`
} `json:"data"`
}
// TaxEvalQueryResponse 查询税务测评结果响应
type TaxEvalQueryResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Data struct {
Status int `json:"status"`
Detail struct {
Files []string `json:"files"`
Error string `json:"error"`
} `json:"detail"`
} `json:"data"`
}
// NewCompanyInfoService 创建公司信息查询服务
func NewCompanyInfoService(userID, privateKey string) *CompanyInfoService {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
httpClient := &HTTPClient{Client: client}
return &CompanyInfoService{
httpClient: httpClient,
userID: userID,
privateKey: privateKey,
}
}
// NewTaxHealthService 创建税务健康测评服务
func NewTaxHealthService(userID, privateKey string) *TaxHealthService {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
httpClient := &HTTPClient{Client: client}
return &TaxHealthService{
httpClient: httpClient,
userID: userID,
privateKey: privateKey,
}
}
// GetCompanyInfo 查询公司信息
func (s *CompanyInfoService) GetCompanyInfo(companyName string, apis []string) (*CompanyInfoResponse, error) {
// 生成随机数和时间戳
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// 构建查询参数
params := url.Values{}
params.Set("name", companyName)
params.Set("api", joinAPIParams(apis))
apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom?" + params.Encode()
// 生成签名
signData := s.userID + timestamp + nonce + apiURL
signature, err := s.generateSignature([]byte(signData))
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
// 设置请求头
headers := map[string]string{
"Accept": "application/json",
"x-gi-user-id": s.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
// 发送GET请求
response, err := s.httpClient.Get(apiURL, headers)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
var result CompanyInfoResponse
err = json.Unmarshal(response, &result)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// EvaluateTaxHealth 执行税务健康测评
func (s *TaxHealthService) EvaluateTaxHealth(request TaxEvalRequest) (*TaxEvalResponse, error) {
// 生成随机数和时间戳
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
apiURL := "https://api.cmdp.cn/v1/eval/tax-risk"
// 序列化请求体
jsonData, err := json.Marshal(request)
fmt.Println(string(jsonData))
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 生成签名
signData := s.userID + timestamp + nonce + apiURL + string(jsonData)
signature, err := s.generateSignature([]byte(signData))
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
// 设置请求头
headers := map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"x-gi-user-id": s.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
// 发送POST请求
response, err := s.httpClient.Post(apiURL, jsonData, "application/json", headers)
fmt.Println(string(response))
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
var result TaxEvalResponse
err = json.Unmarshal(response, &result)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// QueryTaxEvalResult 查询税务测评结果
func (s *TaxHealthService) QueryTaxEvalResult(orderNo string) (*TaxEvalQueryResponse, error) {
// 生成随机数和时间戳
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
apiURL := fmt.Sprintf("https://api.cmdp.cn/v1/eval/order/%s", orderNo)
// 生成签名
signData := s.userID + timestamp + nonce + apiURL
signature, err := s.generateSignature([]byte(signData))
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
// 设置请求头
headers := map[string]string{
"Accept": "application/json",
"x-gi-user-id": s.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
// 发送GET请求
response, err := s.httpClient.Get(apiURL, headers)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
var result TaxEvalQueryResponse
err = json.Unmarshal(response, &result)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// generateSignature 生成签名
func (s *CompanyInfoService) generateSignature(plainText []byte) (string, error) {
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
}
digest := sm3.Sm3Sum(plainText)
signature, err := privateKey.Sign(rand.Reader, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// generateSignature 生成签名
func (s *TaxHealthService) generateSignature(plainText []byte) (string, error) {
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
}
digest := sm3.Sm3Sum(plainText)
signature, err := privateKey.Sign(rand.Reader, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// parsePrivateKeyFromHex 从十六进制字符串解析私钥
func parsePrivateKeyFromHex(privateKeyHex string) (*sm2.PrivateKey, error) {
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("解码私钥失败: %v", err)
}
d := new(big.Int).SetBytes(privateKeyBytes)
privateKey := &sm2.PrivateKey{
PublicKey: sm2.PublicKey{
Curve: sm2.P256Sm2(),
},
D: d,
}
privateKey.PublicKey.X, privateKey.PublicKey.Y = privateKey.PublicKey.Curve.ScalarBaseMult(d.Bytes())
return privateKey, nil
}
// joinAPIParams 连接API参数
func joinAPIParams(params []string) string {
result := ""
for i, param := range params {
if i > 0 {
result += ","
}
result += param
}
return result
}
// 使用示例
func ExampleUsage() {
// 替换为实际的用户ID和私钥
userID := "your_user_id"
privateKey := "your_private_key_hex"
// 创建公司信息查询服务
companyService := NewCompanyInfoService(userID, privateKey)
// 查询公司信息
companyInfo, err := companyService.GetCompanyInfo("深圳市致远速联信息技术有限公司", []string{"p_info", "p_biddings_all"})
if err != nil {
fmt.Printf("查询公司信息失败: %v\n", err)
} else {
fmt.Printf("公司信息查询结果: %+v\n", companyInfo)
}
// 创建税务健康测评服务
taxService := NewTaxHealthService(userID, privateKey)
// 准备税务测评请求数据
request := TaxEvalRequest{
ReqNo: "REQ001",
Detail: TaxEvalDetail{
CompanyName: "深圳市致远速联信息技术有限公司",
Period: "年度",
Year: "2024",
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
}
// 执行税务测评
evalResult, err := taxService.EvaluateTaxHealth(request)
if err != nil {
fmt.Printf("税务测评失败: %v\n", err)
} else {
fmt.Printf("税务测评提交结果: %+v\n", evalResult)
// 查询测评结果
if evalResult.Data.OrderNo != "" {
result, err := taxService.QueryTaxEvalResult(evalResult.Data.OrderNo)
if err != nil {
fmt.Printf("查询测评结果失败: %v\n", err)
} else {
fmt.Printf("测评结果: %+v\n", result)
}
}
}
}

114
internal/api/http.go Normal file
View File

@@ -0,0 +1,114 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// HTTPClient 封装了常用的HTTP请求方法
type HTTPClient struct {
Client *http.Client
}
// NewHTTPClient 创建一个新的HTTP客户端
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
Client: &http.Client{},
}
}
// Get 发送GET请求
func (c *HTTPClient) Get(url string, headers map[string]string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("创建GET请求失败: %v", err)
}
// 设置请求头
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送GET请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("GET请求失败,状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %v", err)
}
return body, nil
}
// PostJSON 发送JSON格式的POST请求
func (c *HTTPClient) PostJSON(url string, data interface{}, headers map[string]string) ([]byte, error) {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("JSON序列化失败: %v", err)
}
return c.Post(url, jsonData, "application/json", headers)
}
// Post 发送POST请求
func (c *HTTPClient) Post(url string, data []byte, contentType string, headers map[string]string) ([]byte, error) {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("创建POST请求失败: %v", err)
}
// 设置Content-Type
req.Header.Set("Content-Type", contentType)
// 设置其他请求头
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送POST请求失败: %v", err)
}
defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
fmt.Printf("关闭响应体失败: %v\n", err)
}
}(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("POST请求失败,状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %v", err)
}
return body, nil
}
// GetWithQuery 发送带查询参数的GET请求
func (c *HTTPClient) GetWithQuery(url string, params map[string]string, headers map[string]string) ([]byte, error) {
// 构建查询参数
if len(params) > 0 {
url += "?"
for key, value := range params {
url += fmt.Sprintf("%s=%s&", key, value)
}
// 移除最后一个多余的 &
url = url[:len(url)-1]
}
return c.Get(url, headers)
}

321
internal/api/llm.go Normal file
View File

@@ -0,0 +1,321 @@
package api
import (
"encoding/json"
"fmt"
"strings"
)
// LLMClient 大语言模型客户端
type LLMClient struct {
httpClient *HTTPClient
config LLMConfig
}
// LLMConfig LLM配置
type LLMConfig struct {
APIKey string `json:"api_key"` // API密钥
BaseURL string `json:"base_url"` // API基础URL
Model string `json:"model"` // 模型名称
Provider string `json:"provider"` // 提供商: "openai", "ollama", "custom"
Temperature float64 `json:"temperature"` // 温度参数
MaxTokens int `json:"max_tokens"` // 最大token数
}
// ChatMessage 聊天消息
type ChatMessage struct {
Role string `json:"role"` // 角色: "system", "user", "assistant"
Content string `json:"content"` // 消息内容
}
// ChatRequest 聊天请求 - OpenAI格式
type ChatRequest struct {
Model string `json:"model"` // 模型名称
Messages []ChatMessage `json:"messages"` // 消息列表
Temperature float64 `json:"temperature,omitempty"` // 温度参数
MaxTokens int `json:"max_tokens,omitempty"` // 最大token数
TopP float64 `json:"top_p,omitempty"` // 采样参数
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` // 频率惩罚
PresencePenalty float64 `json:"presence_penalty,omitempty"` // 存在惩罚
Stream bool `json:"stream,omitempty"` // 是否流式输出
}
// OllamaMessage Ollama消息格式
type OllamaMessage struct {
Role string `json:"role"` // 角色: "system", "user", "assistant"
Content string `json:"content"` // 消息内容
}
// OllamaRequest Ollama请求格式
type OllamaRequest struct {
Model string `json:"model"` // 模型名称
Messages []OllamaMessage `json:"messages"` // 消息列表
Stream bool `json:"stream"` // 是否流式输出
Options OllamaOptions `json:"options,omitempty"` // 选项
KeepAlive string `json:"keep_alive,omitempty"` // 保持连接时间
Think bool `json:"think"` // 是否思考模式
}
// OllamaOptions Ollama选项
type OllamaOptions struct {
Temperature float64 `json:"temperature,omitempty"` // 温度参数
MaxTokens int `json:"num_predict,omitempty"` // 最大预测token数
TopP float64 `json:"top_p,omitempty"` // 采样参数
}
// ChatResponse 聊天响应 - OpenAI格式
type ChatResponse struct {
ID string `json:"id"` // 响应ID
Object string `json:"object"` // 对象类型
Created int64 `json:"created"` // 创建时间
Model string `json:"model"` // 模型名称
Choices []Choice `json:"choices"` // 选择列表
Usage Usage `json:"usage"` // 使用情况
}
// OllamaResponse Ollama响应格式
type OllamaResponse struct {
Model string `json:"model"` // 模型名称
CreatedAt string `json:"created_at"` // 创建时间
Message OllamaMessage `json:"message"` // 消息
Done bool `json:"done"` // 是否完成
TotalDuration int64 `json:"total_duration,omitempty"` // 总耗时
LoadDuration int64 `json:"load_duration,omitempty"` // 加载耗时
PromptEvalCount int `json:"prompt_eval_count,omitempty"` // 提示评估计数
PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"` // 提示评估耗时
EvalCount int `json:"eval_count,omitempty"` // 评估计数
EvalDuration int64 `json:"eval_duration,omitempty"` // 评估耗时
Response string `json:"response,omitempty"` // 响应内容(流式响应时)
}
// Choice 选择 - OpenAI格式
type Choice struct {
Index int `json:"index"` // 索引
Message ChatMessage `json:"message"` // 消息
FinishReason string `json:"finish_reason"` // 结束原因
}
// Usage 使用情况
type Usage struct {
PromptTokens int `json:"prompt_tokens"` // 提示token数
CompletionTokens int `json:"completion_tokens"` // 完成token数
TotalTokens int `json:"total_tokens"` // 总token数
}
// NewLLMClient 创建LLM客户端
func NewLLMClient(config LLMConfig) *LLMClient {
httpClient := NewHTTPClient()
return &LLMClient{
httpClient: httpClient,
config: config,
}
}
// NewOpenAIClient 创建OpenAI客户端
func NewOpenAIClient(apiKey, model string) *LLMClient {
config := LLMConfig{
APIKey: apiKey,
BaseURL: "https://api.openai.com/v1",
Model: model,
Provider: "openai",
Temperature: 0.1,
MaxTokens: 16000,
}
return NewLLMClient(config)
}
// NewOllamaClient 创建Ollama客户端
func NewOllamaClient(baseURL, model string) *LLMClient {
config := LLMConfig{
BaseURL: baseURL,
Model: model,
Provider: "ollama",
Temperature: 0.1,
MaxTokens: 16000,
}
return NewLLMClient(config)
}
// Chat 发送聊天请求
func (c *LLMClient) Chat(messages []ChatMessage) (*ChatResponse, error) {
if c.config.Provider == "ollama" {
// 对于Ollama,我们需要转换格式
return c.chatWithOllama(messages)
} else {
// 对于OpenAI和其他提供商,使用标准格式
request := ChatRequest{
Model: c.config.Model,
Messages: messages,
Temperature: c.config.Temperature,
MaxTokens: c.config.MaxTokens,
}
return c.sendChatRequest(request)
}
}
// chatWithOllama 使用Ollama格式发送请求
func (c *LLMClient) chatWithOllama(messages []ChatMessage) (*ChatResponse, error) {
// 转换消息格式
ollamaMessages := make([]OllamaMessage, len(messages))
for i, msg := range messages {
ollamaMessages[i] = OllamaMessage{
Role: msg.Role,
Content: msg.Content,
}
}
// 创建Ollama请求
request := OllamaRequest{
Model: c.config.Model,
Messages: ollamaMessages,
Stream: false,
Think: false,
Options: OllamaOptions{
Temperature: c.config.Temperature,
MaxTokens: c.config.MaxTokens,
},
}
// 发送Ollama请求
apiURL := c.buildAPIURL("/api/chat")
headers := c.buildHeaders()
response, err := c.httpClient.PostJSON(apiURL, request, headers)
if err != nil {
return nil, fmt.Errorf("发送Ollama请求失败: %v", err)
}
// 解析Ollama响应
var ollamaResp OllamaResponse
err = json.Unmarshal(response, &ollamaResp)
if err != nil {
return nil, fmt.Errorf("解析Ollama响应失败: %v", err)
}
// 转换Ollama响应为统一的ChatResponse格式
chatResponse := &ChatResponse{
ID: "ollama-" + c.config.Model,
Object: "chat.completion",
Model: c.config.Model,
Choices: []Choice{
{
Index: 0,
Message: ChatMessage{
Role: ollamaResp.Message.Role,
Content: ollamaResp.Message.Content,
},
FinishReason: "stop",
},
},
}
return chatResponse, nil
}
// ChatWithSystem 发送带系统消息的聊天请求
func (c *LLMClient) ChatWithSystem(systemPrompt string, userMessage string) (*ChatResponse, error) {
messages := []ChatMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userMessage},
}
return c.Chat(messages)
}
// ChatConversation 发送多轮对话
func (c *LLMClient) ChatConversation(conversation []ChatMessage) (*ChatResponse, error) {
return c.Chat(conversation)
}
// sendChatRequest 发送标准聊天请求 (OpenAI格式)
func (c *LLMClient) sendChatRequest(request ChatRequest) (*ChatResponse, error) {
apiURL := c.buildAPIURL("/chat/completions")
headers := c.buildHeaders()
response, err := c.httpClient.PostJSON(apiURL, request, headers)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
var result ChatResponse
err = json.Unmarshal(response, &result)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// buildAPIURL 构建API URL
func (c *LLMClient) buildAPIURL(endpoint string) string {
baseURL := strings.TrimSuffix(c.config.BaseURL, "/")
return baseURL + endpoint
}
// buildHeaders 构建请求头
func (c *LLMClient) buildHeaders() map[string]string {
headers := map[string]string{
"Content-Type": "application/json",
}
if c.config.APIKey != "" {
headers["Authorization"] = "Bearer " + c.config.APIKey
}
return headers
}
// SetTemperature 设置温度参数
func (c *LLMClient) SetTemperature(temperature float64) {
c.config.Temperature = temperature
}
// SetMaxTokens 设置最大token数
func (c *LLMClient) SetMaxTokens(maxTokens int) {
c.config.MaxTokens = maxTokens
}
// SetModel 设置模型
func (c *LLMClient) SetModel(model string) {
c.config.Model = model
}
// GetConfig 获取配置
func (c *LLMClient) GetConfig() LLMConfig {
return c.config
}
// CommonConfigs 预定义的常用配置
var CommonConfigs = struct {
OpenAI LLMConfig
Ollama LLMConfig
Groq LLMConfig
DeepSeek LLMConfig
}{
OpenAI: LLMConfig{
BaseURL: "https://api.openai.com/v1",
Provider: "openai",
Temperature: 0.1,
MaxTokens: 16000,
},
Ollama: LLMConfig{
BaseURL: "http://localhost:11434",
Provider: "ollama",
Temperature: 0.1,
MaxTokens: 16000,
},
Groq: LLMConfig{
BaseURL: "https://api.groq.com/openai/v1",
Provider: "groq",
Temperature: 0.1,
MaxTokens: 16000,
},
DeepSeek: LLMConfig{
BaseURL: "https://api.deepseek.com/v1",
Provider: "deepseek",
Temperature: 0.1,
MaxTokens: 16000,
},
}

View File

@@ -0,0 +1,358 @@
package api
import (
"encoding/json"
"fmt"
"testing"
"time"
)
// TestCreateTaxEvalRequest 测试创建税务评估请求
func TestCreateTaxEvalRequest(t *testing.T) {
parsedJSON := `{
"LC0000003E_T0I0331": 741213833.14,
"LC0000003E_T0I0101": 578064138.06,
"GC0016669E_T0I0331": 0,
"GC0016669E_T0I0101": 0,
"GC0006222E_T0I0331": 0,
"GC0006222E_T0I0101": 0,
"LC0000005E_T0I0331": 28204209.19,
"LC0000005E_T0I0101": 25024200,
"LC0000006E_T0I0331": 372847956.26,
"LC0000006E_T0I0101": 461664605.22,
"GC0018627E_T0I0331": 0,
"GC0018627E_T0I0101": 0,
"LC0000007E_T0I0331": 13850463.15,
"LC0000007E_T0I0101": 11466957.75,
"LC0000010E_T0I0331": 1279972.41,
"LC0000010E_T0I0101": 2394706.37,
"LC0000011E_T0I0331": 71200,
"LC0000011E_T0I0101": 0,
"GC0016671E_T0I0331": 0,
"GC0016671E_T0I0101": 0,
"GC0007742E_T0I0331": 0,
"GC0007742E_T0I0101": 0,
"LC0000012E_T0I0331": 0,
"LC0000012E_T0I0101": 0,
"LC0000013E_T0I0331": 30401431.89,
"LC0000013E_T0I0101": 31017339.59,
"LC0000014E_T0I0331": 1198146906.32,
"LC0000014E_T0I0101": 1114982561.27,
"GC0016672E_T0I0331": 0,
"GC0016672E_T0I0101": 0,
"GC0016673E_T0I0331": 0,
"GC0016673E_T0I0101": 0,
"LC0000017E_T0I0331": 1410014797.11,
"LC0000017E_T0I0101": 1362056986.2,
"LC0000018E_T0I0331": 40000000,
"LC0000018E_T0I0101": 40000000,
"GC0016674E_T0I0331": 0,
"GC0016674E_T0I0101": 0,
"GC0016675E_T0I0331": 0,
"GC0016675E_T0I0101": 0,
"LC0000019E_T0I0331": 0,
"LC0000019E_T0I0101": 0,
"LC0000020E_T0I0331": 1710521807.17,
"LC0000020E_T0I0101": 1809249671.18,
"LC0000021E_T0I0331": 10670524.65,
"LC0000021E_T0I0101": 12170137.69,
"LC0000024E_T0I0331": 0,
"LC0000024E_T0I0101": 0,
"LC0000025E_T0I0331": 0,
"LC0000025E_T0I0101": 0,
"GC0018492E_T0I0331": 0,
"GC0018492E_T0I0101": 0,
"LC0000026E_T0I0331": 243623902.74,
"LC0000026E_T0I0101": 248385213.62,
"LC0000027E_T0I0331": 22259377.92,
"LC0000027E_T0I0101": 16876375.57,
"LC0000028E_T0I0331": 0,
"LC0000028E_T0I0101": 0,
"LC0000029E_T0I0331": 66195945.84,
"LC0000029E_T0I0101": 63182454.15,
"LC0000030E_T0I0331": 0,
"LC0000030E_T0I0101": 0,
"LC0000031E_T0I0331": 0,
"LC0000031E_T0I0101": 0,
"LC0000032E_T0I0331": 3503286355.43,
"LC0000032E_T0I0101": 3551920838.41,
"LC0000033E_T0I0331": 4701433261.75,
"LC0000033E_T0I0101": 4666903399.68,
"LC0000034E_T0I0331": 0,
"LC0000034E_T0I0101": 0,
"GC0016676E_T0I0331": 0,
"GC0016676E_T0I0101": 0,
"GC0006227E_T0I0331": 0,
"GC0006227E_T0I0101": 0,
"LC0000036E_T0I0331": 327465356.48,
"LC0000036E_T0I0101": 324791092.1,
"LC0000037E_T0I0331": 3987467.19,
"LC0000037E_T0I0101": 24162739.9,
"LC0000038E_T0I0331": 6216843.39,
"LC0000038E_T0I0101": 6107830.87,
"GC0016678E_T0I0331": 0,
"GC0016678E_T0I0101": 0,
"LC0000039E_T0I0331": 0,
"LC0000039E_T0I0101": 0,
"LC0000040E_T0I0331": 22441281.17,
"LC0000040E_T0I0101": 11487537.15,
"LC0000043E_T0I0331": 99859088.81,
"LC0000043E_T0I0101": 66507161.39,
"GC0007743E_T0I0331": 0,
"GC0007743E_T0I0101": 0,
"LC0000044E_T0I0331": 0,
"LC0000044E_T0I0101": 0,
"LC0000045E_T0I0331": 150724.81,
"LC0000045E_T0I0101": 0,
"LC0000046E_T0I0331": 459997495.210001,
"LC0000046E_T0I0101": 433462971.100001,
"LC0000047E_T0I0331": 0,
"LC0000047E_T0I0101": 0,
"LC0000048E_T0I0331": 0,
"LC0000048E_T0I0101": 0,
"GC0007952E_T0I0331": 0,
"GC0007952E_T0I0101": 0,
"GC0007953E_T0I0331": 0,
"GC0007953E_T0I0101": 0,
"GC0018493E_T0I0331": 0,
"GC0018493E_T0I0101": 0,
"LC0000049E_T0I0331": 0,
"LC0000049E_T0I0101": 0,
"LC0000051E_T0I0331": 62303168,
"LC0000051E_T0I0101": 83817131.51,
"GC0007955E_T0I0331": 0,
"GC0007955E_T0I0101": 0,
"LC0000052E_T0I0331": 0,
"LC0000052E_T0I0101": 0,
"LC0000053E_T0I0331": 2204446.46,
"LC0000053E_T0I0101": 1516443.48,
"LC0000054E_T0I0331": 64507614.46,
"LC0000054E_T0I0101": 85333574.99,
"LC0000055E_T0I0331": 524505109.670001,
"LC0000055E_T0I0101": 518796546.090001,
"LC0000056E_T0I0331": 3740951803.56,
"LC0000056E_T0I0101": 3740951803.56,
"GC0007956E_T0I0331": 0,
"GC0007956E_T0I0101": 0,
"GC0007957E_T0I0331": 0,
"GC0007957E_T0I0101": 0,
"GC0007958E_T0I0331": 0,
"GC0007958E_T0I0101": 0,
"LC0000057E_T0I0331": 55443838.1,
"LC0000057E_T0I0101": 55443838.1,
"LC0000058E_T0I0331": 0,
"LC0000058E_T0I0101": 0,
"GC0007750E_T0I0331": 0,
"GC0007750E_T0I0101": 0,
"LC0000059E_T0I0331": 0,
"LC0000059E_T0I0101": 0,
"LC0000060E_T0I0331": 180349254.88,
"LC0000060E_T0I0101": 176067125.03,
"LC0000062E_T0I0331": 200183255.54,
"LC0000062E_T0I0101": 175644086.9,
"LC0000063E_T0I0331": 4176928152.08,
"LC0000063E_T0I0101": 4148106853.59,
"LC0000064E_T0I0331": 4701433261.75,
"LC0000064E_T0I0101": 4666903399.68,
"LC0010003E_T0M0103": 1001498985.16,
"LC0010003E_T-1M0103": 970511823.21,
"LC0010008E_T0M0103": 844977443.69,
"LC0010008E_T-1M0103": 800450961.39,
"LC0010016E_T0M0103": 30422209.55,
"LC0010016E_T-1M0103": 30211704.62,
"LC0010017E_T0M0103": 2985920.84,
"LC0010017E_T-1M0103": 2344075.83,
"LC0010018E_T0M0103": 180378961.52,
"LC0010018E_T-1M0103": 160479498.41,
"GC0016679E_T0M0103": 0,
"GC0016679E_T-1M0103": 0,
"LC0010019E_T0M0103": -2498823.79,
"LC0010019E_T-1M0103": -1463147.42,
"GC0016680E_T0M0103": 0,
"GC0016680E_T-1M0103": 0,
"GC0016681E_T0M0103": 0,
"GC0016681E_T-1M0103": 0,
"GC0014225E_T0M0103": 0,
"GC0014225E_T-1M0103": 0,
"LC0010022E_T0M0103": 36357810.91,
"LC0010022E_T-1M0103": 35468582.16,
"LC0010023E_T0M0103": 0,
"LC0010023E_T-1M0103": 0,
"GC0017132E_T0M0103": 0,
"GC0017132E_T-1M0103": 0,
"GC0016683E_T0M0103": 0,
"GC0016683E_T-1M0103": 0,
"LC0010021E_T0M0103": 0,
"LC0010021E_T-1M0103": 0,
"GC0016682E_T0M0103": 0,
"GC0016682E_T-1M0103": 0,
"LC0010020E_T0M0103": 0,
"LC0010020E_T-1M0103": 0,
"GC0014761E_T0M0103": 0,
"GC0014761E_T-1M0103": 0,
"LC0010025E_T0M0103": -18408915.7400001,
"LC0010025E_T-1M0103": 13957312.54,
"LC0010026E_T0M0103": 62014210.36,
"LC0010026E_T-1M0103": 74680343.91,
"LC0010027E_T0M0103": 95993.15,
"LC0010027E_T-1M0103": 190072.12,
"LC0010029E_T0M0103": 43509301.4699999,
"LC0010029E_T-1M0103": 88447584.33,
"LC0010030E_T0M0103": 688002.98,
"LC0010030E_T-1M0103": 171084.05,
"LC0010031E_T0M0103": 42821298.4899999,
"LC0010031E_T-1M0103": 88276500.28,
"GC0014762E_T0M0103": 0,
"GC0014762E_T-1M0103": 0,
"GC0014763E_T0M0103": 0,
"GC0014763E_T-1M0103": 0,
"LC0010038E_T0M0103": 0,
"LC0010038E_T-1M0103": 0,
"GC0007744E_T0M0103": 0,
"GC0007744E_T-1M0103": 0,
"GC0007960E_T0M0103": 0,
"GC0007960E_T-1M0103": 0,
"GC0007961E_T0M0103": 0,
"GC0007961E_T-1M0103": 0,
"GC0016684E_T0M0103": 0,
"GC0016684E_T-1M0103": 0,
"GC0016685E_T0M0103": 0,
"GC0016685E_T-1M0103": 0,
"GC0016686E_T0M0103": 0,
"GC0016686E_T-1M0103": 0,
"GC0007745E_T0M0103": 0,
"GC0007745E_T-1M0103": 0,
"GC0007962E_T0M0103": 0,
"GC0007962E_T-1M0103": 0,
"GC0016687E_T0M0103": 0,
"GC0016687E_T-1M0103": 0,
"GC0016688E_T0M0103": 0,
"GC0016688E_T-1M0103": 0,
"GC0016689E_T0M0103": 0,
"GC0016689E_T-1M0103": 0,
"GC0016690E_T0M0103": 0,
"GC0016690E_T-1M0103": 0,
"GC0007966E_T0M0103": 0,
"GC0007966E_T-1M0103": 0,
"GC0007967E_T0M0103": 0,
"GC0007967E_T-1M0103": 0,
"LC0010039E_T0M0103": 0,
"LC0010039E_T-1M0103": 0,
"LC0010036E_T0M0103": 0,
"LC0010036E_T-1M0103": 0,
"LC0010037E_T0M0103": 0,
"LC0010037E_T-1M0103": 0,
"LC0020002E_T0M0103": 0,
"LC0020002E_T-1M0103": 0,
"LC0020013E_T0M0103": 46619.83,
"LC0020013E_T-1M0103": 0,
"LC0020014E_T0M0103": 0,
"LC0020014E_T-1M0103": 0,
"LC0020015E_T0M0103": 13566300.1,
"LC0020015E_T-1M0103": 13332652.63,
"LC0020016E_T0M0103": 680142.74,
"LC0020016E_T-1M0103": 1039719.92,
"LC0020022E_T0M0103": 657058.13,
"LC0020022E_T-1M0103": 567405.52,
"LC0020023E_T0M0103": -162817.27,
"LC0020023E_T-1M0103": 250470.71,
"LC0020024E_T0M0103": 0,
"LC0020024E_T-1M0103": 0,
"LC0020025E_T0M0103": -4745909.49,
"LC0020025E_T-1M0103": 2063373.12,
"LC0020026E_T0M0103": 0,
"LC0020026E_T-1M0103": 0,
"LC0020027E_T0M0103": 0,
"LC0020027E_T-1M0103": 0,
"LC0020028E_T0M0103": 0,
"LC0020028E_T-1M0103": 0,
"LC0020029E_T0M0103": 0,
"LC0020029E_T-1M0103": 0,
"LC0020030E_T0M0103": 0,
"LC0020030E_T-1M0103": 0,
"LC0020031E_T0M0103": 0,
"LC0020031E_T-1M0103": 0,
"LC0020032E_T0M0103": 16388.07,
"LC0020032E_T-1M0103": 6007.97,
"LC0020033E_T0M0103": 0,
"LC0020033E_T-1M0103": 0,
"LC0020034E_T0M0103": 0,
"LC0020034E_T-1M0103": 0,
"LC0020036E_T0M0103": 0,
"LC0020036E_T-1M0103": 0,
"LC0020037E_T0M0103": 0,
"LC0020037E_T-1M0103": 0,
"LC0020038E_T0M0103": -16388.07,
"LC0020038E_T-1M0103": -6007.97,
"LC0020039E_T0M0103": 0,
"LC0020039E_T-1M0103": 0,
"LC0020040E_T0M0103": 0,
"LC0020040E_T-1M0103": 0,
"LC0020042E_T0M0103": 0,
"LC0020042E_T-1M0103": 0,
"LC0020044E_T0M0103": 0,
"LC0020044E_T-1M0103": 0,
"LC0020045E_T0M0103": 0,
"LC0020045E_T-1M0103": 0,
"LC0020046E_T0M0103": 0,
"LC0020046E_T-1M0103": 0,
"LC0020047E_T0M0103": 0,
"LC0020047E_T-1M0103": 0,
"LC0020049E_T0M0103": 0,
"LC0020049E_T-1M0103": 0,
"LC0020050E_T0M0103": 0,
"LC0020050E_T-1M0103": 0,
"LC0020051E_T0M0103": 0,
"LC0020051E_T-1M0103": 0,
"LC0020052E_T0M0103": 691605.56,
"LC0020052E_T-1M0103": 2057365.15,
"LC0020053E_T0M0103": 3233993.43,
"LC0020053E_T-1M0103": 6614143.33,
"LC0020054E_T0I0101": 3925598.99,
"LC0020054E_T-1I0101": 8671508.48,
"LC0020054E_T0I0331": 9948154.38,
"LC0020054E_T-1I0331": 17253621.9,
"GE8020001E_T0M0112": 0,
"GE8020001E_T-1M0112": 0,
"GE8020002E_T0M0112": 0,
"GE8020002E_T-1M0112": 0,
"GE8020003E_T0M0112": 0,
"GE8020003E_T-1M0112": 0,
"GE8020004E_T0M0112": 0,
"GE8020004E_T-1M0112": 0,
"GE8000080E": 0,
"GE0000000E": 0
}`
// 创建税务健康服务客户端
// 注意:这里需要替换为实际的用户ID和私钥
taxHealthService := NewTaxHealthService("1560142251348287489", "")
// 将解析的JSON解析为map结构
var companyData map[string]interface{}
if err := json.Unmarshal([]byte(parsedJSON), &companyData); err != nil {
fmt.Printf("❌ 解析JSON数据失败: %v\n", err)
return
}
// 构建税务评估请求
request := TaxEvalRequest{
ReqNo: fmt.Sprintf("REQ%v", time.Now().Unix()), // 使用时间戳生成请求号
Detail: TaxEvalDetail{
CompanyName: "郑州惠兴", // 可以从数据中提取公司名称
Data: companyData,
},
}
// 调用税务健康API
taxResponse, err := taxHealthService.EvaluateTaxHealth(request)
if err != nil {
fmt.Printf("❌ 调用税务健康API失败: %v\n", err)
return
}
fmt.Println("\n========== 税务健康API反馈结果 ==========")
fmt.Printf("API响应: %+v\n", taxResponse)
fmt.Println("======================================")
t.Logf("创建税务评估请求成功,包含%d个财务指标", len(request.Detail.Data))
}

View File

@@ -1,7 +1,8 @@
package main
package excel
import (
"archive/zip"
"bytes"
"fmt"
"io"
"os"
@@ -17,87 +18,37 @@ type SheetInfo struct {
}
type XLSXReader struct {
files map[string][]byte
sheetMap map[string]SheetInfo
Files map[string][]byte
SheetMap map[string]SheetInfo
}
func main() {
fmt.Println("========== Excel转Markdown表格 ==========")
type SharedStrings struct {
Strings []string `xml:"si>t"`
}
excelFile := "yst郑州惠兴财务报表2021(3).xlsx"
fmt.Printf("Excel文件: %s\n\n", excelFile)
type Sheet struct {
Name string `xml:"name,attr"`
SheetID int `xml:"sheetId,attr"`
Ref string `xml:"id,attr"`
}
reader, err := NewXLSXReader(excelFile)
if err != nil {
fmt.Printf("❌ 创建XLSX读取器失败: %v\n", err)
return
}
fmt.Println("✓ 创建XLSX读取器成功")
type Sheets struct {
Sheets []Sheet `xml:"sheet"`
}
// 打印所有文件列表
fmt.Println("\n--- 压缩包内文件列表 ---")
for name := range reader.files {
if strings.HasPrefix(name, "xl/worksheets/") {
fmt.Printf(" %s\n", name)
}
}
type Row struct {
Cells []Cell `xml:"c"`
RowNum int `xml:"r,attr"`
}
sheets, err := reader.GetSheetNames()
if err != nil {
fmt.Printf("❌ 获取工作表名称失败: %v\n", err)
return
}
fmt.Printf("\n✓ 获取到 %d 个工作表:\n", len(sheets))
for i, sheet := range sheets {
if info, ok := reader.sheetMap[sheet]; ok {
fmt.Printf(" %d. %s -> rId=%s, path=%s\n", i+1, sheet, info.RID, info.Path)
} else {
fmt.Printf(" %d. %s\n", i+1, sheet)
}
}
type Cell struct {
Ref string `xml:"r,attr"`
Type string `xml:"t,attr"`
Val string `xml:"v"`
}
for i, sheetName := range sheets {
fmt.Printf("\n========== 工作表: %s ==========\n", sheetName)
data, err := reader.GetSheetData(sheetName)
if err != nil {
fmt.Printf("❌ 读取工作表失败: %v\n", err)
continue
}
if len(data) == 0 {
fmt.Println("⚠️ 工作表为空")
continue
}
fmt.Printf("找到 %d 行数据\n", len(data))
if len(data[0]) > 0 {
fmt.Printf("表格尺寸: %d 行 × %d 列\n", len(data), len(data[0]))
fmt.Printf("前3行预览:\n")
for r := 0; r < min(len(data), 3); r++ {
rowData := make([]string, 0, len(data[r]))
for c := 0; c < min(len(data[r]), 4); c++ {
val := data[r][c]
if len(val) > 20 {
val = val[:20] + "..."
}
rowData = append(rowData, fmt.Sprintf("%q", val))
}
fmt.Printf(" 第%d行: %s\n", r+1, strings.Join(rowData, ", "))
}
}
markdown := ConvertToMarkdown(data)
safeName := sanitizeFileName(sheetName)
err = SaveToFile(markdown, fmt.Sprintf("sheet%d_%s.md", i+1, safeName))
if err != nil {
fmt.Printf("❌ 保存文件失败: %v\n", err)
} else {
fmt.Printf("✓ 已保存到: sheet%d_%s.md\n", i+1, safeName)
}
}
fmt.Println("\n✅ 所有工作表已转换完成!")
type Worksheet struct {
Rows []Row `xml:"sheetData>row"`
}
func sanitizeFileName(name string) string {
@@ -120,42 +71,51 @@ func NewXLSXReader(filePath string) (*XLSXReader, error) {
}
defer f.Close()
fi, err := f.Stat()
_, err = f.Stat()
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
zr, err := zip.NewReader(f, fi.Size())
fileBytes, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %v", err)
}
return NewXLSXReaderFromBytes(fileBytes)
}
func NewXLSXReaderFromBytes(data []byte) (*XLSXReader, error) {
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, fmt.Errorf("创建zip读取器失败: %v", err)
}
files := make(map[string][]byte)
for _, file := range zr.File {
f, err := file.Open()
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("打开内部文件失败: %v", err)
}
content, err := io.ReadAll(f)
f.Close()
content, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("读取内部文件失败: %v", err)
}
files[file.Name] = content
}
reader := &XLSXReader{files: files}
reader := &XLSXReader{Files: files}
reader.parseWorkbook()
return reader, nil
}
func (r *XLSXReader) parseWorkbook() {
r.sheetMap = make(map[string]SheetInfo)
r.SheetMap = make(map[string]SheetInfo)
// 收集所有可用的worksheet文件
var availableSheets []string
for file := range r.files {
for file := range r.Files {
if strings.HasPrefix(file, "xl/worksheets/sheet") && strings.HasSuffix(file, ".xml") {
availableSheets = append(availableSheets, file)
}
@@ -166,7 +126,7 @@ func (r *XLSXReader) parseWorkbook() {
ridToPath := r.readWorkbookRels()
fmt.Printf("✓ 从rels文件读取到 %d 个映射关系\n", len(ridToPath))
content, ok := r.files["xl/workbook.xml"]
content, ok := r.Files["xl/workbook.xml"]
if !ok {
return
}
@@ -223,7 +183,7 @@ func (r *XLSXReader) parseWorkbook() {
path := ""
if rid != "" && ridToPath[rid] != "" {
path = ridToPath[rid]
if r.files[path] == nil {
if r.Files[path] == nil {
path = ""
}
}
@@ -231,21 +191,21 @@ func (r *XLSXReader) parseWorkbook() {
if path == "" && rid != "" {
sheetNum := strings.TrimPrefix(rid, "rId")
path = fmt.Sprintf("xl/worksheets/sheet%s.xml", sheetNum)
if r.files[path] == nil {
if r.Files[path] == nil {
path = ""
}
}
if path == "" && sheetID > 0 {
path = fmt.Sprintf("xl/worksheets/sheet%d.xml", sheetID)
if r.files[path] == nil {
if r.Files[path] == nil {
path = ""
}
}
if path == "" && len(availableSheets) > 0 {
usedPaths := make(map[string]bool)
for _, info := range r.sheetMap {
for _, info := range r.SheetMap {
usedPaths[info.Path] = true
}
for _, availPath := range availableSheets {
@@ -257,7 +217,7 @@ func (r *XLSXReader) parseWorkbook() {
}
sheetNames = append(sheetNames, name)
r.sheetMap[name] = SheetInfo{
r.SheetMap[name] = SheetInfo{
Name: name,
SheetID: sheetID,
RID: rid,
@@ -273,7 +233,7 @@ func (r *XLSXReader) parseWorkbook() {
func (r *XLSXReader) readWorkbookRels() map[string]string {
result := make(map[string]string)
content, ok := r.files["xl/_rels/workbook.xml.rels"]
content, ok := r.Files["xl/_rels/workbook.xml.rels"]
if !ok {
fmt.Println("⚠️ 找不到 xl/_rels/workbook.xml.rels 文件")
return result
@@ -328,14 +288,14 @@ func (r *XLSXReader) readWorkbookRels() map[string]string {
func (r *XLSXReader) GetSheetNames() ([]string, error) {
var names []string
for name := range r.sheetMap {
for name := range r.SheetMap {
names = append(names, name)
}
return names, nil
}
func (r *XLSXReader) GetSheetData(sheetName string) ([][]string, error) {
sheetInfo, ok := r.sheetMap[sheetName]
sheetInfo, ok := r.SheetMap[sheetName]
if !ok {
return nil, fmt.Errorf("找不到工作表: %s", sheetName)
}
@@ -344,12 +304,12 @@ func (r *XLSXReader) GetSheetData(sheetName string) ([][]string, error) {
return nil, fmt.Errorf("无法确定工作表 %s 的文件路径", sheetName)
}
sheetContent, ok := r.files[sheetInfo.Path]
sheetContent, ok := r.Files[sheetInfo.Path]
if !ok {
// 尝试查找所有worksheet文件
fmt.Printf("⚠️ 找不到文件: %s\n", sheetInfo.Path)
fmt.Println(" 可用的worksheet文件:")
for file := range r.files {
for file := range r.Files {
if strings.HasPrefix(file, "xl/worksheets/") {
fmt.Printf(" - %s\n", file)
}
@@ -364,7 +324,7 @@ func (r *XLSXReader) GetSheetData(sheetName string) ([][]string, error) {
}
func (r *XLSXReader) loadSharedStrings() []string {
content, ok := r.files["xl/sharedStrings.xml"]
content, ok := r.Files["xl/sharedStrings.xml"]
if !ok {
return nil
}
@@ -623,14 +583,3 @@ func escapeMarkdown(text string) string {
text = strings.ReplaceAll(text, "_", "\\_")
return text
}
func SaveToFile(content, filename string) error {
return os.WriteFile(filename, []byte(content), 0644)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -1,5 +1,12 @@
按照以下对应关系的规则,将excel表中的数据进行提取,并且逐条填入json的对应位置。只填入数值。最终生成完整的json内容,输出在对话框中,我复制你的json内容。
注意:1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。3、excel是年度财务报表,以下的json也是年度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“期末余额”等同于“本年年末”,“期初余额”等同于“上年年末”,“本期金额”等同于“本年度”,“上期金额”等同于“上年度”。4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。7、最终的json内容样例:{"LC0000003E_T0I1231":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
注意:
1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。
2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。
3、excel是年度财务报表,以下的json也是年度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“期末余额”等同于“本年年末”,“期初余额”等同于“上年年末”,“本期金额”等同于“本年度”,“上期金额”等同于“上年度”。
4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。
5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。
6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。
7、最终的json内容样例:{"LC0000003E_T0I1231":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
对应关系规则如下:

View File

@@ -1,5 +1,12 @@
按照以下对应关系的规则,将excel表中的数据进行提取,并且逐条填入json的对应位置。只填入数值。最终生成完整的json内容,输出在对话框中,我复制你的json内容。
注意:1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。7、最终的json内容样例:{"LC0000003E_T0I0331":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
注意:
1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。
2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。
3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。
4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。
5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。
6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。
7、最终的json内容样例:{"LC0000003E_T0I0331":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}

View File

@@ -1,5 +1,12 @@
按照以下对应关系的规则,将excel表中的数据进行提取,并且逐条填入json的对应位置。只填入数值。最终生成完整的json内容,输出在对话框中,我复制你的json内容。
注意:1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。7、最终的json内容样例:{"LC0000003E_T0I0630":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
注意:
1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。
2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。
3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。
4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。
5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。
6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。
7、最终的json内容样例:{"LC0000003E_T0I0630":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
对应关系规则如下:

View File

@@ -1,5 +1,12 @@
按照以下对应关系的规则,将excel表中的数据进行提取,并且逐条填入json的对应位置。只填入数值。最终生成完整的json内容,输出在对话框中,我复制你的json内容。
注意:1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。7、最终的json内容样例:{"LC0000003E_T0I0930":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
注意:
1、生成的json中,数据的单位均要求为元。提取过程中需要确认excel中的单位,如果excel中单位为万元,需要进行计算再转换为元。
2、提取过程中,会计科目的名称可能有出入,你根据会计知识,自行处理。例如:“公允价值变动收益”等同于“公允价值变动损益”。
3、excel是季度财务报表,以下的json也是季度财务报表的内容,所以时间的描述不相同时,你自行处理。例如:“本期期末余额”等同于“本季度末”,“本期发生额”等同于“本季度”。
4、excel表格中会有数值为空的情况,你自行按照“零”进行处理。
5、生成json时,如果规则中的“键名”没有在excel中找到对应的值,“键名”必须在json中保留,“值”你自行按照“零”进行处理。
6、提取过程中,如果发现内容会产生歧义或者有偏差,先处理对应正确的数据,最后罗列有歧义的内容,等待我的指令。例如,excel提供的是“上一季度末”的各项数据,而json需要的是“本年年初余额”的各项数据,excel提供的是“上一季度”的各项数据,而json需要的是“上年同期发生额”的各项数据,处理完对应正确的数据后,无法对应的内容告知我。
7、最终的json内容样例:{"LC0000003E_T0I0930":155341.51,"LC0000003E_T0I0101":103743.54,......"GE8000080E":0,"GE0000000E":0}
对应关系规则如下:

View File

@@ -1,90 +0,0 @@
package main
import (
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/sm3"
)
type SignUtils struct {
privateKey *sm2.PrivateKey
}
func NewSignUtils() *SignUtils {
return &SignUtils{}
}
func (su *SignUtils) Sign(plainText []byte, prvKey string) (string, error) {
privateKey, err := sm2.PrivateKeyFromHex(prvKey)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
}
signature, err := privateKey.Sign(nil, plainText, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func (su *SignUtils) SignWithSM3(plainText []byte, prvKey string) (string, error) {
digest := sm3.Sm3Sum(plainText)
privateKey, err := sm2.PrivateKeyFromHex(prvKey)
if err != nil {
return "", fmt.Errorf("解析私钥失败: %v", err)
}
signature, err := privateKey.Sign(nil, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func (su *SignUtils) VerifySign(plainText []byte, signatureBase64 string, pubKey string) (bool, error) {
publicKey, err := sm2.PublicKeyFromHex(pubKey)
if err != nil {
return false, fmt.Errorf("解析公钥失败: %v", err)
}
signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return false, fmt.Errorf("解码签名失败: %v", err)
}
return publicKey.Verify(nil, plainText, signature), nil
}
func (su *SignUtils) VerifySignWithSM3(plainText []byte, signatureBase64 string, pubKey string) (bool, error) {
digest := sm3.Sm3Sum(plainText)
publicKey, err := sm2.PublicKeyFromHex(pubKey)
if err != nil {
return false, fmt.Errorf("解析公钥失败: %v", err)
}
signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return false, fmt.Errorf("解码签名失败: %v", err)
}
return publicKey.Verify(nil, digest, signature), nil
}
func (su *SignUtils) GenerateKeyPair() (string, string, error) {
privateKey, err := sm2.GenerateKey(nil)
if err != nil {
return "", "", fmt.Errorf("生成密钥对失败: %v", err)
}
privateKeyHex := hex.EncodeToString(privateKey.D.Bytes())
publicKeyHex := hex.EncodeToString(privateKey.PublicKey.X.Bytes()) + hex.EncodeToString(privateKey.PublicKey.Y.Bytes())
return privateKeyHex, publicKeyHex, nil
}

View File

@@ -1,351 +0,0 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/sm3"
)
const (
TaxEvalAPIURL = "https://api.cmdp.cn/v1/eval/tax-risk"
TaxEvalQueryURL = "https://api.cmdp.cn/v1/eval/order/%s"
CompanyInfoAPIURL = "https://a.cmdp.cn/cmds/v1/company/info-rest/custom"
)
type TaxEvalRequest struct {
ReqNo string `json:"req_no"`
Callback string `json:"callback,omitempty"`
Detail TaxEvalDetail `json:"detail"`
}
type TaxEvalDetail struct {
FileURL string `json:"file_url,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
CompanyName string `json:"company_name"`
Period string `json:"period"`
Year string `json:"year"`
IsVerify bool `json:"is_verify,omitempty"`
CIT float64 `json:"cit"`
VAT float64 `json:"vat"`
InputVAT float64 `json:"input_vat"`
OutputVAT float64 `json:"output_vat"`
PreCIT float64 `json:"pre_cit"`
PreVAT float64 `json:"pre_vat"`
PreInputVAT float64 `json:"pre_input_vat"`
PreOutputVAT float64 `json:"pre_output_vat"`
}
type TaxEvalResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Data struct {
ReqNo string `json:"req_no"`
OrderNo string `json:"order_no"`
} `json:"data"`
}
type TaxEvalQueryResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Data struct {
Status int `json:"status"`
Detail struct {
Files []string `json:"files"`
Error string `json:"error"`
} `json:"detail"`
} `json:"data"`
}
type APIRequest struct {
UserID string
PrivateKey string
Nonce string
Timestamp string
URI string
Body []byte
}
type APIResponse struct {
StatusCode int
Header http.Header
Body []byte
}
type TaxHealthClient struct {
client *http.Client
userID string
privateKey *sm2.PrivateKey
}
func NewTaxHealthClient(userID string, privateKeyHex string) (*TaxHealthClient, error) {
privateKey, err := sm2.PrivateKeyFromHex(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("解析私钥失败: %v", err)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return &TaxHealthClient{
client: &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
},
userID: userID,
privateKey: privateKey,
}, nil
}
func (c *TaxHealthClient) generateNonce() string {
return strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.Itoa(rand.Intn(10000))
}
func (c *TaxHealthClient) generateTimestamp() string {
return strconv.FormatInt(time.Now().Unix(), 10)
}
func (c *TaxHealthClient) signData(data []byte) (string, error) {
digest := sm3.Sm3Sum(data)
signature, err := c.privateKey.Sign(nil, digest, nil)
if err != nil {
return "", fmt.Errorf("签名失败: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func (c *TaxHealthClient) buildSignature(nonce, timestamp, uri string, body []byte) (string, error) {
signData := c.userID + timestamp + nonce + uri
if body != nil && len(body) > 0 {
signData += string(body)
}
return c.signData([]byte(signData))
}
func (c *TaxHealthClient) doRequest(method, url string, body []byte, headers map[string]string) (*APIResponse, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
return &APIResponse{
StatusCode: resp.StatusCode,
Header: resp.Header,
Body: respBody,
}, nil
}
func (c *TaxHealthClient) TaxEval(request TaxEvalRequest) (*TaxEvalResponse, error) {
nonce := c.generateNonce()
timestamp := c.generateTimestamp()
jsonBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %v", err)
}
signature, err := c.buildSignature(nonce, timestamp, TaxEvalAPIURL, jsonBody)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
headers := map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"x-gi-user-id": c.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
resp, err := c.doRequest("POST", TaxEvalAPIURL, jsonBody, headers)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API返回状态码 %d: %s", resp.StatusCode, string(resp.Body))
}
var response TaxEvalResponse
err = json.Unmarshal(resp.Body, &response)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if response.Code != 200 {
return nil, fmt.Errorf("API错误: %s", response.Message)
}
return &response, nil
}
func (c *TaxHealthClient) QueryResult(orderNo string) (*TaxEvalQueryResponse, error) {
nonce := c.generateNonce()
timestamp := c.generateTimestamp()
queryURL := fmt.Sprintf(TaxEvalQueryURL, orderNo)
signature, err := c.buildSignature(nonce, timestamp, queryURL, nil)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
headers := map[string]string{
"Accept": "application/json",
"x-gi-user-id": c.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
resp, err := c.doRequest("GET", queryURL, nil, headers)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API返回状态码 %d: %s", resp.StatusCode, string(resp.Body))
}
var response TaxEvalQueryResponse
err = json.Unmarshal(resp.Body, &response)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if response.Code != 200 {
return nil, fmt.Errorf("API错误: %s", response.Message)
}
return &response, nil
}
func (c *TaxHealthClient) QueryCompanyInfo(companyName string, apiParams []string) (*CompanyInfoResponse, error) {
nonce := c.generateNonce()
timestamp := c.generateTimestamp()
params := url.Values{}
params.Set("name", companyName)
params.Set("api", joinAPIParams(apiParams))
queryURL := fmt.Sprintf("%s?%s", CompanyInfoAPIURL, params.Encode())
signature, err := c.buildSignature(nonce, timestamp, queryURL, nil)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
headers := map[string]string{
"Accept": "application/json",
"x-gi-user-id": c.userID,
"x-gi-user-sig": signature,
"x-gi-nonce": nonce,
"x-gi-timestamp": timestamp,
}
resp, err := c.doRequest("GET", queryURL, nil, headers)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API返回状态码 %d: %s", resp.StatusCode, string(resp.Body))
}
var response CompanyInfoResponse
err = json.Unmarshal(resp.Body, &response)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if response.Code != 0 {
return nil, fmt.Errorf("API错误: %s", response.Message)
}
return &response, nil
}
func (c *TaxHealthClient) DownloadFile(fileURL string, localPath string) error {
signature, err := c.buildSignature("", "", fileURL, nil)
if err != nil {
return fmt.Errorf("生成签名失败: %v", err)
}
headers := map[string]string{
"x-gi-user-id": c.userID,
"x-gi-user-sig": signature,
}
resp, err := c.doRequest("GET", fileURL, nil, headers)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败,状态码 %d", resp.StatusCode)
}
err = ioutil.WriteFile(localPath, resp.Body, 0644)
if err != nil {
return fmt.Errorf("保存文件失败: %v", err)
}
return nil
}
func joinAPIParams(params []string) string {
result := ""
for i, param := range params {
if i > 0 {
result += ","
}
result += param
}
return result
}
func ValidateTaxEvalRequest(request TaxEvalRequest) error {
if request.ReqNo == "" {
return fmt.Errorf("req_no不能为空")
}
if request.Detail.CompanyName == "" {
return fmt.Errorf("company_name不能为空")
}
if request.Detail.Period == "" {
return fmt.Errorf("period不能为空")
}
if request.Detail.Year == "" {
return fmt.Errorf("year不能为空")
}
if request.Detail.FileURL == "" && request.Detail.Data == nil {
return fmt.Errorf("file_url和data必须填写其中一个")
}
return nil
}

View File

@@ -1,445 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strconv"
"testing"
"time"
"github.com/tjfoc/gmsm/sm2"
)
func TestGenerateKeyPair(t *testing.T) {
privateKey, err := sm2.GenerateKey(nil)
if err != nil {
t.Fatalf("生成密钥对失败: %v", err)
}
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
publicKeyHex := fmt.Sprintf("%x", privateKey.PublicKey.X.Bytes()) + fmt.Sprintf("%x", privateKey.PublicKey.Y.Bytes())
t.Logf("私钥长度: %d", len(privateKeyHex))
t.Logf("公钥长度: %d", len(publicKeyHex))
if len(privateKeyHex) != 64 {
t.Errorf("私钥长度应为64,实际为%d", len(privateKeyHex))
}
if len(publicKeyHex) != 128 {
t.Errorf("公钥长度应为128,实际为%d", len(publicKeyHex))
}
}
func TestSignData(t *testing.T) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, err := NewTaxHealthClient("test_user", privateKeyHex)
if err != nil {
t.Fatalf("创建客户端失败: %v", err)
}
testData := []byte("test data")
signature, err := client.signData(testData)
if err != nil {
t.Fatalf("签名失败: %v", err)
}
t.Logf("签名结果: %s", signature)
if signature == "" {
t.Error("签名结果为空")
}
}
func TestBuildSignature(t *testing.T) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, err := NewTaxHealthClient("test_user", privateKeyHex)
if err != nil {
t.Fatalf("创建客户端失败: %v", err)
}
tests := []struct {
name string
nonce string
ts string
uri string
body []byte
wantErr bool
}{
{"完整参数", "abc123", "1694400142", "/api/test", []byte(`{"test":"data"}`), false},
{"空body", "abc123", "1694400142", "/api/test", nil, false},
{"空nonce", "", "1694400142", "/api/test", nil, false},
{"空uri", "abc123", "1694400142", "", nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sig, err := client.buildSignature(tt.nonce, tt.ts, tt.uri, tt.body)
if (err != nil) != tt.wantErr {
t.Errorf("buildSignature() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && sig == "" {
t.Error("buildSignature() 返回空签名")
}
t.Logf("签名: %s", sig)
})
}
}
func TestValidateTaxEvalRequest(t *testing.T) {
tests := []struct {
name string
req TaxEvalRequest
wantErr bool
}{
{
"完整请求",
TaxEvalRequest{
ReqNo: "REQ001",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
FileURL: "http://example.com/test.xlsx",
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
},
false,
},
{
"使用data字段",
TaxEvalRequest{
ReqNo: "REQ002",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
Data: map[string]interface{}{"key": "value"},
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
},
false,
},
{
"缺少req_no",
TaxEvalRequest{
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
FileURL: "http://example.com/test.xlsx",
},
},
true,
},
{
"缺少company_name",
TaxEvalRequest{
ReqNo: "REQ003",
Detail: TaxEvalDetail{
Period: "年度",
Year: "2024",
FileURL: "http://example.com/test.xlsx",
},
},
true,
},
{
"缺少period",
TaxEvalRequest{
ReqNo: "REQ004",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Year: "2024",
FileURL: "http://example.com/test.xlsx",
},
},
true,
},
{
"缺少year",
TaxEvalRequest{
ReqNo: "REQ005",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
FileURL: "http://example.com/test.xlsx",
},
},
true,
},
{
"file_url和data都为空",
TaxEvalRequest{
ReqNo: "REQ006",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateTaxEvalRequest(tt.req)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateTaxEvalRequest() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGenerateNonce(t *testing.T) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, _ := NewTaxHealthClient("test_user", privateKeyHex)
nonces := make(map[string]bool)
for i := 0; i < 100; i++ {
nonce := client.generateNonce()
if nonces[nonce] {
t.Errorf("生成了重复的nonce: %s", nonce)
}
nonces[nonce] = true
t.Logf("nonce[%d]: %s", i, nonce)
}
}
func TestGenerateTimestamp(t *testing.T) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, _ := NewTaxHealthClient("test_user", privateKeyHex)
ts := client.generateTimestamp()
t.Logf("时间戳: %s", ts)
_, err := time.Parse("20060102150405", ts)
if err != nil {
_, err = strconv.ParseInt(ts, 10, 64)
if err != nil {
t.Errorf("时间戳格式不正确: %s", ts)
}
}
}
func TestJSONMarshalConsistency(t *testing.T) {
req := TaxEvalRequest{
ReqNo: "REQ001",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
Data: map[string]interface{}{
"LC0000003E_T0I1231": 155341.51,
"LC0000003E_T0I0101": 103743.54,
},
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
}
json1, err := json.Marshal(req)
if err != nil {
t.Fatalf("第一次序列化失败: %v", err)
}
json2, err := json.Marshal(req)
if err != nil {
t.Fatalf("第二次序列化失败: %v", err)
}
if string(json1) != string(json2) {
t.Errorf("JSON序列化不一致\n第一次: %s\n第二次: %s", json1, json2)
}
t.Logf("JSON长度: %d", len(json1))
t.Logf("JSON内容: %s", json1)
}
func TestTaxEvalIntegration(t *testing.T) {
skIP := os.Getenv("TAX_HEALTH_SKIP_INTEGRATION")
if skIP == "true" {
t.Skip("跳过集成测试")
}
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, err := NewTaxHealthClient("test_user", privateKeyHex)
if err != nil {
t.Fatalf("创建客户端失败: %v", err)
}
req := TaxEvalRequest{
ReqNo: "TEST_REQ_" + fmt.Sprintf("%d", time.Now().Unix()),
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
Data: map[string]interface{}{
"LC0000003E_T0I1231": 155341.51,
"LC0000003E_T0I0101": 103743.54,
},
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
}
err = ValidateTaxEvalRequest(req)
if err != nil {
t.Fatalf("验证请求失败: %v", err)
}
t.Log("发送税务健康测评请求...")
resp, err := client.TaxEval(req)
if err != nil {
t.Logf("API调用失败(可能是预期的): %v", err)
t.Skip("API调用失败,跳过后续测试")
}
t.Logf("响应: code=%d, req_no=%s, order_no=%s", resp.Code, resp.Data.ReqNo, resp.Data.OrderNo)
if resp.Code != 200 {
t.Errorf("期望状态码200,实际为%d", resp.Code)
}
if resp.Data.ReqNo != req.ReqNo {
t.Errorf("req_no不匹配: 期望%s, 实际%s", req.ReqNo, resp.Data.ReqNo)
}
if resp.Data.OrderNo == "" {
t.Error("order_no为空")
}
t.Log("查询测评结果...")
queryResp, err := client.QueryResult(resp.Data.OrderNo)
if err != nil {
t.Logf("查询结果失败(可能是预期的): %v", err)
return
}
t.Logf("查询响应: code=%d, status=%d", queryResp.Code, queryResp.Data.Status)
if queryResp.Code != 200 {
t.Errorf("期望状态码200,实际为%d", queryResp.Code)
}
}
func TestQueryCompanyInfoIntegration(t *testing.T) {
skIP := os.Getenv("TAX_HEALTH_SKIP_INTEGRATION")
if skIP == "true" {
t.Skip("跳过集成测试")
}
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, err := NewTaxHealthClient("test_user", privateKeyHex)
if err != nil {
t.Fatalf("创建客户端失败: %v", err)
}
t.Log("查询公司信息...")
resp, err := client.QueryCompanyInfo("深圳市致远速联信息技术有限公司", []string{"p_info", "p_biddings_all"})
if err != nil {
t.Logf("API调用失败(可能是预期的): %v", err)
t.Skip("API调用失败")
}
t.Logf("响应: code=%d", resp.Code)
if resp.Code != 0 {
t.Errorf("期望状态码0,实际为%d", resp.Code)
}
}
func BenchmarkSignData(b *testing.B) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, _ := NewTaxHealthClient("test_user", privateKeyHex)
testData := []byte("test data for benchmark")
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.signData(testData)
}
}
func BenchmarkBuildSignature(b *testing.B) {
privateKey, _ := sm2.GenerateKey(nil)
privateKeyHex := fmt.Sprintf("%x", privateKey.D.Bytes())
client, _ := NewTaxHealthClient("test_user", privateKeyHex)
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.buildSignature("abc123", "1694400142", "/api/test", []byte(`{"test":"data"}`))
}
}
func BenchmarkJSONMarshal(b *testing.B) {
req := TaxEvalRequest{
ReqNo: "REQ001",
Detail: TaxEvalDetail{
CompanyName: "测试公司",
Period: "年度",
Year: "2024",
Data: map[string]interface{}{
"LC0000003E_T0I1231": 155341.51,
"LC0000033E_T0I1231": 1000000.00,
},
CIT: 100.0,
VAT: 50.0,
InputVAT: 30.0,
OutputVAT: 80.0,
PreCIT: 90.0,
PreVAT: 45.0,
PreInputVAT: 25.0,
PreOutputVAT: 70.0,
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(req)
}
}

View File

@@ -1,92 +0,0 @@
package main
import (
"fmt"
)
func main() {
testSM2Sign()
}
func testSM2Sign() {
userId := "userId"
timestamp := "1694400142"
uri := ""
nonce := ""
body := ""
prvKey := ""
signData := userId + timestamp + nonce + uri + body
fmt.Printf("签名源数据:%s\n", signData)
signUtils := NewSignUtils()
signStr, err := signUtils.Sign([]byte(signData), prvKey)
if err != nil {
fmt.Printf("签名失败: %v\n", err)
return
}
fmt.Printf("签名后数据:%s\n", signStr)
testKeyGeneration()
}
func testKeyGeneration() {
signUtils := NewSignUtils()
privateKey, publicKey, err := signUtils.GenerateKeyPair()
if err != nil {
fmt.Printf("生成密钥对失败: %v\n", err)
return
}
fmt.Printf("\n生成的密钥对:\n")
fmt.Printf("私钥: %s\n", privateKey)
fmt.Printf("公钥: %s\n", publicKey)
testData := "test data for signing"
signature, err := signUtils.Sign([]byte(testData), privateKey)
if err != nil {
fmt.Printf("签名测试失败: %v\n", err)
return
}
fmt.Printf("\n测试签名: %s\n", signature)
valid, err := signUtils.VerifySign([]byte(testData), signature, publicKey)
if err != nil {
fmt.Printf("验证签名失败: %v\n", err)
return
}
fmt.Printf("签名验证结果: %t\n", valid)
}
func testAPISignature() {
signUtils := NewSignUtils()
userId := "user001"
timestamp := "1694400142"
uri := "/api/v1/company/info"
nonce := "abc123"
body := `{"name":"test"}`
prvKey := ""
if prvKey == "" {
fmt.Println("请设置有效的私钥进行测试")
return
}
signData := userId + timestamp + nonce + uri + body
fmt.Printf("API签名源数据:%s\n", signData)
signStr, err := signUtils.Sign([]byte(signData), prvKey)
if err != nil {
fmt.Printf("API签名失败: %v\n", err)
return
}
fmt.Printf("API签名结果:%s\n", signStr)
}

64
web/README.md Normal file
View File

@@ -0,0 +1,64 @@
# WASM企业行业分类器
这是一个使用WebAssembly技术的前端版本企业行业分类器,允许在浏览器中直接运行Go代码。
## 功能特性
- 在浏览器中直接运行Go代码(通过WASM)
- 输入企业名称进行行业分类
- 显示主要行业和次要行业信息
- 响应式设计,适配不同设备
## 架构说明
- `cmd/wasm/main.go`: Go源代码,编译为WASM
- `web/index.html`: 前端界面
- `build_wasm.bat`: Windows编译脚本
## 编译步骤
### Windows
1. 确保已安装Go 1.12或更高版本
2. 运行编译脚本:
```cmd
build_wasm.bat
```
### 手动编译(Linux/Mac)
```bash
# 复制WASM执行器
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" web/
# 编译为WASM
GOOS=js GOARCH=wasm go build -o web/main.wasm cmd/wasm/main.go
```
## 运行
由于浏览器安全策略限制,需要通过HTTP服务器运行,不能直接打开HTML文件:
```bash
# 使用Python 3
cd web
python -m http.server 8080
# 或使用Node.js
npx http-server web -p 8080
# 然后在浏览器中访问 http://localhost:8080/index.html
```
## 技术细节
- 使用Go的`syscall/js`包实现Go与JavaScript互操作
- WASM模块负责调用行业分类算法
- 前端界面使用原生JavaScript和CSS
- 支持异步API调用和错误处理
## 注意事项
- WASM版本目前使用固定的API配置
- 实际部署时需要考虑安全性,如API密钥管理
- 需要后端服务支持API调用(企业信息获取、LLM调用等)

546
web/index.html Normal file
View File

@@ -0,0 +1,546 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WASM企业分析工具套件</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.container {
background-color: white;
border-radius: 15px;
padding: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
color: #7f8c8d;
font-size: 1.2em;
margin-bottom: 30px;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 40px 0;
}
.feature-card {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
text-align: center;
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.feature-icon {
font-size: 2.5em;
margin-bottom: 15px;
display: block;
}
.feature-title {
font-size: 1.3em;
color: #2c3e50;
margin-bottom: 10px;
}
.feature-desc {
color: #7f8c8d;
}
.section {
margin: 30px 0;
padding: 25px;
background: #e3f2fd;
border-radius: 10px;
}
label {
display: block;
margin-bottom: 12px;
font-weight: bold;
color: #34495e;
font-size: 1.1em;
}
input[type="text"], textarea, input[type="file"] {
width: 100%;
padding: 12px;
border: 2px solid #bbdefb;
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s;
margin-bottom: 15px;
}
input[type="text"]:focus, textarea:focus {
outline: none;
border-color: #64b5f6;
}
button {
background: linear-gradient(45deg, #2196F3, #21CBF3);
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
margin-top: 10px;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4);
}
button:disabled {
background: #b0bec5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.result-section {
margin-top: 20px;
padding: 20px;
background-color: #e8f5e9;
border-radius: 10px;
display: none;
}
.result-section.show {
display: block;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.result-item {
margin-bottom: 15px;
padding: 15px;
background-color: white;
border-radius: 8px;
border-left: 5px solid #4caf50;
}
.loading {
text-align: center;
padding: 20px;
display: none;
}
.loading.show {
display: block;
}
.error {
color: #f44336;
background-color: #ffebee;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
display: none;
}
.error.show {
display: block;
}
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
border-radius: 8px 8px 0 0;
}
.tab button {
background: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
font-size: 17px;
margin: 0;
box-shadow: none;
}
.tab button:hover {
background-color: #ddd;
}
.tab button.active {
background-color: #2196F3;
color: white;
}
.tabcontent {
display: none;
padding: 20px;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 8px 8px;
background-color: white;
}
footer {
text-align: center;
margin-top: 40px;
color: #95a5a6;
font-size: 0.9em;
}
</style>
</head>
<body>
<header>
<h1>🏭 WASM企业分析工具套件</h1>
<p class="subtitle">基于WebAssembly的多功能企业数据分析平台</p>
</header>
<div class="container">
<div class="features">
<div class="feature-card">
<span class="feature-icon">📊</span>
<h3 class="feature-title">行业分析</h3>
<p class="feature-desc">基于企业信息的国民经济行业分类</p>
</div>
<div class="feature-card">
<span class="feature-icon">📄</span>
<h3 class="feature-title">Excel解析</h3>
<p class="feature-desc">在浏览器中直接解析Excel文件</p>
</div>
<div class="feature-card">
<span class="feature-icon">🤖</span>
<h3 class="feature-title">AI分析</h3>
<p class="feature-desc">利用大模型进行智能数据分析</p>
</div>
<div class="feature-card">
<span class="feature-icon">💰</span>
<h3 class="feature-title">税务健康</h3>
<p class="feature-desc">企业税务健康状况评估</p>
</div>
</div>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'company')">企业行业分析</button>
<button class="tablinks" onclick="openTab(event, 'excel')">Excel解析</button>
<button class="tablinks" onclick="openTab(event, 'llm')">AI分析</button>
<button class="tablinks" onclick="openTab(event, 'tax')">税务健康</button>
</div>
<!-- 企业行业分析标签页 -->
<div id="company" class="tabcontent" style="display: block;">
<div class="section">
<label for="companyName">🔍 请输入企业全称:</label>
<input type="text" id="companyName" placeholder="例如:深圳市致远速联信息技术有限公司" value="深圳市致远速联信息技术有限公司">
<button id="analyzeCompanyBtn" disabled>分析企业行业</button>
</div>
</div>
<!-- Excel解析标签页 -->
<div id="excel" class="tabcontent">
<div class="section">
<label for="excelFile">📁 请选择Excel文件:</label>
<input type="file" id="excelFile" accept=".xlsx,.xls">
<button id="parseExcelBtn" disabled>解析Excel文件</button>
</div>
</div>
<!-- AI分析标签页 -->
<div id="llm" class="tabcontent">
<div class="section">
<label for="llmPrompt">💬 请输入分析提示:</label>
<textarea id="llmPrompt" rows="4" placeholder="请输入您想要分析的内容...">请分析以下企业数据并给出建议</textarea>
<button id="callLLMBtn" disabled>调用AI分析</button>
</div>
</div>
<!-- 税务健康标签页 -->
<div id="tax" class="tabcontent">
<div class="section">
<label for="taxJson">💼 请输入企业JSON数据:</label>
<textarea id="taxJson" rows="6" placeholder='请输入企业数据,例如: {"revenue": 1000000, "expenses": 800000}'>{"revenue": 1000000, "expenses": 800000, "tax_paid": 50000}</textarea>
<button id="callTaxBtn" disabled>评估税务健康</button>
</div>
</div>
<div class="loading" id="loading">
<p>🔄 正在处理请求...请稍候</p>
</div>
<div class="error" id="error"></div>
<div class="result-section" id="resultSection">
<h2>📊 分析结果</h2>
<div id="resultContent"></div>
</div>
</div>
<footer>
<p>Powered by WebAssembly & Go | 企业分析工具套件 v1.0</p>
</footer>
<!-- 加载WASM运行时 -->
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
// WASM加载完成后启用所有按钮
document.getElementById('analyzeCompanyBtn').disabled = false;
document.getElementById('parseExcelBtn').disabled = false;
document.getElementById('callLLMBtn').disabled = false;
document.getElementById('callTaxBtn').disabled = false;
console.log('WASM模块加载完成');
}).catch((err) => {
console.error('WASM加载失败:', err);
showError('WASM模块加载失败: ' + err.message);
});
// 获取DOM元素
const analyzeCompanyBtn = document.getElementById('analyzeCompanyBtn');
const parseExcelBtn = document.getElementById('parseExcelBtn');
const callLLMBtn = document.getElementById('callLLMBtn');
const callTaxBtn = document.getElementById('callTaxBtn');
const companyNameInput = document.getElementById('companyName');
const excelFileInput = document.getElementById('excelFile');
const llmPromptInput = document.getElementById('llmPrompt');
const taxJsonInput = document.getElementById('taxJson');
const resultSection = document.getElementById('resultSection');
const resultContent = document.getElementById('resultContent');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
// 企业行业分析按钮事件
analyzeCompanyBtn.addEventListener('click', async () => {
const companyName = companyNameInput.value.trim();
if (!companyName) {
showError('请输入企业名称');
return;
}
showLoading(true);
hideError();
resultSection.classList.remove('show');
try {
const result = await window.analyzeCompanyIndustryAsync(companyName);
if (result && result.error) {
showError(result.error);
} else {
displayResults('企业行业分析结果', result);
}
} catch (error) {
showError('分析过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
});
// Excel解析按钮事件
parseExcelBtn.addEventListener('click', async () => {
const file = excelFileInput.files[0];
if (!file) {
showError('请选择一个Excel文件');
return;
}
showLoading(true);
hideError();
resultSection.classList.remove('show');
try {
// 读取文件为ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
// 调用WASM中的解析函数
const result = await window.parseExcelAsync(arrayBuffer);
if (result && result.error) {
showError(result.error);
} else {
displayResults('Excel解析结果', result);
}
} catch (error) {
showError('解析Excel过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
});
// AI分析按钮事件
callLLMBtn.addEventListener('click', async () => {
const prompt = llmPromptInput.value.trim();
if (!prompt) {
showError('请输入分析提示');
return;
}
showLoading(true);
hideError();
resultSection.classList.remove('show');
try {
const result = await window.callLLMAsync(prompt);
if (result && result.error) {
showError(result.error);
} else {
displayResults('AI分析结果', result);
}
} catch (error) {
showError('AI分析过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
});
// 税务健康按钮事件
callTaxBtn.addEventListener('click', async () => {
const jsonData = taxJsonInput.value.trim();
if (!jsonData) {
showError('请输入企业JSON数据');
return;
}
showLoading(true);
hideError();
resultSection.classList.remove('show');
try {
const result = await window.callTaxHealthAsync(jsonData);
if (result && result.error) {
showError(result.error);
} else {
displayResults('税务健康评估结果', result);
}
} catch (error) {
showError('税务健康评估过程中发生错误: ' + error.message);
} finally {
showLoading(false);
}
});
// 显示加载状态
function showLoading(show) {
if (show) {
loadingDiv.classList.add('show');
// 禁用所有按钮以防止重复点击
analyzeCompanyBtn.disabled = true;
parseExcelBtn.disabled = true;
callLLMBtn.disabled = true;
callTaxBtn.disabled = true;
} else {
loadingDiv.classList.remove('show');
// 重新启用按钮(但保持初始状态由WASM加载决定)
// 这里简单地重新启用
setTimeout(() => {
analyzeCompanyBtn.disabled = false;
parseExcelBtn.disabled = false;
callLLMBtn.disabled = false;
callTaxBtn.disabled = false;
}, 100);
}
}
// 显示错误信息
function showError(message) {
errorDiv.textContent = message;
errorDiv.classList.add('show');
}
// 隐藏错误信息
function hideError() {
errorDiv.classList.remove('show');
}
// 显示结果
function displayResults(title, result) {
if (!result) {
showError('未能获取分析结果');
return;
}
let html = `<div class="result-item"><h3>${title}</h3>`;
// 格式化输出结果
html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
html += '</div>';
resultContent.innerHTML = html;
resultSection.classList.add('show');
}
// 标签页功能
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
// 回车键触发当前活动标签页的分析
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const activeTab = document.querySelector('.tabcontent[style*="block"]');
if (activeTab.id === 'company') {
analyzeCompanyBtn.click();
} else if (activeTab.id === 'llm') {
callLLMBtn.click();
} else if (activeTab.id === 'tax') {
callTaxBtn.click();
}
}
});
</script>
</body>
</html>

575
web/wasm_exec.js Normal file
View File

@@ -0,0 +1,575 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@@ -1,366 +0,0 @@
package main
import (
"archive/zip"
"encoding/xml"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
)
type XLSXReader struct {
files map[string]*zip.File
}
type SharedStrings struct {
Strings []string `xml:"si>t"`
}
type Sheet struct {
Name string `xml:"name,attr"`
SheetID int `xml:"sheetId,attr"`
Ref string `xml:"id,attr"`
}
type Sheets struct {
Sheets []Sheet `xml:"sheet"`
}
type Row struct {
Cells []Cell `xml:"c"`
RowNum int `xml:"r,attr"`
}
type Cell struct {
Ref string `xml:"r,attr"`
Type string `xml:"t,attr"`
Val string `xml:"v"`
}
type Worksheet struct {
Rows []Row `xml:"sheetData>row"`
}
func NewXLSXReader(filePath string) (*XLSXReader, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
zr, err := zip.NewReader(f, fi.Size())
if err != nil {
return nil, fmt.Errorf("创建zip读取器失败: %v", err)
}
files := make(map[string]*zip.File)
for _, file := range zr.File {
files[file.Name] = file
}
return &XLSXReader{files: files}, nil
}
func (r *XLSXReader) GetSheetNames() ([]string, error) {
contentTypesFile, ok := r.files["[Content_Types].xml"]
if !ok {
return nil, fmt.Errorf("找不到[Content_Types].xml")
}
f, err := contentTypesFile.Open()
if err != nil {
return nil, fmt.Errorf("打开[Content_Types].xml失败: %v", err)
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("读取[Content_Types].xml失败: %v", err)
}
sheetsFile, ok := r.files["xl/workbook.xml"]
if !ok {
return nil, fmt.Errorf("找不到xl/workbook.xml")
}
f, err = sheetsFile.Open()
if err != nil {
return nil, fmt.Errorf("打开xl/workbook.xml失败: %v", err)
}
defer f.Close()
content, err = io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("读取xl/workbook.xml失败: %v", err)
}
var sheets Sheets
err = xml.Unmarshal(content, &sheets)
if err != nil {
return nil, fmt.Errorf("解析xl/workbook.xml失败: %v", err)
}
names := make([]string, 0, len(sheets.Sheets))
for _, sheet := range sheets.Sheets {
names = append(names, sheet.Name)
}
return names, nil
}
func (r *XLSXReader) GetSheetData(sheetName string) ([][]string, error) {
sheetsFile, ok := r.files["xl/workbook.xml"]
if !ok {
return nil, fmt.Errorf("找不到xl/workbook.xml")
}
f, err := sheetsFile.Open()
if err != nil {
return nil, fmt.Errorf("打开xl/workbook.xml失败: %v", err)
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("读取xl/workbook.xml失败: %v", err)
}
var sheets Sheets
err = xml.Unmarshal(content, &sheets)
if err != nil {
return nil, fmt.Errorf("解析xl/workbook.xml失败: %v", err)
}
var targetSheet Sheet
for _, sheet := range sheets.Sheets {
if sheet.Name == sheetName {
targetSheet = sheet
break
}
}
if targetSheet.Name == "" {
return nil, fmt.Errorf("找不到工作表: %s", sheetName)
}
sheetID := strings.TrimPrefix(targetSheet.Ref, "rId")
sheetFilePath := fmt.Sprintf("xl/worksheets/sheet%s.xml", sheetID)
sheetFile, ok := r.files[sheetFilePath]
if !ok {
return nil, fmt.Errorf("找不到工作表文件: %s", sheetFilePath)
}
f, err = sheetFile.Open()
if err != nil {
return nil, fmt.Errorf("打开工作表文件失败: %v", err)
}
defer f.Close()
content, err = io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("读取工作表文件失败: %v", err)
}
var worksheet Worksheet
err = xml.Unmarshal(content, &worksheet)
if err != nil {
return nil, fmt.Errorf("解析工作表失败: %v", err)
}
var sharedStrings []string
sharedStringsFile, ok := r.files["xl/sharedStrings.xml"]
if ok {
f, err := sharedStringsFile.Open()
if err == nil {
defer f.Close()
sharedContent, _ := io.ReadAll(f)
var ss SharedStrings
if xml.Unmarshal(sharedContent, &ss) == nil {
sharedStrings = ss.Strings
}
}
}
maxCol := 0
for _, row := range worksheet.Rows {
for _, cell := range row.Cells {
col := getColumnIndex(cell.Ref)
if col > maxCol {
maxCol = col
}
}
}
if len(worksheet.Rows) == 0 {
return [][]string{}, nil
}
result := make([][]string, len(worksheet.Rows))
for i, row := range worksheet.Rows {
result[i] = make([]string, maxCol+1)
for _, cell := range row.Cells {
col := getColumnIndex(cell.Ref)
val := cell.Val
if cell.Type == "s" && sharedStrings != nil {
idx, _ := strconv.Atoi(val)
if idx >= 0 && idx < len(sharedStrings) {
val = sharedStrings[idx]
}
}
if col < len(result[i]) {
result[i][col] = val
}
}
}
return result, nil
}
func (r *XLSXReader) GetCellValue(sheetName, cellRef string) (string, error) {
data, err := r.GetSheetData(sheetName)
if err != nil {
return "", err
}
rowIdx, colIdx := parseCellRef(cellRef)
if rowIdx >= len(data) {
return "", fmt.Errorf("行索引超出范围")
}
if colIdx >= len(data[rowIdx]) {
return "", fmt.Errorf("列索引超出范围")
}
return data[rowIdx][colIdx], nil
}
func (r *XLSXReader) GetCellByPosition(sheetName string, row, col int) (string, error) {
data, err := r.GetSheetData(sheetName)
if err != nil {
return "", err
}
if row < 0 || row >= len(data) {
return "", fmt.Errorf("行索引超出范围: %d", row)
}
if col < 0 || col >= len(data[row]) {
return "", fmt.Errorf("列索引超出范围: %d", col)
}
return data[row][col], nil
}
func parseCellRef(cellRef string) (row, col int) {
i := 0
for i < len(cellRef) && cellRef[i] >= 'A' && cellRef[i] <= 'Z' {
col = col*26 + int(cellRef[i]-'A') + 1
i++
}
row, _ = strconv.Atoi(cellRef[i:])
return row - 1, col - 1
}
func getColumnIndex(cellRef string) int {
i := 0
for i < len(cellRef) && cellRef[i] >= 'A' && cellRef[i] <= 'Z' {
i++
}
colStr := cellRef[:i]
col := 0
for _, c := range colStr {
col = col*26 + int(c-'A') + 1
}
return col - 1
}
func ExtractFinancialDataPureGo(excelFile, mappingFile, sheetName, itemName, columnName string) (float64, error) {
reader, err := NewXLSXReader(excelFile)
if err != nil {
return 0, fmt.Errorf("创建XLSX读取器失败: %v", err)
}
data, err := reader.GetSheetData(sheetName)
if err != nil {
return 0, fmt.Errorf("获取工作表数据失败: %v", err)
}
if len(data) < 2 {
return 0, fmt.Errorf("工作表 %s 数据不足", sheetName)
}
colIndex := -1
for _, cell := range data[0] {
if strings.Contains(strings.TrimSpace(cell), columnName) {
for j, c := range data[0] {
if strings.Contains(strings.TrimSpace(c), columnName) {
colIndex = j
break
}
}
break
}
}
if colIndex == -1 {
for _, row := range data {
for j, cell := range row {
if strings.Contains(strings.TrimSpace(cell), columnName) {
colIndex = j
break
}
}
if colIndex != -1 {
break
}
}
}
if colIndex == -1 {
return 0, fmt.Errorf("未找到列 %s 在工作表 %s", columnName, sheetName)
}
for _, row := range data {
if len(row) > 0 && strings.Contains(strings.TrimSpace(row[0]), itemName) {
if len(row) > colIndex {
value := strings.TrimSpace(row[colIndex])
return parseFinancialValue(value)
}
return 0, fmt.Errorf("列索引 %d 处未找到值", colIndex)
}
}
return 0, fmt.Errorf("未找到项目 %s 在工作表 %s", itemName, sheetName)
}
func parseFinancialValue(value string) (float64, error) {
re := regexp.MustCompile(`([\d.,]+)\s*万`)
if match := re.FindStringSubmatch(value); len(match) == 2 {
numStr := strings.ReplaceAll(match[1], ",", "")
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, err
}
return num * 10000, nil
}
value = strings.ReplaceAll(value, ",", "")
value = strings.ReplaceAll(value, "元", "")
value = strings.ReplaceAll(value, "万元", "")
value = strings.TrimSpace(value)
if value == "" || value == "-" || value == "无" {
return 0, nil
}
return strconv.ParseFloat(value, 64)
}

View File

@@ -1,251 +0,0 @@
package main
import (
"fmt"
"os"
"testing"
)
func TestNewXLSXReader(t *testing.T) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
t.Skip("测试文件不存在,跳过测试")
}
reader, err := NewXLSXReader(testFile)
if err != nil {
t.Fatalf("创建XLSX读取器失败: %v", err)
}
if reader.files == nil {
t.Error("文件映射为空")
}
t.Logf("成功读取 %d 个文件", len(reader.files))
}
func TestGetSheetNames(t *testing.T) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
t.Skip("测试文件不存在,跳过测试")
}
reader, err := NewXLSXReader(testFile)
if err != nil {
t.Fatalf("创建XLSX读取器失败: %v", err)
}
sheets, err := reader.GetSheetNames()
if err != nil {
t.Fatalf("获取工作表名称失败: %v", err)
}
t.Logf("工作表列表: %v", sheets)
if len(sheets) == 0 {
t.Error("未找到任何工作表")
}
}
func TestGetSheetData(t *testing.T) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
t.Skip("测试文件不存在,跳过测试")
}
reader, err := NewXLSXReader(testFile)
if err != nil {
t.Fatalf("创建XLSX读取器失败: %v", err)
}
sheets, err := reader.GetSheetNames()
if err != nil || len(sheets) == 0 {
t.Skip("未找到工作表,跳过测试")
}
targetSheet := ""
for _, sheet := range sheets {
if sheet == "资产负债表" || sheet == "利润表" || sheet == "现金流量表" {
targetSheet = sheet
break
}
}
if targetSheet == "" {
targetSheet = sheets[0]
}
data, err := reader.GetSheetData(targetSheet)
if err != nil {
t.Fatalf("获取工作表数据失败: %v", err)
}
t.Logf("工作表 %s 有 %d 行数据", targetSheet, len(data))
if len(data) == 0 {
t.Error("工作表数据为空")
} else {
t.Logf("第一行数据: %v", data[0])
}
}
func TestGetCellValue(t *testing.T) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
t.Skip("测试文件不存在,跳过测试")
}
reader, err := NewXLSXReader(testFile)
if err != nil {
t.Fatalf("创建XLSX读取器失败: %v", err)
}
sheets, err := reader.GetSheetNames()
if err != nil || len(sheets) == 0 {
t.Skip("未找到工作表,跳过测试")
}
value, err := reader.GetCellValue(sheets[0], "A1")
if err != nil {
t.Fatalf("获取单元格值失败: %v", err)
}
t.Logf("单元格 A1 的值: %s", value)
}
func TestParseCellRef(t *testing.T) {
tests := []struct {
input string
wantRow int
wantCol int
}{
{"A1", 0, 0},
{"B2", 1, 1},
{"Z10", 9, 25},
{"AA1", 0, 26},
{"AB100", 99, 27},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
row, col := parseCellRef(tt.input)
if row != tt.wantRow {
t.Errorf("parseCellRef(%q) row = %d, want %d", tt.input, row, tt.wantRow)
}
if col != tt.wantCol {
t.Errorf("parseCellRef(%q) col = %d, want %d", tt.input, col, tt.wantCol)
}
})
}
}
func TestParseFinancialValue(t *testing.T) {
tests := []struct {
input string
want float64
wantErr bool
}{
{"100", 100, false},
{"100.5", 100.5, false},
{"100万", 1000000, false},
{"100.5万", 1005000, false},
{"1,234.56万", 12345600, false},
{"-", 0, false},
{"", 0, false},
{"无", 0, false},
{"100元", 100, false},
{"100万元", 1000000, false},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%q", tt.input), func(t *testing.T) {
got, err := parseFinancialValue(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseFinancialValue(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseFinancialValue(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestExtractFinancialDataPureGo(t *testing.T) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
t.Skip("测试文件不存在,跳过测试")
}
reader, err := NewXLSXReader(testFile)
if err != nil {
t.Fatalf("创建XLSX读取器失败: %v", err)
}
sheets, err := reader.GetSheetNames()
if err != nil || len(sheets) == 0 {
t.Skip("未找到工作表,跳过测试")
}
var targetSheet string
for _, sheet := range sheets {
if sheet == "资产负债表" {
targetSheet = sheet
break
}
}
if targetSheet == "" {
t.Skip("未找到资产负债表,跳过测试")
}
data, err := reader.GetSheetData(targetSheet)
if err != nil {
t.Fatalf("获取工作表数据失败: %v", err)
}
if len(data) > 0 && len(data[0]) > 0 {
t.Logf("成功读取工作表 %s,共 %d 行", targetSheet, len(data))
t.Logf("表头: %v", data[0])
}
}
func BenchmarkXLSXReader(b *testing.B) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
b.Skip("测试文件不存在")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader, _ := NewXLSXReader(testFile)
if reader != nil {
reader.GetSheetNames()
}
}
}
func BenchmarkGetSheetData(b *testing.B) {
testFile := "yst郑州惠兴财务报表2021(3).xlsx"
if _, err := os.Stat(testFile); os.IsNotExist(err) {
b.Skip("测试文件不存在")
}
reader, _ := NewXLSXReader(testFile)
sheets, _ := reader.GetSheetNames()
if len(sheets) == 0 {
b.Skip("未找到工作表")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader.GetSheetData(sheets[0])
}
}