update
This commit is contained in:
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))
|
||||
}
|
||||
585
internal/excel/loader.go
Normal file
585
internal/excel/loader.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user