inti
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# updata_hfb
|
||||||
|
|
||||||
|
一个 Go 桌面 GUI 上传工具。用户打开程序后填写服务器、上传人、上传密钥,粘贴账号数据,点击“预览 JSON”或“上传”即可,不需要使用命令行参数。
|
||||||
|
|
||||||
|
## 本地运行
|
||||||
|
|
||||||
|
在项目根目录直接运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以运行 cmd 入口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/updata_hfb
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后会打开窗口:
|
||||||
|
|
||||||
|
- `服务器`:默认 `http://127.0.0.1:8080`
|
||||||
|
- `上传人`:对应服务端的 `uploaderName`
|
||||||
|
- `上传密钥`:服务端外部上传密钥,未配置可留空
|
||||||
|
- `账号数据`:粘贴 `字段:值` 格式的数据
|
||||||
|
- `预览 JSON`:只转换,不上传
|
||||||
|
- `上传`:提交到服务端接口
|
||||||
|
|
||||||
|
## 文本格式
|
||||||
|
|
||||||
|
支持中文冒号 `:` 和英文冒号 `:`。
|
||||||
|
|
||||||
|
```text
|
||||||
|
上号方式:QQ扫码
|
||||||
|
哈夫币纯币:156
|
||||||
|
段位:铂金
|
||||||
|
等级:60
|
||||||
|
保险格数:9
|
||||||
|
每日消耗:10
|
||||||
|
体力:7
|
||||||
|
负重:7
|
||||||
|
AWM子弹数量:80
|
||||||
|
6头数量:13
|
||||||
|
6甲数量:12
|
||||||
|
皮肤:暗星
|
||||||
|
绝密KD:0.2
|
||||||
|
押金:350
|
||||||
|
号主在线时间:11:00 至 00:00
|
||||||
|
封禁记录:无
|
||||||
|
常用地区:海南
|
||||||
|
回收比例:1:47.0
|
||||||
|
回收租金:332
|
||||||
|
联系电话:15203616450
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
程序会读取这些环境变量作为窗口默认值:
|
||||||
|
|
||||||
|
- `HFB_SERVER`
|
||||||
|
- `HFB_UPLOADER_NAME`
|
||||||
|
- `HFB_UPLOAD_SECRET`
|
||||||
|
|
||||||
|
## 对接接口
|
||||||
|
|
||||||
|
- `POST /api/open/listing-uploads`
|
||||||
|
- 请求体是 JSON
|
||||||
|
- 如果填写上传密钥,会自动发送:
|
||||||
|
- `X-HFB-Timestamp`
|
||||||
|
- `X-HFB-Signature`
|
||||||
|
|
||||||
|
签名方式与服务端一致:`HMAC-SHA256(timestamp + "." + body)`。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
go vet ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
当前 Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o dist/updata_hfb ./cmd/updata_hfb
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要给 Windows 打包,建议在 Windows 环境里构建,或者准备完整的 Fyne 交叉编译环境。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "updata_hfb/internal/app"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
module updata_hfb
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require fyne.io/fyne/v2 v2.8.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
fyne.io/systray v1.12.2 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/FyshOS/fancyfs v0.0.1 // indirect
|
||||||
|
github.com/anthonynsimon/bild v0.14.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 // indirect
|
||||||
|
github.com/fyne-io/glfw-js v0.4.0 // indirect
|
||||||
|
github.com/fyne-io/image v0.1.1 // indirect
|
||||||
|
github.com/fyne-io/oksvg v0.2.0 // indirect
|
||||||
|
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect
|
||||||
|
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a // indirect
|
||||||
|
github.com/go-text/render v0.2.1 // indirect
|
||||||
|
github.com/go-text/typesetting v0.3.4 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
|
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||||
|
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rymdport/portal v0.4.2 // indirect
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
|
golang.org/x/image v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
fyne.io/fyne/v2 v2.8.0 h1:KNUdIk1eKsXSPy/wU6MdiR1hppAPvyzbjPbtJ8h6EUQ=
|
||||||
|
fyne.io/fyne/v2 v2.8.0/go.mod h1:tLJK7CVtUBOnMiSDR+J88t/quiGuEhwGs09tIVM1RXg=
|
||||||
|
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||||
|
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/FyshOS/fancyfs v0.0.1 h1:kgvm7VvwOMLkYTqSflplp62SlMVWQ2uAoHw9CXwXHYg=
|
||||||
|
github.com/FyshOS/fancyfs v0.0.1/go.mod h1:S5SHVz/5R72iCXOxCqdcyTPSlg3JxNd0gaHyGBSrY8A=
|
||||||
|
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
|
||||||
|
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
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/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
|
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
|
||||||
|
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 h1:0kdPD/GEntpWmZEK5Zu/xE6Tr37jYCVDf9QP8lA/QK8=
|
||||||
|
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||||
|
github.com/fyne-io/glfw-js v0.4.0 h1:I9hREBeFyI10cNIqbMKYb1PRidyPDgwob8o2la9SfQo=
|
||||||
|
github.com/fyne-io/glfw-js v0.4.0/go.mod h1:SDchsFZh4n7nVuBoiowOhOgIBdz+qUQVeC1w9fe2yVU=
|
||||||
|
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||||
|
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||||
|
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
|
||||||
|
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||||
|
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
|
||||||
|
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||||
|
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a h1:HWK0MBggT/T6YH7VffE10xBIhqeTq8JzIUPJXrRy87g=
|
||||||
|
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
|
||||||
|
github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
|
||||||
|
github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
|
||||||
|
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
|
||||||
|
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
|
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||||
|
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||||
|
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||||
|
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
|
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/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||||
|
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
fyneapp "fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
"updata_hfb/internal/hfb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sampleText = `上号方式:QQ扫码
|
||||||
|
哈夫币纯币:156
|
||||||
|
段位:铂金
|
||||||
|
等级:60
|
||||||
|
保险格数:9
|
||||||
|
每日消耗:10
|
||||||
|
体力:7
|
||||||
|
负重:7
|
||||||
|
AWM子弹数量:80
|
||||||
|
6头数量:13
|
||||||
|
6甲数量:12
|
||||||
|
皮肤:暗星
|
||||||
|
绝密KD:0.2
|
||||||
|
押金:350
|
||||||
|
号主在线时间:11:00 至 00:00
|
||||||
|
封禁记录:无
|
||||||
|
常用地区:海南
|
||||||
|
回收比例:1:47.0
|
||||||
|
回收租金:332
|
||||||
|
联系电话:15203616450`
|
||||||
|
|
||||||
|
func Run() {
|
||||||
|
a := fyneapp.NewWithID("com.hfb.updata")
|
||||||
|
w := a.NewWindow("哈夫币外部上传")
|
||||||
|
w.Resize(fyne.NewSize(920, 760))
|
||||||
|
|
||||||
|
serverEntry := widget.NewEntry()
|
||||||
|
serverEntry.SetText(envDefault("HFB_SERVER", "http://127.0.0.1:8080"))
|
||||||
|
serverEntry.SetPlaceHolder("例如:http://127.0.0.1:8080")
|
||||||
|
|
||||||
|
uploaderEntry := widget.NewEntry()
|
||||||
|
uploaderEntry.SetText(strings.TrimSpace(os.Getenv("HFB_UPLOADER_NAME")))
|
||||||
|
uploaderEntry.SetPlaceHolder("后台上传管理员名称")
|
||||||
|
|
||||||
|
secretEntry := widget.NewEntry()
|
||||||
|
secretEntry.Password = true
|
||||||
|
secretEntry.SetText(strings.TrimSpace(os.Getenv("HFB_UPLOAD_SECRET")))
|
||||||
|
secretEntry.SetPlaceHolder("未配置密钥可留空")
|
||||||
|
|
||||||
|
timeoutEntry := widget.NewEntry()
|
||||||
|
timeoutEntry.SetText("15s")
|
||||||
|
timeoutEntry.SetPlaceHolder("例如:15s")
|
||||||
|
|
||||||
|
dataEntry := widget.NewMultiLineEntry()
|
||||||
|
dataEntry.Wrapping = fyne.TextWrapWord
|
||||||
|
dataEntry.SetMinRowsVisible(18)
|
||||||
|
dataEntry.SetPlaceHolder(sampleText)
|
||||||
|
|
||||||
|
resultEntry := widget.NewMultiLineEntry()
|
||||||
|
resultEntry.Wrapping = fyne.TextWrapWord
|
||||||
|
resultEntry.SetMinRowsVisible(8)
|
||||||
|
resultEntry.Disable()
|
||||||
|
|
||||||
|
statusLabel := widget.NewLabel("准备就绪")
|
||||||
|
|
||||||
|
setResult := func(text string) {
|
||||||
|
resultEntry.Enable()
|
||||||
|
resultEntry.SetText(text)
|
||||||
|
resultEntry.Disable()
|
||||||
|
}
|
||||||
|
setBusy := func(uploadBtn *widget.Button, previewBtn *widget.Button, busy bool) {
|
||||||
|
if busy {
|
||||||
|
uploadBtn.Disable()
|
||||||
|
previewBtn.Disable()
|
||||||
|
statusLabel.SetText("正在上传,请稍等...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadBtn.Enable()
|
||||||
|
previewBtn.Enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadBtn *widget.Button
|
||||||
|
var previewBtn *widget.Button
|
||||||
|
|
||||||
|
loadBtn := widget.NewButton("打开文件", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("打开文件失败:%w", err), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("读取文件失败:%w", err), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataEntry.SetText(string(content))
|
||||||
|
statusLabel.SetText("已载入文件")
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
|
||||||
|
sampleBtn := widget.NewButton("填入示例", func() {
|
||||||
|
dataEntry.SetText(sampleText)
|
||||||
|
statusLabel.SetText("已填入示例数据")
|
||||||
|
})
|
||||||
|
|
||||||
|
clearBtn := widget.NewButton("清空", func() {
|
||||||
|
dataEntry.SetText("")
|
||||||
|
setResult("")
|
||||||
|
statusLabel.SetText("已清空")
|
||||||
|
})
|
||||||
|
|
||||||
|
previewBtn = widget.NewButton("预览 JSON", func() {
|
||||||
|
req, err := buildRequest(uploaderEntry.Text, dataEntry.Text)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("解析失败")
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := hfb.MarshalPretty(req)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("生成 JSON 失败")
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResult(string(out))
|
||||||
|
statusLabel.SetText("预览已生成")
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadBtn = widget.NewButton("上传", func() {
|
||||||
|
timeout, err := time.ParseDuration(strings.TrimSpace(timeoutEntry.Text))
|
||||||
|
if err != nil || timeout <= 0 {
|
||||||
|
dialog.ShowError(fmt.Errorf("请求超时时间不正确:%s", timeoutEntry.Text), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req, err := buildRequest(uploaderEntry.Text, dataEntry.Text)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("解析失败")
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := strings.TrimSpace(serverEntry.Text)
|
||||||
|
secret := strings.TrimSpace(secretEntry.Text)
|
||||||
|
setBusy(uploadBtn, previewBtn, true)
|
||||||
|
setResult("")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
resp, err := hfb.Client{
|
||||||
|
Server: server,
|
||||||
|
Secret: secret,
|
||||||
|
Timeout: timeout,
|
||||||
|
}.Upload(ctx, req)
|
||||||
|
|
||||||
|
fyne.Do(func() {
|
||||||
|
setBusy(uploadBtn, previewBtn, false)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("上传失败")
|
||||||
|
setResult(err.Error())
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statusLabel.SetText("上传成功")
|
||||||
|
setResult(resp)
|
||||||
|
dialog.ShowInformation("上传成功", "数据已经提交到服务器。", w)
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
uploadBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
form := widget.NewForm(
|
||||||
|
widget.NewFormItem("服务器", serverEntry),
|
||||||
|
widget.NewFormItem("上传人", uploaderEntry),
|
||||||
|
widget.NewFormItem("上传密钥", secretEntry),
|
||||||
|
widget.NewFormItem("超时时间", timeoutEntry),
|
||||||
|
)
|
||||||
|
toolbar := container.NewHBox(loadBtn, sampleBtn, clearBtn, previewBtn, uploadBtn)
|
||||||
|
content := container.NewBorder(
|
||||||
|
nil,
|
||||||
|
container.NewVBox(statusLabel),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
container.NewVScroll(container.NewVBox(
|
||||||
|
widget.NewLabel("连接配置"),
|
||||||
|
form,
|
||||||
|
toolbar,
|
||||||
|
widget.NewLabel("账号数据"),
|
||||||
|
dataEntry,
|
||||||
|
widget.NewLabel("结果"),
|
||||||
|
resultEntry,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
w.SetContent(content)
|
||||||
|
w.ShowAndRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequest(uploaderName string, rawText string) (hfb.ExternalUploadRequest, error) {
|
||||||
|
if strings.TrimSpace(rawText) == "" {
|
||||||
|
return hfb.ExternalUploadRequest{}, fmt.Errorf("账号数据不能为空")
|
||||||
|
}
|
||||||
|
parsed, err := hfb.ParseUploadText(rawText)
|
||||||
|
if err != nil {
|
||||||
|
return hfb.ExternalUploadRequest{}, err
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(uploaderName)
|
||||||
|
if name == "" {
|
||||||
|
name = parsed.UploaderName
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return hfb.ExternalUploadRequest{}, fmt.Errorf("缺少上传人名称:请在“上传人”里填写,或在文本中增加“上传人:姓名”")
|
||||||
|
}
|
||||||
|
return hfb.NewExternalUploadRequest(name, parsed.Account, time.Now()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envDefault(key string, fallback string) string {
|
||||||
|
value := strings.TrimSpace(os.Getenv(key))
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package hfb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var numberPattern = regexp.MustCompile(`[-+]?\d+(?:\.\d+)?`)
|
||||||
|
|
||||||
|
func ParseUploadText(text string) (ParsedUpload, error) {
|
||||||
|
values := make(map[string]string)
|
||||||
|
for lineNo, line := range strings.Split(text, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value, ok := splitKeyValue(line)
|
||||||
|
if !ok {
|
||||||
|
return ParsedUpload{}, fmt.Errorf("第 %d 行格式不正确,应为“字段:值”", lineNo+1)
|
||||||
|
}
|
||||||
|
key = normalizeKey(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values[key] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
account := ExternalAccountData{}
|
||||||
|
var err error
|
||||||
|
account.LoginMethod = firstValue(values, "上号方式", "登录方式")
|
||||||
|
account.Rank = firstValue(values, "段位")
|
||||||
|
account.Level, err = parseRequiredInt(values, "等级")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.SafeSlots, err = parseRequiredInt(values, "保险格数", "保险箱格数")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.DailyLossM, err = parseRequiredFloat(values, "每日消耗")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.SecretKD, err = parseRequiredFloat(values, "绝密KD", "绝密kd")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Deposit, err = parseRequiredFloat(values, "押金")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.BanRecord = optionalDefault(firstValue(values, "封禁记录"), "无")
|
||||||
|
account.CommonRegion = firstValue(values, "常用地区", "常用区域")
|
||||||
|
account.ContactPhone = firstValue(values, "联系电话", "手机号", "电话")
|
||||||
|
account.OwnerOnlineTime = firstValue(values, "号主在线时间", "在线时间")
|
||||||
|
|
||||||
|
account.Currency.HafuCoin, err = parseRequiredFloat(values, "哈夫币纯币", "哈夫币")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Currency.RecycleRatio, err = parseRequiredRatio(values, "回收比例")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Currency.RecycleRent, err = parseRequiredFloat(values, "回收租金")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.DailyConsumption.Stamina, err = parseRequiredInt(values, "体力")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.DailyConsumption.Weight, err = parseRequiredInt(values, "负重")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Inventory.AWMBullets, err = parseOptionalInt(values, "AWM子弹数量", "AWM子弹")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Inventory.Level6Helmets, err = parseOptionalInt(values, "6头数量", "六头数量", "6头")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Inventory.Level6Armor, err = parseOptionalInt(values, "6甲数量", "六甲数量", "6甲")
|
||||||
|
if err != nil {
|
||||||
|
return ParsedUpload{}, err
|
||||||
|
}
|
||||||
|
account.Inventory.Skins = parseSkins(firstValue(values, "皮肤"))
|
||||||
|
|
||||||
|
if strings.TrimSpace(account.LoginMethod) == "" {
|
||||||
|
return ParsedUpload{}, fmt.Errorf("缺少必填字段:上号方式")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(account.Rank) == "" {
|
||||||
|
return ParsedUpload{}, fmt.Errorf("缺少必填字段:段位")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedUpload{
|
||||||
|
UploaderName: firstValue(values, "上传人", "上传者", "上传账号", "上传管理员"),
|
||||||
|
Account: account,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitKeyValue(line string) (string, string, bool) {
|
||||||
|
if idx := strings.Index(line, ":"); idx >= 0 {
|
||||||
|
return line[:idx], line[idx+len(":"):], true
|
||||||
|
}
|
||||||
|
if idx := strings.Index(line, ":"); idx >= 0 {
|
||||||
|
return line[:idx], line[idx+1:], true
|
||||||
|
}
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKey(key string) string {
|
||||||
|
return strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(key), " ", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstValue(values map[string]string, labels ...string) string {
|
||||||
|
for _, label := range labels {
|
||||||
|
if value, ok := values[normalizeKey(label)]; ok {
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalDefault(value, fallback string) string {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequiredInt(values map[string]string, labels ...string) (int, error) {
|
||||||
|
value := firstValue(values, labels...)
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||||||
|
}
|
||||||
|
return parseIntLabel(labels[0], value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptionalInt(values map[string]string, labels ...string) (int, error) {
|
||||||
|
value := firstValue(values, labels...)
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return parseIntLabel(labels[0], value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntLabel(label, value string) (int, error) {
|
||||||
|
number, err := parseFloatText(value, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s 数字格式不正确:%s", label, value)
|
||||||
|
}
|
||||||
|
return int(number), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequiredFloat(values map[string]string, labels ...string) (float64, error) {
|
||||||
|
value := firstValue(values, labels...)
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||||||
|
}
|
||||||
|
number, err := parseFloatText(value, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s 数字格式不正确:%s", labels[0], value)
|
||||||
|
}
|
||||||
|
return number, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequiredRatio(values map[string]string, labels ...string) (float64, error) {
|
||||||
|
value := firstValue(values, labels...)
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||||||
|
}
|
||||||
|
number, err := parseFloatText(value, true)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s 数字格式不正确:%s", labels[0], value)
|
||||||
|
}
|
||||||
|
return number, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloatText(value string, preferLast bool) (float64, error) {
|
||||||
|
value = strings.TrimSpace(strings.ReplaceAll(value, ",", ""))
|
||||||
|
matches := numberPattern.FindAllString(value, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return 0, strconv.ErrSyntax
|
||||||
|
}
|
||||||
|
pick := matches[0]
|
||||||
|
if preferLast {
|
||||||
|
pick = matches[len(matches)-1]
|
||||||
|
}
|
||||||
|
return strconv.ParseFloat(pick, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSkins(value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" || value == "无" || strings.EqualFold(value, "none") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.FieldsFunc(value, func(r rune) bool {
|
||||||
|
return r == ',' || r == ',' || r == '、' || r == ';' || r == ';'
|
||||||
|
})
|
||||||
|
skins := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part != "" && part != "无" {
|
||||||
|
skins = append(skins, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skins
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package hfb
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const sampleText = `上号方式:QQ扫码
|
||||||
|
哈夫币纯币:156
|
||||||
|
段位:铂金
|
||||||
|
等级:60
|
||||||
|
保险格数:9
|
||||||
|
每日消耗:10
|
||||||
|
体力:7
|
||||||
|
负重:7
|
||||||
|
AWM子弹数量:80
|
||||||
|
6头数量:13
|
||||||
|
6甲数量:12
|
||||||
|
皮肤:暗星
|
||||||
|
绝密KD:0.2
|
||||||
|
押金:350
|
||||||
|
号主在线时间:11:00 至 00:00
|
||||||
|
封禁记录:无
|
||||||
|
常用地区:海南
|
||||||
|
回收比例:1:47.0
|
||||||
|
回收租金:332
|
||||||
|
联系电话:15203616450`
|
||||||
|
|
||||||
|
func TestParseUploadTextSample(t *testing.T) {
|
||||||
|
parsed, err := ParseUploadText(sampleText)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseUploadText() error = %v", err)
|
||||||
|
}
|
||||||
|
account := parsed.Account
|
||||||
|
if account.LoginMethod != "QQ扫码" {
|
||||||
|
t.Fatalf("LoginMethod = %q", account.LoginMethod)
|
||||||
|
}
|
||||||
|
if account.Currency.HafuCoin != 156 {
|
||||||
|
t.Fatalf("HafuCoin = %v", account.Currency.HafuCoin)
|
||||||
|
}
|
||||||
|
if account.Currency.RecycleRatio != 47 {
|
||||||
|
t.Fatalf("RecycleRatio = %v", account.Currency.RecycleRatio)
|
||||||
|
}
|
||||||
|
if account.ContactPhone != "15203616450" {
|
||||||
|
t.Fatalf("ContactPhone = %q", account.ContactPhone)
|
||||||
|
}
|
||||||
|
if got := len(account.Inventory.Skins); got != 1 || account.Inventory.Skins[0] != "暗星" {
|
||||||
|
t.Fatalf("Skins = %#v", account.Inventory.Skins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUploadTextASCIIDelimiterKeepsRatioValue(t *testing.T) {
|
||||||
|
text := sampleText + "\n上传人:张三"
|
||||||
|
parsed, err := ParseUploadText(text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseUploadText() error = %v", err)
|
||||||
|
}
|
||||||
|
if parsed.UploaderName != "张三" {
|
||||||
|
t.Fatalf("UploaderName = %q", parsed.UploaderName)
|
||||||
|
}
|
||||||
|
if parsed.Account.Currency.RecycleRatio != 47 {
|
||||||
|
t.Fatalf("RecycleRatio = %v", parsed.Account.Currency.RecycleRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUploadTextMissingRequiredField(t *testing.T) {
|
||||||
|
_, err := ParseUploadText("上号方式:QQ扫码")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ParseUploadText() expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package hfb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ParsedUpload struct {
|
||||||
|
UploaderName string
|
||||||
|
Account ExternalAccountData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalUploadRequest struct {
|
||||||
|
UploadTime int64 `json:"uploadTime"`
|
||||||
|
UploaderName string `json:"uploaderName"`
|
||||||
|
Data ExternalAccountData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalAccountData struct {
|
||||||
|
LoginMethod string `json:"loginMethod"`
|
||||||
|
Rank string `json:"rank"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
SafeSlots int `json:"safeSlots"`
|
||||||
|
SecretKD float64 `json:"secretKD"`
|
||||||
|
DailyLossM float64 `json:"dailyLossM"`
|
||||||
|
Deposit float64 `json:"deposit"`
|
||||||
|
BanRecord string `json:"banRecord"`
|
||||||
|
CommonRegion string `json:"commonRegion"`
|
||||||
|
ContactPhone string `json:"contactPhone"`
|
||||||
|
OwnerOnlineTime string `json:"ownerOnlineTime"`
|
||||||
|
Currency ExternalUploadCurrency `json:"currency"`
|
||||||
|
DailyConsumption ExternalDailyConsumption `json:"dailyConsumption"`
|
||||||
|
Inventory ExternalUploadInventory `json:"inventory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalUploadCurrency struct {
|
||||||
|
HafuCoin float64 `json:"hafuCoin"`
|
||||||
|
RecycleRatio float64 `json:"recycleRatio"`
|
||||||
|
RecycleRent float64 `json:"recycleRent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalDailyConsumption struct {
|
||||||
|
Stamina int `json:"stamina"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalUploadInventory struct {
|
||||||
|
AWMBullets int `json:"awmBullets"`
|
||||||
|
Level6Helmets int `json:"level6Helmets"`
|
||||||
|
Level6Armor int `json:"level6Armor"`
|
||||||
|
Skins []string `json:"skins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExternalUploadRequest(uploaderName string, account ExternalAccountData, now time.Time) ExternalUploadRequest {
|
||||||
|
return ExternalUploadRequest{
|
||||||
|
UploadTime: now.Unix(),
|
||||||
|
UploaderName: uploaderName,
|
||||||
|
Data: account,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalPretty(req ExternalUploadRequest) ([]byte, error) {
|
||||||
|
return json.MarshalIndent(req, "", " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package hfb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const uploadPath = "/api/open/listing-uploads"
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Server string
|
||||||
|
Secret string
|
||||||
|
Timeout time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Upload(ctx context.Context, req ExternalUploadRequest) (string, error) {
|
||||||
|
body, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("生成上传 JSON 失败:%w", err)
|
||||||
|
}
|
||||||
|
endpoint, err := normalizeEndpoint(c.Server)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建上传请求失败:%w", err)
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("User-Agent", "updata_hfb/1.0")
|
||||||
|
if strings.TrimSpace(c.Secret) != "" {
|
||||||
|
timestamp := strconvFormatUnix(time.Now())
|
||||||
|
httpReq.Header.Set("X-HFB-Timestamp", timestamp)
|
||||||
|
httpReq.Header.Set("X-HFB-Signature", signBody(c.Secret, timestamp, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := c.HTTPClient
|
||||||
|
if httpClient == nil {
|
||||||
|
timeout := c.Timeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
httpClient = &http.Client{Timeout: timeout}
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("请求服务器失败:%w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取服务器响应失败:%w", err)
|
||||||
|
}
|
||||||
|
text := strings.TrimSpace(string(respBody))
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
if text == "" {
|
||||||
|
text = resp.Status
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("服务器返回失败状态 %s:%s", resp.Status, text)
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
return resp.Status, nil
|
||||||
|
}
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEndpoint(raw string) (string, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return "", fmt.Errorf("服务器地址不能为空")
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(raw)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return "", fmt.Errorf("服务器地址不正确:%s", raw)
|
||||||
|
}
|
||||||
|
if strings.Contains(parsed.Path, uploadPath) {
|
||||||
|
return parsed.String(), nil
|
||||||
|
}
|
||||||
|
basePath := strings.TrimRight(parsed.Path, "/")
|
||||||
|
parsed.Path = basePath + uploadPath
|
||||||
|
return parsed.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func signBody(secret, timestamp string, body []byte) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(secret)))
|
||||||
|
mac.Write([]byte(timestamp))
|
||||||
|
mac.Write([]byte("."))
|
||||||
|
mac.Write(body)
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func strconvFormatUnix(t time.Time) string {
|
||||||
|
return fmt.Sprintf("%d", t.Unix())
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package hfb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUploadSendsSignedRequest(t *testing.T) {
|
||||||
|
const secret = "test-secret"
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != uploadPath {
|
||||||
|
t.Fatalf("path = %q", r.URL.Path)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadAll() error = %v", err)
|
||||||
|
}
|
||||||
|
timestamp := r.Header.Get("X-HFB-Timestamp")
|
||||||
|
signature := r.Header.Get("X-HFB-Signature")
|
||||||
|
if timestamp == "" || signature == "" {
|
||||||
|
t.Fatal("missing signature headers")
|
||||||
|
}
|
||||||
|
if signature != expectedSignature(secret, timestamp, body) {
|
||||||
|
t.Fatalf("signature = %q", signature)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := Client{Server: server.URL, Secret: secret, Timeout: time.Second}
|
||||||
|
req := NewExternalUploadRequest("张三", mustSampleAccount(t), time.Unix(100, 0))
|
||||||
|
resp, err := client.Upload(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Upload() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp != `{"ok":true}` {
|
||||||
|
t.Fatalf("resp = %q", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeEndpoint(t *testing.T) {
|
||||||
|
got, err := normalizeEndpoint("http://127.0.0.1:8080")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeEndpoint() error = %v", err)
|
||||||
|
}
|
||||||
|
want := "http://127.0.0.1:8080" + uploadPath
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("endpoint = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSampleAccount(t *testing.T) ExternalAccountData {
|
||||||
|
t.Helper()
|
||||||
|
parsed, err := ParseUploadText(sampleText)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseUploadText() error = %v", err)
|
||||||
|
}
|
||||||
|
return parsed.Account
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedSignature(secret, timestamp string, body []byte) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
|
mac.Write([]byte(timestamp))
|
||||||
|
mac.Write([]byte("."))
|
||||||
|
mac.Write(body)
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user