This commit is contained in:
lychang
2026-05-19 19:42:55 +08:00
commit 5e3dac3020
21 changed files with 8611 additions and 0 deletions

1496
annual.txt Normal file

File diff suppressed because it is too large Load Diff

18
go.mod Normal file
View File

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

105
go.sum Normal file
View File

@@ -0,0 +1,105 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

636
main.go Normal file
View File

@@ -0,0 +1,636 @@
package main
import (
"archive/zip"
"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
}
func main() {
fmt.Println("========== Excel转Markdown表格 ==========")
excelFile := "yst郑州惠兴财务报表2021(3).xlsx"
fmt.Printf("Excel文件: %s\n\n", excelFile)
reader, err := NewXLSXReader(excelFile)
if err != nil {
fmt.Printf("❌ 创建XLSX读取器失败: %v\n", err)
return
}
fmt.Println("✓ 创建XLSX读取器成功")
// 打印所有文件列表
fmt.Println("\n--- 压缩包内文件列表 ---")
for name := range reader.files {
if strings.HasPrefix(name, "xl/worksheets/") {
fmt.Printf(" %s\n", name)
}
}
sheets, err := reader.GetSheetNames()
if err != nil {
fmt.Printf("❌ 获取工作表名称失败: %v\n", err)
return
}
fmt.Printf("\n✓ 获取到 %d 个工作表:\n", len(sheets))
for i, sheet := range sheets {
if info, ok := reader.sheetMap[sheet]; ok {
fmt.Printf(" %d. %s -> rId=%s, path=%s\n", i+1, sheet, info.RID, info.Path)
} else {
fmt.Printf(" %d. %s\n", i+1, sheet)
}
}
for i, sheetName := range sheets {
fmt.Printf("\n========== 工作表: %s ==========\n", sheetName)
data, err := reader.GetSheetData(sheetName)
if err != nil {
fmt.Printf("❌ 读取工作表失败: %v\n", err)
continue
}
if len(data) == 0 {
fmt.Println("⚠️ 工作表为空")
continue
}
fmt.Printf("找到 %d 行数据\n", len(data))
if len(data[0]) > 0 {
fmt.Printf("表格尺寸: %d 行 × %d 列\n", len(data), len(data[0]))
fmt.Printf("前3行预览:\n")
for r := 0; r < min(len(data), 3); r++ {
rowData := make([]string, 0, len(data[r]))
for c := 0; c < min(len(data[r]), 4); c++ {
val := data[r][c]
if len(val) > 20 {
val = val[:20] + "..."
}
rowData = append(rowData, fmt.Sprintf("%q", val))
}
fmt.Printf(" 第%d行: %s\n", r+1, strings.Join(rowData, ", "))
}
}
markdown := ConvertToMarkdown(data)
safeName := sanitizeFileName(sheetName)
err = SaveToFile(markdown, fmt.Sprintf("sheet%d_%s.md", i+1, safeName))
if err != nil {
fmt.Printf("❌ 保存文件失败: %v\n", err)
} else {
fmt.Printf("✓ 已保存到: sheet%d_%s.md\n", i+1, safeName)
}
}
fmt.Println("\n✅ 所有工作表已转换完成!")
}
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()
fi, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
zr, err := zip.NewReader(f, fi.Size())
if err != nil {
return nil, fmt.Errorf("创建zip读取器失败: %v", err)
}
files := make(map[string][]byte)
for _, file := range zr.File {
f, err := file.Open()
if err != nil {
return nil, fmt.Errorf("打开内部文件失败: %v", err)
}
content, err := io.ReadAll(f)
f.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
}
func SaveToFile(content, filename string) error {
return os.WriteFile(filename, []byte(content), 0644)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

1557
quarterly1.txt Normal file

File diff suppressed because it is too large Load Diff

1555
quarterly2.txt Normal file

File diff suppressed because it is too large Load Diff

1556
quarterly3.txt Normal file

File diff suppressed because it is too large Load Diff

93
requirements.md Normal file
View File

@@ -0,0 +1,93 @@
## 公司信息查询接口
"https://a.cmdp.cn/cmds/v1/company/info-rest/custom?name=深圳市致远速联信息技术有限公司&api=p_info,p_biddings_all"
## 税务健康测评接口
使用步骤:
1,调用通用加密服务获取签名
2,通过key 以及查询参数,获取下载url
3,通过2中的url 下载数据文件
1.1数据签名
采用国密算法,使用用户私钥(SM2)对消息(用户ID+时间戳+请求的Path,包括参数等)的哈希值(SM3)进行签名后的base64字符串,存放在Http协议请求头(Header)中
base64(SM2(SM3({user-id}{timestamp}{nonce}{uri}{Body})))
其中参数
参数名称 是否必填 参数描述
user-id 是 用户ID
timestamp 是 时间戳,精确到秒,一分钟超时
nonce 否 随机数
uri 是 请求接口的完整url地址
Body 否 请求体json,签名时的json和请求接口的json必须完全一致(包含格式化等情况)
示例代码如下:
PS:签名时的数据(例如:随机数和时间戳等)必须和请求接口时的保持一致
2.1 税务健康测评接口
请求方式:post
接口url https://api.cmdp.cn/v1/eval/tax-risk
接口说明:
执行税务健康测评
2.2.1 请求头参数值
英文名称 中文名称 类型 是否必填 说明
x-gi-user-id 用记名 string 是
x-gi-user-sig 签名 string 是 步骤1 中获取
x-gi-nonce 随机数 String 否
x-gi-timestamp 时间戳 Int 是
2.2.2 输入参数值
英文名称 中文名称 类型 是否必填 说明示例
req_no 第三方申请单号 string 是 第三方申请单号
callback 结果回调地址 string 否 结果回调地址
detail.file_url 用于评测的财务报表文件地址(目前仅支持Excel格式) string 否 用于评测的财务报表文件地址(目前仅支持Excel格式),该字段与data字段选择其中一个填写即可
detail.data
用于评测的财务报表Json数据 Object 否 用于评测的财务报表Json数据(具体数据项字段可参考字段表),该字段与file_url字段选择其中一个填写即可
detail.company_name 企业名称,工商注册的企业全称 string 否 企业名称,工商注册的企业全称
detail.period 测评周期,年度,季度(第一季度,第二季度...) string 是 测评周期,年度,季度(第一季度,第二季度...)
detail.year 报告期 string 是 报告期
detail.is_verify 是否校验参数数值 bool 否 是否校验参数数值
detail.cit 企业所得税应纳税额合计,单位:万元 number 是 企业所得税应纳税额合计,单位:万元
detail.vat 增值税应纳税额合计,单位:万元 number 是 增值税应纳税额合计,单位:万元
detail.input_vat 增值税进项税额,单位:万元 number 是 增值税进项税额,单位:万元
detail.output_vat 增值税销项税额,单位:万元 number 是 增值税销项税额,单位:万元
detail.pre_cit 上一期企业所得税应纳税额合计,单位:万元 number 是 上一期企业所得税应纳税额合计,单位:万元
detail.pre_vat 上一期增值税应纳税额合计,单位:万元 number 是 上一期增值税应纳税额合计,单位:万元
detail.pre_input_vat 上一期增值税进项税额,单位:万元 number 是 上一期增值税进项税额,单位:万元
detail.pre_output_vat 上一期增值税销项税额,单位:万元 number 是 上一期增值税销项税额,单位:万元
2.3 输出参数值
英文名称 类型 说明
code
Int 结果状态码,200为成功,其他均为失败
msg
string 状态码信息
Data.req_no
string 第三方申请单号
Data.order_no string CMDP订单号
3.查询结果接口
请求方式:GET
接口url https://api.cmdp.cn/v1/eval/order/{order_no}
接口说明:
获取结果文件的下载url, {order_no}为步骤2接口的返回
3.1请求头参数值
英文名称 中文名称 类型 是否必填 说明
x-gi-user-id 用记名 string 是
x-gi-user-sig 签名 string 是 步骤1 中获取
x-gi-nonce 随机数 String 否
x-gi-timestamp 时间戳 Int 是
3.2. 输出参数值
英文名称 中文名称 类型 单位 说明
status 订单状态 Int -1/无效 ,0/正在执行,99/执行成功, 98/执行失败
detail 订单详情 objecy
detail.files 结果文件地址 Array
detail.error 错误提示信息 string

90
sign_utils.go Normal file
View File

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

351
tax_eval_api.go Normal file
View File

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

445
tax_eval_test.go Normal file
View File

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

92
test_sign.go Normal file
View File

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

366
xlsx_reader.go Normal file
View File

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

251
xlsx_reader_test.go Normal file
View File

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

Binary file not shown.

BIN
税务健康测评API.docx Normal file

Binary file not shown.