update
This commit is contained in:
28
build_wasm.bat
Normal file
28
build_wasm.bat
Normal file
@@ -0,0 +1,28 @@
|
||||
@echo off
|
||||
REM 编译Go代码为WASM的批处理脚本
|
||||
|
||||
echo 正在编译Go代码为WASM...
|
||||
|
||||
REM 检查是否安装了Go
|
||||
go version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 未找到Go。请先安装Go并将其添加到PATH。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 设置目标目录
|
||||
set TARGET_DIR=E:\code\go\wasm-go\web
|
||||
|
||||
REM 检查目标目录是否存在,不存在则创建
|
||||
if not exist "%TARGET_DIR%" (
|
||||
mkdir "%TARGET_DIR%"
|
||||
)
|
||||
|
||||
REM 编译Go代码为WASM
|
||||
echo 编译WASM...
|
||||
set GOOS=js
|
||||
set GOARCH=wasm
|
||||
go build -o "%TARGET_DIR%\main.wasm" E:\code\go\wasm-go\cmd\wasm\main.go
|
||||
|
||||
pause
|
||||
188
cmd/main.go
Normal file
188
cmd/main.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"wasm-go/internal/api"
|
||||
"wasm-go/internal/excel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("========== Excel转JSON ==========")
|
||||
|
||||
excelFile := "yst郑州惠兴财务报表2021(3).xlsx"
|
||||
|
||||
// 检查Excel文件是否存在
|
||||
if _, err := os.Stat(excelFile); os.IsNotExist(err) {
|
||||
fmt.Printf("❌ Excel文件不存在: %s\n", excelFile)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取Excel文件为bytes
|
||||
excelBytes, err := os.ReadFile(excelFile)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 读取Excel文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("✓ 读取Excel文件为字节成功")
|
||||
|
||||
// 使用NewXLSXReaderFromBytes从bytes创建读取器
|
||||
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 创建XLSX读取器失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("✓ 创建XLSX读取器成功")
|
||||
|
||||
// 读取所有的sheets并转换为markdown格式
|
||||
var allMarkdownTables strings.Builder
|
||||
allMarkdownTables.WriteString("以下是Excel表格的数据:\n\n")
|
||||
|
||||
sheets, err := reader.GetSheetNames()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 获取工作表名称失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, sheetName := range sheets {
|
||||
fmt.Printf("\n========== 工作表: %s ==========\n", sheetName)
|
||||
|
||||
data, err := reader.GetSheetData(sheetName)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 读取工作表失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
fmt.Println("⚠️ 工作表为空")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("找到 %d 行数据\n", len(data))
|
||||
|
||||
// 转换为markdown格式
|
||||
markdown := excel.ConvertToMarkdown(data)
|
||||
allMarkdownTables.WriteString(fmt.Sprintf("## 工作表: %s\n", sheetName))
|
||||
allMarkdownTables.WriteString(markdown)
|
||||
allMarkdownTables.WriteString("\n\n")
|
||||
}
|
||||
|
||||
fmt.Println("\n✅ 所有工作表已转换为Markdown格式!")
|
||||
|
||||
// 读取quarterly1.txt的prompt
|
||||
promptBytes, err := os.ReadFile("./quarterly1.txt")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 读取quarterly1.txt失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
prompt := string(promptBytes)
|
||||
|
||||
// 合并prompt和markdown表格
|
||||
finalPrompt := fmt.Sprintf("%s\n\n%s", prompt, allMarkdownTables.String())
|
||||
|
||||
// 尝试连接到不同的LLM提供商
|
||||
var response *api.ChatResponse
|
||||
var llmErr error
|
||||
|
||||
// 首先尝试Ollama
|
||||
fmt.Println("\n🔄 尝试连接到Ollama服务...")
|
||||
if ollamaClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b"); ollamaClient != nil {
|
||||
response, llmErr = ollamaClient.Chat([]api.ChatMessage{
|
||||
{Role: "user", Content: finalPrompt},
|
||||
})
|
||||
if llmErr == nil {
|
||||
fmt.Println("✓ 成功使用Ollama生成响应")
|
||||
} else {
|
||||
fmt.Printf("⚠ Ollama调用失败: %v\n", llmErr)
|
||||
|
||||
// 检查是否是连接问题
|
||||
if strings.Contains(llmErr.Error(), "connection refused") || strings.Contains(llmErr.Error(), "404") {
|
||||
fmt.Println("💡 提示:Ollama服务可能未启动,或模型未安装")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n========== LLM生成的JSON ==========")
|
||||
// 解析LLM返回的响应,提取```json{内容}```中的JSON部分
|
||||
parsedJSON := extractJSONFromResponse(response.Choices[0].Message.Content)
|
||||
fmt.Println(parsedJSON)
|
||||
fmt.Println("==================================")
|
||||
|
||||
// 使用解析出的JSON调用税务健康API
|
||||
fmt.Println("\n🔄 正在使用解析的JSON调用税务健康API...")
|
||||
|
||||
// 创建税务健康服务客户端
|
||||
// 注意:这里需要替换为实际的用户ID和私钥
|
||||
taxHealthService := api.NewTaxHealthService("1560142251348287489", "")
|
||||
|
||||
// 将解析的JSON解析为map结构
|
||||
var companyData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(parsedJSON), &companyData); err != nil {
|
||||
fmt.Printf("❌ 解析JSON数据失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建税务评估请求
|
||||
request := api.TaxEvalRequest{
|
||||
ReqNo: fmt.Sprintf("REQ%v", time.Now().Unix()), // 使用时间戳生成请求号
|
||||
Detail: api.TaxEvalDetail{
|
||||
CompanyName: "企业名称", // 可以从数据中提取公司名称
|
||||
Data: companyData,
|
||||
},
|
||||
}
|
||||
|
||||
// 调用税务健康API
|
||||
taxResponse, err := taxHealthService.EvaluateTaxHealth(request)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 调用税务健康API失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n========== 税务健康API反馈结果 ==========")
|
||||
fmt.Printf("API响应: %+v\n", taxResponse)
|
||||
fmt.Println("======================================")
|
||||
}
|
||||
|
||||
// extractJSONFromResponse 从LLM响应中提取JSON内容
|
||||
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
|
||||
func extractJSONFromResponse(response string) string {
|
||||
// 首先尝试匹配 ```json 开头的代码块
|
||||
start := strings.Index(response, "```json")
|
||||
if start != -1 {
|
||||
start += 7 // 跳过 "```json"
|
||||
end := strings.Index(response[start:], "```")
|
||||
if end != -1 {
|
||||
jsonContent := strings.TrimSpace(response[start : start+end])
|
||||
return jsonContent
|
||||
}
|
||||
}
|
||||
|
||||
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
|
||||
start = strings.Index(response, "```")
|
||||
if start != -1 {
|
||||
start += 3 // 跳过 "```"
|
||||
end := strings.Index(response[start:], "```")
|
||||
if end != -1 {
|
||||
jsonContent := strings.TrimSpace(response[start : start+end])
|
||||
// 检查是否看起来像JSON
|
||||
jsonContent = strings.TrimSpace(jsonContent)
|
||||
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
|
||||
return jsonContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
|
||||
response = strings.TrimSpace(response)
|
||||
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
|
||||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
|
||||
return response
|
||||
}
|
||||
|
||||
// 如果以上都失败,返回原内容
|
||||
return response
|
||||
}
|
||||
55
cmd/test/main.go
Normal file
55
cmd/test/main.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"wasm-go/internal/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== 企业行业分类示例 ===")
|
||||
|
||||
// 创建LLM客户端 - 这里使用Ollama作为示例
|
||||
// 如果Ollama不可用,也可以使用OpenAI
|
||||
var llmClient *api.LLMClient
|
||||
|
||||
// 尝试连接到Ollama
|
||||
fmt.Println("尝试连接到Ollama服务...")
|
||||
llmClient = api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
|
||||
|
||||
// 如果需要使用OpenAI,取消下面的注释并替换API KEY
|
||||
/*
|
||||
llmClient = api.NewOpenAIClient("your-openai-api-key", "gpt-3.5-turbo")
|
||||
*/
|
||||
|
||||
// 创建企业行业分类器
|
||||
classifier := api.NewCompanyIndustryClassifier(llmClient)
|
||||
|
||||
// 示例公司名称 - 可以从命令行参数获取
|
||||
companyName := "深圳市致远速联信息技术有限公司"
|
||||
|
||||
fmt.Printf("正在分析公司: %s\n", companyName)
|
||||
|
||||
// 分析企业行业
|
||||
result, err := classifier.ClassifyCompany(companyName)
|
||||
if err != nil {
|
||||
log.Printf("分析失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
fmt.Printf("\n✅ 企业行业分析完成!\n")
|
||||
fmt.Printf("公司名称: %s\n", result.CompanyName)
|
||||
fmt.Printf("主要行业: %s - %s\n", result.Primary.Code, result.Primary.Name)
|
||||
fmt.Printf("判断依据: %s\n", result.Primary.Reason)
|
||||
|
||||
if len(result.Secondary) > 0 {
|
||||
fmt.Printf("次要行业:\n")
|
||||
for _, sec := range result.Secondary {
|
||||
fmt.Printf(" %s - %s\n", sec.Code, sec.Name)
|
||||
fmt.Printf(" 判断依据: %s\n", sec.Reason)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("次要行业: 无\n")
|
||||
}
|
||||
}
|
||||
334
cmd/wasm/main.go
Normal file
334
cmd/wasm/main.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
"wasm-go/internal/api"
|
||||
"wasm-go/internal/excel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 注册各种功能函数
|
||||
js.Global().Set("analyzeCompanyIndustryAsync", js.FuncOf(analyzeCompanyIndustryAsync))
|
||||
js.Global().Set("parseExcelAsync", js.FuncOf(parseExcelAsync))
|
||||
js.Global().Set("callLLMAsync", js.FuncOf(callLLMAsync))
|
||||
js.Global().Set("callTaxHealthAsync", js.FuncOf(callTaxHealthAsync))
|
||||
|
||||
// 保持程序运行
|
||||
select {} // 无限循环,保持程序运行
|
||||
}
|
||||
|
||||
// analyzeCompanyIndustryAsync 异步版本的函数,返回Promise
|
||||
func analyzeCompanyIndustryAsync(this js.Value, args []js.Value) interface{} {
|
||||
// 获取公司名称参数
|
||||
companyName := args[0].String()
|
||||
|
||||
// 创建一个通道来处理结果
|
||||
resultChan := make(chan interface{}, 1)
|
||||
errorChan := make(chan error, 1) // 使用error类型而不是interface{}
|
||||
|
||||
// 在goroutine中执行异步操作
|
||||
go func() {
|
||||
// 创建LLM客户端
|
||||
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
|
||||
|
||||
// 创建企业行业分类器
|
||||
classifier := api.NewCompanyIndustryClassifier(llmClient)
|
||||
|
||||
// 分析企业行业
|
||||
result, err := classifier.ClassifyCompany(companyName)
|
||||
if err != nil {
|
||||
// 发送错误到错误通道
|
||||
errorChan <- err
|
||||
} else {
|
||||
// 发送结果到结果通道
|
||||
resultChan <- result
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建Promise
|
||||
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
|
||||
if len(promiseArgs) < 2 {
|
||||
// 如果参数不足,返回一个立即拒绝的Promise
|
||||
return nil
|
||||
}
|
||||
|
||||
resolve := promiseArgs[0]
|
||||
reject := promiseArgs[1]
|
||||
|
||||
// 启动一个goroutine来处理结果
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
// 将复杂结构体序列化为JSON
|
||||
jsonResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// 序列化失败,返回错误
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": "JSON序列化失败: " + err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 将JSON字符串解析为JavaScript对象
|
||||
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
|
||||
resolve.Invoke(jsResult)
|
||||
case err := <-errorChan:
|
||||
// 将错误转换为JavaScript值
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": err.Error(),
|
||||
}))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// parseExcelAsync 解析Excel文件的异步函数
|
||||
func parseExcelAsync(this js.Value, args []js.Value) interface{} {
|
||||
// 获取Excel文件的ArrayBuffer
|
||||
arrayBuffer := args[0]
|
||||
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
|
||||
|
||||
// 创建字节切片
|
||||
length := uint8Array.Get("length").Int()
|
||||
excelBytes := make([]byte, length)
|
||||
|
||||
// 将数据复制到Go字节切片
|
||||
for i := 0; i < length; i++ {
|
||||
excelBytes[i] = byte(uint8Array.Index(i).Int())
|
||||
}
|
||||
|
||||
// 创建一个通道来处理结果
|
||||
resultChan := make(chan interface{}, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
// 在goroutine中执行异步操作
|
||||
go func() {
|
||||
// 创建Excel读取器
|
||||
reader, err := excel.NewXLSXReaderFromBytes(excelBytes)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// 读取所有工作表
|
||||
sheetNames, err := reader.GetSheetNames()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// 创建一个map来存储所有工作表的数据
|
||||
sheets := make(map[string][][]string)
|
||||
|
||||
// 读取每个工作表的数据
|
||||
for _, sheetName := range sheetNames {
|
||||
sheetData, err := reader.GetSheetData(sheetName)
|
||||
if err != nil {
|
||||
// 记录错误但继续处理其他工作表
|
||||
continue
|
||||
}
|
||||
sheets[sheetName] = sheetData
|
||||
}
|
||||
|
||||
// 将结果转换为适当的格式
|
||||
result := map[string]interface{}{
|
||||
"sheets": sheets,
|
||||
}
|
||||
|
||||
resultChan <- result
|
||||
}()
|
||||
|
||||
// 创建Promise
|
||||
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
|
||||
if len(promiseArgs) < 2 {
|
||||
// 如果参数不足,返回一个立即拒绝的Promise
|
||||
return nil
|
||||
}
|
||||
|
||||
resolve := promiseArgs[0]
|
||||
reject := promiseArgs[1]
|
||||
|
||||
// 启动一个goroutine来处理结果
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
// 将复杂结构体序列化为JSON
|
||||
jsonResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// 序列化失败,返回错误
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": "JSON序列化失败: " + err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 将JSON字符串解析为JavaScript对象
|
||||
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
|
||||
resolve.Invoke(jsResult)
|
||||
case err := <-errorChan:
|
||||
// 将错误转换为JavaScript值
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": err.Error(),
|
||||
}))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// callLLMAsync 调用大模型的异步函数
|
||||
func callLLMAsync(this js.Value, args []js.Value) interface{} {
|
||||
// 获取提示词参数
|
||||
prompt := args[0].String()
|
||||
|
||||
// 创建一个通道来处理结果
|
||||
resultChan := make(chan interface{}, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
// 在goroutine中执行异步操作
|
||||
go func() {
|
||||
// 创建LLM客户端(可以根据需要选择不同的模型)
|
||||
llmClient := api.NewOllamaClient("http://localhost:11434", "qwen3.5:9b")
|
||||
|
||||
// 调用大模型
|
||||
response, err := llmClient.SimpleChat(prompt)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"response": response,
|
||||
}
|
||||
|
||||
resultChan <- result
|
||||
}()
|
||||
|
||||
// 创建Promise
|
||||
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
|
||||
if len(promiseArgs) < 2 {
|
||||
// 如果参数不足,返回一个立即拒绝的Promise
|
||||
return nil
|
||||
}
|
||||
|
||||
resolve := promiseArgs[0]
|
||||
reject := promiseArgs[1]
|
||||
|
||||
// 启动一个goroutine来处理结果
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
// 将复杂结构体序列化为JSON
|
||||
jsonResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// 序列化失败,返回错误
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": "JSON序列化失败: " + err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 将JSON字符串解析为JavaScript对象
|
||||
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
|
||||
resolve.Invoke(jsResult)
|
||||
case err := <-errorChan:
|
||||
// 将错误转换为JavaScript值
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": err.Error(),
|
||||
}))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// callTaxHealthAsync 调用税务健康API的异步函数
|
||||
// 注意:由于WASM环境的CORS限制,我们在这里提供模拟实现
|
||||
func callTaxHealthAsync(this js.Value, args []js.Value) interface{} {
|
||||
// 获取公司信息参数
|
||||
companyInfoJSON := args[0].String()
|
||||
|
||||
// 创建一个通道来处理结果
|
||||
resultChan := make(chan interface{}, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
// 在goroutine中执行异步操作
|
||||
go func() {
|
||||
// 由于WASM环境无法直接调用外部API(CORS限制)
|
||||
// 我们返回一个模拟的响应
|
||||
simulatedResponse := map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"req_no": "REQ" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
|
||||
"order_no": "ORDER" + fmt.Sprint(js.Global().Get("Date").New().Call("getTime").Int()),
|
||||
},
|
||||
"message": "模拟的税务健康评估结果",
|
||||
}
|
||||
|
||||
// 解析输入的JSON以获取一些信息用于模拟
|
||||
var companyInfo map[string]interface{}
|
||||
err := json.Unmarshal([]byte(companyInfoJSON), &companyInfo)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// 根据输入数据添加一些模拟的评估结果
|
||||
simulatedResponse["company_data"] = companyInfo
|
||||
|
||||
resultChan <- simulatedResponse
|
||||
}()
|
||||
|
||||
// 创建Promise
|
||||
promise := js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
|
||||
if len(promiseArgs) < 2 {
|
||||
// 如果参数不足,返回一个立即拒绝的Promise
|
||||
return nil
|
||||
}
|
||||
|
||||
resolve := promiseArgs[0]
|
||||
reject := promiseArgs[1]
|
||||
|
||||
// 启动一个goroutine来处理结果
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
// 将复杂结构体序列化为JSON
|
||||
jsonResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// 序列化失败,返回错误
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": "JSON序列化失败: " + err.Error(),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 将JSON字符串解析为JavaScript对象
|
||||
jsResult := js.Global().Get("JSON").Call("parse", string(jsonResult))
|
||||
resolve.Invoke(jsResult)
|
||||
case err := <-errorChan:
|
||||
// 将错误转换为JavaScript值
|
||||
reject.Invoke(js.ValueOf(map[string]string{
|
||||
"error": err.Error(),
|
||||
}))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
return promise
|
||||
}
|
||||
15
go.mod
15
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
|
||||
)
|
||||
)
|
||||
34
go.sum
34
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=
|
||||
|
||||
309
internal/api/classifier.go
Normal file
309
internal/api/classifier.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CompanyIndustryClassifier 企业行业分类器
|
||||
type CompanyIndustryClassifier struct {
|
||||
httpClient *http.Client
|
||||
llmClient *LLMClient
|
||||
}
|
||||
|
||||
// CompanyData 企业数据
|
||||
type CompanyData struct {
|
||||
Description string `json:"description"`
|
||||
Business string `json:"business"`
|
||||
BiddingTitles []string `json:"bidding_titles"`
|
||||
}
|
||||
|
||||
// IndustryClassification 行业分类结果
|
||||
type IndustryClassification struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
Primary PrimaryInfo `json:"primary"`
|
||||
Secondary []Secondary `json:"secondary"`
|
||||
}
|
||||
|
||||
// PrimaryInfo 主要行业信息
|
||||
type PrimaryInfo struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Secondary 次要行业信息
|
||||
type Secondary struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// NewCompanyIndustryClassifier 创建企业行业分类器
|
||||
func NewCompanyIndustryClassifier(llmClient *LLMClient) *CompanyIndustryClassifier {
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return &CompanyIndustryClassifier{
|
||||
httpClient: client,
|
||||
llmClient: llmClient,
|
||||
}
|
||||
}
|
||||
|
||||
// getDescriptionText 获取简介文本
|
||||
func getDescriptionText(description string) string {
|
||||
if description == "" {
|
||||
return "(无简介)"
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
// getBusinessText 获取经营范围文本
|
||||
func getBusinessText(business string) string {
|
||||
if business == "" {
|
||||
return "(无经营范围)"
|
||||
}
|
||||
return business
|
||||
}
|
||||
|
||||
// FetchCompanyData 从cmdp API获取企业工商和招投标数据
|
||||
func (c *CompanyIndustryClassifier) FetchCompanyData(companyName string) (map[string]interface{}, error) {
|
||||
apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom"
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("name", companyName)
|
||||
params.Set("api", "p_info,p_biddings_all")
|
||||
|
||||
fullURL := apiURL + "?" + params.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应JSON失败: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExtractFields 从API响应中提取关键字段
|
||||
func (c *CompanyIndustryClassifier) ExtractFields(data map[string]interface{}) (string, string, []string) {
|
||||
description := ""
|
||||
business := ""
|
||||
var biddingTitles []string
|
||||
|
||||
// 兼容不同的响应结构
|
||||
payload := data
|
||||
if data["data"] != nil {
|
||||
if payloadData, ok := data["data"].(map[string]interface{}); ok {
|
||||
payload = payloadData
|
||||
}
|
||||
}
|
||||
|
||||
// p_info -> description, business
|
||||
if pInfo := payload["p_info"]; pInfo != nil {
|
||||
var pInfoObj map[string]interface{}
|
||||
|
||||
switch v := pInfo.(type) {
|
||||
case []interface{}:
|
||||
if len(v) > 0 {
|
||||
if item, ok := v[0].(map[string]interface{}); ok {
|
||||
pInfoObj = item
|
||||
}
|
||||
}
|
||||
case map[string]interface{}:
|
||||
pInfoObj = v
|
||||
}
|
||||
|
||||
if pInfoObj != nil {
|
||||
if desc, ok := pInfoObj["description"].(string); ok {
|
||||
description = desc
|
||||
}
|
||||
if bus, ok := pInfoObj["business"].(string); ok {
|
||||
business = bus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// p_biddings_all -> title list
|
||||
if biddingsRaw := payload["p_biddings_all"]; biddingsRaw != nil {
|
||||
var biddingsList []interface{}
|
||||
|
||||
switch v := biddingsRaw.(type) {
|
||||
case []interface{}:
|
||||
biddingsList = v
|
||||
case map[string]interface{}:
|
||||
// 尝试从data或list字段获取
|
||||
if dataField, ok := v["data"].([]interface{}); ok {
|
||||
biddingsList = dataField
|
||||
} else if listField, ok := v["list"].([]interface{}); ok {
|
||||
biddingsList = listField
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range biddingsList {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if title, ok := itemMap["title"].(string); ok && strings.TrimSpace(title) != "" {
|
||||
biddingTitles = append(biddingTitles, strings.TrimSpace(title))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return description, business, biddingTitles
|
||||
}
|
||||
|
||||
// BuildPrompt 构建提示词
|
||||
func (c *CompanyIndustryClassifier) BuildPrompt(companyName, description, business string, biddingTitles []string) string {
|
||||
titlesText := "(无招投标记录)"
|
||||
if len(biddingTitles) > 0 {
|
||||
var sb strings.Builder
|
||||
for i, title := range biddingTitles {
|
||||
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, title))
|
||||
}
|
||||
titlesText = sb.String()
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是国民经济行业分类专家,请根据以下企业信息,依据《国民经济行业分类》(GB/T 4754-2017)进行行业大类判断。
|
||||
|
||||
## 企业名称
|
||||
%s
|
||||
|
||||
## 企业简介(description)
|
||||
%s
|
||||
|
||||
## 经营范围(business)
|
||||
%s
|
||||
|
||||
## 招投标项目标题(bidding titles)
|
||||
%s
|
||||
|
||||
## 任务要求
|
||||
1. 判断该企业**主要**所属的行业大类(两位数字编号 + 名称)
|
||||
2. 如有次要业务涉及其他大类,也一并列出(最多2个)
|
||||
3. 给出简要判断依据(不超过100字)
|
||||
|
||||
## 输出格式(严格 JSON,不要 Markdown 代码块)
|
||||
{
|
||||
"company_name": "%s",
|
||||
"primary": {
|
||||
"code": "65",
|
||||
"name": "软件和信息技术服务业",
|
||||
"reason": "判断依据..."
|
||||
},
|
||||
"secondary": [
|
||||
{
|
||||
"code": "72",
|
||||
"name": "商务服务业",
|
||||
"reason": "轻微涉及原因..."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
注意:secondary 可以为空列表 [];只输出 JSON,不要任何额外说明。`,
|
||||
companyName,
|
||||
getDescriptionText(description),
|
||||
getBusinessText(business),
|
||||
titlesText,
|
||||
companyName)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// ClassifyCompany 完整流程:获取数据 → 提取字段 → 构建提示词 → 调用 LLM → 返回 JSON
|
||||
func (c *CompanyIndustryClassifier) ClassifyCompany(companyName string) (*IndustryClassification, error) {
|
||||
// 1. 获取企业数据
|
||||
rawData, err := c.FetchCompanyData(companyName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取企业数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 提取关键字段
|
||||
description, business, biddingTitles := c.ExtractFields(rawData)
|
||||
|
||||
// 3. 构建提示词
|
||||
prompt := c.BuildPrompt(companyName, description, business, biddingTitles)
|
||||
// 4. 调用LLM并解析JSON
|
||||
response, err := c.llmClient.Chat([]ChatMessage{
|
||||
{Role: "user", Content: prompt},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("调用LLM失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取JSON部分(如果LLM返回了markdown代码块)
|
||||
jsonStr := extractJSONFromResponse(response.Choices[0].Message.Content)
|
||||
|
||||
var result IndustryClassification
|
||||
err = json.Unmarshal([]byte(jsonStr), &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析LLM返回的JSON失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// extractJSONFromResponse 从LLM响应中提取JSON内容
|
||||
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
|
||||
func extractJSONFromResponse(response string) string {
|
||||
// 首先尝试匹配 ```json 开头的代码块
|
||||
start := strings.Index(response, "```json")
|
||||
if start != -1 {
|
||||
start += 7 // 跳过 "```json"
|
||||
end := strings.Index(response[start:], "```")
|
||||
if end != -1 {
|
||||
jsonContent := strings.TrimSpace(response[start : start+end])
|
||||
return jsonContent
|
||||
}
|
||||
}
|
||||
|
||||
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
|
||||
start = strings.Index(response, "```")
|
||||
if start != -1 {
|
||||
start += 3 // 跳过 "```"
|
||||
end := strings.Index(response[start:], "```")
|
||||
if end != -1 {
|
||||
jsonContent := strings.TrimSpace(response[start : start+end])
|
||||
// 检查是否看起来像JSON
|
||||
jsonContent = strings.TrimSpace(jsonContent)
|
||||
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
|
||||
return jsonContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
|
||||
response = strings.TrimSpace(response)
|
||||
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
|
||||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
|
||||
return response
|
||||
}
|
||||
|
||||
// 如果以上都失败,返回原内容
|
||||
return response
|
||||
}
|
||||
389
internal/api/client.go
Normal file
389
internal/api/client.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tjfoc/gmsm/sm2"
|
||||
"github.com/tjfoc/gmsm/sm3"
|
||||
)
|
||||
|
||||
// CompanyInfoService 公司信息查询服务
|
||||
type CompanyInfoService struct {
|
||||
httpClient *HTTPClient
|
||||
userID string
|
||||
privateKey string
|
||||
}
|
||||
|
||||
// TaxHealthService 税务健康测评服务
|
||||
type TaxHealthService struct {
|
||||
httpClient *HTTPClient
|
||||
userID string
|
||||
privateKey string
|
||||
}
|
||||
|
||||
// CompanyInfoRequest 公司信息查询请求
|
||||
type CompanyInfoRequest struct {
|
||||
Name string `json:"name"`
|
||||
Apis []string `json:"apis"`
|
||||
}
|
||||
|
||||
// CompanyInfoResponse 公司信息查询响应
|
||||
type CompanyInfoResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// TaxEvalRequest 税务测评请求
|
||||
type TaxEvalRequest struct {
|
||||
ReqNo string `json:"req_no"`
|
||||
Callback string `json:"callback,omitempty"`
|
||||
Detail TaxEvalDetail `json:"detail"`
|
||||
}
|
||||
|
||||
// TaxEvalDetail 税务测评详情
|
||||
type TaxEvalDetail struct {
|
||||
FileURL string `json:"file_url,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Period string `json:"period"`
|
||||
Year string `json:"year"`
|
||||
IsVerify bool `json:"is_verify,omitempty"`
|
||||
CIT float64 `json:"cit"`
|
||||
VAT float64 `json:"vat"`
|
||||
InputVAT float64 `json:"input_vat"`
|
||||
OutputVAT float64 `json:"output_vat"`
|
||||
PreCIT float64 `json:"pre_cit"`
|
||||
PreVAT float64 `json:"pre_vat"`
|
||||
PreInputVAT float64 `json:"pre_input_vat"`
|
||||
PreOutputVAT float64 `json:"pre_output_vat"`
|
||||
}
|
||||
|
||||
// TaxEvalResponse 税务测评响应
|
||||
type TaxEvalResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data struct {
|
||||
ReqNo string `json:"req_no"`
|
||||
OrderNo string `json:"order_no"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// TaxEvalQueryResponse 查询税务测评结果响应
|
||||
type TaxEvalQueryResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data struct {
|
||||
Status int `json:"status"`
|
||||
Detail struct {
|
||||
Files []string `json:"files"`
|
||||
Error string `json:"error"`
|
||||
} `json:"detail"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// NewCompanyInfoService 创建公司信息查询服务
|
||||
func NewCompanyInfoService(userID, privateKey string) *CompanyInfoService {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
httpClient := &HTTPClient{Client: client}
|
||||
|
||||
return &CompanyInfoService{
|
||||
httpClient: httpClient,
|
||||
userID: userID,
|
||||
privateKey: privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTaxHealthService 创建税务健康测评服务
|
||||
func NewTaxHealthService(userID, privateKey string) *TaxHealthService {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
httpClient := &HTTPClient{Client: client}
|
||||
|
||||
return &TaxHealthService{
|
||||
httpClient: httpClient,
|
||||
userID: userID,
|
||||
privateKey: privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCompanyInfo 查询公司信息
|
||||
func (s *CompanyInfoService) GetCompanyInfo(companyName string, apis []string) (*CompanyInfoResponse, error) {
|
||||
// 生成随机数和时间戳
|
||||
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
|
||||
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
// 构建查询参数
|
||||
params := url.Values{}
|
||||
params.Set("name", companyName)
|
||||
params.Set("api", joinAPIParams(apis))
|
||||
|
||||
apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom?" + params.Encode()
|
||||
|
||||
// 生成签名
|
||||
signData := s.userID + timestamp + nonce + apiURL
|
||||
signature, err := s.generateSignature([]byte(signData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成签名失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
headers := map[string]string{
|
||||
"Accept": "application/json",
|
||||
"x-gi-user-id": s.userID,
|
||||
"x-gi-user-sig": signature,
|
||||
"x-gi-nonce": nonce,
|
||||
"x-gi-timestamp": timestamp,
|
||||
}
|
||||
|
||||
// 发送GET请求
|
||||
response, err := s.httpClient.Get(apiURL, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
|
||||
var result CompanyInfoResponse
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// EvaluateTaxHealth 执行税务健康测评
|
||||
func (s *TaxHealthService) EvaluateTaxHealth(request TaxEvalRequest) (*TaxEvalResponse, error) {
|
||||
// 生成随机数和时间戳
|
||||
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
|
||||
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
apiURL := "https://api.cmdp.cn/v1/eval/tax-risk"
|
||||
|
||||
// 序列化请求体
|
||||
jsonData, err := json.Marshal(request)
|
||||
fmt.Println(string(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求体失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
signData := s.userID + timestamp + nonce + apiURL + string(jsonData)
|
||||
signature, err := s.generateSignature([]byte(signData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成签名失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"x-gi-user-id": s.userID,
|
||||
"x-gi-user-sig": signature,
|
||||
"x-gi-nonce": nonce,
|
||||
"x-gi-timestamp": timestamp,
|
||||
}
|
||||
// 发送POST请求
|
||||
response, err := s.httpClient.Post(apiURL, jsonData, "application/json", headers)
|
||||
fmt.Println(string(response))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
var result TaxEvalResponse
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// QueryTaxEvalResult 查询税务测评结果
|
||||
func (s *TaxHealthService) QueryTaxEvalResult(orderNo string) (*TaxEvalQueryResponse, error) {
|
||||
// 生成随机数和时间戳
|
||||
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
|
||||
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatInt(r.Int64(), 10)
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
apiURL := fmt.Sprintf("https://api.cmdp.cn/v1/eval/order/%s", orderNo)
|
||||
|
||||
// 生成签名
|
||||
signData := s.userID + timestamp + nonce + apiURL
|
||||
signature, err := s.generateSignature([]byte(signData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成签名失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
headers := map[string]string{
|
||||
"Accept": "application/json",
|
||||
"x-gi-user-id": s.userID,
|
||||
"x-gi-user-sig": signature,
|
||||
"x-gi-nonce": nonce,
|
||||
"x-gi-timestamp": timestamp,
|
||||
}
|
||||
|
||||
// 发送GET请求
|
||||
response, err := s.httpClient.Get(apiURL, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
|
||||
var result TaxEvalQueryResponse
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// generateSignature 生成签名
|
||||
func (s *CompanyInfoService) generateSignature(plainText []byte) (string, error) {
|
||||
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析私钥失败: %v", err)
|
||||
}
|
||||
|
||||
digest := sm3.Sm3Sum(plainText)
|
||||
signature, err := privateKey.Sign(rand.Reader, digest, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %v", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// generateSignature 生成签名
|
||||
func (s *TaxHealthService) generateSignature(plainText []byte) (string, error) {
|
||||
privateKey, err := parsePrivateKeyFromHex(s.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析私钥失败: %v", err)
|
||||
}
|
||||
|
||||
digest := sm3.Sm3Sum(plainText)
|
||||
signature, err := privateKey.Sign(rand.Reader, digest, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %v", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// parsePrivateKeyFromHex 从十六进制字符串解析私钥
|
||||
func parsePrivateKeyFromHex(privateKeyHex string) (*sm2.PrivateKey, error) {
|
||||
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码私钥失败: %v", err)
|
||||
}
|
||||
|
||||
d := new(big.Int).SetBytes(privateKeyBytes)
|
||||
|
||||
privateKey := &sm2.PrivateKey{
|
||||
PublicKey: sm2.PublicKey{
|
||||
Curve: sm2.P256Sm2(),
|
||||
},
|
||||
D: d,
|
||||
}
|
||||
|
||||
privateKey.PublicKey.X, privateKey.PublicKey.Y = privateKey.PublicKey.Curve.ScalarBaseMult(d.Bytes())
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// joinAPIParams 连接API参数
|
||||
func joinAPIParams(params []string) string {
|
||||
result := ""
|
||||
for i, param := range params {
|
||||
if i > 0 {
|
||||
result += ","
|
||||
}
|
||||
result += param
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
func ExampleUsage() {
|
||||
// 替换为实际的用户ID和私钥
|
||||
userID := "your_user_id"
|
||||
privateKey := "your_private_key_hex"
|
||||
|
||||
// 创建公司信息查询服务
|
||||
companyService := NewCompanyInfoService(userID, privateKey)
|
||||
|
||||
// 查询公司信息
|
||||
companyInfo, err := companyService.GetCompanyInfo("深圳市致远速联信息技术有限公司", []string{"p_info", "p_biddings_all"})
|
||||
if err != nil {
|
||||
fmt.Printf("查询公司信息失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("公司信息查询结果: %+v\n", companyInfo)
|
||||
}
|
||||
|
||||
// 创建税务健康测评服务
|
||||
taxService := NewTaxHealthService(userID, privateKey)
|
||||
|
||||
// 准备税务测评请求数据
|
||||
request := TaxEvalRequest{
|
||||
ReqNo: "REQ001",
|
||||
Detail: TaxEvalDetail{
|
||||
CompanyName: "深圳市致远速联信息技术有限公司",
|
||||
Period: "年度",
|
||||
Year: "2024",
|
||||
CIT: 100.0,
|
||||
VAT: 50.0,
|
||||
InputVAT: 30.0,
|
||||
OutputVAT: 80.0,
|
||||
PreCIT: 90.0,
|
||||
PreVAT: 45.0,
|
||||
PreInputVAT: 25.0,
|
||||
PreOutputVAT: 70.0,
|
||||
},
|
||||
}
|
||||
|
||||
// 执行税务测评
|
||||
evalResult, err := taxService.EvaluateTaxHealth(request)
|
||||
if err != nil {
|
||||
fmt.Printf("税务测评失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("税务测评提交结果: %+v\n", evalResult)
|
||||
|
||||
// 查询测评结果
|
||||
if evalResult.Data.OrderNo != "" {
|
||||
result, err := taxService.QueryTaxEvalResult(evalResult.Data.OrderNo)
|
||||
if err != nil {
|
||||
fmt.Printf("查询测评结果失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("测评结果: %+v\n", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
internal/api/http.go
Normal file
114
internal/api/http.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPClient 封装了常用的HTTP请求方法
|
||||
type HTTPClient struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// NewHTTPClient 创建一个新的HTTP客户端
|
||||
func NewHTTPClient() *HTTPClient {
|
||||
return &HTTPClient{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get 发送GET请求
|
||||
func (c *HTTPClient) Get(url string, headers map[string]string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建GET请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送GET请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("GET请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体失败: %v", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// PostJSON 发送JSON格式的POST请求
|
||||
func (c *HTTPClient) PostJSON(url string, data interface{}, headers map[string]string) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("JSON序列化失败: %v", err)
|
||||
}
|
||||
|
||||
return c.Post(url, jsonData, "application/json", headers)
|
||||
}
|
||||
|
||||
// Post 发送POST请求
|
||||
func (c *HTTPClient) Post(url string, data []byte, contentType string, headers map[string]string) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建POST请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置Content-Type
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
|
||||
// 设置其他请求头
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送POST请求失败: %v", err)
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err = Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("关闭响应体失败: %v\n", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("POST请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体失败: %v", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// GetWithQuery 发送带查询参数的GET请求
|
||||
func (c *HTTPClient) GetWithQuery(url string, params map[string]string, headers map[string]string) ([]byte, error) {
|
||||
// 构建查询参数
|
||||
if len(params) > 0 {
|
||||
url += "?"
|
||||
for key, value := range params {
|
||||
url += fmt.Sprintf("%s=%s&", key, value)
|
||||
}
|
||||
// 移除最后一个多余的 &
|
||||
url = url[:len(url)-1]
|
||||
}
|
||||
|
||||
return c.Get(url, headers)
|
||||
}
|
||||
321
internal/api/llm.go
Normal file
321
internal/api/llm.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LLMClient 大语言模型客户端
|
||||
type LLMClient struct {
|
||||
httpClient *HTTPClient
|
||||
config LLMConfig
|
||||
}
|
||||
|
||||
// LLMConfig LLM配置
|
||||
type LLMConfig struct {
|
||||
APIKey string `json:"api_key"` // API密钥
|
||||
BaseURL string `json:"base_url"` // API基础URL
|
||||
Model string `json:"model"` // 模型名称
|
||||
Provider string `json:"provider"` // 提供商: "openai", "ollama", "custom"
|
||||
Temperature float64 `json:"temperature"` // 温度参数
|
||||
MaxTokens int `json:"max_tokens"` // 最大token数
|
||||
}
|
||||
|
||||
// ChatMessage 聊天消息
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"` // 角色: "system", "user", "assistant"
|
||||
Content string `json:"content"` // 消息内容
|
||||
}
|
||||
|
||||
// ChatRequest 聊天请求 - OpenAI格式
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"` // 模型名称
|
||||
Messages []ChatMessage `json:"messages"` // 消息列表
|
||||
Temperature float64 `json:"temperature,omitempty"` // 温度参数
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // 最大token数
|
||||
TopP float64 `json:"top_p,omitempty"` // 采样参数
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` // 频率惩罚
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"` // 存在惩罚
|
||||
Stream bool `json:"stream,omitempty"` // 是否流式输出
|
||||
}
|
||||
|
||||
// OllamaMessage Ollama消息格式
|
||||
type OllamaMessage struct {
|
||||
Role string `json:"role"` // 角色: "system", "user", "assistant"
|
||||
Content string `json:"content"` // 消息内容
|
||||
}
|
||||
|
||||
// OllamaRequest Ollama请求格式
|
||||
type OllamaRequest struct {
|
||||
Model string `json:"model"` // 模型名称
|
||||
Messages []OllamaMessage `json:"messages"` // 消息列表
|
||||
Stream bool `json:"stream"` // 是否流式输出
|
||||
Options OllamaOptions `json:"options,omitempty"` // 选项
|
||||
KeepAlive string `json:"keep_alive,omitempty"` // 保持连接时间
|
||||
Think bool `json:"think"` // 是否思考模式
|
||||
}
|
||||
|
||||
// OllamaOptions Ollama选项
|
||||
type OllamaOptions struct {
|
||||
Temperature float64 `json:"temperature,omitempty"` // 温度参数
|
||||
MaxTokens int `json:"num_predict,omitempty"` // 最大预测token数
|
||||
TopP float64 `json:"top_p,omitempty"` // 采样参数
|
||||
}
|
||||
|
||||
// ChatResponse 聊天响应 - OpenAI格式
|
||||
type ChatResponse struct {
|
||||
ID string `json:"id"` // 响应ID
|
||||
Object string `json:"object"` // 对象类型
|
||||
Created int64 `json:"created"` // 创建时间
|
||||
Model string `json:"model"` // 模型名称
|
||||
Choices []Choice `json:"choices"` // 选择列表
|
||||
Usage Usage `json:"usage"` // 使用情况
|
||||
}
|
||||
|
||||
// OllamaResponse Ollama响应格式
|
||||
type OllamaResponse struct {
|
||||
Model string `json:"model"` // 模型名称
|
||||
CreatedAt string `json:"created_at"` // 创建时间
|
||||
Message OllamaMessage `json:"message"` // 消息
|
||||
Done bool `json:"done"` // 是否完成
|
||||
TotalDuration int64 `json:"total_duration,omitempty"` // 总耗时
|
||||
LoadDuration int64 `json:"load_duration,omitempty"` // 加载耗时
|
||||
PromptEvalCount int `json:"prompt_eval_count,omitempty"` // 提示评估计数
|
||||
PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"` // 提示评估耗时
|
||||
EvalCount int `json:"eval_count,omitempty"` // 评估计数
|
||||
EvalDuration int64 `json:"eval_duration,omitempty"` // 评估耗时
|
||||
Response string `json:"response,omitempty"` // 响应内容(流式响应时)
|
||||
}
|
||||
|
||||
// Choice 选择 - OpenAI格式
|
||||
type Choice struct {
|
||||
Index int `json:"index"` // 索引
|
||||
Message ChatMessage `json:"message"` // 消息
|
||||
FinishReason string `json:"finish_reason"` // 结束原因
|
||||
}
|
||||
|
||||
// Usage 使用情况
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"` // 提示token数
|
||||
CompletionTokens int `json:"completion_tokens"` // 完成token数
|
||||
TotalTokens int `json:"total_tokens"` // 总token数
|
||||
}
|
||||
|
||||
// NewLLMClient 创建LLM客户端
|
||||
func NewLLMClient(config LLMConfig) *LLMClient {
|
||||
httpClient := NewHTTPClient()
|
||||
|
||||
return &LLMClient{
|
||||
httpClient: httpClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOpenAIClient 创建OpenAI客户端
|
||||
func NewOpenAIClient(apiKey, model string) *LLMClient {
|
||||
config := LLMConfig{
|
||||
APIKey: apiKey,
|
||||
BaseURL: "https://api.openai.com/v1",
|
||||
Model: model,
|
||||
Provider: "openai",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
}
|
||||
return NewLLMClient(config)
|
||||
}
|
||||
|
||||
// NewOllamaClient 创建Ollama客户端
|
||||
func NewOllamaClient(baseURL, model string) *LLMClient {
|
||||
config := LLMConfig{
|
||||
BaseURL: baseURL,
|
||||
Model: model,
|
||||
Provider: "ollama",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
}
|
||||
return NewLLMClient(config)
|
||||
}
|
||||
|
||||
// Chat 发送聊天请求
|
||||
func (c *LLMClient) Chat(messages []ChatMessage) (*ChatResponse, error) {
|
||||
if c.config.Provider == "ollama" {
|
||||
// 对于Ollama,我们需要转换格式
|
||||
return c.chatWithOllama(messages)
|
||||
} else {
|
||||
// 对于OpenAI和其他提供商,使用标准格式
|
||||
request := ChatRequest{
|
||||
Model: c.config.Model,
|
||||
Messages: messages,
|
||||
Temperature: c.config.Temperature,
|
||||
MaxTokens: c.config.MaxTokens,
|
||||
}
|
||||
|
||||
return c.sendChatRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
// chatWithOllama 使用Ollama格式发送请求
|
||||
func (c *LLMClient) chatWithOllama(messages []ChatMessage) (*ChatResponse, error) {
|
||||
// 转换消息格式
|
||||
ollamaMessages := make([]OllamaMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
ollamaMessages[i] = OllamaMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Ollama请求
|
||||
request := OllamaRequest{
|
||||
Model: c.config.Model,
|
||||
Messages: ollamaMessages,
|
||||
Stream: false,
|
||||
Think: false,
|
||||
Options: OllamaOptions{
|
||||
Temperature: c.config.Temperature,
|
||||
MaxTokens: c.config.MaxTokens,
|
||||
},
|
||||
}
|
||||
|
||||
// 发送Ollama请求
|
||||
apiURL := c.buildAPIURL("/api/chat")
|
||||
headers := c.buildHeaders()
|
||||
|
||||
response, err := c.httpClient.PostJSON(apiURL, request, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送Ollama请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析Ollama响应
|
||||
var ollamaResp OllamaResponse
|
||||
err = json.Unmarshal(response, &ollamaResp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析Ollama响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 转换Ollama响应为统一的ChatResponse格式
|
||||
chatResponse := &ChatResponse{
|
||||
ID: "ollama-" + c.config.Model,
|
||||
Object: "chat.completion",
|
||||
Model: c.config.Model,
|
||||
Choices: []Choice{
|
||||
{
|
||||
Index: 0,
|
||||
Message: ChatMessage{
|
||||
Role: ollamaResp.Message.Role,
|
||||
Content: ollamaResp.Message.Content,
|
||||
},
|
||||
FinishReason: "stop",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return chatResponse, nil
|
||||
}
|
||||
|
||||
// ChatWithSystem 发送带系统消息的聊天请求
|
||||
func (c *LLMClient) ChatWithSystem(systemPrompt string, userMessage string) (*ChatResponse, error) {
|
||||
messages := []ChatMessage{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: userMessage},
|
||||
}
|
||||
return c.Chat(messages)
|
||||
}
|
||||
|
||||
// ChatConversation 发送多轮对话
|
||||
func (c *LLMClient) ChatConversation(conversation []ChatMessage) (*ChatResponse, error) {
|
||||
return c.Chat(conversation)
|
||||
}
|
||||
|
||||
// sendChatRequest 发送标准聊天请求 (OpenAI格式)
|
||||
func (c *LLMClient) sendChatRequest(request ChatRequest) (*ChatResponse, error) {
|
||||
apiURL := c.buildAPIURL("/chat/completions")
|
||||
|
||||
headers := c.buildHeaders()
|
||||
|
||||
response, err := c.httpClient.PostJSON(apiURL, request, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
|
||||
var result ChatResponse
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// buildAPIURL 构建API URL
|
||||
func (c *LLMClient) buildAPIURL(endpoint string) string {
|
||||
baseURL := strings.TrimSuffix(c.config.BaseURL, "/")
|
||||
return baseURL + endpoint
|
||||
}
|
||||
|
||||
// buildHeaders 构建请求头
|
||||
func (c *LLMClient) buildHeaders() map[string]string {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
if c.config.APIKey != "" {
|
||||
headers["Authorization"] = "Bearer " + c.config.APIKey
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// SetTemperature 设置温度参数
|
||||
func (c *LLMClient) SetTemperature(temperature float64) {
|
||||
c.config.Temperature = temperature
|
||||
}
|
||||
|
||||
// SetMaxTokens 设置最大token数
|
||||
func (c *LLMClient) SetMaxTokens(maxTokens int) {
|
||||
c.config.MaxTokens = maxTokens
|
||||
}
|
||||
|
||||
// SetModel 设置模型
|
||||
func (c *LLMClient) SetModel(model string) {
|
||||
c.config.Model = model
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *LLMClient) GetConfig() LLMConfig {
|
||||
return c.config
|
||||
}
|
||||
|
||||
// CommonConfigs 预定义的常用配置
|
||||
var CommonConfigs = struct {
|
||||
OpenAI LLMConfig
|
||||
Ollama LLMConfig
|
||||
Groq LLMConfig
|
||||
DeepSeek LLMConfig
|
||||
}{
|
||||
OpenAI: LLMConfig{
|
||||
BaseURL: "https://api.openai.com/v1",
|
||||
Provider: "openai",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
},
|
||||
Ollama: LLMConfig{
|
||||
BaseURL: "http://localhost:11434",
|
||||
Provider: "ollama",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
},
|
||||
Groq: LLMConfig{
|
||||
BaseURL: "https://api.groq.com/openai/v1",
|
||||
Provider: "groq",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
},
|
||||
DeepSeek: LLMConfig{
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Provider: "deepseek",
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 16000,
|
||||
},
|
||||
}
|
||||
358
internal/api/tax_health_test.go
Normal file
358
internal/api/tax_health_test.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCreateTaxEvalRequest 测试创建税务评估请求
|
||||
func TestCreateTaxEvalRequest(t *testing.T) {
|
||||
parsedJSON := `{
|
||||
"LC0000003E_T0I0331": 741213833.14,
|
||||
"LC0000003E_T0I0101": 578064138.06,
|
||||
"GC0016669E_T0I0331": 0,
|
||||
"GC0016669E_T0I0101": 0,
|
||||
"GC0006222E_T0I0331": 0,
|
||||
"GC0006222E_T0I0101": 0,
|
||||
"LC0000005E_T0I0331": 28204209.19,
|
||||
"LC0000005E_T0I0101": 25024200,
|
||||
"LC0000006E_T0I0331": 372847956.26,
|
||||
"LC0000006E_T0I0101": 461664605.22,
|
||||
"GC0018627E_T0I0331": 0,
|
||||
"GC0018627E_T0I0101": 0,
|
||||
"LC0000007E_T0I0331": 13850463.15,
|
||||
"LC0000007E_T0I0101": 11466957.75,
|
||||
"LC0000010E_T0I0331": 1279972.41,
|
||||
"LC0000010E_T0I0101": 2394706.37,
|
||||
"LC0000011E_T0I0331": 71200,
|
||||
"LC0000011E_T0I0101": 0,
|
||||
"GC0016671E_T0I0331": 0,
|
||||
"GC0016671E_T0I0101": 0,
|
||||
"GC0007742E_T0I0331": 0,
|
||||
"GC0007742E_T0I0101": 0,
|
||||
"LC0000012E_T0I0331": 0,
|
||||
"LC0000012E_T0I0101": 0,
|
||||
"LC0000013E_T0I0331": 30401431.89,
|
||||
"LC0000013E_T0I0101": 31017339.59,
|
||||
"LC0000014E_T0I0331": 1198146906.32,
|
||||
"LC0000014E_T0I0101": 1114982561.27,
|
||||
"GC0016672E_T0I0331": 0,
|
||||
"GC0016672E_T0I0101": 0,
|
||||
"GC0016673E_T0I0331": 0,
|
||||
"GC0016673E_T0I0101": 0,
|
||||
"LC0000017E_T0I0331": 1410014797.11,
|
||||
"LC0000017E_T0I0101": 1362056986.2,
|
||||
"LC0000018E_T0I0331": 40000000,
|
||||
"LC0000018E_T0I0101": 40000000,
|
||||
"GC0016674E_T0I0331": 0,
|
||||
"GC0016674E_T0I0101": 0,
|
||||
"GC0016675E_T0I0331": 0,
|
||||
"GC0016675E_T0I0101": 0,
|
||||
"LC0000019E_T0I0331": 0,
|
||||
"LC0000019E_T0I0101": 0,
|
||||
"LC0000020E_T0I0331": 1710521807.17,
|
||||
"LC0000020E_T0I0101": 1809249671.18,
|
||||
"LC0000021E_T0I0331": 10670524.65,
|
||||
"LC0000021E_T0I0101": 12170137.69,
|
||||
"LC0000024E_T0I0331": 0,
|
||||
"LC0000024E_T0I0101": 0,
|
||||
"LC0000025E_T0I0331": 0,
|
||||
"LC0000025E_T0I0101": 0,
|
||||
"GC0018492E_T0I0331": 0,
|
||||
"GC0018492E_T0I0101": 0,
|
||||
"LC0000026E_T0I0331": 243623902.74,
|
||||
"LC0000026E_T0I0101": 248385213.62,
|
||||
"LC0000027E_T0I0331": 22259377.92,
|
||||
"LC0000027E_T0I0101": 16876375.57,
|
||||
"LC0000028E_T0I0331": 0,
|
||||
"LC0000028E_T0I0101": 0,
|
||||
"LC0000029E_T0I0331": 66195945.84,
|
||||
"LC0000029E_T0I0101": 63182454.15,
|
||||
"LC0000030E_T0I0331": 0,
|
||||
"LC0000030E_T0I0101": 0,
|
||||
"LC0000031E_T0I0331": 0,
|
||||
"LC0000031E_T0I0101": 0,
|
||||
"LC0000032E_T0I0331": 3503286355.43,
|
||||
"LC0000032E_T0I0101": 3551920838.41,
|
||||
"LC0000033E_T0I0331": 4701433261.75,
|
||||
"LC0000033E_T0I0101": 4666903399.68,
|
||||
"LC0000034E_T0I0331": 0,
|
||||
"LC0000034E_T0I0101": 0,
|
||||
"GC0016676E_T0I0331": 0,
|
||||
"GC0016676E_T0I0101": 0,
|
||||
"GC0006227E_T0I0331": 0,
|
||||
"GC0006227E_T0I0101": 0,
|
||||
"LC0000036E_T0I0331": 327465356.48,
|
||||
"LC0000036E_T0I0101": 324791092.1,
|
||||
"LC0000037E_T0I0331": 3987467.19,
|
||||
"LC0000037E_T0I0101": 24162739.9,
|
||||
"LC0000038E_T0I0331": 6216843.39,
|
||||
"LC0000038E_T0I0101": 6107830.87,
|
||||
"GC0016678E_T0I0331": 0,
|
||||
"GC0016678E_T0I0101": 0,
|
||||
"LC0000039E_T0I0331": 0,
|
||||
"LC0000039E_T0I0101": 0,
|
||||
"LC0000040E_T0I0331": 22441281.17,
|
||||
"LC0000040E_T0I0101": 11487537.15,
|
||||
"LC0000043E_T0I0331": 99859088.81,
|
||||
"LC0000043E_T0I0101": 66507161.39,
|
||||
"GC0007743E_T0I0331": 0,
|
||||
"GC0007743E_T0I0101": 0,
|
||||
"LC0000044E_T0I0331": 0,
|
||||
"LC0000044E_T0I0101": 0,
|
||||
"LC0000045E_T0I0331": 150724.81,
|
||||
"LC0000045E_T0I0101": 0,
|
||||
"LC0000046E_T0I0331": 459997495.210001,
|
||||
"LC0000046E_T0I0101": 433462971.100001,
|
||||
"LC0000047E_T0I0331": 0,
|
||||
"LC0000047E_T0I0101": 0,
|
||||
"LC0000048E_T0I0331": 0,
|
||||
"LC0000048E_T0I0101": 0,
|
||||
"GC0007952E_T0I0331": 0,
|
||||
"GC0007952E_T0I0101": 0,
|
||||
"GC0007953E_T0I0331": 0,
|
||||
"GC0007953E_T0I0101": 0,
|
||||
"GC0018493E_T0I0331": 0,
|
||||
"GC0018493E_T0I0101": 0,
|
||||
"LC0000049E_T0I0331": 0,
|
||||
"LC0000049E_T0I0101": 0,
|
||||
"LC0000051E_T0I0331": 62303168,
|
||||
"LC0000051E_T0I0101": 83817131.51,
|
||||
"GC0007955E_T0I0331": 0,
|
||||
"GC0007955E_T0I0101": 0,
|
||||
"LC0000052E_T0I0331": 0,
|
||||
"LC0000052E_T0I0101": 0,
|
||||
"LC0000053E_T0I0331": 2204446.46,
|
||||
"LC0000053E_T0I0101": 1516443.48,
|
||||
"LC0000054E_T0I0331": 64507614.46,
|
||||
"LC0000054E_T0I0101": 85333574.99,
|
||||
"LC0000055E_T0I0331": 524505109.670001,
|
||||
"LC0000055E_T0I0101": 518796546.090001,
|
||||
"LC0000056E_T0I0331": 3740951803.56,
|
||||
"LC0000056E_T0I0101": 3740951803.56,
|
||||
"GC0007956E_T0I0331": 0,
|
||||
"GC0007956E_T0I0101": 0,
|
||||
"GC0007957E_T0I0331": 0,
|
||||
"GC0007957E_T0I0101": 0,
|
||||
"GC0007958E_T0I0331": 0,
|
||||
"GC0007958E_T0I0101": 0,
|
||||
"LC0000057E_T0I0331": 55443838.1,
|
||||
"LC0000057E_T0I0101": 55443838.1,
|
||||
"LC0000058E_T0I0331": 0,
|
||||
"LC0000058E_T0I0101": 0,
|
||||
"GC0007750E_T0I0331": 0,
|
||||
"GC0007750E_T0I0101": 0,
|
||||
"LC0000059E_T0I0331": 0,
|
||||
"LC0000059E_T0I0101": 0,
|
||||
"LC0000060E_T0I0331": 180349254.88,
|
||||
"LC0000060E_T0I0101": 176067125.03,
|
||||
"LC0000062E_T0I0331": 200183255.54,
|
||||
"LC0000062E_T0I0101": 175644086.9,
|
||||
"LC0000063E_T0I0331": 4176928152.08,
|
||||
"LC0000063E_T0I0101": 4148106853.59,
|
||||
"LC0000064E_T0I0331": 4701433261.75,
|
||||
"LC0000064E_T0I0101": 4666903399.68,
|
||||
"LC0010003E_T0M0103": 1001498985.16,
|
||||
"LC0010003E_T-1M0103": 970511823.21,
|
||||
"LC0010008E_T0M0103": 844977443.69,
|
||||
"LC0010008E_T-1M0103": 800450961.39,
|
||||
"LC0010016E_T0M0103": 30422209.55,
|
||||
"LC0010016E_T-1M0103": 30211704.62,
|
||||
"LC0010017E_T0M0103": 2985920.84,
|
||||
"LC0010017E_T-1M0103": 2344075.83,
|
||||
"LC0010018E_T0M0103": 180378961.52,
|
||||
"LC0010018E_T-1M0103": 160479498.41,
|
||||
"GC0016679E_T0M0103": 0,
|
||||
"GC0016679E_T-1M0103": 0,
|
||||
"LC0010019E_T0M0103": -2498823.79,
|
||||
"LC0010019E_T-1M0103": -1463147.42,
|
||||
"GC0016680E_T0M0103": 0,
|
||||
"GC0016680E_T-1M0103": 0,
|
||||
"GC0016681E_T0M0103": 0,
|
||||
"GC0016681E_T-1M0103": 0,
|
||||
"GC0014225E_T0M0103": 0,
|
||||
"GC0014225E_T-1M0103": 0,
|
||||
"LC0010022E_T0M0103": 36357810.91,
|
||||
"LC0010022E_T-1M0103": 35468582.16,
|
||||
"LC0010023E_T0M0103": 0,
|
||||
"LC0010023E_T-1M0103": 0,
|
||||
"GC0017132E_T0M0103": 0,
|
||||
"GC0017132E_T-1M0103": 0,
|
||||
"GC0016683E_T0M0103": 0,
|
||||
"GC0016683E_T-1M0103": 0,
|
||||
"LC0010021E_T0M0103": 0,
|
||||
"LC0010021E_T-1M0103": 0,
|
||||
"GC0016682E_T0M0103": 0,
|
||||
"GC0016682E_T-1M0103": 0,
|
||||
"LC0010020E_T0M0103": 0,
|
||||
"LC0010020E_T-1M0103": 0,
|
||||
"GC0014761E_T0M0103": 0,
|
||||
"GC0014761E_T-1M0103": 0,
|
||||
"LC0010025E_T0M0103": -18408915.7400001,
|
||||
"LC0010025E_T-1M0103": 13957312.54,
|
||||
"LC0010026E_T0M0103": 62014210.36,
|
||||
"LC0010026E_T-1M0103": 74680343.91,
|
||||
"LC0010027E_T0M0103": 95993.15,
|
||||
"LC0010027E_T-1M0103": 190072.12,
|
||||
"LC0010029E_T0M0103": 43509301.4699999,
|
||||
"LC0010029E_T-1M0103": 88447584.33,
|
||||
"LC0010030E_T0M0103": 688002.98,
|
||||
"LC0010030E_T-1M0103": 171084.05,
|
||||
"LC0010031E_T0M0103": 42821298.4899999,
|
||||
"LC0010031E_T-1M0103": 88276500.28,
|
||||
"GC0014762E_T0M0103": 0,
|
||||
"GC0014762E_T-1M0103": 0,
|
||||
"GC0014763E_T0M0103": 0,
|
||||
"GC0014763E_T-1M0103": 0,
|
||||
"LC0010038E_T0M0103": 0,
|
||||
"LC0010038E_T-1M0103": 0,
|
||||
"GC0007744E_T0M0103": 0,
|
||||
"GC0007744E_T-1M0103": 0,
|
||||
"GC0007960E_T0M0103": 0,
|
||||
"GC0007960E_T-1M0103": 0,
|
||||
"GC0007961E_T0M0103": 0,
|
||||
"GC0007961E_T-1M0103": 0,
|
||||
"GC0016684E_T0M0103": 0,
|
||||
"GC0016684E_T-1M0103": 0,
|
||||
"GC0016685E_T0M0103": 0,
|
||||
"GC0016685E_T-1M0103": 0,
|
||||
"GC0016686E_T0M0103": 0,
|
||||
"GC0016686E_T-1M0103": 0,
|
||||
"GC0007745E_T0M0103": 0,
|
||||
"GC0007745E_T-1M0103": 0,
|
||||
"GC0007962E_T0M0103": 0,
|
||||
"GC0007962E_T-1M0103": 0,
|
||||
"GC0016687E_T0M0103": 0,
|
||||
"GC0016687E_T-1M0103": 0,
|
||||
"GC0016688E_T0M0103": 0,
|
||||
"GC0016688E_T-1M0103": 0,
|
||||
"GC0016689E_T0M0103": 0,
|
||||
"GC0016689E_T-1M0103": 0,
|
||||
"GC0016690E_T0M0103": 0,
|
||||
"GC0016690E_T-1M0103": 0,
|
||||
"GC0007966E_T0M0103": 0,
|
||||
"GC0007966E_T-1M0103": 0,
|
||||
"GC0007967E_T0M0103": 0,
|
||||
"GC0007967E_T-1M0103": 0,
|
||||
"LC0010039E_T0M0103": 0,
|
||||
"LC0010039E_T-1M0103": 0,
|
||||
"LC0010036E_T0M0103": 0,
|
||||
"LC0010036E_T-1M0103": 0,
|
||||
"LC0010037E_T0M0103": 0,
|
||||
"LC0010037E_T-1M0103": 0,
|
||||
"LC0020002E_T0M0103": 0,
|
||||
"LC0020002E_T-1M0103": 0,
|
||||
"LC0020013E_T0M0103": 46619.83,
|
||||
"LC0020013E_T-1M0103": 0,
|
||||
"LC0020014E_T0M0103": 0,
|
||||
"LC0020014E_T-1M0103": 0,
|
||||
"LC0020015E_T0M0103": 13566300.1,
|
||||
"LC0020015E_T-1M0103": 13332652.63,
|
||||
"LC0020016E_T0M0103": 680142.74,
|
||||
"LC0020016E_T-1M0103": 1039719.92,
|
||||
"LC0020022E_T0M0103": 657058.13,
|
||||
"LC0020022E_T-1M0103": 567405.52,
|
||||
"LC0020023E_T0M0103": -162817.27,
|
||||
"LC0020023E_T-1M0103": 250470.71,
|
||||
"LC0020024E_T0M0103": 0,
|
||||
"LC0020024E_T-1M0103": 0,
|
||||
"LC0020025E_T0M0103": -4745909.49,
|
||||
"LC0020025E_T-1M0103": 2063373.12,
|
||||
"LC0020026E_T0M0103": 0,
|
||||
"LC0020026E_T-1M0103": 0,
|
||||
"LC0020027E_T0M0103": 0,
|
||||
"LC0020027E_T-1M0103": 0,
|
||||
"LC0020028E_T0M0103": 0,
|
||||
"LC0020028E_T-1M0103": 0,
|
||||
"LC0020029E_T0M0103": 0,
|
||||
"LC0020029E_T-1M0103": 0,
|
||||
"LC0020030E_T0M0103": 0,
|
||||
"LC0020030E_T-1M0103": 0,
|
||||
"LC0020031E_T0M0103": 0,
|
||||
"LC0020031E_T-1M0103": 0,
|
||||
"LC0020032E_T0M0103": 16388.07,
|
||||
"LC0020032E_T-1M0103": 6007.97,
|
||||
"LC0020033E_T0M0103": 0,
|
||||
"LC0020033E_T-1M0103": 0,
|
||||
"LC0020034E_T0M0103": 0,
|
||||
"LC0020034E_T-1M0103": 0,
|
||||
"LC0020036E_T0M0103": 0,
|
||||
"LC0020036E_T-1M0103": 0,
|
||||
"LC0020037E_T0M0103": 0,
|
||||
"LC0020037E_T-1M0103": 0,
|
||||
"LC0020038E_T0M0103": -16388.07,
|
||||
"LC0020038E_T-1M0103": -6007.97,
|
||||
"LC0020039E_T0M0103": 0,
|
||||
"LC0020039E_T-1M0103": 0,
|
||||
"LC0020040E_T0M0103": 0,
|
||||
"LC0020040E_T-1M0103": 0,
|
||||
"LC0020042E_T0M0103": 0,
|
||||
"LC0020042E_T-1M0103": 0,
|
||||
"LC0020044E_T0M0103": 0,
|
||||
"LC0020044E_T-1M0103": 0,
|
||||
"LC0020045E_T0M0103": 0,
|
||||
"LC0020045E_T-1M0103": 0,
|
||||
"LC0020046E_T0M0103": 0,
|
||||
"LC0020046E_T-1M0103": 0,
|
||||
"LC0020047E_T0M0103": 0,
|
||||
"LC0020047E_T-1M0103": 0,
|
||||
"LC0020049E_T0M0103": 0,
|
||||
"LC0020049E_T-1M0103": 0,
|
||||
"LC0020050E_T0M0103": 0,
|
||||
"LC0020050E_T-1M0103": 0,
|
||||
"LC0020051E_T0M0103": 0,
|
||||
"LC0020051E_T-1M0103": 0,
|
||||
"LC0020052E_T0M0103": 691605.56,
|
||||
"LC0020052E_T-1M0103": 2057365.15,
|
||||
"LC0020053E_T0M0103": 3233993.43,
|
||||
"LC0020053E_T-1M0103": 6614143.33,
|
||||
"LC0020054E_T0I0101": 3925598.99,
|
||||
"LC0020054E_T-1I0101": 8671508.48,
|
||||
"LC0020054E_T0I0331": 9948154.38,
|
||||
"LC0020054E_T-1I0331": 17253621.9,
|
||||
"GE8020001E_T0M0112": 0,
|
||||
"GE8020001E_T-1M0112": 0,
|
||||
"GE8020002E_T0M0112": 0,
|
||||
"GE8020002E_T-1M0112": 0,
|
||||
"GE8020003E_T0M0112": 0,
|
||||
"GE8020003E_T-1M0112": 0,
|
||||
"GE8020004E_T0M0112": 0,
|
||||
"GE8020004E_T-1M0112": 0,
|
||||
"GE8000080E": 0,
|
||||
"GE0000000E": 0
|
||||
}`
|
||||
// 创建税务健康服务客户端
|
||||
// 注意:这里需要替换为实际的用户ID和私钥
|
||||
taxHealthService := NewTaxHealthService("1560142251348287489", "")
|
||||
|
||||
// 将解析的JSON解析为map结构
|
||||
var companyData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(parsedJSON), &companyData); err != nil {
|
||||
fmt.Printf("❌ 解析JSON数据失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建税务评估请求
|
||||
request := TaxEvalRequest{
|
||||
ReqNo: fmt.Sprintf("REQ%v", time.Now().Unix()), // 使用时间戳生成请求号
|
||||
Detail: TaxEvalDetail{
|
||||
CompanyName: "郑州惠兴", // 可以从数据中提取公司名称
|
||||
Data: companyData,
|
||||
},
|
||||
}
|
||||
|
||||
// 调用税务健康API
|
||||
taxResponse, err := taxHealthService.EvaluateTaxHealth(request)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 调用税务健康API失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n========== 税务健康API反馈结果 ==========")
|
||||
fmt.Printf("API响应: %+v\n", taxResponse)
|
||||
fmt.Println("======================================")
|
||||
|
||||
t.Logf("创建税务评估请求成功,包含%d个财务指标", len(request.Detail.Data))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
|
||||
对应关系规则如下:
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
对应关系规则如下:
|
||||
@@ -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}
|
||||
|
||||
|
||||
对应关系规则如下:
|
||||
@@ -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
|
||||
}
|
||||
351
tax_eval_api.go
351
tax_eval_api.go
@@ -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
|
||||
}
|
||||
445
tax_eval_test.go
445
tax_eval_test.go
@@ -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)
|
||||
}
|
||||
}
|
||||
92
test_sign.go
92
test_sign.go
@@ -1,92 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
testSM2Sign()
|
||||
}
|
||||
|
||||
func testSM2Sign() {
|
||||
userId := "userId"
|
||||
timestamp := "1694400142"
|
||||
uri := ""
|
||||
nonce := ""
|
||||
body := ""
|
||||
prvKey := ""
|
||||
|
||||
signData := userId + timestamp + nonce + uri + body
|
||||
|
||||
fmt.Printf("签名源数据:%s\n", signData)
|
||||
|
||||
signUtils := NewSignUtils()
|
||||
|
||||
signStr, err := signUtils.Sign([]byte(signData), prvKey)
|
||||
if err != nil {
|
||||
fmt.Printf("签名失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("签名后数据:%s\n", signStr)
|
||||
|
||||
testKeyGeneration()
|
||||
}
|
||||
|
||||
func testKeyGeneration() {
|
||||
signUtils := NewSignUtils()
|
||||
|
||||
privateKey, publicKey, err := signUtils.GenerateKeyPair()
|
||||
if err != nil {
|
||||
fmt.Printf("生成密钥对失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n生成的密钥对:\n")
|
||||
fmt.Printf("私钥: %s\n", privateKey)
|
||||
fmt.Printf("公钥: %s\n", publicKey)
|
||||
|
||||
testData := "test data for signing"
|
||||
signature, err := signUtils.Sign([]byte(testData), privateKey)
|
||||
if err != nil {
|
||||
fmt.Printf("签名测试失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n测试签名: %s\n", signature)
|
||||
|
||||
valid, err := signUtils.VerifySign([]byte(testData), signature, publicKey)
|
||||
if err != nil {
|
||||
fmt.Printf("验证签名失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("签名验证结果: %t\n", valid)
|
||||
}
|
||||
|
||||
func testAPISignature() {
|
||||
signUtils := NewSignUtils()
|
||||
|
||||
userId := "user001"
|
||||
timestamp := "1694400142"
|
||||
uri := "/api/v1/company/info"
|
||||
nonce := "abc123"
|
||||
body := `{"name":"test"}`
|
||||
prvKey := ""
|
||||
|
||||
if prvKey == "" {
|
||||
fmt.Println("请设置有效的私钥进行测试")
|
||||
return
|
||||
}
|
||||
|
||||
signData := userId + timestamp + nonce + uri + body
|
||||
fmt.Printf("API签名源数据:%s\n", signData)
|
||||
|
||||
signStr, err := signUtils.Sign([]byte(signData), prvKey)
|
||||
if err != nil {
|
||||
fmt.Printf("API签名失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("API签名结果:%s\n", signStr)
|
||||
}
|
||||
64
web/README.md
Normal file
64
web/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# WASM企业行业分类器
|
||||
|
||||
这是一个使用WebAssembly技术的前端版本企业行业分类器,允许在浏览器中直接运行Go代码。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 在浏览器中直接运行Go代码(通过WASM)
|
||||
- 输入企业名称进行行业分类
|
||||
- 显示主要行业和次要行业信息
|
||||
- 响应式设计,适配不同设备
|
||||
|
||||
## 架构说明
|
||||
|
||||
- `cmd/wasm/main.go`: Go源代码,编译为WASM
|
||||
- `web/index.html`: 前端界面
|
||||
- `build_wasm.bat`: Windows编译脚本
|
||||
|
||||
## 编译步骤
|
||||
|
||||
### Windows
|
||||
|
||||
1. 确保已安装Go 1.12或更高版本
|
||||
2. 运行编译脚本:
|
||||
```cmd
|
||||
build_wasm.bat
|
||||
```
|
||||
|
||||
### 手动编译(Linux/Mac)
|
||||
|
||||
```bash
|
||||
# 复制WASM执行器
|
||||
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" web/
|
||||
|
||||
# 编译为WASM
|
||||
GOOS=js GOARCH=wasm go build -o web/main.wasm cmd/wasm/main.go
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
由于浏览器安全策略限制,需要通过HTTP服务器运行,不能直接打开HTML文件:
|
||||
|
||||
```bash
|
||||
# 使用Python 3
|
||||
cd web
|
||||
python -m http.server 8080
|
||||
|
||||
# 或使用Node.js
|
||||
npx http-server web -p 8080
|
||||
|
||||
# 然后在浏览器中访问 http://localhost:8080/index.html
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
- 使用Go的`syscall/js`包实现Go与JavaScript互操作
|
||||
- WASM模块负责调用行业分类算法
|
||||
- 前端界面使用原生JavaScript和CSS
|
||||
- 支持异步API调用和错误处理
|
||||
|
||||
## 注意事项
|
||||
|
||||
- WASM版本目前使用固定的API配置
|
||||
- 实际部署时需要考虑安全性,如API密钥管理
|
||||
- 需要后端服务支持API调用(企业信息获取、LLM调用等)
|
||||
546
web/index.html
Normal file
546
web/index.html
Normal file
@@ -0,0 +1,546 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WASM企业分析工具套件</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.3em;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 30px 0;
|
||||
padding: 25px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-weight: bold;
|
||||
color: #34495e;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
input[type="text"], textarea, input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #bbdefb;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #64b5f6;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(45deg, #2196F3, #21CBF3);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #b0bec5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-section.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border-left: 5px solid #4caf50;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
background-color: #ffebee;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab {
|
||||
overflow: hidden;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.tab button {
|
||||
background: inherit;
|
||||
float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 14px 16px;
|
||||
transition: 0.3s;
|
||||
font-size: 17px;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tab button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.tab button.active {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tabcontent {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
color: #95a5a6;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🏭 WASM企业分析工具套件</h1>
|
||||
<p class="subtitle">基于WebAssembly的多功能企业数据分析平台</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📊</span>
|
||||
<h3 class="feature-title">行业分析</h3>
|
||||
<p class="feature-desc">基于企业信息的国民经济行业分类</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📄</span>
|
||||
<h3 class="feature-title">Excel解析</h3>
|
||||
<p class="feature-desc">在浏览器中直接解析Excel文件</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🤖</span>
|
||||
<h3 class="feature-title">AI分析</h3>
|
||||
<p class="feature-desc">利用大模型进行智能数据分析</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">💰</span>
|
||||
<h3 class="feature-title">税务健康</h3>
|
||||
<p class="feature-desc">企业税务健康状况评估</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<button class="tablinks active" onclick="openTab(event, 'company')">企业行业分析</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'excel')">Excel解析</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'llm')">AI分析</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'tax')">税务健康</button>
|
||||
</div>
|
||||
|
||||
<!-- 企业行业分析标签页 -->
|
||||
<div id="company" class="tabcontent" style="display: block;">
|
||||
<div class="section">
|
||||
<label for="companyName">🔍 请输入企业全称:</label>
|
||||
<input type="text" id="companyName" placeholder="例如:深圳市致远速联信息技术有限公司" value="深圳市致远速联信息技术有限公司">
|
||||
<button id="analyzeCompanyBtn" disabled>分析企业行业</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Excel解析标签页 -->
|
||||
<div id="excel" class="tabcontent">
|
||||
<div class="section">
|
||||
<label for="excelFile">📁 请选择Excel文件:</label>
|
||||
<input type="file" id="excelFile" accept=".xlsx,.xls">
|
||||
<button id="parseExcelBtn" disabled>解析Excel文件</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI分析标签页 -->
|
||||
<div id="llm" class="tabcontent">
|
||||
<div class="section">
|
||||
<label for="llmPrompt">💬 请输入分析提示:</label>
|
||||
<textarea id="llmPrompt" rows="4" placeholder="请输入您想要分析的内容...">请分析以下企业数据并给出建议</textarea>
|
||||
<button id="callLLMBtn" disabled>调用AI分析</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 税务健康标签页 -->
|
||||
<div id="tax" class="tabcontent">
|
||||
<div class="section">
|
||||
<label for="taxJson">💼 请输入企业JSON数据:</label>
|
||||
<textarea id="taxJson" rows="6" placeholder='请输入企业数据,例如: {"revenue": 1000000, "expenses": 800000}'>{"revenue": 1000000, "expenses": 800000, "tax_paid": 50000}</textarea>
|
||||
<button id="callTaxBtn" disabled>评估税务健康</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<p>🔄 正在处理请求...请稍候</p>
|
||||
</div>
|
||||
|
||||
<div class="error" id="error"></div>
|
||||
|
||||
<div class="result-section" id="resultSection">
|
||||
<h2>📊 分析结果</h2>
|
||||
<div id="resultContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Powered by WebAssembly & Go | 企业分析工具套件 v1.0</p>
|
||||
</footer>
|
||||
|
||||
<!-- 加载WASM运行时 -->
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
|
||||
// WASM加载完成后启用所有按钮
|
||||
document.getElementById('analyzeCompanyBtn').disabled = false;
|
||||
document.getElementById('parseExcelBtn').disabled = false;
|
||||
document.getElementById('callLLMBtn').disabled = false;
|
||||
document.getElementById('callTaxBtn').disabled = false;
|
||||
console.log('WASM模块加载完成');
|
||||
}).catch((err) => {
|
||||
console.error('WASM加载失败:', err);
|
||||
showError('WASM模块加载失败: ' + err.message);
|
||||
});
|
||||
|
||||
// 获取DOM元素
|
||||
const analyzeCompanyBtn = document.getElementById('analyzeCompanyBtn');
|
||||
const parseExcelBtn = document.getElementById('parseExcelBtn');
|
||||
const callLLMBtn = document.getElementById('callLLMBtn');
|
||||
const callTaxBtn = document.getElementById('callTaxBtn');
|
||||
const companyNameInput = document.getElementById('companyName');
|
||||
const excelFileInput = document.getElementById('excelFile');
|
||||
const llmPromptInput = document.getElementById('llmPrompt');
|
||||
const taxJsonInput = document.getElementById('taxJson');
|
||||
const resultSection = document.getElementById('resultSection');
|
||||
const resultContent = document.getElementById('resultContent');
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
const errorDiv = document.getElementById('error');
|
||||
|
||||
// 企业行业分析按钮事件
|
||||
analyzeCompanyBtn.addEventListener('click', async () => {
|
||||
const companyName = companyNameInput.value.trim();
|
||||
|
||||
if (!companyName) {
|
||||
showError('请输入企业名称');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
hideError();
|
||||
resultSection.classList.remove('show');
|
||||
|
||||
try {
|
||||
const result = await window.analyzeCompanyIndustryAsync(companyName);
|
||||
|
||||
if (result && result.error) {
|
||||
showError(result.error);
|
||||
} else {
|
||||
displayResults('企业行业分析结果', result);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('分析过程中发生错误: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Excel解析按钮事件
|
||||
parseExcelBtn.addEventListener('click', async () => {
|
||||
const file = excelFileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
showError('请选择一个Excel文件');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
hideError();
|
||||
resultSection.classList.remove('show');
|
||||
|
||||
try {
|
||||
// 读取文件为ArrayBuffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// 调用WASM中的解析函数
|
||||
const result = await window.parseExcelAsync(arrayBuffer);
|
||||
|
||||
if (result && result.error) {
|
||||
showError(result.error);
|
||||
} else {
|
||||
displayResults('Excel解析结果', result);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('解析Excel过程中发生错误: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// AI分析按钮事件
|
||||
callLLMBtn.addEventListener('click', async () => {
|
||||
const prompt = llmPromptInput.value.trim();
|
||||
|
||||
if (!prompt) {
|
||||
showError('请输入分析提示');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
hideError();
|
||||
resultSection.classList.remove('show');
|
||||
|
||||
try {
|
||||
const result = await window.callLLMAsync(prompt);
|
||||
|
||||
if (result && result.error) {
|
||||
showError(result.error);
|
||||
} else {
|
||||
displayResults('AI分析结果', result);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('AI分析过程中发生错误: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 税务健康按钮事件
|
||||
callTaxBtn.addEventListener('click', async () => {
|
||||
const jsonData = taxJsonInput.value.trim();
|
||||
|
||||
if (!jsonData) {
|
||||
showError('请输入企业JSON数据');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
hideError();
|
||||
resultSection.classList.remove('show');
|
||||
|
||||
try {
|
||||
const result = await window.callTaxHealthAsync(jsonData);
|
||||
|
||||
if (result && result.error) {
|
||||
showError(result.error);
|
||||
} else {
|
||||
displayResults('税务健康评估结果', result);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('税务健康评估过程中发生错误: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading(show) {
|
||||
if (show) {
|
||||
loadingDiv.classList.add('show');
|
||||
// 禁用所有按钮以防止重复点击
|
||||
analyzeCompanyBtn.disabled = true;
|
||||
parseExcelBtn.disabled = true;
|
||||
callLLMBtn.disabled = true;
|
||||
callTaxBtn.disabled = true;
|
||||
} else {
|
||||
loadingDiv.classList.remove('show');
|
||||
// 重新启用按钮(但保持初始状态由WASM加载决定)
|
||||
// 这里简单地重新启用
|
||||
setTimeout(() => {
|
||||
analyzeCompanyBtn.disabled = false;
|
||||
parseExcelBtn.disabled = false;
|
||||
callLLMBtn.disabled = false;
|
||||
callTaxBtn.disabled = false;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
function showError(message) {
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.add('show');
|
||||
}
|
||||
|
||||
// 隐藏错误信息
|
||||
function hideError() {
|
||||
errorDiv.classList.remove('show');
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
function displayResults(title, result) {
|
||||
if (!result) {
|
||||
showError('未能获取分析结果');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div class="result-item"><h3>${title}</h3>`;
|
||||
|
||||
// 格式化输出结果
|
||||
html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
|
||||
html += '</div>';
|
||||
|
||||
resultContent.innerHTML = html;
|
||||
resultSection.classList.add('show');
|
||||
}
|
||||
|
||||
// 标签页功能
|
||||
function openTab(evt, tabName) {
|
||||
var i, tabcontent, tablinks;
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
// 回车键触发当前活动标签页的分析
|
||||
document.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const activeTab = document.querySelector('.tabcontent[style*="block"]');
|
||||
if (activeTab.id === 'company') {
|
||||
analyzeCompanyBtn.click();
|
||||
} else if (activeTab.id === 'llm') {
|
||||
callLLMBtn.click();
|
||||
} else if (activeTab.id === 'tax') {
|
||||
callTaxBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
575
web/wasm_exec.js
Normal file
575
web/wasm_exec.js
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.path) {
|
||||
globalThis.path = {
|
||||
resolve(...pathSegments) {
|
||||
return pathSegments.join("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const testCallExport = (a, b) => {
|
||||
this._inst.exports.testExport0();
|
||||
return this._inst.exports.testExport(a, b);
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
callExport: testCallExport,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
366
xlsx_reader.go
366
xlsx_reader.go
@@ -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)
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user