diff --git a/build_wasm.bat b/build_wasm.bat new file mode 100644 index 0000000..8580108 --- /dev/null +++ b/build_wasm.bat @@ -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 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..3875ecf --- /dev/null +++ b/cmd/main.go @@ -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 +} diff --git a/cmd/test/main.go b/cmd/test/main.go new file mode 100644 index 0000000..d6ad3fc --- /dev/null +++ b/cmd/test/main.go @@ -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") + } +} diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go new file mode 100644 index 0000000..a05ec25 --- /dev/null +++ b/cmd/wasm/main.go @@ -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 +} diff --git a/go.mod b/go.mod index 61b4363..f9add2b 100644 --- a/go.mod +++ b/go.mod @@ -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 -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index b1faaff..b379cd2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/api/classifier.go b/internal/api/classifier.go new file mode 100644 index 0000000..c1a558a --- /dev/null +++ b/internal/api/classifier.go @@ -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 +} diff --git a/internal/api/client.go b/internal/api/client.go new file mode 100644 index 0000000..44d60e1 --- /dev/null +++ b/internal/api/client.go @@ -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) + } + } + } +} diff --git a/internal/api/http.go b/internal/api/http.go new file mode 100644 index 0000000..83c6f46 --- /dev/null +++ b/internal/api/http.go @@ -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) +} diff --git a/internal/api/llm.go b/internal/api/llm.go new file mode 100644 index 0000000..ea7a7ef --- /dev/null +++ b/internal/api/llm.go @@ -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, + }, +} diff --git a/internal/api/tax_health_test.go b/internal/api/tax_health_test.go new file mode 100644 index 0000000..c981566 --- /dev/null +++ b/internal/api/tax_health_test.go @@ -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)) +} diff --git a/main.go b/internal/excel/loader.go similarity index 78% rename from main.go rename to internal/excel/loader.go index c38b737..8076090 100644 --- a/main.go +++ b/internal/excel/loader.go @@ -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 -} diff --git a/annual.txt b/resource/annual.txt similarity index 97% rename from annual.txt rename to resource/annual.txt index 7ec4e2e..f389b67 100644 --- a/annual.txt +++ b/resource/annual.txt @@ -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} 对应关系规则如下: diff --git a/quarterly1.txt b/resource/quarterly1.txt similarity index 97% rename from quarterly1.txt rename to resource/quarterly1.txt index b32ca34..948323a 100644 --- a/quarterly1.txt +++ b/resource/quarterly1.txt @@ -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} diff --git a/quarterly2.txt b/resource/quarterly2.txt similarity index 97% rename from quarterly2.txt rename to resource/quarterly2.txt index 5855fc6..6e14db5 100644 --- a/quarterly2.txt +++ b/resource/quarterly2.txt @@ -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} 对应关系规则如下: diff --git a/quarterly3.txt b/resource/quarterly3.txt similarity index 97% rename from quarterly3.txt rename to resource/quarterly3.txt index 101a0a1..4d186d7 100644 --- a/quarterly3.txt +++ b/resource/quarterly3.txt @@ -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} 对应关系规则如下: diff --git a/sign_utils.go b/sign_utils.go deleted file mode 100644 index 23fda91..0000000 --- a/sign_utils.go +++ /dev/null @@ -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 -} diff --git a/tax_eval_api.go b/tax_eval_api.go deleted file mode 100644 index e0989af..0000000 --- a/tax_eval_api.go +++ /dev/null @@ -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 -} diff --git a/tax_eval_test.go b/tax_eval_test.go deleted file mode 100644 index c8f0f91..0000000 --- a/tax_eval_test.go +++ /dev/null @@ -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) - } -} diff --git a/test_sign.go b/test_sign.go deleted file mode 100644 index ba83df2..0000000 --- a/test_sign.go +++ /dev/null @@ -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) -} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..0ffa120 --- /dev/null +++ b/web/README.md @@ -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调用等) \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..bf845bd --- /dev/null +++ b/web/index.html @@ -0,0 +1,546 @@ + + + + + + WASM企业分析工具套件 + + + +
+

🏭 WASM企业分析工具套件

+

基于WebAssembly的多功能企业数据分析平台

+
+ +
+
+
+ 📊 +

行业分析

+

基于企业信息的国民经济行业分类

+
+
+ 📄 +

Excel解析

+

在浏览器中直接解析Excel文件

+
+
+ 🤖 +

AI分析

+

利用大模型进行智能数据分析

+
+
+ 💰 +

税务健康

+

企业税务健康状况评估

+
+
+ +
+ + + + +
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ +
+

🔄 正在处理请求...请稍候

+
+ +
+ +
+

📊 分析结果

+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/web/wasm_exec.js b/web/wasm_exec.js new file mode 100644 index 0000000..d71af9e --- /dev/null +++ b/web/wasm_exec.js @@ -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; + }; + } + } +})(); diff --git a/xlsx_reader.go b/xlsx_reader.go deleted file mode 100644 index a82073d..0000000 --- a/xlsx_reader.go +++ /dev/null @@ -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) -} diff --git a/xlsx_reader_test.go b/xlsx_reader_test.go deleted file mode 100644 index cd8a980..0000000 --- a/xlsx_reader_test.go +++ /dev/null @@ -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]) - } -} diff --git a/税务健康测评API/税务健康输入数据字段表_一季度.docx b/税务健康测评API/税务健康输入数据字段表_一季度.docx deleted file mode 100644 index ab9f24e..0000000 Binary files a/税务健康测评API/税务健康输入数据字段表_一季度.docx and /dev/null differ diff --git a/税务健康测评API/税务健康输入数据字段表_三季度.docx b/税务健康测评API/税务健康输入数据字段表_三季度.docx deleted file mode 100644 index 7e49b62..0000000 Binary files a/税务健康测评API/税务健康输入数据字段表_三季度.docx and /dev/null differ diff --git a/税务健康测评API/税务健康输入数据字段表_二季度.docx b/税务健康测评API/税务健康输入数据字段表_二季度.docx deleted file mode 100644 index 1596c3d..0000000 Binary files a/税务健康测评API/税务健康输入数据字段表_二季度.docx and /dev/null differ diff --git a/税务健康测评API/税务健康输入数据字段表_四季度.docx b/税务健康测评API/税务健康输入数据字段表_四季度.docx deleted file mode 100644 index 11a692c..0000000 Binary files a/税务健康测评API/税务健康输入数据字段表_四季度.docx and /dev/null differ diff --git a/税务健康测评API/税务健康输入数据字段表_年度.docx b/税务健康测评API/税务健康输入数据字段表_年度.docx deleted file mode 100644 index ffccbfd..0000000 Binary files a/税务健康测评API/税务健康输入数据字段表_年度.docx and /dev/null differ