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

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

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

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

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

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

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

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

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

View File

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

585
internal/excel/loader.go Normal file
View File

@@ -0,0 +1,585 @@
package excel
import (
"archive/zip"
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
)
type SheetInfo struct {
Name string
SheetID int
RID string
Path string
}
type XLSXReader struct {
Files map[string][]byte
SheetMap map[string]SheetInfo
}
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 sanitizeFileName(name string) string {
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, "\\", "_")
name = strings.ReplaceAll(name, ":", "_")
name = strings.ReplaceAll(name, "*", "_")
name = strings.ReplaceAll(name, "?", "_")
name = strings.ReplaceAll(name, "\"", "_")
name = strings.ReplaceAll(name, "<", "_")
name = strings.ReplaceAll(name, ">", "_")
name = strings.ReplaceAll(name, "|", "_")
return name
}
func NewXLSXReader(filePath string) (*XLSXReader, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer f.Close()
_, err = f.Stat()
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
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 {
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("打开内部文件失败: %v", err)
}
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.parseWorkbook()
return reader, nil
}
func (r *XLSXReader) parseWorkbook() {
r.SheetMap = make(map[string]SheetInfo)
// 收集所有可用的worksheet文件
var availableSheets []string
for file := range r.Files {
if strings.HasPrefix(file, "xl/worksheets/sheet") && strings.HasSuffix(file, ".xml") {
availableSheets = append(availableSheets, file)
}
}
fmt.Printf("✓ 发现 %d 个可用的worksheet文件: %v\n", len(availableSheets), availableSheets)
// 读取rels文件获取正确的映射关系
ridToPath := r.readWorkbookRels()
fmt.Printf("✓ 从rels文件读取到 %d 个映射关系\n", len(ridToPath))
content, ok := r.Files["xl/workbook.xml"]
if !ok {
return
}
contentStr := string(content)
var sheetNames []string
idx := 0
for {
sheetStart := strings.Index(contentStr[idx:], "<sheet ")
if sheetStart == -1 {
break
}
sheetStart += idx
nameAttrStart := strings.Index(contentStr[sheetStart:], `name="`)
if nameAttrStart == -1 {
idx = sheetStart + 6
continue
}
nameAttrStart += sheetStart + 6
nameAttrEnd := strings.Index(contentStr[nameAttrStart:], `"`)
if nameAttrEnd == -1 {
idx = nameAttrStart + 1
continue
}
nameAttrEnd += nameAttrStart
name := contentStr[nameAttrStart:nameAttrEnd]
sheetID := 0
sheetIdAttr := strings.Index(contentStr[nameAttrEnd:], `sheetId="`)
if sheetIdAttr != -1 {
sheetIdAttr += nameAttrEnd + 9
sheetIdEnd := strings.Index(contentStr[sheetIdAttr:], `"`)
if sheetIdEnd != -1 {
sheetIdEnd += sheetIdAttr
if id, err := strconv.Atoi(contentStr[sheetIdAttr:sheetIdEnd]); err == nil {
sheetID = id
}
}
}
rid := ""
ridAttr := strings.Index(contentStr[nameAttrEnd:], `r:id="`)
if ridAttr != -1 {
ridAttr += nameAttrEnd + 6
ridEnd := strings.Index(contentStr[ridAttr:], `"`)
if ridEnd != -1 {
ridEnd += ridAttr
rid = contentStr[ridAttr:ridEnd]
}
}
path := ""
if rid != "" && ridToPath[rid] != "" {
path = ridToPath[rid]
if r.Files[path] == nil {
path = ""
}
}
if path == "" && rid != "" {
sheetNum := strings.TrimPrefix(rid, "rId")
path = fmt.Sprintf("xl/worksheets/sheet%s.xml", sheetNum)
if r.Files[path] == nil {
path = ""
}
}
if path == "" && sheetID > 0 {
path = fmt.Sprintf("xl/worksheets/sheet%d.xml", sheetID)
if r.Files[path] == nil {
path = ""
}
}
if path == "" && len(availableSheets) > 0 {
usedPaths := make(map[string]bool)
for _, info := range r.SheetMap {
usedPaths[info.Path] = true
}
for _, availPath := range availableSheets {
if !usedPaths[availPath] {
path = availPath
break
}
}
}
sheetNames = append(sheetNames, name)
r.SheetMap[name] = SheetInfo{
Name: name,
SheetID: sheetID,
RID: rid,
Path: path,
}
idx = nameAttrEnd + 1
}
fmt.Printf("✓ 工作簿中定义了 %d 个工作表\n", len(sheetNames))
}
func (r *XLSXReader) readWorkbookRels() map[string]string {
result := make(map[string]string)
content, ok := r.Files["xl/_rels/workbook.xml.rels"]
if !ok {
fmt.Println("⚠️ 找不到 xl/_rels/workbook.xml.rels 文件")
return result
}
contentStr := string(content)
idx := 0
for {
relStart := strings.Index(contentStr[idx:], `<Relationship `)
if relStart == -1 {
break
}
relStart += idx
idAttr := strings.Index(contentStr[relStart:], `Id="`)
if idAttr == -1 {
idx = relStart + 14
continue
}
idAttr += relStart + 4
idEnd := strings.Index(contentStr[idAttr:], `"`)
if idEnd == -1 {
idx = idAttr + 1
continue
}
idEnd += idAttr
rid := contentStr[idAttr:idEnd]
targetAttr := strings.Index(contentStr[idEnd:], `Target="`)
if targetAttr == -1 {
idx = idEnd + 1
continue
}
targetAttr += idEnd + 8
targetEnd := strings.Index(contentStr[targetAttr:], `"`)
if targetEnd == -1 {
idx = targetAttr + 1
continue
}
targetEnd += targetAttr
target := contentStr[targetAttr:targetEnd]
if strings.HasPrefix(target, "worksheets/") {
result[rid] = "xl/" + target
}
idx = targetEnd + 1
}
return result
}
func (r *XLSXReader) GetSheetNames() ([]string, error) {
var names []string
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]
if !ok {
return nil, fmt.Errorf("找不到工作表: %s", sheetName)
}
if sheetInfo.Path == "" {
return nil, fmt.Errorf("无法确定工作表 %s 的文件路径", sheetName)
}
sheetContent, ok := r.Files[sheetInfo.Path]
if !ok {
// 尝试查找所有worksheet文件
fmt.Printf("⚠️ 找不到文件: %s\n", sheetInfo.Path)
fmt.Println(" 可用的worksheet文件:")
for file := range r.Files {
if strings.HasPrefix(file, "xl/worksheets/") {
fmt.Printf(" - %s\n", file)
}
}
return nil, fmt.Errorf("找不到工作表文件: %s", sheetInfo.Path)
}
sharedStrings := r.loadSharedStrings()
fmt.Printf("✓ 加载到 %d 个共享字符串\n", len(sharedStrings))
return r.parseSheetData(string(sheetContent), sharedStrings), nil
}
func (r *XLSXReader) loadSharedStrings() []string {
content, ok := r.Files["xl/sharedStrings.xml"]
if !ok {
return nil
}
contentStr := string(content)
var sharedStrings []string
siIdx := 0
for {
siStart := strings.Index(contentStr[siIdx:], "<si>")
if siStart == -1 {
break
}
siStart += siIdx
siEnd := strings.Index(contentStr[siStart:], "</si>")
if siEnd == -1 {
break
}
siEnd += siStart
siContent := contentStr[siStart+4 : siEnd]
var text strings.Builder
tIdx := 0
for {
tStart := strings.Index(siContent[tIdx:], "<t")
if tStart == -1 {
break
}
tStart += tIdx
tCloseTag := strings.Index(siContent[tStart:], ">")
if tCloseTag == -1 {
break
}
tCloseTag += tStart
tEnd := strings.Index(siContent[tCloseTag:], "</t>")
if tEnd == -1 {
break
}
tEnd += tCloseTag
text.WriteString(siContent[tCloseTag+1 : tEnd])
tIdx = tEnd + 4
}
sharedStrings = append(sharedStrings, text.String())
siIdx = siEnd + 5
}
return sharedStrings
}
func (r *XLSXReader) parseSheetData(contentStr string, sharedStrings []string) [][]string {
sheetDataStart := strings.Index(contentStr, "<sheetData>")
if sheetDataStart == -1 {
return nil
}
sheetDataEnd := strings.Index(contentStr[sheetDataStart:], "</sheetData>")
if sheetDataEnd == -1 {
return nil
}
sheetDataEnd += sheetDataStart
sheetData := contentStr[sheetDataStart+11 : sheetDataEnd]
var result [][]string
maxCol := 0
rowIdx := 0
for {
rowStart := strings.Index(sheetData[rowIdx:], "<row ")
if rowStart == -1 {
break
}
rowStart += rowIdx
rowEnd := strings.Index(sheetData[rowStart:], "</row>")
if rowEnd == -1 {
break
}
rowEnd += rowStart
rowContent := sheetData[rowStart : rowEnd+6]
rowNumber := len(result) + 1
rowNumAttr := strings.Index(rowContent, ` r="`)
if rowNumAttr != -1 {
rowNumAttr += 4
rowNumEnd := strings.Index(rowContent[rowNumAttr:], `"`)
if rowNumEnd != -1 {
if rn, err := strconv.Atoi(rowContent[rowNumAttr : rowNumAttr+rowNumEnd]); err == nil {
for len(result) < rn-1 {
result = append(result, []string{})
}
rowNumber = rn
}
}
}
rowData := []string{}
cellIdx := 0
for {
cellStart := strings.Index(rowContent[cellIdx:], "<c ")
if cellStart == -1 {
break
}
cellStart += cellIdx
cellEnd := strings.Index(rowContent[cellStart:], "</c>")
if cellEnd == -1 {
break
}
cellEnd += cellStart
cellContent := rowContent[cellStart : cellEnd+4]
cellRef := ""
refAttr := strings.Index(cellContent, ` r="`)
if refAttr != -1 {
refAttr += 4
refEnd := strings.Index(cellContent[refAttr:], `"`)
if refEnd != -1 {
cellRef = cellContent[refAttr : refAttr+refEnd]
}
}
if cellRef == "" {
cellIdx = cellEnd + 4
continue
}
colIndex := getColumnIndex(cellRef)
cellType := ""
typeAttr := strings.Index(cellContent, ` t="`)
if typeAttr != -1 {
typeAttr += 4
typeEnd := strings.Index(cellContent[typeAttr:], `"`)
if typeEnd != -1 {
cellType = cellContent[typeAttr : typeAttr+typeEnd]
}
}
val := ""
vStart := strings.Index(cellContent, "<v>")
if vStart != -1 {
vStart += 3
vEnd := strings.Index(cellContent[vStart:], "</v>")
if vEnd != -1 {
val = cellContent[vStart : vStart+vEnd]
if cellType == "s" && sharedStrings != nil {
if idx, err := strconv.Atoi(val); err == nil && idx >= 0 && idx < len(sharedStrings) {
val = sharedStrings[idx]
}
}
}
}
for len(rowData) <= colIndex {
rowData = append(rowData, "")
}
rowData[colIndex] = val
if colIndex > maxCol {
maxCol = colIndex
}
cellIdx = cellEnd + 4
}
for len(result) < rowNumber {
result = append(result, []string{})
}
result[rowNumber-1] = rowData
rowIdx = rowEnd + 6
}
for i := range result {
for len(result[i]) <= maxCol {
result[i] = append(result[i], "")
}
}
return result
}
func getColumnIndex(cellRef string) int {
i := 0
for i < len(cellRef) {
c := cellRef[i]
if c < 'A' || c > 'Z' {
break
}
i++
}
colStr := cellRef[:i]
col := 0
for _, c := range colStr {
col = col*26 + int(c-'A') + 1
}
return col - 1
}
func ConvertToMarkdown(data [][]string) string {
if len(data) == 0 {
return "表格为空"
}
var result strings.Builder
numCols := 0
for _, row := range data {
if len(row) > numCols {
numCols = len(row)
}
}
for rowIdx, row := range data {
result.WriteString("|")
for colIdx := 0; colIdx < numCols; colIdx++ {
val := ""
if colIdx < len(row) {
val = escapeMarkdown(row[colIdx])
}
result.WriteString(fmt.Sprintf(" %s |", val))
}
result.WriteString("\n")
if rowIdx == 0 {
result.WriteString("|")
for colIdx := 0; colIdx < numCols; colIdx++ {
result.WriteString(" --- |")
}
result.WriteString("\n")
}
}
return result.String()
}
func escapeMarkdown(text string) string {
text = strings.ReplaceAll(text, "\\", "\\\\")
text = strings.ReplaceAll(text, "|", "\\|")
text = strings.ReplaceAll(text, "[", "\\[")
text = strings.ReplaceAll(text, "]", "\\]")
text = strings.ReplaceAll(text, "(", "\\(")
text = strings.ReplaceAll(text, ")", "\\)")
text = strings.ReplaceAll(text, "#", "\\#")
text = strings.ReplaceAll(text, "*", "\\*")
text = strings.ReplaceAll(text, "_", "\\_")
return text
}