```
feat(supplier): 新增核销卡发送任务类型及优化签名生成逻辑 - 为 `aibo.go` 添加 `generateSign` 方法用于生成 MD5 签名,并替换原有全局函数调用方式 - 引入 `sort` 和 `maputil` 包以支持参数排序和键提取 - 在多个文件中将 `fakeuseragent.RandomUserAgent()` 替换为 `useragent.GetUserAgentByPlatform(useragent.PlatformPhone)` 以统一 User-Agent 生成策略 - 新增 `SendCardTaskTypeEnumNuclear` 类型及其完整实现,包括订单创建、验证码识别、支付提交等流程 -为部分 HTTP 请求显式添加 `SetContext(ctx)`以确保上下文传递正确 - 更新依赖版本,包括 go version、golang.org/x/text 及新增 golang.org/x/image 等模块- 增加指纹生成测试用例和工具函数 ```
This commit is contained in:
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(go build:*)",
|
||||
"Bash(go test:*)",
|
||||
"Bash(go run:*)",
|
||||
"Bash(timeout:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
8
go.mod
8
go.mod
@@ -1,8 +1,8 @@
|
||||
module gateway
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
toolchain go1.24.6
|
||||
|
||||
require github.com/beego/beego/v2 v2.3.8
|
||||
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-stomp/stomp/v3 v3.1.3
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/iunary/fakeuseragent v1.0.0
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
@@ -41,6 +42,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
||||
golang.org/x/image v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -80,7 +82,7 @@ require (
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -55,6 +55,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stomp/stomp/v3 v3.1.3 h1:5/wi+bI38O1Qkf2cc7Gjlw7N5beHMWB/BxpX+4p/MGI=
|
||||
github.com/go-stomp/stomp/v3 v3.1.3/go.mod h1:ztzZej6T2W4Y6FlD+Tb5n7HQP3/O5UNQiuC169pIp10=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -189,6 +191,8 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -250,8 +254,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
8
internal/cache/redis.go
vendored
8
internal/cache/redis.go
vendored
@@ -81,7 +81,7 @@ func (r *RedisClient) GetSize(ctx context.Context, key string) (int64, error) {
|
||||
return r.Client.DBSize(ctx).Result()
|
||||
}
|
||||
|
||||
// 获取以 prefix 为前缀的键值数量
|
||||
// GetSizeByPrefix 获取以 prefix 为前缀的键值数量
|
||||
func (r *RedisClient) GetSizeByPrefix(ctx context.Context, prefix string) (int64, error) {
|
||||
keys, err := r.Client.Keys(ctx, prefix+"*").Result()
|
||||
if err != nil {
|
||||
@@ -253,7 +253,7 @@ func (r *RedisClient) XDel(ctx context.Context, key string, ids ...string) (int6
|
||||
return r.Client.XDel(ctx, key, ids...).Result()
|
||||
}
|
||||
|
||||
// XRead 自动反序列化到 map[string]interface{}
|
||||
// XReadUnmarshal 自动反序列化到 map[string]interface{}
|
||||
func (r *RedisClient) XReadUnmarshal(ctx context.Context, key, start string, count int64, block time.Duration) ([]map[string]interface{}, error) {
|
||||
args := &redis.XReadArgs{
|
||||
Streams: []string{key, start},
|
||||
@@ -279,8 +279,8 @@ func (r *RedisClient) XReadUnmarshal(ctx context.Context, key, start string, cou
|
||||
|
||||
// XRange 区间查询 Stream
|
||||
func (r *RedisClient) XRange(ctx context.Context, key, start, end string, count int64) ([]redis.XMessage, error) {
|
||||
msgs, err := r.Client.XRangeN(ctx, key, start, end, count).Result()
|
||||
return msgs, err
|
||||
msg, err := r.Client.XRangeN(ctx, key, start, end, count).Result()
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// XLen 获取 Stream 长度
|
||||
|
||||
@@ -39,7 +39,7 @@ func (c *OCRClient) Calc(ctx context.Context, input *CalcInput) (string, error)
|
||||
webClient := resty.New()
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
resp, err := webClient.R().SetFormData(map[string]string{
|
||||
resp, err := webClient.R().SetContext(ctx).SetFormData(map[string]string{
|
||||
"category": input.Category,
|
||||
"length": fmt.Sprintf("%d", input.Length),
|
||||
}).SetFileReader("file", input.FileName, bytes.NewReader(input.File)).
|
||||
|
||||
@@ -68,7 +68,7 @@ func SendOrderNotify(ctx context.Context, bankOrderId string) {
|
||||
webClient := resty.New().SetTimeout(time.Second * 5).SetRetryCount(3).SetRetryMaxWaitTime(5 * time.Second)
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
response, err := webClient.R().Get(notifyInfo.Url)
|
||||
response, err := webClient.R().SetContext(ctx).Get(notifyInfo.Url)
|
||||
if err == nil {
|
||||
otelTrace.Logger.WithContext(ctx).Info("回调结果", zap.String("bankOrderId", bankOrderId), zap.String("response", response.String()))
|
||||
}
|
||||
|
||||
@@ -203,7 +203,9 @@ func SolvePayFail(ctx context.Context, bankOrderId, transId string, cardReturnDa
|
||||
attribute.String("cardReturnData", cardReturnData),
|
||||
))
|
||||
defer cancel()
|
||||
|
||||
if cardReturnData == "" {
|
||||
cardReturnData = "核销失败"
|
||||
}
|
||||
o := orm.NewOrm()
|
||||
err := o.DoTxWithCtx(ctx, func(ctx context.Context, txOrm orm.TxOrmer) error {
|
||||
var orderTmp order.OrderInfo
|
||||
|
||||
20
internal/service/supplier/third_party/aibo.go
vendored
20
internal/service/supplier/third_party/aibo.go
vendored
@@ -20,6 +20,7 @@ import (
|
||||
"gateway/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -44,6 +46,20 @@ func (c *AiboCardImpl) HasDependencyHTML() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 生成md5加密
|
||||
func (c *AiboCardImpl) generateSign(ctx context.Context, params map[string]any, key string) string {
|
||||
keys := maputil.Keys(params)
|
||||
sort.Strings(keys)
|
||||
sign_ := ""
|
||||
for _, key2 := range keys {
|
||||
if params[key2] == "" {
|
||||
continue
|
||||
}
|
||||
sign_ += key2 + "=" + convertor.ToString(params[key2]) + "&"
|
||||
}
|
||||
sign_ += "key=" + key
|
||||
return utils.EncodeMd5Str(sign_)
|
||||
}
|
||||
func (c *AiboCardImpl) SendCard(ctx context.Context, jsonStr string, cardInfo supplier.RedeemCardInfo, attach string) (bool, string) {
|
||||
// 构建数据结构
|
||||
cfg := config.Config{}
|
||||
@@ -72,7 +88,7 @@ func (c *AiboCardImpl) SendCard(ctx context.Context, jsonStr string, cardInfo su
|
||||
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
"sign": "",
|
||||
}
|
||||
data["sign"] = generateSign(ctx, data, gojson.Json(jsonStr).Get("key").Tostring())
|
||||
data["sign"] = c.generateSign(ctx, data, gojson.Json(jsonStr).Get("key").Tostring())
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求sign", zap.Any("sign", data["sign"]), zap.Any("params", data))
|
||||
req, _ := httplib.NewBeegoRequestWithCtx(ctx, "http://aibomoyi.cn/index.php/api/api/sendcard", "POST").
|
||||
SetTransport(otelhttp.NewTransport(http.DefaultTransport)).
|
||||
@@ -207,7 +223,7 @@ func (c *AiboCardImpl) PayNotify() {
|
||||
c.Ctx.WriteString("FAIL")
|
||||
return
|
||||
}
|
||||
tmpSign := generateSign(ctx, params, gojson.Json(roadInfo.Params).Get("key").Tostring())
|
||||
tmpSign := c.generateSign(ctx, params, gojson.Json(roadInfo.Params).Get("key").Tostring())
|
||||
if tmpSign != c.GetString("sign") {
|
||||
otelTrace.Logger.WithContext(ctx).Error("【爱博】回调验签失败,订单号=",
|
||||
zap.String("attach", convertor.ToString(params["orderId"])),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service/client"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
"github.com/dubonzi/otelresty"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
@@ -134,7 +134,7 @@ func (c *cardTypeQuery) xinTianQuery(ctx context.Context) (balance float64, err
|
||||
}
|
||||
|
||||
func (c *cardTypeQuery) outletCardQuery(ctx context.Context) (balance float64, err error) {
|
||||
ua := fakeuseragent.RandomUserAgent()
|
||||
ua := useragent.GetUserAgentByPlatform(useragent.PlatformPhone)
|
||||
webClient := resty.New().EnableTrace().SetTimeout(5 * time.Second).SetRetryCount(3).SetHeaders(map[string]string{
|
||||
"user-agent": ua,
|
||||
}).SetHeaders(map[string]string{
|
||||
@@ -162,7 +162,7 @@ func (c *cardTypeQuery) outletCardQuery(ctx context.Context) (balance float64, e
|
||||
if len(ocrResp) != 5 {
|
||||
continue
|
||||
}
|
||||
resp, err = webClient.R().SetFormData(map[string]string{
|
||||
resp, err = webClient.R().SetContext(ctx).SetFormData(map[string]string{
|
||||
"card": c.CardNo,
|
||||
"pass": c.CardPwd,
|
||||
"code": ocrResp,
|
||||
@@ -370,7 +370,7 @@ func (c *cardTypeQuery) chinagPayQuery(ctx context.Context) (balance float64, er
|
||||
if proxy != "" {
|
||||
webClient.SetProxy(proxy)
|
||||
}
|
||||
captcha, err2 := webClient.R().Get("https://gpaymp.chinagpay.com/api/noAuth/card/getCaptcha?" + strconv.FormatInt(time.Now().UnixMicro(), 10))
|
||||
captcha, err2 := webClient.R().SetContext(ctx).Get("https://gpaymp.chinagpay.com/api/noAuth/card/getCaptcha?" + strconv.FormatInt(time.Now().UnixMicro(), 10))
|
||||
if err2 != nil || captcha.IsError() {
|
||||
proxyId = utils.GenerateId()
|
||||
continue
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestFatSixImpl_Scan(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFatSixImpl_SendCard(t *testing.T) {
|
||||
ua := fakeuseragent.RandomUserAgent()
|
||||
ua := useragent.GetUserAgentByPlatform(useragent.PlatformPhone)
|
||||
randStrList := random.RandStringSlice(random.Numeral+random.LowwerLetters, 1, 6)
|
||||
client := resty.New().SetHeaders(map[string]string{
|
||||
"user-agent": ua,
|
||||
|
||||
469
internal/service/supplier/third_party/nuclear.go
vendored
Normal file
469
internal/service/supplier/third_party/nuclear.go
vendored
Normal file
@@ -0,0 +1,469 @@
|
||||
package third_party
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gateway/internal/config"
|
||||
"gateway/internal/models/merchant"
|
||||
"gateway/internal/models/merchant_deploy"
|
||||
"gateway/internal/models/order"
|
||||
"gateway/internal/models/payfor"
|
||||
"gateway/internal/models/road"
|
||||
"gateway/internal/models/supply_model"
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service"
|
||||
"gateway/internal/service/supplier"
|
||||
"gateway/internal/service/supplier/third_party/pool/card_sender"
|
||||
"gateway/internal/utils"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/httplib"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type NuclearImpl struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// HasDependencyHTML 是否有单独的支付页面
|
||||
func (c *NuclearImpl) HasDependencyHTML() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 生成md5加密
|
||||
func (c *NuclearImpl) generateSign(ctx context.Context, params map[string]string, key string) string {
|
||||
keys := maputil.Keys(params)
|
||||
sort.Strings(keys)
|
||||
sign_ := ""
|
||||
for _, key2 := range keys {
|
||||
if params[key2] == "" || key2 == "SignType" {
|
||||
continue
|
||||
}
|
||||
sign_ += key2 + "=" + convertor.ToString(params[key2]) + "&"
|
||||
}
|
||||
sign_ = strings.Trim(sign_, "&")
|
||||
sign_ += key
|
||||
return utils.EncodeMd5Str(sign_)
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) sendCard(ctx context.Context, jsonStr string, cardInfo supplier.RedeemCardInfo, attach string, road road.RoadInfo, merchantInfo merchant.MerchantInfo, roadInfo road.RoadInfo) (bool, string) {
|
||||
orderAmount, _ := (&cardTypeQuery{
|
||||
OrderNo: attach,
|
||||
QueryType: gojson.Json(roadInfo.Params).Get("queryType").Tostring(),
|
||||
CardNo: cardInfo.CardNo,
|
||||
ChannelCode: gojson.Json(roadInfo.Params).Get("channelCode").Tostring(),
|
||||
CardPwd: cardInfo.Data,
|
||||
Balance: cardInfo.GetFaceTypeFloat(ctx),
|
||||
}).GetBalance(ctx)
|
||||
|
||||
cardInfo.FaceType = strconv.FormatFloat(orderAmount, 'f', 2, 64)
|
||||
merchantDeployInfo := merchant_deploy.GetMerchantDeployByUidAndRoadUid(ctx, merchantInfo.MerchantUid, road.RoadUid)
|
||||
if merchantDeployInfo == nil {
|
||||
return false, "商户配置不存在"
|
||||
}
|
||||
if merchantDeployInfo.SubmitStrategy == merchant_deploy.SUBMIT_STRATEGY_POOL {
|
||||
if err := orderPoolService.PushOrder(ctx, card_sender.SendCardTask{
|
||||
CardInfo: cardInfo,
|
||||
RoadUid: road.RoadUid,
|
||||
LocalOrderID: attach,
|
||||
SendCardTaskType: card_sender.SendCardTaskTypeEnumNuclear,
|
||||
NeedQuery: gojson.Json(road.Params).Get("needQuery").Tostring() == "1",
|
||||
}); err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("推送订单失败", zap.Error(err))
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, "订单提交中"
|
||||
}
|
||||
if err := orderPoolService.SubmitOrder(ctx, card_sender.SendCardTask{
|
||||
CardInfo: cardInfo,
|
||||
RoadUid: road.RoadUid,
|
||||
LocalOrderID: attach,
|
||||
SendCardTaskType: card_sender.SendCardTaskTypeEnumNuclear,
|
||||
NeedQuery: gojson.Json(road.Params).Get("needQuery").Tostring() == "1",
|
||||
}); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, "订单提交中"
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) Scan(ctx context.Context, orderInfo order.OrderInfo, roadInfo road.RoadInfo, merchantInfo merchant.MerchantInfo) supplier.ScanData {
|
||||
ctx, cancel := otelTrace.SpanFunc(ctx, "qixi", "Scan", trace.WithAttributes(
|
||||
attribute.String("bankOrderId", orderInfo.BankOrderId),
|
||||
attribute.String("merchantUid", orderInfo.MerchantUid),
|
||||
attribute.String("exValue", orderInfo.ExValue),
|
||||
))
|
||||
defer cancel()
|
||||
cdata := supplier.RedeemCardInfo{}
|
||||
err := json.Unmarshal([]byte(orderInfo.ExValue), &cdata)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("格式化数据失败", zap.String("ExValue", orderInfo.ExValue))
|
||||
return supplier.ScanData{Status: "-1", Msg: "订单有误,请稍后再试"}
|
||||
}
|
||||
ok, str := c.sendCard(ctx, roadInfo.Params, cdata, orderInfo.BankOrderId, roadInfo, merchantInfo, roadInfo)
|
||||
var scanData supplier.ScanData
|
||||
if !ok {
|
||||
scanData = supplier.ScanData{
|
||||
Status: "-1",
|
||||
Msg: "订单有误,请稍后再试:" + str,
|
||||
BankNo: orderInfo.MerchantOrderId,
|
||||
OrderNo: orderInfo.BankOrderId,
|
||||
ReturnData: str,
|
||||
}
|
||||
return scanData
|
||||
}
|
||||
scanData.Status = "00"
|
||||
scanData.OrderNo = orderInfo.BankOrderId
|
||||
scanData.BankNo = orderInfo.MerchantOrderId
|
||||
scanData.OrderPrice = strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64)
|
||||
scanData.ReturnData = str
|
||||
return scanData
|
||||
}
|
||||
|
||||
// KMEncrypt 加密卡密
|
||||
func (c *NuclearImpl) kMEncrypt(kf, appSecret string) (string, error) {
|
||||
ctx := context.Background()
|
||||
secret := utils.GetMd5Lower(appSecret)[:16] // 加密秘钥
|
||||
block, err := aes.NewCipher([]byte(secret))
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("nuclear: AesDecrypt failed to NewCipher")
|
||||
return "", err
|
||||
}
|
||||
// 数据填充
|
||||
plaintext := utils.PadType(kf)
|
||||
iv := "0102030405060708"
|
||||
mode := cipher.NewCBCEncrypter(block, []byte(iv))
|
||||
mode.CryptBlocks(plaintext, plaintext)
|
||||
return base64.StdEncoding.EncodeToString(plaintext), nil
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayNotify() {
|
||||
ctx := c.Ctx.Request.Context()
|
||||
ctx, span := otelTrace.Span(ctx, "NuclearImpl", "NuclearImpl.PayNotify")
|
||||
defer span.End()
|
||||
|
||||
response := struct {
|
||||
AppId string `json:"AppId" form:"AppId"`
|
||||
OrderId string `json:"OrderId" form:"OrderId"`
|
||||
Status string `json:"Status" form:"Status"`
|
||||
Amount string `json:"Amount" form:"Amount"`
|
||||
ActualAmount string `json:"ActualAmount" form:"ActualAmount"`
|
||||
SettleAmount string `json:"SettleAmount" form:"SettleAmount"`
|
||||
TradeOrderId string `json:"TradeOrderId" form:"TradeOrderId"`
|
||||
CompleteTime string `json:"CompleteTime" form:"CompleteTime"`
|
||||
Attach string `json:"Attach" form:"Attach"`
|
||||
Msg string `json:"Msg" form:"Msg"`
|
||||
Sign string `json:"Sign" form:"Sign"`
|
||||
SignType string `json:"SignType" form:"SignType"`
|
||||
}{}
|
||||
if err := c.Bind(&response); err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("【nuclear】回调参数解析失败", zap.Error(err))
|
||||
c.Ctx.WriteString("FAIL")
|
||||
return
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.String("bankOrderId", convertor.ToString(response.OrderId)),
|
||||
attribute.String("formData", convertor.ToString(response)),
|
||||
)
|
||||
|
||||
//通form表单里提取数据
|
||||
orderInfo := order.GetOrderByBankOrderId(ctx, convertor.ToString(response.OrderId))
|
||||
if orderInfo.BankOrderId == "" || len(orderInfo.BankOrderId) == 0 {
|
||||
otelTrace.Logger.WithContext(ctx).Error("【nuclear】回调的订单号不存在,订单号=", zap.String("attach", convertor.ToString(response.OrderId)))
|
||||
c.Ctx.WriteString("FAIL")
|
||||
return
|
||||
}
|
||||
|
||||
if orderInfo.Status != "wait" {
|
||||
c.Ctx.WriteString("订单已经回调")
|
||||
return
|
||||
}
|
||||
|
||||
roadInfo := road.GetRoadInfoByRoadUid(ctx, orderInfo.RoadUid)
|
||||
if roadInfo.RoadUid == "" || len(roadInfo.RoadUid) == 0 {
|
||||
otelTrace.Logger.WithContext(ctx).Error("【nuclear】支付通道已经关系或者删除,不进行回调")
|
||||
c.Ctx.WriteString("FAIL")
|
||||
return
|
||||
}
|
||||
|
||||
if response.Status == "Success" {
|
||||
successAmount, _ := strconv.ParseFloat(response.ActualAmount, 64)
|
||||
isOk := false
|
||||
if successAmount == orderInfo.OrderAmount {
|
||||
isOk = service.SolvePaySuccess(ctx, orderInfo.BankOrderId, successAmount, orderInfo.MerchantUid, response.Msg)
|
||||
} else {
|
||||
isOk = service.SolvePayFail(ctx, orderInfo.BankOrderId, response.TradeOrderId, fmt.Sprintf("金额不一致 卡面金额:%.2f 订单金额:%.2f", orderInfo.OrderAmount, successAmount))
|
||||
}
|
||||
if isOk {
|
||||
c.Ctx.WriteString("success")
|
||||
} else {
|
||||
c.Ctx.WriteString("FAIL")
|
||||
}
|
||||
} else {
|
||||
isOk := service.SolvePayFail(ctx, orderInfo.BankOrderId, response.TradeOrderId, response.Msg)
|
||||
if isOk {
|
||||
c.Ctx.WriteString("success")
|
||||
} else {
|
||||
c.Ctx.WriteString("FAIL")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 查询订单
|
||||
func (c *NuclearImpl) bindSearch(ctx context.Context, orderNo string) bool {
|
||||
req := httplib.NewBeegoRequestWithCtx(ctx, "http://api.qixika.com/blindSearch.html", "POST")
|
||||
req.Param("orderId", orderNo)
|
||||
type bindSearchResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
CustomerId int `json:"customerId"`
|
||||
OrderId string `json:"orderId"`
|
||||
SystemOrderId string `json:"systemOrderId"`
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
var resp bindSearchResp
|
||||
err := req.ToJSON(&resp)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("【nuclear】查询订单失败", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
otelTrace.Logger.WithContext(ctx).Info("【nuclear】查询订单返回", zap.Any("resp", resp))
|
||||
return resp.Data.Status == "0"
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayQuery(orderInfo order.OrderInfo, roadInfo road.RoadInfo) bool {
|
||||
params := map[string]any{}
|
||||
ctx := context.Background()
|
||||
cardData, err := sonic.GetFromString(orderInfo.CardReturnData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
orderId, err := cardData.Get("order_id").String()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
params["order_id"] = orderId
|
||||
params["app_key"] = gojson.Json(roadInfo.Params).Get("appKey").Tostring()
|
||||
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
params["sign"] = utils.GetMD5SignMF(params, gojson.Json(roadInfo.Params).Get("appSecret").Tostring())
|
||||
|
||||
cfg := config.Config{}
|
||||
url, err := cfg.GetMFCardQueryUrl()
|
||||
|
||||
req := httplib.Post(url)
|
||||
marshal, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
|
||||
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
|
||||
req.Header("Content-Type", "application/json")
|
||||
req.Body(marshal)
|
||||
req.Header("Accept-Charset", "utf-8")
|
||||
|
||||
response, err := req.String()
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("MF GetToken 请求失败:", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
|
||||
|
||||
resData, err := sonic.GetFromString(response)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
resStatus, err := resData.Get("data").Get("status").Int64()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
resCode, err := resData.Get("code").Int64()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if resCode == 0 && resStatus == 9 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayQueryV2(orderInfo order.OrderInfo, roadInfo road.RoadInfo) supply_model.MsgModel {
|
||||
params := map[string]any{}
|
||||
ctx := context.Background()
|
||||
cardData, err := sonic.GetFromString(orderInfo.CardReturnData)
|
||||
if err != nil {
|
||||
return supply_model.CardMsgErr
|
||||
}
|
||||
|
||||
orderId, err := cardData.Get("order_id").String()
|
||||
if err != nil {
|
||||
return supply_model.CardMsgErr
|
||||
}
|
||||
|
||||
params["order_id"] = orderId
|
||||
params["app_key"] = gojson.Json(roadInfo.Params).Get("appKey").Tostring()
|
||||
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
params["sign"] = utils.GetMD5SignMF(params, gojson.Json(roadInfo.Params).Get("appSecret").Tostring())
|
||||
|
||||
cfg := config.Config{}
|
||||
url, err := cfg.GetMFCardQueryUrl()
|
||||
|
||||
req := httplib.Post(url)
|
||||
marshal, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
|
||||
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
|
||||
return supply_model.DataErr
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
|
||||
req.Header("Content-Type", "application/json")
|
||||
req.Body(marshal)
|
||||
req.Header("Accept-Charset", "utf-8")
|
||||
|
||||
response, err := req.String()
|
||||
if err != nil {
|
||||
return supply_model.RemoteDataErr
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
|
||||
|
||||
resData, err := sonic.GetFromString(response)
|
||||
if err != nil {
|
||||
return supply_model.RemoteDataErr
|
||||
}
|
||||
|
||||
resStatus, err := resData.Get("data").Get("status").Int64()
|
||||
if err != nil {
|
||||
return supply_model.RemoteDataErr
|
||||
}
|
||||
|
||||
resCode, err := resData.Get("code").Int64()
|
||||
if err != nil {
|
||||
return supply_model.RemoteDataErr
|
||||
}
|
||||
|
||||
if resCode == 0 {
|
||||
switch resStatus {
|
||||
case 9:
|
||||
return supply_model.RemoteSuccess
|
||||
case 2, 3, 4:
|
||||
return supply_model.RemoteDataDealing
|
||||
case 7:
|
||||
return supply_model.RemoteDataHandErr
|
||||
case 8:
|
||||
return supply_model.RemoteDataHealingErr
|
||||
}
|
||||
}
|
||||
return supply_model.RemoteDataErr
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayFor(info payfor.PayforInfo) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayForQuery(payFor payfor.PayforInfo) (string, string) {
|
||||
cfg := config.Config{}
|
||||
url, err := cfg.GetMFCardQueryUrl()
|
||||
ctx := context.Background()
|
||||
if err != nil {
|
||||
return config.PAYFOR_FAIL, ""
|
||||
}
|
||||
|
||||
params := map[string]string{}
|
||||
params["order_id"] = payFor.BankOrderId
|
||||
params["app_key"] = gojson.Json("").Get("appKey").Tostring()
|
||||
|
||||
req := httplib.Post(url)
|
||||
marshal, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
|
||||
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
|
||||
return config.PAYFOR_FAIL, "内部错误请稍后再试试"
|
||||
}
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
|
||||
req.Header("Content-Type", "application/json")
|
||||
req.Body(marshal)
|
||||
req.Header("Accept-Charset", "utf-8")
|
||||
|
||||
response, err := req.String()
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("MF GetToken 请求失败:", zap.Error(err))
|
||||
return config.PAYFOR_FAIL, ""
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
|
||||
|
||||
if gojson.Json(response).Get("code").Tostring() == "" {
|
||||
otelTrace.Logger.WithContext(ctx).Error("远程调用失败")
|
||||
return config.PAYFOR_BANKING, ""
|
||||
}
|
||||
|
||||
if gojson.Json(response).Get("code").Tostring() == "0" {
|
||||
|
||||
type data struct {
|
||||
OrderID int64 `json:"order_id"`
|
||||
CardNo string `json:"card_no"`
|
||||
CardPwd string `json:"card_pwd"`
|
||||
Status int `json:"status"`
|
||||
RspInfo string `json:"rsp_info"`
|
||||
FaceVal int `json:"face_val"`
|
||||
Amount int `json:"amount"`
|
||||
Discount string `json:"discount"`
|
||||
}
|
||||
|
||||
var d data
|
||||
|
||||
err2 := json.Unmarshal([]byte(gojson.Json(response).Get("data").Tostring()), &d)
|
||||
if err2 != nil {
|
||||
return config.PAYFOR_FAIL, ""
|
||||
}
|
||||
|
||||
if d.Status == 9 {
|
||||
return config.PAYFOR_SUCCESS, ""
|
||||
}
|
||||
|
||||
if d.Status == 4 {
|
||||
return config.PAYFOR_BANKING, ""
|
||||
}
|
||||
|
||||
if d.Status == 7 || d.Status == 8 {
|
||||
return config.PAYFOR_FAIL, ""
|
||||
}
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Error("远程调用失败")
|
||||
return config.PAYFOR_BANKING, ""
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) BalanceQuery(roadInfo road.RoadInfo) float64 {
|
||||
return 0.00
|
||||
}
|
||||
|
||||
func (c *NuclearImpl) PayForNotify() string {
|
||||
return ""
|
||||
}
|
||||
46
internal/service/supplier/third_party/nuclear_test.go
vendored
Normal file
46
internal/service/supplier/third_party/nuclear_test.go
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package third_party
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/utils"
|
||||
"testing"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestNuclearImpl_sendCard(t *testing.T) {
|
||||
params := map[string]string{
|
||||
"AppId": "20099032",
|
||||
"ChannelCode": "138",
|
||||
"Amount": "10",
|
||||
"OrderId": utils.GenerateId(),
|
||||
"NonceStr": utils.GenerateId(),
|
||||
"NotifyUrl": "https://baidu.com",
|
||||
"ClientId": "12353543534",
|
||||
"SignType": "MD5",
|
||||
"Sign": "",
|
||||
}
|
||||
params["Sign"] = (&NuclearImpl{}).generateSign(t.Context(), params, "55j2P5sE6eGJk83OZWk9jdGs4auOgMO0")
|
||||
otelTrace.Logger.WithContext(t.Context()).Info("构造sign参数", zap.Any("sign", params))
|
||||
webClient := resty.New()
|
||||
|
||||
respData := struct {
|
||||
Code string `json:"Code"`
|
||||
Payload struct {
|
||||
OrderId string `json:"OrderId"`
|
||||
TradeOrderId string `json:"TradeOrderId"`
|
||||
Amount float64 `json:"Amount"`
|
||||
AppId string `json:"AppId"`
|
||||
BankId int `json:"BankId"`
|
||||
Data string `json:"Data"`
|
||||
DataType int `json:"DataType"`
|
||||
}
|
||||
}{}
|
||||
|
||||
response, _ := webClient.R().SetFormData(params).Post("https://gate.nuclearpow.net:2087/api/order/create")
|
||||
_ = json.Unmarshal(response.Body(), &respData)
|
||||
otelTrace.Logger.WithContext(t.Context()).Info("请求结果", zap.Any("result", respData))
|
||||
t.Log(response.String())
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (s *SendCardTaskTypeCareless) CreateOrder(ctx context.Context, roadUid stri
|
||||
if createdOrderUrl == "" {
|
||||
createdOrderUrl = "https://api.wuxinpay.xyz/api/v1/payment/init"
|
||||
}
|
||||
response, err := client.R().SetBody(reqStruct).Post(createdOrderUrl)
|
||||
response, err := client.R().SetContext(ctx).SetBody(reqStruct).Post(createdOrderUrl)
|
||||
if err != nil {
|
||||
return orderPoolItem, errors.New("请求失败")
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ func (c *eggplantProductCode) channelThree(ctx context.Context, forwardUrl *url.
|
||||
func (c *eggplantProductCode) channelFour(ctx context.Context, forwardUrl *url.URL) (bool, string, string) {
|
||||
webClient := resty.New()
|
||||
otelresty.TraceClient(webClient)
|
||||
queryResp, err := webClient.R().EnableTrace().SetContext(ctx).Get(forwardUrl.String())
|
||||
queryResp, err := webClient.R().SetContext(ctx).EnableTrace().SetContext(ctx).Get(forwardUrl.String())
|
||||
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("请求失败", zap.Error(err))
|
||||
|
||||
@@ -43,6 +43,7 @@ const (
|
||||
SendCardTaskTypeEnumWTR SendCardTaskEnum = "WTR"
|
||||
SendCardTaskTypeEnumUp SendCardTaskEnum = "UP"
|
||||
SendCardTaskTypeEnumOct SendCardTaskEnum = "OCT"
|
||||
SendCardTaskTypeEnumNuclear SendCardTaskEnum = "NUCLEAR"
|
||||
)
|
||||
|
||||
func GetAllSendCardTaskType() []SendCardTaskEnum {
|
||||
@@ -65,6 +66,7 @@ func GetAllSendCardTaskType() []SendCardTaskEnum {
|
||||
SendCardTaskTypeEnumWTR,
|
||||
SendCardTaskTypeEnumUp,
|
||||
SendCardTaskTypeEnumOct,
|
||||
SendCardTaskTypeEnumNuclear,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +110,8 @@ func (sendCardTaskTypeEnum SendCardTaskEnum) GetSendCardTaskType() sendCardTaskT
|
||||
return &SendCardTaskTypeWtr{}
|
||||
case SendCardTaskTypeEnumUp:
|
||||
return &SendCardTaskTypeUp{}
|
||||
case SendCardTaskTypeEnumNuclear:
|
||||
return &SendCardTaskTypeNuclear{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service/client"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -26,7 +27,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/structs"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -107,10 +107,10 @@ func (s *SendCardTaskTypeFatSix) CreateOrder(ctx context.Context, roadUid string
|
||||
return orderPoolItem, err
|
||||
}
|
||||
//创建session
|
||||
webClient := resty.New().EnableTrace().SetTimeout(3 * time.Second).SetHeaders(map[string]string{
|
||||
webClient := resty.New().SetTimeout(3 * time.Second).SetHeaders(map[string]string{
|
||||
"Origin": "https://api.xxsbm.com",
|
||||
}).SetHeaders(map[string]string{
|
||||
"user-agent": fakeuseragent.RandomUserAgent(),
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
|
||||
})
|
||||
|
||||
otelresty.TraceClient(webClient)
|
||||
@@ -174,7 +174,7 @@ func (s *SendCardTaskTypeFatSix) HandleSendCardTask(ctx context.Context, orderIt
|
||||
needChangeProxyId := utils.GenerateId()
|
||||
webClient := resty.New().EnableTrace().SetRetryCount(1).
|
||||
SetTimeout(5 * time.Second).SetHeaders(map[string]string{
|
||||
"user-agent": fakeuseragent.RandomUserAgent(),
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
|
||||
}).SetHeaders(map[string]string{
|
||||
"Origin": "https://api.xxsbm.com",
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"gateway/internal/models/road"
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/pointer"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -104,7 +104,7 @@ func (s *SendCardTaskTypeFlyFish) CreateOrder(ctx context.Context, roadUid strin
|
||||
}
|
||||
|
||||
// 随机,点击链接
|
||||
randomAgent := fakeuseragent.RandomUserAgent()
|
||||
randomAgent := useragent.GetUserAgentByPlatform(useragent.PlatformPhone)
|
||||
if random.RandInt(1, 100) <= 30 {
|
||||
// 解析url
|
||||
payUrl, err := url.Parse(respData.Data.Url)
|
||||
@@ -204,7 +204,7 @@ func (s *SendCardTaskTypeFlyFish) HandleSendCardTask(ctx context.Context, orderI
|
||||
randomAgent, ok := orderItem.Params["random_agent"]
|
||||
randomAgentStr, ok2 := randomAgent.(string)
|
||||
if !ok2 || !ok || randomAgentStr == "" {
|
||||
randomAgentStr = fakeuseragent.RandomUserAgent()
|
||||
randomAgentStr = useragent.GetUserAgentByPlatform(useragent.PlatformPhone)
|
||||
_ = s.ClickLink(ctx, paths[len(paths)-1], randomAgentStr, task.LocalOrderID)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *SendCardTaskTypeFlyFishV2) CreateOrder(ctx context.Context, roadUid str
|
||||
SetHeader("Accept", "application/json").SetTimeout(time.Second * 5).SetRetryCount(1)
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
resp, err := webClient.R().SetFormData(formData).Post("https://apify.fkpay.online/submitPay.html")
|
||||
resp, err := webClient.R().SetContext(ctx).SetFormData(formData).Post("https://apify.fkpay.online/submitPay.html")
|
||||
if err != nil {
|
||||
return OrderPoolItem{}, errors.Join(err, errors.New("飞鱼下单请求失败"))
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func (s *SendCardTaskTypeFlyFishV2) HandleSendCardTask(ctx context.Context, orde
|
||||
})
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
queryRes, err := webClient.R().Get(queryUrl.String())
|
||||
queryRes, err := webClient.R().SetContext(ctx).Get(queryUrl.String())
|
||||
if err != nil {
|
||||
return errors.New("请求提交地址失败")
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (s *SendCardTaskTypeFlyFishV2) HandleSendCardTask(ctx context.Context, orde
|
||||
"productCode": productCode,
|
||||
}
|
||||
|
||||
resp, err := webClient.R().SetFormData(formData).Post("https://apify.fkpay.online/submitcard")
|
||||
resp, err := webClient.R().SetContext(ctx).SetFormData(formData).Post("https://apify.fkpay.online/submitcard")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service/client"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -23,7 +24,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/structs"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -71,7 +71,7 @@ func (s *SendCardTaskTypeMagicFish) CreateOrder(ctx context.Context, roadUid str
|
||||
webClient := resty.New().EnableTrace().SetTimeout(3 * time.Second).SetRetryCount(1).SetHeaders(map[string]string{
|
||||
"Origin": "http://api.zbgj79.com",
|
||||
}).SetHeaders(map[string]string{
|
||||
"user-agent": fakeuseragent.RandomUserAgent(),
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
|
||||
})
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (s *SendCardTaskTypeMyself) CreateOrder(ctx context.Context, roadUid string
|
||||
}
|
||||
|
||||
func (s *SendCardTaskTypeMyself) HandleSendCardTask(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
|
||||
ctx, span := otelTrace.Span(ctx, "SendCardTaskTypeMyself", "SendCardTaskTypeMyself.HandleSendCardTask", trace.WithAttributes(
|
||||
ctx, span := otelTrace.Span(ctx, "SendCardTaskTypeNuclear", "SendCardTaskTypeNuclear.HandleSendCardTask", trace.WithAttributes(
|
||||
attribute.String("bankOrderId", task.LocalOrderID),
|
||||
attribute.String("cardNo", task.CardInfo.CardNo),
|
||||
attribute.String("cardPassword", task.CardInfo.Data),
|
||||
|
||||
404
internal/service/supplier/third_party/pool/card_sender/nuclear.go
vendored
Normal file
404
internal/service/supplier/third_party/pool/card_sender/nuclear.go
vendored
Normal file
@@ -0,0 +1,404 @@
|
||||
package card_sender
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gateway/internal/cache"
|
||||
"gateway/internal/config"
|
||||
"gateway/internal/models/road"
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service/client"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/fingerprint"
|
||||
"gateway/internal/utils/useragent"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dubonzi/otelresty"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/pointer"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SendCardTaskTypeNuclear struct {
|
||||
sendCardTaskTypeSendCardTaskBase
|
||||
}
|
||||
|
||||
func (s *SendCardTaskTypeNuclear) getRandomId(ctx context.Context) (string, string) {
|
||||
redisClient := cache.GetRedisClient()
|
||||
|
||||
// 检查是否已经有足够的ID在Redis中
|
||||
keys, err := redisClient.Client.Keys(ctx, "nuclear_random_ids:*").Result()
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Redis keys error", zap.Error(err))
|
||||
}
|
||||
|
||||
if len(keys) < 20000 {
|
||||
// 生成2w个随机数
|
||||
for i := 0; i < 20000; i++ {
|
||||
nuclearRandomId := utils.GetMd5Lower(utils.GenerateId())
|
||||
// 生成浏览器指纹哈希
|
||||
fingerprintHash := fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
err = redisClient.Set(ctx, "nuclear_random_ids:"+nuclearRandomId, fingerprintHash, 0)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Failed to set nuclear ID", zap.Error(err))
|
||||
}
|
||||
}
|
||||
// 重新获取keys
|
||||
keys, err = redisClient.Client.Keys(ctx, "nuclear_random_ids:*").Result()
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Redis keys error after generation", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
// 如果获取失败,生成一个临时ID
|
||||
tempId := utils.GetMd5Lower(utils.GenerateId())
|
||||
tempFingerprint := fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
return tempId, tempFingerprint
|
||||
}
|
||||
|
||||
// 随机选择一个key
|
||||
selectedKey := keys[rand.IntN(len(keys))]
|
||||
|
||||
// 万分之一概率随机删除一个key-value
|
||||
if rand.IntN(10000) == 0 {
|
||||
// 随机选择一个key删除
|
||||
selectedKeyToDelete := keys[rand.IntN(len(keys))]
|
||||
if err2 := redisClient.Delete(ctx, selectedKeyToDelete); err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Failed to delete random key",
|
||||
zap.String("key", selectedKeyToDelete),
|
||||
zap.Error(err2))
|
||||
}
|
||||
}
|
||||
|
||||
// 从key中提取ID部分
|
||||
parts := strings.Split(selectedKey, ":")
|
||||
if len(parts) != 2 {
|
||||
// 如果格式不对,生成一个临时ID
|
||||
tempId := utils.GetMd5Lower(utils.GenerateId())
|
||||
tempFingerprint := fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
return tempId, tempFingerprint
|
||||
}
|
||||
|
||||
randomId := parts[1]
|
||||
|
||||
// 获取对应的浏览器指纹
|
||||
var fingerprintHash string
|
||||
err = redisClient.Get(ctx, selectedKey, &fingerprintHash)
|
||||
if err != nil {
|
||||
// 如果获取指纹失败,生成一个新的
|
||||
fingerprintHash = fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
err = redisClient.Set(ctx, selectedKey, fingerprintHash, 0)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Failed to set fingerprint", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return randomId, fingerprintHash
|
||||
}
|
||||
|
||||
// 生成md5加密
|
||||
func (s *SendCardTaskTypeNuclear) generateSign(ctx context.Context, params map[string]string, key string) string {
|
||||
keys := maputil.Keys(params)
|
||||
sort.Strings(keys)
|
||||
sign_ := ""
|
||||
for _, key2 := range keys {
|
||||
if params[key2] == "" || key2 == "SignType" {
|
||||
continue
|
||||
}
|
||||
sign_ += key2 + "=" + convertor.ToString(params[key2]) + "&"
|
||||
}
|
||||
sign_ = strings.Trim(sign_, "&")
|
||||
sign_ += key
|
||||
return utils.EncodeMd5Str(sign_)
|
||||
}
|
||||
|
||||
func (s *SendCardTaskTypeNuclear) CreateOrder(ctx context.Context, roadUid string, faceValue float64) (OrderPoolItem, error) {
|
||||
orderPoolItem := OrderPoolItem{}
|
||||
roadInfo := road.GetRoadInfoByRoadUid(ctx, roadUid)
|
||||
cfg := config.GetConfig()
|
||||
|
||||
orderId := utils.GenerateId()
|
||||
clientId, fingerprintHash := s.getRandomId(ctx)
|
||||
params := map[string]string{
|
||||
"AppId": gojson.Json(roadInfo.Params).Get("appId").Tostring(),
|
||||
"ChannelCode": gojson.Json(roadInfo.Params).Get("channelCode").Tostring(),
|
||||
"Amount": strconv.FormatFloat(faceValue, 'f', 0, 64),
|
||||
"OrderId": orderId,
|
||||
"NonceStr": utils.GenerateId(),
|
||||
"NotifyUrl": cfg.GatewayAddr() + "/nuclear/notify",
|
||||
"ClientId": clientId,
|
||||
"SignType": "MD5",
|
||||
"Sign": "",
|
||||
}
|
||||
params["Sign"] = s.generateSign(ctx, params, gojson.Json(roadInfo.Params).Get("key").Tostring())
|
||||
otelTrace.Logger.WithContext(ctx).Info("构造sign参数", zap.Any("sign", params))
|
||||
webClient := resty.New().SetTimeout(time.Second * 5).SetRetryCount(2)
|
||||
otelresty.TraceClient(webClient)
|
||||
response, err := webClient.R().SetContext(ctx).SetFormData(params).
|
||||
Post("https://gate.nuclearpow.net:2087/api/order/create")
|
||||
if err != nil {
|
||||
return orderPoolItem, err
|
||||
}
|
||||
respData := struct {
|
||||
Code string `json:"Code"`
|
||||
Payload struct {
|
||||
OrderId string `json:"OrderId"`
|
||||
TradeOrderId string `json:"TradeOrderId"`
|
||||
Amount float64 `json:"Amount"`
|
||||
AppId string `json:"AppId"`
|
||||
BankId int `json:"BankId"`
|
||||
Data string `json:"Data"`
|
||||
DataType int `json:"DataType"`
|
||||
}
|
||||
}{}
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求结果", zap.Any("result", respData))
|
||||
if err2 := json.Unmarshal(response.Body(), &respData); err2 != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("请求结果解析失败", zap.Error(err2))
|
||||
return orderPoolItem, err2
|
||||
}
|
||||
|
||||
//创建session
|
||||
webClient2 := resty.New().SetTimeout(3 * time.Second).SetHeaders(map[string]string{
|
||||
"Origin": "pay50.baolong18080.com",
|
||||
}).SetHeaders(map[string]string{
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformAndroid),
|
||||
})
|
||||
|
||||
otelresty.TraceClient(webClient2)
|
||||
createTime := time.Now()
|
||||
captchaCode := ""
|
||||
for range 5 {
|
||||
captchaRes, err2 := webClient2.R().SetContext(ctx).SetQueryParamsFromValues(url.Values{
|
||||
"oe": []string{
|
||||
respData.Payload.TradeOrderId,
|
||||
},
|
||||
}).Get("https://pay50.baolong18080.com/pay/index/captcha.html")
|
||||
if err2 != nil || pointer.IsNil(captchaRes) {
|
||||
continue
|
||||
}
|
||||
ocrResp, err2 := client.NewOCRClient().Calc(ctx, &client.CalcInput{
|
||||
File: captchaRes.Body(),
|
||||
FileName: "captcha.jpg",
|
||||
Category: "calc",
|
||||
})
|
||||
if err2 != nil || ocrResp == "" {
|
||||
continue
|
||||
}
|
||||
captchaCode = ocrResp
|
||||
break
|
||||
}
|
||||
|
||||
return OrderPoolItem{
|
||||
OrderID: orderId,
|
||||
CreateTime: createTime,
|
||||
PayURL: respData.Payload.Data,
|
||||
RoadUid: roadUid,
|
||||
FaceValue: faceValue,
|
||||
DispatchCount: 0,
|
||||
SendCardTaskType: SendCardTaskTypeEnumNuclear,
|
||||
RemoteOrderID: respData.Payload.TradeOrderId,
|
||||
Params: map[string]any{
|
||||
"captchaCode": captchaCode,
|
||||
"cookie": webClient.Cookies,
|
||||
"fingerprintHash": fingerprintHash,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SendCardTaskTypeNuclear) HandleSendCardTask(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
|
||||
ctx, span := otelTrace.Span(ctx, "SendCardTaskTypeNuclear", "SendCardTaskTypeNuclear.HandleSendCardTask", trace.WithAttributes(
|
||||
attribute.String("bankOrderId", task.LocalOrderID),
|
||||
attribute.String("cardNo", task.CardInfo.CardNo),
|
||||
attribute.String("cardPassword", task.CardInfo.Data),
|
||||
attribute.String("orderId", orderItem.OrderID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
webClient := resty.New().EnableTrace().SetRetryCount(1).
|
||||
SetTimeout(5 * time.Second).SetHeaders(map[string]string{
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformAndroid),
|
||||
}).SetHeaders(map[string]string{
|
||||
"Origin": "pay50.baolong18080.com",
|
||||
})
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
ocrResp := ""
|
||||
if ocrRespCode, ok := orderItem.Params["captchaCode"]; ok {
|
||||
ocrResp, _ = ocrRespCode.(string)
|
||||
}
|
||||
if cookie, ok := orderItem.Params["cookie"]; ok {
|
||||
if cookies, ok2 := cookie.([]*http.Cookie); ok2 {
|
||||
webClient.SetCookies(cookies)
|
||||
}
|
||||
}
|
||||
fingerprintHash := ""
|
||||
if fingerprintHashCode, ok := orderItem.Params["fingerprintHash"]; ok {
|
||||
fingerprintHash, _ = fingerprintHashCode.(string)
|
||||
} else {
|
||||
fingerprintHash = fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
}
|
||||
|
||||
span.AddEvent("changeProxy")
|
||||
//needChangeProxyId := utils.GenerateId()
|
||||
var errRes error
|
||||
var err error
|
||||
|
||||
for range 20 {
|
||||
errRes = nil
|
||||
//span.AddEvent("getProxy")
|
||||
//proxy, err2 := utils.GetProxy(ctx, needChangeProxyId, SendCardTaskTypeEnumNuclear.String())
|
||||
//if err2 != nil {
|
||||
// otelTrace.Logger.WithContext(ctx).Error("获取代理失败", zap.Error(err))
|
||||
// continue
|
||||
//}
|
||||
//span.AddEvent("endGetProxy")
|
||||
//webClient = webClient.SetProxy(proxy)
|
||||
|
||||
if ocrResp == "" {
|
||||
span.AddEvent("startGetCaptcha")
|
||||
resp, err3 := webClient.R().SetContext(ctx).SetQueryParamsFromValues(url.Values{
|
||||
"oe": []string{
|
||||
orderItem.RemoteOrderID,
|
||||
},
|
||||
}).Get("https://pay50.baolong18080.com/pay/index/captcha.html")
|
||||
span.AddEvent("finishGetCaptcha")
|
||||
if err3 != nil {
|
||||
//needChangeProxyId = utils.GenerateId()
|
||||
otelTrace.Logger.WithContext(ctx).Error("获取验证码失败", zap.Error(err3))
|
||||
continue
|
||||
}
|
||||
span.AddEvent("getCaptchaOCRResult")
|
||||
ocrResp, err = client.NewOCRClient().Calc(ctx, &client.CalcInput{
|
||||
File: resp.Body(),
|
||||
FileName: "captcha.jpg",
|
||||
Category: "calc",
|
||||
})
|
||||
span.AddEvent("finishCaptchaOCRResult")
|
||||
if err != nil {
|
||||
errRes = errors.New("识别验证码失败,需要重新提交")
|
||||
otelTrace.Logger.WithContext(ctx).Error("识别验证码失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if ocrResp == "" {
|
||||
errRes = errors.New("识别验证码失败,需要重新提交")
|
||||
continue
|
||||
}
|
||||
}
|
||||
if time.Since(orderItem.CreateTime).Seconds() <= 10 {
|
||||
time.Sleep(time.Duration(random.RandInt(int(10-time.Since(orderItem.CreateTime).Seconds()), 10)) * time.Second)
|
||||
}
|
||||
|
||||
span.AddEvent("startSubmitData")
|
||||
bodyData := map[string]string{
|
||||
"cardNo": task.CardInfo.CardNo,
|
||||
"pass": task.CardInfo.Data,
|
||||
"verifyCode": ocrResp,
|
||||
"token": fingerprintHash,
|
||||
}
|
||||
if token, ok := orderItem.Params["token"].(string); ok {
|
||||
bodyData["token"] = token
|
||||
}
|
||||
resp, err2 := webClient.R().SetContext(ctx).SetBody(bodyData).SetQueryParamsFromValues(url.Values{
|
||||
"oe": []string{
|
||||
orderItem.RemoteOrderID,
|
||||
},
|
||||
}).Post("https://pay50.baolong18080.com/pay/PxyAlai/setParams")
|
||||
span.AddEvent("finishSubmitData")
|
||||
|
||||
//重置验证码
|
||||
ocrResp = ""
|
||||
|
||||
if err2 != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("提交支付失败", zap.Error(err2))
|
||||
//needChangeProxyId = utils.GenerateId()
|
||||
continue
|
||||
}
|
||||
submitRespStr := struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}{}
|
||||
otelTrace.Logger.WithContext(ctx).Info("提交支付返回", zap.Any("response", resp), zap.Any("formData", map[string]string{
|
||||
"card_no": task.CardInfo.CardNo,
|
||||
"pass": task.CardInfo.Data,
|
||||
"captcha": ocrResp,
|
||||
}))
|
||||
err2 = json.Unmarshal(resp.Body(), &submitRespStr)
|
||||
if err2 != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("json解析失败", zap.Error(err2), zap.Any("response", resp))
|
||||
continue
|
||||
}
|
||||
msgList := []string{
|
||||
"必须是数字",
|
||||
"验证码计算错误",
|
||||
}
|
||||
if slices.ContainsFunc(msgList, func(msg string) bool {
|
||||
return strings.Contains(submitRespStr.Msg, msg)
|
||||
}) {
|
||||
errRes = errors.New("识别验证码失败,需要重新提交")
|
||||
continue
|
||||
}
|
||||
if submitRespStr.Code != 0 {
|
||||
otelTrace.Logger.WithContext(ctx).Error("支付失败", zap.Any("response", resp))
|
||||
return fmt.Errorf("支付失败:%s", submitRespStr.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if errRes != nil {
|
||||
return errRes
|
||||
}
|
||||
return errors.New("提交失败,请重新提交")
|
||||
}
|
||||
|
||||
func (s *SendCardTaskTypeNuclear) QueryOrder(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFingerprintHashByRandomId 通过randomId获取对应的fingerprintHash
|
||||
func (s *SendCardTaskTypeNuclear) getFingerprintHashByRandomId(ctx context.Context, randomId string) string {
|
||||
redisClient := cache.GetRedisClient()
|
||||
|
||||
// 构造Redis key
|
||||
key := "nuclear_random_ids:" + randomId
|
||||
|
||||
// 从Redis中获取指纹
|
||||
var fingerprintHash string
|
||||
err := redisClient.Get(ctx, key, &fingerprintHash)
|
||||
|
||||
if err != nil || fingerprintHash == "" {
|
||||
// 如果获取失败或指纹为空,重新生成一个指纹
|
||||
otelTrace.Logger.WithContext(ctx).Info("Fingerprint not found or empty, generating new one",
|
||||
zap.String("randomId", randomId),
|
||||
zap.String("key", key),
|
||||
zap.Error(err))
|
||||
|
||||
// 生成新的浏览器指纹哈希
|
||||
fingerprintHash = fingerprint.GenerateRandomBrowserFingerprintHash()
|
||||
|
||||
// 将新指纹存入Redis
|
||||
err = redisClient.Set(ctx, key, fingerprintHash, 0)
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("Failed to set new fingerprint hash",
|
||||
zap.String("randomId", randomId),
|
||||
zap.String("key", key),
|
||||
zap.Error(err))
|
||||
}
|
||||
return fingerprintHash
|
||||
}
|
||||
return fingerprintHash
|
||||
}
|
||||
32
internal/service/supplier/third_party/pool/card_sender/nuclear_test.go
vendored
Normal file
32
internal/service/supplier/third_party/pool/card_sender/nuclear_test.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package card_sender
|
||||
|
||||
import (
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func TestSendCardTaskTypeNuclear_getRandomId(t *testing.T) {
|
||||
id, _ := (&SendCardTaskTypeNuclear{}).getRandomId(t.Context())
|
||||
id2, _ := (&SendCardTaskTypeNuclear{}).getRandomId(t.Context())
|
||||
t.Log(id, id2)
|
||||
}
|
||||
|
||||
func TestSendCardTaskTypeNuclear_CreateOrder(t *testing.T) {
|
||||
//创建session
|
||||
webClient2 := resty.New().SetTimeout(3 * time.Second).SetHeaders(map[string]string{
|
||||
"Origin": "pay50.baolong18080.com",
|
||||
}).SetHeaders(map[string]string{
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformAndroid),
|
||||
})
|
||||
captchaRes, _ := webClient2.R().SetContext(t.Context()).SetQueryParamsFromValues(url.Values{
|
||||
"oe": []string{
|
||||
"BL20250922175154004154",
|
||||
},
|
||||
}).Get("https://pay50.baolong18080.com/pay/index/captcha.html")
|
||||
os.WriteFile("captcha.jpg", captchaRes.Body(), 0666)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/structs"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -150,7 +150,7 @@ func (s *SendCardTaskTypeSesame) HandleSendCardTask(ctx context.Context, orderIt
|
||||
|
||||
for range 5 {
|
||||
webClient.SetHeaders(map[string]string{
|
||||
"user-agent": fakeuseragent.RandomUserAgent(),
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
|
||||
})
|
||||
resp, err = webClient.R().SetContext(ctx).SetFormData(map[string]string{
|
||||
"payOrderId": orderNo,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/service"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/structs"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -195,7 +195,7 @@ func (s *SendCardTaskTypeShengtian) HandleSendCardTask(ctx context.Context, orde
|
||||
// continue
|
||||
// }
|
||||
webClient := resty.New().SetTimeout(5 * time.Second).SetRetryCount(3).SetHeaders(map[string]string{
|
||||
"user-agent": fakeuseragent.RandomUserAgent(),
|
||||
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
|
||||
})
|
||||
resp, err2 := webClient.R().SetContext(ctx).SetBody(struct {
|
||||
PayOrderId string `json:"payOrderId"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"gateway/internal/models/road"
|
||||
"gateway/internal/otelTrace"
|
||||
"gateway/internal/utils"
|
||||
"gateway/internal/utils/useragent"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/pointer"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/iunary/fakeuseragent"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -106,14 +106,14 @@ func (s *SendCardTaskTypeWtr) HandleSendCardTask(ctx context.Context, orderItem
|
||||
attribute.String("orderId", orderItem.OrderID),
|
||||
))
|
||||
defer span.End()
|
||||
//ua := fakeuseragent.RandomUserAgent()
|
||||
//ua := useragent.GetUserAgentByPlatform(useragent.PlatformPhone)
|
||||
|
||||
// 创建resty客户端
|
||||
client := resty.New().
|
||||
SetRetryCount(3).
|
||||
SetRetryWaitTime(5*time.Second).
|
||||
SetRetryMaxWaitTime(10*time.Second).
|
||||
SetHeader("User-Agent", fakeuseragent.RandomUserAgent()).
|
||||
SetHeader("User-Agent", useragent.GetUserAgentByPlatform(useragent.PlatformPhone)).
|
||||
SetTimeout(5 * time.Second).OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
|
||||
proxy, _ := utils.GetProxy(ctx, utils.GenerateId(), "SendCardTaskTypeWtr_cardTask")
|
||||
if proxy != "" {
|
||||
|
||||
49
internal/service/supplier/third_party/qixi.go
vendored
49
internal/service/supplier/third_party/qixi.go
vendored
@@ -19,7 +19,6 @@ import (
|
||||
"gateway/internal/service"
|
||||
"gateway/internal/service/supplier"
|
||||
"gateway/internal/utils"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -27,13 +26,13 @@ import (
|
||||
"github.com/beego/beego/v2/client/httplib"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/dubonzi/otelresty"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/widuu/gojson"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -48,7 +47,7 @@ func (c *QiXiCardImpl) HasDependencyHTML() bool {
|
||||
}
|
||||
|
||||
// 生成md5加密
|
||||
func generateSign(ctx context.Context, params map[string]any, key string) string {
|
||||
func (c *QiXiCardImpl) generateSign(ctx context.Context, params map[string]any, key string) string {
|
||||
keys := maputil.Keys(params)
|
||||
sort.Strings(keys)
|
||||
sign_ := ""
|
||||
@@ -59,7 +58,6 @@ func generateSign(ctx context.Context, params map[string]any, key string) string
|
||||
sign_ += key2 + "=" + convertor.ToString(params[key2]) + "&"
|
||||
}
|
||||
sign_ += "key=" + key
|
||||
otelTrace.Logger.WithContext(ctx).Info("构造sign参数", zap.Any("sign", sign_))
|
||||
return utils.EncodeMd5Str(sign_)
|
||||
}
|
||||
|
||||
@@ -81,26 +79,26 @@ func (c *QiXiCardImpl) SendCard(ctx context.Context, jsonStr string, cardInfo su
|
||||
"timestamp": strconv.Itoa(int(time.Now().Unix())),
|
||||
}
|
||||
|
||||
params["sign"] = generateSign(ctx, params, gojson.Json(jsonStr).Get("key").Tostring())
|
||||
params["sign"] = c.generateSign(ctx, params, gojson.Json(jsonStr).Get("key").Tostring())
|
||||
|
||||
req := httplib.NewBeegoRequestWithCtx(ctx,
|
||||
fmt.Sprintf("%s/tocard.html", gojson.Json(jsonStr).Get("url").Tostring()), "POST").
|
||||
SetTransport(otelhttp.NewTransport(http.DefaultTransport)).
|
||||
SetTimeout(time.Second*30, time.Second*30).Retries(3)
|
||||
webClient := resty.New().SetTimeout(time.Second * 5).SetRetryCount(1)
|
||||
otelresty.TraceClient(webClient)
|
||||
|
||||
for key, value := range params {
|
||||
req.Param(key, convertor.ToString(value))
|
||||
paramsStr := map[string]string{}
|
||||
for a, b := range params {
|
||||
paramsStr[a] = convertor.ToString(b)
|
||||
}
|
||||
response, err := webClient.R().SetContext(ctx).SetFormData(paramsStr).
|
||||
Post(fmt.Sprintf("%s/tocard.html", gojson.Json(jsonStr).Get("url").Tostring()))
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求参数:", zap.Any("params", params))
|
||||
|
||||
response, err := req.String()
|
||||
resp, _ := utils.UnescapeUnicode([]byte(response))
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("七喜 请求失败:", zap.Error(err))
|
||||
return false, ""
|
||||
otelTrace.Logger.WithContext(ctx).Error("七喜 获取卡密失败:", zap.Error(err))
|
||||
return false, "内部数据处理失败"
|
||||
}
|
||||
|
||||
otelTrace.Logger.WithContext(ctx).Info("请求参数", zap.Any("params", params))
|
||||
resp, _ := utils.UnescapeUnicode([]byte(response.String()))
|
||||
|
||||
type AutoGenerated struct {
|
||||
XMLName xml.Name `xml:"think"`
|
||||
Code int `json:"code" xml:"code"`
|
||||
@@ -108,10 +106,10 @@ func (c *QiXiCardImpl) SendCard(ctx context.Context, jsonStr string, cardInfo su
|
||||
}
|
||||
|
||||
resData := AutoGenerated{}
|
||||
err = xml.Unmarshal([]byte(response), &resData)
|
||||
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:", zap.Any("response", string(resp)), zap.Any("解析结果", resData))
|
||||
err = xml.Unmarshal(resp, &resData)
|
||||
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:", zap.Any("response", string(resp)))
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("json解析失败", zap.Error(err), zap.String("response", response))
|
||||
otelTrace.Logger.WithContext(ctx).Error("json解析失败", zap.Error(err))
|
||||
return false, "内部数据处理失败"
|
||||
}
|
||||
if resData.Code == 1 {
|
||||
@@ -189,18 +187,23 @@ func (c *QiXiCardImpl) PayNotify() {
|
||||
}
|
||||
|
||||
ctx, span := otelTrace.Span(
|
||||
ctx, "ScreamImpl", "ScreamImpl.PayNotify", trace.WithAttributes(
|
||||
ctx, "ScreamImpl", "NuclearImpl.PayNotify", trace.WithAttributes(
|
||||
attribute.String("bankOrderId", convertor.ToString(params["orderId"])),
|
||||
attribute.String("systemOrderId", convertor.ToString(params["systemOrderId"])),
|
||||
attribute.String("amount", convertor.ToString(params["amount"])),
|
||||
attribute.String("successAmount", convertor.ToString(params["successAmount"])),
|
||||
attribute.String("status", convertor.ToString(params["status"])),
|
||||
attribute.String("message", convertor.ToString(params["message"])),
|
||||
),
|
||||
)
|
||||
sdktrace.AlwaysSample()
|
||||
|
||||
defer span.End()
|
||||
|
||||
if params["status"] == "1" {
|
||||
otelTrace.Logger.WithContext(ctx).Info("【七喜】回调了正在处理的订单")
|
||||
c.Ctx.WriteString("FAIL")
|
||||
}
|
||||
|
||||
//通form表单里提取数据
|
||||
orderInfo := order.GetOrderByBankOrderId(ctx, convertor.ToString(params["orderId"]))
|
||||
if orderInfo.BankOrderId == "" || len(orderInfo.BankOrderId) == 0 {
|
||||
|
||||
@@ -60,9 +60,7 @@ func (s *ScreamImpl) generateSign(ctx context.Context, params map[string]any, ke
|
||||
}
|
||||
|
||||
func (s *ScreamImpl) sendCard(ctx context.Context, jsonStr string, cardInfo supplier.RedeemCardInfo, attach string) (bool, string) {
|
||||
ctx, span := otelTrace.Span(ctx, "ScreamImpl",
|
||||
"ScreamImpl.sendCard",
|
||||
trace.WithAttributes(attribute.String("bankOrderId", attach)))
|
||||
ctx, span := otelTrace.Span(ctx, "ScreamImpl", "ScreamImpl.sendCard", trace.WithAttributes(attribute.String("bankOrderId", attach)))
|
||||
defer span.End()
|
||||
|
||||
cfg := new(config.Config)
|
||||
@@ -82,9 +80,9 @@ func (s *ScreamImpl) sendCard(ctx context.Context, jsonStr string, cardInfo supp
|
||||
|
||||
params["sign"] = s.generateSign(ctx, params, gojson.Json(jsonStr).Get("key").Tostring())
|
||||
|
||||
webClient := resty.New().SetTimeout(time.Second * 5).SetRetryCount(3)
|
||||
webClient := resty.New().SetTimeout(time.Second * 5).SetRetryCount(1)
|
||||
otelresty.TraceClient(webClient)
|
||||
resp, err := webClient.R().SetBody(params).Post("http://writeoff.jianjiao6.com/api/order/create")
|
||||
resp, err := webClient.R().SetContext(ctx).SetBody(params).Post("http://writeoff.jianjiao6.com/api/order/create")
|
||||
if err != nil {
|
||||
otelTrace.Logger.WithContext(ctx).Error("请求失败", zap.Error(err))
|
||||
return false, "内部数据处理失败"
|
||||
|
||||
254
internal/utils/fingerprint/audio.go
Normal file
254
internal/utils/fingerprint/audio.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AudioFingerprint 音频指纹生成器
|
||||
type AudioFingerprint struct {
|
||||
sampleRate int
|
||||
channelCount int
|
||||
bufferSize int
|
||||
oscillatorType string
|
||||
filterType string
|
||||
filterFreq float64
|
||||
filterQ float64
|
||||
compressor bool
|
||||
dynamics bool
|
||||
}
|
||||
|
||||
// NewAudioFingerprint 创建新的音频指纹生成器
|
||||
func NewAudioFingerprint() *AudioFingerprint {
|
||||
return &AudioFingerprint{
|
||||
sampleRate: 44100,
|
||||
channelCount: 2,
|
||||
bufferSize: 4096,
|
||||
oscillatorType: "sine",
|
||||
filterType: "lowpass",
|
||||
filterFreq: 1000.0,
|
||||
filterQ: 1.0,
|
||||
compressor: true,
|
||||
dynamics: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成音频指纹
|
||||
func (af *AudioFingerprint) Generate() string {
|
||||
var fingerprint strings.Builder
|
||||
|
||||
// 添加基本音频配置
|
||||
fingerprint.WriteString(fmt.Sprintf("SAMPLE_RATE:%d|", af.sampleRate))
|
||||
fingerprint.WriteString(fmt.Sprintf("CHANNELS:%d|", af.channelCount))
|
||||
fingerprint.WriteString(fmt.Sprintf("BUFFER_SIZE:%d|", af.bufferSize))
|
||||
|
||||
// 添加振荡器信息
|
||||
fingerprint.WriteString(fmt.Sprintf("OSCILLATOR:%s|", af.oscillatorType))
|
||||
|
||||
// 添加滤波器信息
|
||||
fingerprint.WriteString(fmt.Sprintf("FILTER:%s@%.2f,Q=%.2f|", af.filterType, af.filterFreq, af.filterQ))
|
||||
|
||||
// 添加效果器信息
|
||||
if af.compressor {
|
||||
fingerprint.WriteString("COMPRESSOR:1|")
|
||||
} else {
|
||||
fingerprint.WriteString("COMPRESSOR:0|")
|
||||
}
|
||||
|
||||
if af.dynamics {
|
||||
fingerprint.WriteString("DYNAMICS:1|")
|
||||
} else {
|
||||
fingerprint.WriteString("DYNAMICS:0|")
|
||||
}
|
||||
|
||||
// 生成模拟音频数据
|
||||
audioData := af.generateAudioData()
|
||||
fingerprint.WriteString(fmt.Sprintf("AUDIO_DATA:%s", audioData))
|
||||
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(fingerprint.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// generateAudioData 生成模拟音频数据(极度简化版本)
|
||||
func (af *AudioFingerprint) generateAudioData() string {
|
||||
var data strings.Builder
|
||||
|
||||
// 极度简化的音频数据生成
|
||||
maxSamples := 128 // 进一步限制样本数量
|
||||
samples := maxSamples
|
||||
|
||||
for i := 0; i < samples; i++ {
|
||||
// 使用简单的伪随机数生成
|
||||
var sample float64
|
||||
|
||||
// 基于振荡器类型生成不同的简单模式
|
||||
switch af.oscillatorType {
|
||||
case "sine":
|
||||
sample = af.sin(float64(i) * 0.1)
|
||||
case "square":
|
||||
sample = float64(i%2)*2 - 1 // 简单的方波
|
||||
case "sawtooth":
|
||||
sample = float64(i%32)/16 - 1 // 简单的锯齿波
|
||||
case "triangle":
|
||||
sample = float64((i%32-16)%16)/8 - 1 // 简单的三角波
|
||||
default:
|
||||
sample = af.sin(float64(i) * 0.1)
|
||||
}
|
||||
|
||||
// 简单的振幅调制
|
||||
sample *= 0.5
|
||||
|
||||
// 转换为16位整数并编码
|
||||
sample16 := int16(sample * 16383) // 降低音量避免削波
|
||||
data.WriteString(fmt.Sprintf("%04x", uint16(sample16)))
|
||||
|
||||
// 添加分隔符
|
||||
if i > 0 && i%16 == 0 {
|
||||
data.WriteString("|")
|
||||
}
|
||||
}
|
||||
|
||||
return data.String()
|
||||
}
|
||||
|
||||
// sineWave 生成正弦波
|
||||
func (af *AudioFingerprint) sineWave(t float64) float64 {
|
||||
return af.wave(t, 440.0) // A4音符
|
||||
}
|
||||
|
||||
// squareWave 生成方波
|
||||
func (af *AudioFingerprint) squareWave(t float64) float64 {
|
||||
sample := af.wave(t, 440.0)
|
||||
if sample > 0 {
|
||||
return 0.8
|
||||
}
|
||||
return -0.8
|
||||
}
|
||||
|
||||
// sawtoothWave 生成锯齿波
|
||||
func (af *AudioFingerprint) sawtoothWave(t float64) float64 {
|
||||
freq := 440.0
|
||||
phase := t * freq * 2 * 3.14159
|
||||
return (2*(phase/(2*3.14159)-float64(int(phase/(2*3.14159)))) - 1) * 0.8
|
||||
}
|
||||
|
||||
// triangleWave 生成三角波
|
||||
func (af *AudioFingerprint) triangleWave(t float64) float64 {
|
||||
freq := 440.0
|
||||
phase := t * freq * 2 * 3.14159
|
||||
saw := 2*(phase/(2*3.14159)-float64(int(phase/(2*3.14159)))) - 1
|
||||
if saw > 0 {
|
||||
return 2*saw - 1
|
||||
}
|
||||
return -2*saw - 1
|
||||
}
|
||||
|
||||
// wave 基础波形生成
|
||||
func (af *AudioFingerprint) wave(t, freq float64) float64 {
|
||||
return af.amplitude(t, freq) * 0.8
|
||||
}
|
||||
|
||||
// amplitude 幅度调制
|
||||
func (af *AudioFingerprint) amplitude(t, freq float64) float64 {
|
||||
// 简单的包络
|
||||
envelope := 1.0
|
||||
if t < 0.1 {
|
||||
envelope = t / 0.1 // 攻击时间
|
||||
} else if t > 0.9 {
|
||||
envelope = (1.0 - t) / 0.1 // 释放时间
|
||||
}
|
||||
|
||||
return envelope * (0.5 + 0.5*af.modulation(t, 5.0)) // 调制
|
||||
}
|
||||
|
||||
// modulation 调制
|
||||
func (af *AudioFingerprint) modulation(t, modFreq float64) float64 {
|
||||
return 0.5 * (1 + af.sin(t*modFreq*2*3.14159))
|
||||
}
|
||||
|
||||
// sin 正弦函数(简化版本)
|
||||
func (af *AudioFingerprint) sin(x float64) float64 {
|
||||
// 使用简化的正弦函数实现,避免复杂的泰勒级数计算
|
||||
for x > 2*3.14159 {
|
||||
x -= 2 * 3.14159
|
||||
}
|
||||
for x < 0 {
|
||||
x += 2 * 3.14159
|
||||
}
|
||||
|
||||
// 使用简单的近似计算
|
||||
return x * (1 - x*x/(2*3.14159*3.14159))
|
||||
}
|
||||
|
||||
// applyFilter 应用滤波器
|
||||
func (af *AudioFingerprint) applyFilter(sample, t float64) float64 {
|
||||
// 简单的低通滤波器模拟
|
||||
if af.filterType == "lowpass" {
|
||||
// 应用简单的滤波效果
|
||||
filterAmount := 0.1
|
||||
sample = sample*(1-filterAmount) + af.sin(t*af.filterFreq*2*3.14159)*filterAmount
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
// applyCompressor 应用压缩器
|
||||
func (af *AudioFingerprint) applyCompressor(sample float64) float64 {
|
||||
threshold := 0.5
|
||||
ratio := 4.0
|
||||
|
||||
if sample > threshold {
|
||||
return threshold + (sample-threshold)/ratio
|
||||
}
|
||||
if sample < -threshold {
|
||||
return -threshold + (sample+threshold)/ratio
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
// applyDynamics 应用动态处理
|
||||
func (af *AudioFingerprint) applyDynamics(sample float64) float64 {
|
||||
// 简单的动态处理
|
||||
return sample * (0.8 + 0.2*af.sin(float64(time.Now().UnixNano()/1e9)))
|
||||
}
|
||||
|
||||
// GenerateRandomAudioFingerprint 生成随机音频指纹
|
||||
func GenerateRandomAudioFingerprint() string {
|
||||
|
||||
af := NewAudioFingerprint()
|
||||
|
||||
// 随机采样率
|
||||
sampleRates := []int{22050, 44100, 48000, 96000}
|
||||
af.sampleRate = sampleRates[rand.Intn(len(sampleRates))]
|
||||
|
||||
// 随机声道数
|
||||
af.channelCount = 1 + rand.Intn(2) // 1-2
|
||||
|
||||
// 随机缓冲区大小 - 限制大小以避免性能问题
|
||||
bufferSizes := []int{256, 512, 1024} // 更小的缓冲区大小
|
||||
af.bufferSize = bufferSizes[rand.Intn(len(bufferSizes))]
|
||||
|
||||
// 随机振荡器类型
|
||||
oscillators := []string{"sine", "square", "sawtooth", "triangle"}
|
||||
af.oscillatorType = oscillators[rand.Intn(len(oscillators))]
|
||||
|
||||
// 随机滤波器类型
|
||||
filters := []string{"lowpass", "highpass", "bandpass", "notch"}
|
||||
af.filterType = filters[rand.Intn(len(filters))]
|
||||
|
||||
// 随机滤波器频率
|
||||
af.filterFreq = 200 + rand.Float64()*3800 // 200-4000 Hz
|
||||
|
||||
// 随机滤波器Q值
|
||||
af.filterQ = 0.5 + rand.Float64()*10 // 0.5-10.5
|
||||
|
||||
// 随机效果器
|
||||
af.compressor = rand.Intn(100) < 70 // 70%概率启用
|
||||
af.dynamics = rand.Intn(100) < 60 // 60%概率启用
|
||||
|
||||
return af.Generate()
|
||||
}
|
||||
239
internal/utils/fingerprint/canvas.go
Normal file
239
internal/utils/fingerprint/canvas.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CanvasFingerprint Canvas指纹生成器
|
||||
type CanvasFingerprint struct {
|
||||
text string
|
||||
fontSize float64
|
||||
fontFamily string
|
||||
background color.Color
|
||||
textColor color.Color
|
||||
overlayColor color.Color
|
||||
}
|
||||
|
||||
// NewCanvasFingerprint 创建新的Canvas指纹生成器
|
||||
func NewCanvasFingerprint() *CanvasFingerprint {
|
||||
return &CanvasFingerprint{
|
||||
text: "奥莱卡 Outlet,com <canvas> 1.0",
|
||||
fontSize: 14,
|
||||
fontFamily: "Arial",
|
||||
background: color.White,
|
||||
textColor: color.RGBA{0, 102, 153, 255}, // #069
|
||||
overlayColor: color.RGBA{102, 204, 0, 178}, // rgba(102, 204, 0, 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Canvas指纹
|
||||
func (cf *CanvasFingerprint) Generate() (string, error) {
|
||||
// 创建画布 - 模拟原代码中的canvas元素
|
||||
img := image.NewRGBA(image.Rect(0, 0, 200, 40))
|
||||
|
||||
// 设置背景色
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{cf.background}, image.Point{}, draw.Src)
|
||||
|
||||
// 模拟 ctx.fillStyle = "#f60"; ctx.fillRect(120,1,62,20);
|
||||
orangeRect := color.RGBA{255, 102, 0, 255} // #f60
|
||||
orangeFill := &image.Uniform{orangeRect}
|
||||
draw.Draw(img, image.Rect(120, 1, 182, 21), orangeFill, image.Point{}, draw.Src)
|
||||
|
||||
// 绘制第一层文本 - ctx.fillStyle = "#069"; ctx.fillText(txt, 2, 15);
|
||||
err := cf.drawText(img, cf.textColor, 2, 15)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to draw first text layer: %v", err)
|
||||
}
|
||||
|
||||
// 绘制第二层文本 - ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText(txt, 4, 17);
|
||||
err = cf.drawText(img, cf.overlayColor, 4, 17)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to draw second text layer: %v", err)
|
||||
}
|
||||
|
||||
// 将图像转换为数据URL (模拟canvas.toDataURL('image/png'))
|
||||
hash := cf.imageToHash(img)
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// drawText 绘制文本
|
||||
func (cf *CanvasFingerprint) drawText(img *image.RGBA, textColor color.Color, x, y int) error {
|
||||
// 简化的文本绘制 - 使用像素模式来模拟文本
|
||||
cf.drawTextPixels(img, textColor, x, y)
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawTextPixels 使用像素模式绘制文本
|
||||
func (cf *CanvasFingerprint) drawTextPixels(img *image.RGBA, textColor color.Color, x, y int) {
|
||||
// 简单的文本像素化绘制
|
||||
// 这里我们使用固定模式的像素来代表文本字符
|
||||
text := cf.text
|
||||
for i, char := range text {
|
||||
charX := x + i*8 // 每个字符8像素宽
|
||||
|
||||
// 为不同字符绘制不同的像素模式
|
||||
switch char {
|
||||
case '奥', '莱', '卡':
|
||||
cf.drawChineseChar(img, charX, y, textColor)
|
||||
case 'O', 'o', 'u', 't', 'l', 'e':
|
||||
cf.drawLatinChar(img, charX, y, textColor)
|
||||
case '<', '>', 'c', 'a', 'n', 'v', 's', '1', '0', ',', ' ':
|
||||
cf.drawSpecialChar(img, charX, y, textColor)
|
||||
default:
|
||||
cf.drawDefaultChar(img, charX, y, textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawChineseChar 绘制中文字符
|
||||
func (cf *CanvasFingerprint) drawChineseChar(img *image.RGBA, x, y int, color color.Color) {
|
||||
// 简单的中文字符模式
|
||||
pattern := [][]bool{
|
||||
{true, true, true, true, true, true},
|
||||
{true, false, false, false, false, true},
|
||||
{true, true, true, true, true, true},
|
||||
{false, false, true, true, false, false},
|
||||
{true, true, true, true, true, true},
|
||||
}
|
||||
cf.drawCharPattern(img, x, y, pattern, color)
|
||||
}
|
||||
|
||||
// drawLatinChar 绘制拉丁字符
|
||||
func (cf *CanvasFingerprint) drawLatinChar(img *image.RGBA, x, y int, color color.Color) {
|
||||
// 简单的拉丁字符模式
|
||||
pattern := [][]bool{
|
||||
{true, true, true, true, true},
|
||||
{true, false, false, false, true},
|
||||
{true, false, false, false, true},
|
||||
{true, false, false, false, true},
|
||||
{true, true, true, true, true},
|
||||
}
|
||||
cf.drawCharPattern(img, x, y, pattern, color)
|
||||
}
|
||||
|
||||
// drawSpecialChar 绘制特殊字符
|
||||
func (cf *CanvasFingerprint) drawSpecialChar(img *image.RGBA, x, y int, color color.Color) {
|
||||
// 简单的特殊字符模式
|
||||
pattern := [][]bool{
|
||||
{true, true, true, true},
|
||||
{false, false, false, true},
|
||||
{true, true, true, true},
|
||||
{true, false, false, false},
|
||||
{true, true, true, true},
|
||||
}
|
||||
cf.drawCharPattern(img, x, y, pattern, color)
|
||||
}
|
||||
|
||||
// drawDefaultChar 绘制默认字符
|
||||
func (cf *CanvasFingerprint) drawDefaultChar(img *image.RGBA, x, y int, color color.Color) {
|
||||
// 默认字符模式
|
||||
pattern := [][]bool{
|
||||
{true, true, true},
|
||||
{true, false, true},
|
||||
{true, false, true},
|
||||
{true, false, true},
|
||||
{true, true, true},
|
||||
}
|
||||
cf.drawCharPattern(img, x, y, pattern, color)
|
||||
}
|
||||
|
||||
// drawCharPattern 绘制字符模式
|
||||
func (cf *CanvasFingerprint) drawCharPattern(img *image.RGBA, x, y int, pattern [][]bool, color color.Color) {
|
||||
for py, row := range pattern {
|
||||
for px, pixel := range row {
|
||||
if pixel {
|
||||
drawX := x + px
|
||||
drawY := y + py
|
||||
if drawX >= 0 && drawX < img.Bounds().Dx() && drawY >= 0 && drawY < img.Bounds().Dy() {
|
||||
img.Set(drawX, drawY, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// imageToHash 将图像转换为MD5哈希
|
||||
func (cf *CanvasFingerprint) imageToHash(img image.Image) string {
|
||||
// 直接使用像素数据生成哈希(模拟canvas.toDataURL的效果)
|
||||
return cf.pixelDataToHash(img)
|
||||
}
|
||||
|
||||
// pixelDataToHash 使用像素数据生成哈希
|
||||
func (cf *CanvasFingerprint) pixelDataToHash(img image.Image) string {
|
||||
bounds := img.Bounds()
|
||||
var pixelData strings.Builder
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
pixelData.WriteString(fmt.Sprintf("%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8))
|
||||
}
|
||||
}
|
||||
|
||||
hash := md5.Sum([]byte(pixelData.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// SetText 设置文本内容
|
||||
func (cf *CanvasFingerprint) SetText(text string) {
|
||||
cf.text = text
|
||||
}
|
||||
|
||||
// SetFontSize 设置字体大小
|
||||
func (cf *CanvasFingerprint) SetFontSize(size float64) {
|
||||
cf.fontSize = size
|
||||
}
|
||||
|
||||
// SetFontFamily 设置字体
|
||||
func (cf *CanvasFingerprint) SetFontFamily(family string) {
|
||||
cf.fontFamily = family
|
||||
}
|
||||
|
||||
// SetColors 设置颜色
|
||||
func (cf *CanvasFingerprint) SetColors(bg, text, overlay color.Color) {
|
||||
cf.background = bg
|
||||
cf.textColor = text
|
||||
cf.overlayColor = overlay
|
||||
}
|
||||
|
||||
// GenerateRandomFingerprint 生成随机Canvas指纹
|
||||
func GenerateRandomFingerprint() (string, error) {
|
||||
|
||||
cf := NewCanvasFingerprint()
|
||||
|
||||
// 添加随机变化
|
||||
if rand.Intn(100) < 30 { // 30%概率改变文本
|
||||
variations := []string{
|
||||
"奥莱卡 Outlet,com <canvas> 1.0",
|
||||
"奥莱卡 Outlet <canvas> 1.1",
|
||||
"奥莱卡 com <canvas> 1.2",
|
||||
"Outlet,com <canvas> 1.0",
|
||||
"奥莱卡 <canvas> 1.3",
|
||||
}
|
||||
cf.SetText(variations[rand.Intn(len(variations))])
|
||||
}
|
||||
|
||||
// 随机字体大小
|
||||
if rand.Intn(100) < 20 { // 20%概率改变字体大小
|
||||
cf.SetFontSize(float64(12 + rand.Intn(4))) // 12-15px
|
||||
}
|
||||
|
||||
// 随机颜色变化
|
||||
if rand.Intn(100) < 15 { // 15%概率改变颜色
|
||||
cf.SetColors(
|
||||
color.White,
|
||||
color.RGBA{uint8(rand.Intn(255)), uint8(rand.Intn(255)), uint8(rand.Intn(255)), 255},
|
||||
color.RGBA{uint8(rand.Intn(255)), uint8(rand.Intn(255)), uint8(rand.Intn(255)), uint8(178 + rand.Intn(77))},
|
||||
)
|
||||
}
|
||||
|
||||
return cf.Generate()
|
||||
}
|
||||
46
internal/utils/fingerprint/doc.go
Normal file
46
internal/utils/fingerprint/doc.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Package fingerprint 浏览器指纹生成工具包
|
||||
//
|
||||
// 这个包提供了完整的浏览器指纹生成功能,包括:
|
||||
// - Canvas 指纹 (canvas.go)
|
||||
// - WebGL 指纹 (webgl.go)
|
||||
// - Audio 指纹 (audio.go)
|
||||
// - Screen 指纹 (screen.go)
|
||||
// - Fonts 指纹 (fonts.go)
|
||||
// - Timezone 指纹 (timezone.go)
|
||||
// - 完整浏览器指纹 (fingerprint.go)
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// // 生成完整的浏览器指纹
|
||||
// fingerprint, err := GenerateRandomBrowserFingerprint()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Browser Fingerprint: %v\n", fingerprint)
|
||||
//
|
||||
// // 生成特定类型的指纹
|
||||
// canvasFp, err := GenerateRandomFingerprint()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Canvas Fingerprint: %s\n", canvasFp)
|
||||
//
|
||||
// // 生成JSON格式的指纹
|
||||
// jsonFp, err := GenerateRandomBrowserFingerprintJSON()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Fingerprint JSON: %s\n", jsonFp)
|
||||
//
|
||||
// 主要组件:
|
||||
// - CanvasFingerprint: 基于Canvas API的指纹生成
|
||||
// - WebGlFingerprint: 基于WebGL渲染的指纹生成
|
||||
// - AudioFingerprint: 基于音频API的指纹生成
|
||||
// - ScreenFingerprint: 基于屏幕信息的指纹生成
|
||||
// - FontFingerprint: 基于字体检测的指纹生成
|
||||
// - TimezoneFingerprint: 基于时区信息的指纹生成
|
||||
// - BrowserFingerprint: 完整的浏览器指纹组合
|
||||
//
|
||||
// 注意:这个包主要用于测试和开发目的,用于模拟不同的浏览器环境。
|
||||
// 在实际应用中,请确保遵守相关法律法规和隐私政策。
|
||||
package fingerprint
|
||||
396
internal/utils/fingerprint/fingerprint.go
Normal file
396
internal/utils/fingerprint/fingerprint.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
)
|
||||
|
||||
// BrowserFingerprint 完整的浏览器指纹
|
||||
type BrowserFingerprint struct {
|
||||
Canvas string `json:"canvas"`
|
||||
WebGL string `json:"webgl"`
|
||||
Audio string `json:"audio"`
|
||||
Screen string `json:"screen"`
|
||||
Fonts string `json:"fonts"`
|
||||
Timezone string `json:"timezone"`
|
||||
Plugins string `json:"plugins"`
|
||||
Cookie string `json:"cookie"`
|
||||
DoNotTrack string `json:"doNotTrack"`
|
||||
Language string `json:"language"`
|
||||
Platform string `json:"platform"`
|
||||
CPU string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
Touch string `json:"touch"`
|
||||
Hardware string `json:"hardware"`
|
||||
Permissions string `json:"permissions"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// PluginInfo 插件信息
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// NewBrowserFingerprint 创建新的浏览器指纹生成器
|
||||
func NewBrowserFingerprint() *BrowserFingerprint {
|
||||
return &BrowserFingerprint{}
|
||||
}
|
||||
|
||||
// Generate 完整的浏览器指纹生成
|
||||
func (bf *BrowserFingerprint) Generate() (*BrowserFingerprint, error) {
|
||||
var err error
|
||||
|
||||
// 生成各个组件的指纹
|
||||
bf.Canvas, err = GenerateRandomFingerprint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate canvas fingerprint: %v", err)
|
||||
}
|
||||
|
||||
bf.WebGL = GenerateRandomWebGLFingerprint()
|
||||
bf.Audio = GenerateRandomAudioFingerprint()
|
||||
bf.Screen = GenerateRandomScreenFingerprint()
|
||||
bf.Fonts = GenerateRandomFontFingerprint()
|
||||
bf.Timezone = GenerateRandomTimezoneFingerprint()
|
||||
bf.Plugins = bf.generatePlugins()
|
||||
bf.Cookie = bf.generateCookie()
|
||||
bf.DoNotTrack = bf.generateDoNotTrack()
|
||||
bf.Language = bf.generateLanguage()
|
||||
bf.Platform = bf.generatePlatform()
|
||||
bf.CPU = bf.generateCPU()
|
||||
bf.Memory = bf.generateMemory()
|
||||
bf.Touch = bf.generateTouch()
|
||||
bf.Hardware = bf.generateHardware()
|
||||
bf.Permissions = bf.generatePermissions()
|
||||
|
||||
// 生成最终哈希
|
||||
bf.Hash = bf.generateHash()
|
||||
|
||||
return bf, nil
|
||||
}
|
||||
|
||||
// generatePlugins 生成插件指纹
|
||||
func (bf *BrowserFingerprint) generatePlugins() string {
|
||||
// 常见插件列表
|
||||
commonPlugins := []PluginInfo{
|
||||
{"Chrome PDF Plugin", ""},
|
||||
{"Chrome PDF Viewer", ""},
|
||||
{"Native Client", ""},
|
||||
{"Widevine Content Decryption Module", "4.10.2391.0"},
|
||||
{"Microsoft Edge PDF Plugin", ""},
|
||||
{"Microsoft Edge PDF Viewer", ""},
|
||||
{"Adobe Acrobat", "32.0.0.238"},
|
||||
{"Adobe Flash Player", "32.0 r0"},
|
||||
{"Java Applet Plug-in", "1.8.0_291"},
|
||||
{"QuickTime Plug-in", "7.7.9"},
|
||||
{"Silverlight Plug-In", "5.1.50918.0"},
|
||||
{"Shockwave Flash", "32.0 r0"},
|
||||
{"VLC Web Plugin", "3.0.16"},
|
||||
{"Google Update", "1.3.36.1"},
|
||||
{"Microsoft Office 2013", "15.0.4569.1506"},
|
||||
{"Microsoft Office 2016", "16.0.4266.1001"},
|
||||
{"Google Talk Plugin", "41.0.3.0"},
|
||||
{"Google Talk Plugin Video Accelerator", "41.0.3.0"},
|
||||
{"DivX Web Player", "2.0.2.50"},
|
||||
{"RealPlayer Plugin", "realplyg.dll"},
|
||||
{"Windows Media Player Plug-in", "12.0"},
|
||||
}
|
||||
|
||||
// 随机选择插件数量
|
||||
numPlugins := 3 + rand.Intn(10) // 3-12个插件
|
||||
selectedPlugins := make([]PluginInfo, 0, numPlugins)
|
||||
|
||||
for i := 0; i < numPlugins && i < len(commonPlugins); i++ {
|
||||
plugin := commonPlugins[rand.Intn(len(commonPlugins))]
|
||||
|
||||
// 避免重复
|
||||
duplicate := false
|
||||
for _, existing := range selectedPlugins {
|
||||
if existing.Name == plugin.Name {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !duplicate {
|
||||
selectedPlugins = append(selectedPlugins, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成插件字符串
|
||||
pluginsJson, _ := json.Marshal(selectedPlugins)
|
||||
return string(pluginsJson)
|
||||
}
|
||||
|
||||
// generateCookie 生成Cookie指纹
|
||||
func (bf *BrowserFingerprint) generateCookie() string {
|
||||
|
||||
cookieFeatures := map[string]bool{
|
||||
"cookieEnabled": true,
|
||||
"sessionStorage": true,
|
||||
"localStorage": true,
|
||||
"indexedDB": true,
|
||||
"webSQL": false,
|
||||
"serviceWorker": rand.Intn(100) < 80,
|
||||
"cache": true,
|
||||
"applicationCache": rand.Intn(100) < 30,
|
||||
"webWorkers": true,
|
||||
"sharedWorkers": rand.Intn(100) < 70,
|
||||
"geolocation": rand.Intn(100) < 90,
|
||||
"notification": rand.Intn(100) < 60,
|
||||
"pushNotification": rand.Intn(100) < 50,
|
||||
"gamepad": rand.Intn(100) < 40,
|
||||
"vr": rand.Intn(100) < 20,
|
||||
"battery": rand.Intn(100) < 30,
|
||||
"deviceOrientation": rand.Intn(100) < 80,
|
||||
"deviceMotion": rand.Intn(100) < 80,
|
||||
"deviceLight": rand.Intn(100) < 10,
|
||||
"deviceProximity": rand.Intn(100) < 10,
|
||||
"userProximity": rand.Intn(100) < 10,
|
||||
}
|
||||
|
||||
cookieJson, _ := json.Marshal(cookieFeatures)
|
||||
return string(cookieJson)
|
||||
}
|
||||
|
||||
// generateDoNotTrack 生成DoNotTrack指纹
|
||||
func (bf *BrowserFingerprint) generateDoNotTrack() string {
|
||||
|
||||
options := []string{"unspecified", "enabled", "disabled"}
|
||||
weights := []int{60, 20, 20} // 60%未指定,20%启用,20%禁用
|
||||
|
||||
totalWeight := 0
|
||||
for _, weight := range weights {
|
||||
totalWeight += weight
|
||||
}
|
||||
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
cumulativeWeight := 0
|
||||
|
||||
for i, weight := range weights {
|
||||
cumulativeWeight += weight
|
||||
if randomWeight < cumulativeWeight {
|
||||
return options[i]
|
||||
}
|
||||
}
|
||||
|
||||
return options[0]
|
||||
}
|
||||
|
||||
// generateLanguage 生成语言指纹
|
||||
func (bf *BrowserFingerprint) generateLanguage() string {
|
||||
|
||||
languages := []string{
|
||||
"en-US", "en-GB", "zh-CN", "zh-TW", "ja-JP", "ko-KR",
|
||||
"fr-FR", "de-DE", "es-ES", "it-IT", "pt-BR", "pt-PT",
|
||||
"ru-RU", "ar-SA", "hi-IN", "th-TH", "vi-VN", "id-ID",
|
||||
"ms-MY", "fil-PH", "tr-TR", "pl-PL", "nl-NL", "sv-SE",
|
||||
"da-DK", "no-NO", "fi-FI", "cs-CZ", "hu-HU", "ro-RO",
|
||||
"bg-BG", "hr-HR", "sk-SK", "sl-SI", "et-EE", "lv-LV",
|
||||
"lt-LT", "mt-MT", "el-GR", "he-IL", "uk-UA", "be-BY",
|
||||
}
|
||||
|
||||
// 1-3种语言
|
||||
numLanguages := 1 + rand.Intn(3)
|
||||
selectedLanguages := make([]string, 0, numLanguages)
|
||||
|
||||
for i := 0; i < numLanguages && i < len(languages); i++ {
|
||||
lang := languages[rand.Intn(len(languages))]
|
||||
|
||||
// 避免重复
|
||||
duplicate := false
|
||||
for _, existing := range selectedLanguages {
|
||||
if existing == lang {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !duplicate {
|
||||
selectedLanguages = append(selectedLanguages, lang)
|
||||
}
|
||||
}
|
||||
|
||||
// 组合成语言字符串
|
||||
result := selectedLanguages[0]
|
||||
for i := 1; i < len(selectedLanguages); i++ {
|
||||
result += "," + selectedLanguages[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// generatePlatform 生成平台指纹
|
||||
func (bf *BrowserFingerprint) generatePlatform() string {
|
||||
|
||||
platforms := []string{
|
||||
"Win32", "Windows", "MacIntel", "MacPPC", "Linux i686", "Linux x86_64",
|
||||
"Linux armv7l", "Linux aarch64", "iPhone", "iPad", "iPod", "Android",
|
||||
"X11", "FreeBSD i386", "FreeBSD amd64", "OpenBSD i386", "NetBSD i386",
|
||||
}
|
||||
|
||||
return platforms[rand.Intn(len(platforms))]
|
||||
}
|
||||
|
||||
// generateCPU 生成CPU指纹
|
||||
func (bf *BrowserFingerprint) generateCPU() string {
|
||||
|
||||
cpus := []string{
|
||||
"x86", "x86_64", "arm", "arm64", "mips", "mips64", "ppc", "ppc64",
|
||||
"s390", "s390x", "ia64", "alpha", "sparc", "sparc64",
|
||||
}
|
||||
|
||||
return cpus[rand.Intn(len(cpus))]
|
||||
}
|
||||
|
||||
// generateMemory 生成内存指纹
|
||||
func (bf *BrowserFingerprint) generateMemory() string {
|
||||
|
||||
// 常见内存大小(GB)
|
||||
memorySizes := []int{2, 4, 8, 16, 32, 64}
|
||||
memory := memorySizes[rand.Intn(len(memorySizes))]
|
||||
|
||||
return fmt.Sprintf("%d", memory)
|
||||
}
|
||||
|
||||
// generateTouch 生成触摸指纹
|
||||
func (bf *BrowserFingerprint) generateTouch() string {
|
||||
|
||||
touchFeatures := map[string]bool{
|
||||
"maxTouchPoints": rand.Intn(100) < 60,
|
||||
"touchSupport": rand.Intn(100) < 50,
|
||||
"ontouchstart": rand.Intn(100) < 50,
|
||||
"ontouchend": rand.Intn(100) < 50,
|
||||
"ontouchmove": rand.Intn(100) < 50,
|
||||
"ontouchcancel": rand.Intn(100) < 50,
|
||||
"msPointerEnabled": rand.Intn(100) < 30,
|
||||
"pointerEnabled": rand.Intn(100) < 70,
|
||||
}
|
||||
|
||||
if touchFeatures["maxTouchPoints"] {
|
||||
// 设置最大触摸点数
|
||||
maxPoints := rand.Intn(11) // 0-10
|
||||
touchFeatures["maxTouchPoints"] = maxPoints > 0
|
||||
}
|
||||
|
||||
touchJson, _ := json.Marshal(touchFeatures)
|
||||
return string(touchJson)
|
||||
}
|
||||
|
||||
// generateHardware 生成硬件指纹
|
||||
func (bf *BrowserFingerprint) generateHardware() string {
|
||||
|
||||
hardwareInfo := map[string]interface{}{
|
||||
"deviceMemory": 2 + rand.Intn(8), // 2-10GB
|
||||
"hardwareConcurrency": 1 + rand.Intn(16), // 1-16 cores
|
||||
"maxTouchPoints": rand.Intn(11), // 0-10
|
||||
"isMobile": rand.Intn(100) < 40,
|
||||
"isTablet": rand.Intn(100) < 15,
|
||||
"isDesktop": rand.Intn(100) < 45,
|
||||
}
|
||||
|
||||
hardwareJson, _ := json.Marshal(hardwareInfo)
|
||||
return string(hardwareJson)
|
||||
}
|
||||
|
||||
// generatePermissions 生成权限指纹
|
||||
func (bf *BrowserFingerprint) generatePermissions() string {
|
||||
|
||||
permissions := map[string]string{
|
||||
"geolocation": bf.randomPermissionState(),
|
||||
"notifications": bf.randomPermissionState(),
|
||||
"push": bf.randomPermissionState(),
|
||||
"midi": bf.randomPermissionState(),
|
||||
"camera": bf.randomPermissionState(),
|
||||
"microphone": bf.randomPermissionState(),
|
||||
"speaker": bf.randomPermissionState(),
|
||||
"device-info": bf.randomPermissionState(),
|
||||
"background-sync": bf.randomPermissionState(),
|
||||
"bluetooth": bf.randomPermissionState(),
|
||||
"persistent-storage": bf.randomPermissionState(),
|
||||
"ambient-light-sensor": bf.randomPermissionState(),
|
||||
"accelerometer": bf.randomPermissionState(),
|
||||
"gyroscope": bf.randomPermissionState(),
|
||||
"magnetometer": bf.randomPermissionState(),
|
||||
"clipboard": bf.randomPermissionState(),
|
||||
"screen-wake-lock": bf.randomPermissionState(),
|
||||
"nfc": bf.randomPermissionState(),
|
||||
"payment": bf.randomPermissionState(),
|
||||
}
|
||||
|
||||
permissionsJson, _ := json.Marshal(permissions)
|
||||
return string(permissionsJson)
|
||||
}
|
||||
|
||||
// randomPermissionState 生成随机权限状态
|
||||
func (bf *BrowserFingerprint) randomPermissionState() string {
|
||||
|
||||
states := []string{"granted", "denied", "prompt"}
|
||||
weights := []int{30, 40, 30} // 30%授权,40%拒绝,30%提示
|
||||
|
||||
totalWeight := 0
|
||||
for _, weight := range weights {
|
||||
totalWeight += weight
|
||||
}
|
||||
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
cumulativeWeight := 0
|
||||
|
||||
for i, weight := range weights {
|
||||
cumulativeWeight += weight
|
||||
if randomWeight < cumulativeWeight {
|
||||
return states[i]
|
||||
}
|
||||
}
|
||||
|
||||
return states[0]
|
||||
}
|
||||
|
||||
// generateHash 生成最终哈希
|
||||
func (bf *BrowserFingerprint) generateHash() string {
|
||||
// 组合所有指纹数据
|
||||
combined := fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s",
|
||||
bf.Canvas, bf.WebGL, bf.Audio, bf.Screen, bf.Fonts, bf.Timezone,
|
||||
bf.Plugins, bf.Cookie, bf.DoNotTrack, bf.Language, bf.Platform,
|
||||
bf.CPU, bf.Memory, bf.Touch, bf.Hardware, bf.Permissions)
|
||||
|
||||
// 生成简单的哈希(在实际应用中应该使用更安全的哈希算法)
|
||||
var hash uint32 = 5381
|
||||
for i := 0; i < len(combined); i++ {
|
||||
hash = ((hash << 5) + hash) + uint32(combined[i])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
// GenerateRandomBrowserFingerprint 生成随机浏览器指纹
|
||||
func GenerateRandomBrowserFingerprint() (*BrowserFingerprint, error) {
|
||||
bf := NewBrowserFingerprint()
|
||||
return bf.Generate()
|
||||
}
|
||||
|
||||
// GenerateRandomBrowserFingerprintJSON 生成随机浏览器指纹的JSON字符串
|
||||
func GenerateRandomBrowserFingerprintJSON() (string, error) {
|
||||
bf, err := GenerateRandomBrowserFingerprint()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := json.MarshalIndent(bf, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal fingerprint: %v", err)
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func GenerateRandomBrowserFingerprintHash() string {
|
||||
bf, err := GenerateRandomBrowserFingerprintJSON()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return cryptor.Md5String(bf)
|
||||
}
|
||||
10
internal/utils/fingerprint/fingerprint_test.go
Normal file
10
internal/utils/fingerprint/fingerprint_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package fingerprint
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenerateRandomBrowserFingerprintJSON(t *testing.T) {
|
||||
for range 100 {
|
||||
fig := GenerateRandomBrowserFingerprintHash()
|
||||
t.Log(fig)
|
||||
}
|
||||
}
|
||||
206
internal/utils/fingerprint/fonts.go
Normal file
206
internal/utils/fingerprint/fonts.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FontFingerprint 字体指纹生成器
|
||||
type FontFingerprint struct {
|
||||
detectedFonts []string
|
||||
defaultFonts []string
|
||||
jsFonts []string
|
||||
flashFonts []string
|
||||
systemFonts []string
|
||||
emojiSupport bool
|
||||
fontSmoothing string
|
||||
textRendering string
|
||||
}
|
||||
|
||||
// NewFontFingerprint 创建新的字体指纹生成器
|
||||
func NewFontFingerprint() *FontFingerprint {
|
||||
return &FontFingerprint{
|
||||
defaultFonts: []string{
|
||||
"Arial", "Times New Roman", "Courier New", "Verdana", "Georgia",
|
||||
"Palatino", "Garamond", "Bookman", "Tahoma", "Trebuchet MS",
|
||||
},
|
||||
jsFonts: []string{
|
||||
"Arial", "Verdana", "Helvetica", "Trebuchet MS", "Times New Roman",
|
||||
"Georgia", "Palatino", "Garamond", "Bookman", "Comic Sans MS",
|
||||
"Courier New", "Impact", "Lucida Console", "Lucida Sans Unicode",
|
||||
},
|
||||
flashFonts: []string{
|
||||
"Arial", "Verdana", "Times New Roman", "Courier New", "Georgia",
|
||||
"Palatino", "Garamond", "Bookman", "Tahoma", "Comic Sans MS",
|
||||
},
|
||||
systemFonts: []string{
|
||||
"Arial", "Helvetica", "Times New Roman", "Courier New", "Georgia",
|
||||
"Verdana", "Palatino", "Garamond", "Bookman", "Impact",
|
||||
},
|
||||
emojiSupport: true,
|
||||
fontSmoothing: "grayscale",
|
||||
textRendering: "optimizeLegibility",
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成字体指纹
|
||||
func (ff *FontFingerprint) Generate() string {
|
||||
var fingerprint strings.Builder
|
||||
|
||||
// 检测到的字体
|
||||
fingerprint.WriteString("DETECTED_FONTS:")
|
||||
for i, font := range ff.detectedFonts {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(font)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
|
||||
// 默认字体
|
||||
fingerprint.WriteString("DEFAULT_FONTS:")
|
||||
for i, font := range ff.defaultFonts {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(font)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
|
||||
// JS字体
|
||||
fingerprint.WriteString("JS_FONTS:")
|
||||
for i, font := range ff.jsFonts {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(font)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
|
||||
// Flash字体(如果支持)
|
||||
if len(ff.flashFonts) > 0 {
|
||||
fingerprint.WriteString("FLASH_FONTS:")
|
||||
for i, font := range ff.flashFonts {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(font)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
}
|
||||
|
||||
// 系统字体
|
||||
fingerprint.WriteString("SYSTEM_FONTS:")
|
||||
for i, font := range ff.systemFonts {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(font)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
|
||||
// Emoji支持
|
||||
if ff.emojiSupport {
|
||||
fingerprint.WriteString("EMOJI:1|")
|
||||
} else {
|
||||
fingerprint.WriteString("EMOJI:0|")
|
||||
}
|
||||
|
||||
// 字体平滑
|
||||
fingerprint.WriteString(fmt.Sprintf("SMOOTHING:%s|", ff.fontSmoothing))
|
||||
|
||||
// 文本渲染
|
||||
fingerprint.WriteString(fmt.Sprintf("RENDERING:%s", ff.textRendering))
|
||||
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(fingerprint.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// detectFonts 检测可用字体(模拟)
|
||||
func (ff *FontFingerprint) detectFonts() {
|
||||
|
||||
// 基础字体集合
|
||||
allFonts := []string{
|
||||
// Windows字体
|
||||
"Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT Bold",
|
||||
"Calibri", "Cambria", "Candara", "Century Gothic",
|
||||
"Comic Sans MS", "Consolas", "Constantia", "Corbel",
|
||||
"Courier New", "Franklin Gothic Medium", "Gabriola",
|
||||
"Georgia", "Impact", "Lucida Console", "Lucida Sans Unicode",
|
||||
"Microsoft Sans Serif", "Palatino Linotype", "Segoe Print",
|
||||
"Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold",
|
||||
"Segoe UI Symbol", "Tahoma", "Times New Roman", "Trebuchet MS",
|
||||
"Verdana", "Webdings",
|
||||
// Mac字体
|
||||
"Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT Bold",
|
||||
"Avenir", "Avenir Next", "Baskerville", "Big Caslon",
|
||||
"Chalkboard", "Chalkboard SE", "Chalkduster", "Cochin",
|
||||
"Comic Sans MS", "Copperplate", "Courier", "Courier New",
|
||||
"Didot", "Futura", "Geneva", "Georgia",
|
||||
"Gill Sans", "Helvetica", "Helvetica Neue", "Impact",
|
||||
"Lucida Grande", "Lucida Sans", "Marker Felt", "Menlo",
|
||||
"Monaco", "Noteworthy", "Optima", "Palatino",
|
||||
"Papyrus", "Phosphate", "Rockwell", "San Francisco",
|
||||
"SignPainter", "Skia", "Snell Roundhand", "Tahoma",
|
||||
"Times", "Times New Roman", "Trebuchet MS", "Verdana",
|
||||
"Zapfino",
|
||||
// Linux字体
|
||||
"Arial", "Arial Black", "Bookman", "Century Schoolbook L",
|
||||
"Courier 10 Pitch", "Courier New", "DejaVu Sans",
|
||||
"DejaVu Sans Mono", "DejaVu Serif", "FreeMono", "FreeSans",
|
||||
"FreeSerif", "Garuda", "Georgia", "Liberation Mono",
|
||||
"Liberation Sans", "Liberation Serif", "Lucida Bright",
|
||||
"Lucida Sans", "Lucida Sans Typewriter", "Luxi Mono",
|
||||
"Luxi Sans", "Luxi Serif", "Nimbus Mono L", "Nimbus Roman No9 L",
|
||||
"Nimbus Sans L", "Palatino", "Standard Symbols L",
|
||||
"Times New Roman", "Trebuchet MS", "Utopia", "Verdana",
|
||||
"WenQuanYi Micro Hei", "WenQuanYi Zen Hei",
|
||||
}
|
||||
|
||||
// 随机选择一些字体作为"检测到"的字体
|
||||
numFonts := 15 + rand.Intn(20) // 15-35个字体
|
||||
ff.detectedFonts = make([]string, 0, numFonts)
|
||||
|
||||
for i := 0; i < numFonts && i < len(allFonts); i++ {
|
||||
font := allFonts[rand.Intn(len(allFonts))]
|
||||
|
||||
// 避免重复
|
||||
duplicate := false
|
||||
for _, existing := range ff.detectedFonts {
|
||||
if existing == font {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !duplicate {
|
||||
ff.detectedFonts = append(ff.detectedFonts, font)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateRandomFontFingerprint 生成随机字体指纹
|
||||
func GenerateRandomFontFingerprint() string {
|
||||
|
||||
ff := NewFontFingerprint()
|
||||
|
||||
// 检测字体
|
||||
ff.detectFonts()
|
||||
|
||||
// 随机Emoji支持
|
||||
ff.emojiSupport = rand.Intn(100) < 90 // 90%概率支持
|
||||
|
||||
// 随机字体平滑
|
||||
smoothingOptions := []string{"auto", "none", "grayscale", "subpixel-antialiased"}
|
||||
ff.fontSmoothing = smoothingOptions[rand.Intn(len(smoothingOptions))]
|
||||
|
||||
// 随机文本渲染
|
||||
renderingOptions := []string{"auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision"}
|
||||
ff.textRendering = renderingOptions[rand.Intn(len(renderingOptions))]
|
||||
|
||||
return ff.Generate()
|
||||
}
|
||||
166
internal/utils/fingerprint/screen.go
Normal file
166
internal/utils/fingerprint/screen.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScreenFingerprint 屏幕指纹生成器
|
||||
type ScreenFingerprint struct {
|
||||
width int
|
||||
height int
|
||||
colorDepth int
|
||||
pixelDepth int
|
||||
orientation string
|
||||
availWidth int
|
||||
availHeight int
|
||||
deviceScale float64
|
||||
colorGamut string
|
||||
forcedColor bool
|
||||
contrast string
|
||||
inverted bool
|
||||
}
|
||||
|
||||
// NewScreenFingerprint 创建新的屏幕指纹生成器
|
||||
func NewScreenFingerprint() *ScreenFingerprint {
|
||||
return &ScreenFingerprint{
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
colorDepth: 24,
|
||||
pixelDepth: 24,
|
||||
orientation: "landscape-primary",
|
||||
availWidth: 1920,
|
||||
availHeight: 1040,
|
||||
deviceScale: 1.0,
|
||||
colorGamut: "srgb",
|
||||
forcedColor: false,
|
||||
contrast: "no-preference",
|
||||
inverted: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成屏幕指纹
|
||||
func (sf *ScreenFingerprint) Generate() string {
|
||||
var fingerprint strings.Builder
|
||||
|
||||
// 屏幕分辨率
|
||||
fingerprint.WriteString(fmt.Sprintf("RESOLUTION:%dx%d|", sf.width, sf.height))
|
||||
|
||||
// 颜色深度
|
||||
fingerprint.WriteString(fmt.Sprintf("COLOR_DEPTH:%d|", sf.colorDepth))
|
||||
fingerprint.WriteString(fmt.Sprintf("PIXEL_DEPTH:%d|", sf.pixelDepth))
|
||||
|
||||
// 可用区域
|
||||
fingerprint.WriteString(fmt.Sprintf("AVAIL:%dx%d|", sf.availWidth, sf.availHeight))
|
||||
|
||||
// 方向
|
||||
fingerprint.WriteString(fmt.Sprintf("ORIENTATION:%s|", sf.orientation))
|
||||
|
||||
// 设备缩放
|
||||
fingerprint.WriteString(fmt.Sprintf("DEVICE_SCALE:%.2f|", sf.deviceScale))
|
||||
|
||||
// 色域
|
||||
fingerprint.WriteString(fmt.Sprintf("COLOR_GAMUT:%s|", sf.colorGamut))
|
||||
|
||||
// 强制颜色
|
||||
if sf.forcedColor {
|
||||
fingerprint.WriteString("FORCED_COLOR:1|")
|
||||
} else {
|
||||
fingerprint.WriteString("FORCED_COLOR:0|")
|
||||
}
|
||||
|
||||
// 对比度
|
||||
fingerprint.WriteString(fmt.Sprintf("CONTRAST:%s|", sf.contrast))
|
||||
|
||||
// 颜色反转
|
||||
if sf.inverted {
|
||||
fingerprint.WriteString("INVERTED:1|")
|
||||
} else {
|
||||
fingerprint.WriteString("INVERTED:0|")
|
||||
}
|
||||
|
||||
// 计算屏幕像素密度
|
||||
dpi := sf.calculateDPI()
|
||||
fingerprint.WriteString(fmt.Sprintf("DPI:%d|", dpi))
|
||||
|
||||
// 计算屏幕对角线
|
||||
diagonal := sf.calculateDiagonal()
|
||||
fingerprint.WriteString(fmt.Sprintf("DIAGONAL:%.1f", diagonal))
|
||||
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(fingerprint.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// calculateDPI 计算DPI
|
||||
func (sf *ScreenFingerprint) calculateDPI() int {
|
||||
// 假设标准DPI值
|
||||
dpiValues := []int{72, 96, 120, 144, 168, 192, 216, 240}
|
||||
return dpiValues[rand.Intn(len(dpiValues))]
|
||||
}
|
||||
|
||||
// calculateDiagonal 计算屏幕对角线英寸数
|
||||
func (sf *ScreenFingerprint) calculateDiagonal() float64 {
|
||||
// 基于分辨率计算对角线
|
||||
widthInches := float64(sf.width) / float64(sf.calculateDPI())
|
||||
heightInches := float64(sf.height) / float64(sf.calculateDPI())
|
||||
return widthInches*widthInches + heightInches*heightInches
|
||||
}
|
||||
|
||||
// GenerateRandomScreenFingerprint 生成随机屏幕指纹
|
||||
func GenerateRandomScreenFingerprint() string {
|
||||
|
||||
sf := NewScreenFingerprint()
|
||||
|
||||
// 常见分辨率
|
||||
resolutions := []struct {
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{1920, 1080}, // Full HD
|
||||
{1366, 768}, // 常见笔记本
|
||||
{1536, 864}, // 常见笔记本
|
||||
{1440, 900}, // MacBook
|
||||
{2560, 1440}, // 2K
|
||||
{3840, 2160}, // 4K
|
||||
{1280, 720}, // HD
|
||||
{1600, 900}, // HD+
|
||||
{2560, 1080}, // 超宽
|
||||
{3440, 1440}, // 超宽2K
|
||||
}
|
||||
|
||||
res := resolutions[rand.Intn(len(resolutions))]
|
||||
sf.width = res.width
|
||||
sf.height = res.height
|
||||
|
||||
// 设置可用区域(减去任务栏等)
|
||||
sf.availWidth = sf.width
|
||||
sf.availHeight = sf.height - rand.Intn(80) // 减去0-80像素
|
||||
|
||||
// 随机颜色深度
|
||||
sf.colorDepth = 24
|
||||
sf.pixelDepth = 24
|
||||
|
||||
// 随机设备缩放
|
||||
scales := []float64{1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0}
|
||||
sf.deviceScale = scales[rand.Intn(len(scales))]
|
||||
|
||||
// 随机色域
|
||||
gamuts := []string{"srgb", "p3", "rec2020"}
|
||||
sf.colorGamut = gamuts[rand.Intn(len(gamuts))]
|
||||
|
||||
// 随机对比度
|
||||
contrasts := []string{"no-preference", "high", "low", "custom"}
|
||||
sf.contrast = contrasts[rand.Intn(len(contrasts))]
|
||||
|
||||
// 随机强制颜色(很少启用)
|
||||
sf.forcedColor = rand.Intn(100) < 5 // 5%概率
|
||||
|
||||
// 随机颜色反转
|
||||
sf.inverted = rand.Intn(100) < 3 // 3%概率
|
||||
|
||||
return sf.Generate()
|
||||
}
|
||||
199
internal/utils/fingerprint/timezone.go
Normal file
199
internal/utils/fingerprint/timezone.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TimezoneFingerprint 时区指纹生成器
|
||||
type TimezoneFingerprint struct {
|
||||
timezone string
|
||||
offset int
|
||||
dst bool
|
||||
dstOffset int
|
||||
timezoneName string
|
||||
locale string
|
||||
calendar string
|
||||
dateFormat string
|
||||
timeFormat string
|
||||
weekStart int
|
||||
firstDayWeek int
|
||||
}
|
||||
|
||||
// NewTimezoneFingerprint 创建新的时区指纹生成器
|
||||
func NewTimezoneFingerprint() *TimezoneFingerprint {
|
||||
return &TimezoneFingerprint{
|
||||
timezone: "UTC",
|
||||
offset: 0,
|
||||
dst: false,
|
||||
dstOffset: 0,
|
||||
timezoneName: "Coordinated Universal Time",
|
||||
locale: "en-US",
|
||||
calendar: "gregorian",
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
timeFormat: "h:mm:ss a",
|
||||
weekStart: 0, // Sunday
|
||||
firstDayWeek: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成时区指纹
|
||||
func (tf *TimezoneFingerprint) Generate() string {
|
||||
var fingerprint strings.Builder
|
||||
|
||||
// 时区信息
|
||||
fingerprint.WriteString(fmt.Sprintf("TIMEZONE:%s|", tf.timezone))
|
||||
fingerprint.WriteString(fmt.Sprintf("OFFSET:%d|", tf.offset))
|
||||
|
||||
// 夏令时信息
|
||||
if tf.dst {
|
||||
fingerprint.WriteString("DST:1|")
|
||||
fingerprint.WriteString(fmt.Sprintf("DST_OFFSET:%d|", tf.dstOffset))
|
||||
} else {
|
||||
fingerprint.WriteString("DST:0|")
|
||||
}
|
||||
|
||||
// 时区名称
|
||||
fingerprint.WriteString(fmt.Sprintf("TZ_NAME:%s|", tf.timezoneName))
|
||||
|
||||
// 区域设置
|
||||
fingerprint.WriteString(fmt.Sprintf("LOCALE:%s|", tf.locale))
|
||||
|
||||
// 日历类型
|
||||
fingerprint.WriteString(fmt.Sprintf("CALENDAR:%s|", tf.calendar))
|
||||
|
||||
// 日期时间格式
|
||||
fingerprint.WriteString(fmt.Sprintf("DATE_FORMAT:%s|", tf.dateFormat))
|
||||
fingerprint.WriteString(fmt.Sprintf("TIME_FORMAT:%s|", tf.timeFormat))
|
||||
|
||||
// 周起始日
|
||||
fingerprint.WriteString(fmt.Sprintf("WEEK_START:%d|", tf.weekStart))
|
||||
fingerprint.WriteString(fmt.Sprintf("FIRST_DAY_WEEK:%d", tf.firstDayWeek))
|
||||
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(fingerprint.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// GenerateRandomTimezoneFingerprint 生成随机时区指纹
|
||||
func GenerateRandomTimezoneFingerprint() string {
|
||||
|
||||
tf := NewTimezoneFingerprint()
|
||||
|
||||
// 常见时区列表
|
||||
timezones := []struct {
|
||||
name string
|
||||
offset int
|
||||
dst bool
|
||||
tzName string
|
||||
locales []string
|
||||
}{
|
||||
{"America/New_York", -300, true, "Eastern Standard Time", []string{"en-US", "en-CA"}},
|
||||
{"America/Chicago", -360, true, "Central Standard Time", []string{"en-US", "en-CA"}},
|
||||
{"America/Denver", -420, true, "Mountain Standard Time", []string{"en-US"}},
|
||||
{"America/Los_Angeles", -480, true, "Pacific Standard Time", []string{"en-US", "en-CA"}},
|
||||
{"America/Toronto", -300, true, "Eastern Standard Time", []string{"en-CA", "fr-CA"}},
|
||||
{"America/Vancouver", -480, true, "Pacific Standard Time", []string{"en-CA", "fr-CA"}},
|
||||
{"America/Mexico_City", -360, true, "Central Standard Time", []string{"es-MX"}},
|
||||
{"America/Sao_Paulo", -180, true, "Brasilia Standard Time", []string{"pt-BR"}},
|
||||
{"Europe/London", 0, true, "Greenwich Mean Time", []string{"en-GB", "en-US", "fr-FR", "de-DE"}},
|
||||
{"Europe/Paris", 60, true, "Central European Time", []string{"fr-FR", "de-DE", "it-IT", "es-ES"}},
|
||||
{"Europe/Berlin", 60, true, "Central European Time", []string{"de-DE", "at-AT", "ch-CH"}},
|
||||
{"Europe/Moscow", 180, false, "Moscow Standard Time", []string{"ru-RU"}},
|
||||
{"Europe/Madrid", 60, true, "Central European Time", []string{"es-ES", "pt-PT"}},
|
||||
{"Europe/Rome", 60, true, "Central European Time", []string{"it-IT"}},
|
||||
{"Asia/Shanghai", 480, false, "China Standard Time", []string{"zh-CN"}},
|
||||
{"Asia/Tokyo", 540, false, "Japan Standard Time", []string{"ja-JP"}},
|
||||
{"Asia/Seoul", 540, false, "Korea Standard Time", []string{"ko-KR"}},
|
||||
{"Asia/Hong_Kong", 480, false, "Hong Kong Standard Time", []string{"zh-HK", "zh-CN"}},
|
||||
{"Asia/Singapore", 480, false, "Singapore Standard Time", []string{"en-SG", "zh-SG"}},
|
||||
{"Asia/Dubai", 240, false, "Gulf Standard Time", []string{"ar-AE", "en-AE"}},
|
||||
{"Asia/Kolkata", 330, false, "India Standard Time", []string{"hi-IN", "en-IN"}},
|
||||
{"Australia/Sydney", 600, true, "Australian Eastern Standard Time", []string{"en-AU"}},
|
||||
{"Pacific/Auckland", 720, true, "New Zealand Standard Time", []string{"en-NZ", "mi-NZ"}},
|
||||
{"UTC", 0, false, "Coordinated Universal Time", []string{"en-US", "en-GB", "fr-FR", "de-DE"}},
|
||||
}
|
||||
|
||||
// 随机选择时区
|
||||
tz := timezones[rand.Intn(len(timezones))]
|
||||
tf.timezone = tz.name
|
||||
tf.offset = tz.offset
|
||||
tf.dst = tz.dst
|
||||
tf.timezoneName = tz.tzName
|
||||
|
||||
// 如果有夏令时,设置夏令时偏移
|
||||
if tf.dst {
|
||||
// 夏令时通常比标准时间多1小时
|
||||
tf.dstOffset = tf.offset + 60
|
||||
}
|
||||
|
||||
// 随机选择区域设置
|
||||
tf.locale = tz.locales[rand.Intn(len(tz.locales))]
|
||||
|
||||
// 根据区域设置设置格式
|
||||
tf.setRegionalFormats()
|
||||
|
||||
// 随机周起始日
|
||||
weekStarts := []int{0, 1, 6} // Sunday, Monday, Saturday
|
||||
tf.weekStart = weekStarts[rand.Intn(len(weekStarts))]
|
||||
|
||||
// 随机每年第一周
|
||||
tf.firstDayWeek = rand.Intn(7)
|
||||
|
||||
return tf.Generate()
|
||||
}
|
||||
|
||||
// setRegionalFormats 根据区域设置设置格式
|
||||
func (tf *TimezoneFingerprint) setRegionalFormats() {
|
||||
// 根据区域设置设置日期时间格式
|
||||
switch tf.locale {
|
||||
case "en-US":
|
||||
tf.dateFormat = "MM/dd/yyyy"
|
||||
tf.timeFormat = "h:mm:ss a"
|
||||
case "en-GB":
|
||||
tf.dateFormat = "dd/MM/yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "fr-FR", "de-DE", "it-IT", "es-ES":
|
||||
tf.dateFormat = "dd.MM.yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "zh-CN", "zh-HK", "zh-SG":
|
||||
tf.dateFormat = "yyyy/MM/dd"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "ja-JP":
|
||||
tf.dateFormat = "yyyy/MM/dd"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "ko-KR":
|
||||
tf.dateFormat = "yyyy.MM.dd"
|
||||
tf.timeFormat = "a h:mm:ss"
|
||||
case "ru-RU":
|
||||
tf.dateFormat = "dd.MM.yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "pt-BR", "pt-PT":
|
||||
tf.dateFormat = "dd/MM/yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "es-MX":
|
||||
tf.dateFormat = "dd/MM/yyyy"
|
||||
tf.timeFormat = "hh:mm:ss a"
|
||||
case "hi-IN", "en-IN":
|
||||
tf.dateFormat = "dd-MM-yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
case "ar-AE":
|
||||
tf.dateFormat = "dd/MM/yyyy"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
default:
|
||||
tf.dateFormat = "yyyy-MM-dd"
|
||||
tf.timeFormat = "HH:mm:ss"
|
||||
}
|
||||
|
||||
// 根据区域设置设置周起始日
|
||||
if strings.HasPrefix(tf.locale, "en-US") || strings.HasPrefix(tf.locale, "ja-JP") || strings.HasPrefix(tf.locale, "ko-KR") {
|
||||
tf.weekStart = 0 // Sunday
|
||||
} else if strings.HasPrefix(tf.locale, "fr-") || strings.HasPrefix(tf.locale, "de-") || strings.HasPrefix(tf.locale, "it-") || strings.HasPrefix(tf.locale, "es-") {
|
||||
tf.weekStart = 1 // Monday
|
||||
} else if strings.HasPrefix(tf.locale, "zh-") {
|
||||
tf.weekStart = 1 // Monday
|
||||
}
|
||||
}
|
||||
181
internal/utils/fingerprint/webgl.go
Normal file
181
internal/utils/fingerprint/webgl.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WebGlFingerprint WebGL指纹生成器
|
||||
type WebGlFingerprint struct {
|
||||
vendor string
|
||||
renderer string
|
||||
version string
|
||||
shadingLang string
|
||||
extensions []string
|
||||
parameters map[string]interface{}
|
||||
}
|
||||
|
||||
// NewWebGlFingerprint 创建新的WebGL指纹生成器
|
||||
func NewWebGlFingerprint() *WebGlFingerprint {
|
||||
return &WebGlFingerprint{
|
||||
vendor: "WebKit",
|
||||
renderer: "WebKit WebGL",
|
||||
version: "WebGL 1.0",
|
||||
shadingLang: "WebGL GLSL ES 1.0",
|
||||
extensions: []string{"ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_texture_filter_anisotropic"},
|
||||
parameters: map[string]interface{}{
|
||||
"VERSION": "WebGL 1.0",
|
||||
"SHADING_LANGUAGE_VERSION": "WebGL GLSL ES 1.0",
|
||||
"VENDOR": "WebKit",
|
||||
"RENDERER": "WebKit WebGL",
|
||||
"MAX_TEXTURE_SIZE": 4096,
|
||||
"MAX_VIEWPORT_DIMS": []int{4096, 4096},
|
||||
"MAX_RENDERBUFFER_SIZE": 4096,
|
||||
"ALIASED_LINE_WIDTH_RANGE": []float32{1, 1},
|
||||
"ALIASED_POINT_SIZE_RANGE": []float32{1, 1},
|
||||
"MAX_TEXTURE_IMAGE_UNITS": 16,
|
||||
"MAX_VERTEX_TEXTURE_IMAGE_UNITS": 0,
|
||||
"MAX_VARYING_VECTORS": 8,
|
||||
"MAX_VERTEX_ATTRIBS": 16,
|
||||
"MAX_VERTEX_UNIFORM_VECTORS": 256,
|
||||
"MAX_FRAGMENT_UNIFORM_VECTORS": 224,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成WebGL指纹
|
||||
func (wf *WebGlFingerprint) Generate() string {
|
||||
var fingerprint strings.Builder
|
||||
|
||||
// 添加基本信息
|
||||
fingerprint.WriteString(fmt.Sprintf("VENDOR:%s|", wf.vendor))
|
||||
fingerprint.WriteString(fmt.Sprintf("RENDERER:%s|", wf.renderer))
|
||||
fingerprint.WriteString(fmt.Sprintf("VERSION:%s|", wf.version))
|
||||
fingerprint.WriteString(fmt.Sprintf("SHADING_LANGUAGE_VERSION:%s|", wf.shadingLang))
|
||||
|
||||
// 添加扩展信息
|
||||
fingerprint.WriteString("EXTENSIONS:")
|
||||
for i, ext := range wf.extensions {
|
||||
if i > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(ext)
|
||||
}
|
||||
fingerprint.WriteString("|")
|
||||
|
||||
// 添加参数信息
|
||||
fingerprint.WriteString("PARAMETERS:")
|
||||
paramCount := 0
|
||||
for key, value := range wf.parameters {
|
||||
if paramCount > 0 {
|
||||
fingerprint.WriteString(",")
|
||||
}
|
||||
fingerprint.WriteString(fmt.Sprintf("%s=%v", key, value))
|
||||
paramCount++
|
||||
}
|
||||
|
||||
// 计算MD5哈希
|
||||
hash := md5.Sum([]byte(fingerprint.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// GenerateRandomWebGLFingerprint 生成随机WebGL指纹
|
||||
func GenerateRandomWebGLFingerprint() string {
|
||||
|
||||
wf := NewWebGlFingerprint()
|
||||
|
||||
// 随机vendor
|
||||
vendors := []string{
|
||||
"WebKit",
|
||||
"Google Inc.",
|
||||
"NVIDIA Corporation",
|
||||
"AMD",
|
||||
"Intel Inc.",
|
||||
"Microsoft",
|
||||
"Mesa/X.org",
|
||||
}
|
||||
wf.vendor = vendors[rand.Intn(len(vendors))]
|
||||
|
||||
// 随机renderer
|
||||
renderers := []string{
|
||||
"WebKit WebGL",
|
||||
"ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0)",
|
||||
"GeForce GTX 1660/PCIe/SSE2",
|
||||
"AMD Radeon RX 580",
|
||||
"Iris(R) Xe Graphics",
|
||||
"Software WebRenderer",
|
||||
}
|
||||
wf.renderer = renderers[rand.Intn(len(renderers))]
|
||||
|
||||
// 随机version
|
||||
versions := []string{
|
||||
"WebGL 1.0",
|
||||
"WebGL 2.0",
|
||||
"OpenGL ES 2.0",
|
||||
"OpenGL ES 3.0",
|
||||
}
|
||||
wf.version = versions[rand.Intn(len(versions))]
|
||||
|
||||
// 随机shading language version
|
||||
shadingVersions := []string{
|
||||
"WebGL GLSL ES 1.0",
|
||||
"WebGL GLSL ES 3.0",
|
||||
"OpenGL ES GLSL ES 1.0",
|
||||
"OpenGL ES GLSL ES 3.0",
|
||||
}
|
||||
wf.shadingLang = shadingVersions[rand.Intn(len(shadingVersions))]
|
||||
|
||||
// 随机扩展
|
||||
allExtensions := []string{
|
||||
"ANGLE_instanced_arrays",
|
||||
"EXT_blend_minmax",
|
||||
"EXT_texture_filter_anisotropic",
|
||||
"EXT_disjoint_timer_query",
|
||||
"EXT_frag_depth",
|
||||
"EXT_shader_texture_lod",
|
||||
"EXT_sRGB",
|
||||
"OES_element_index_uint",
|
||||
"OES_standard_derivatives",
|
||||
"OES_texture_float",
|
||||
"OES_texture_half_float",
|
||||
"OES_vertex_array_object",
|
||||
"WEBGL_compressed_texture_s3tc",
|
||||
"WEBGL_debug_renderer_info",
|
||||
"WEBGL_debug_shaders",
|
||||
"WEBGL_depth_texture",
|
||||
"WEBGL_lose_context",
|
||||
}
|
||||
|
||||
// 随机选择8-15个扩展
|
||||
numExtensions := 8 + rand.Intn(8)
|
||||
randExtensions := make([]string, 0, numExtensions)
|
||||
for i := 0; i < numExtensions && i < len(allExtensions); i++ {
|
||||
ext := allExtensions[rand.Intn(len(allExtensions))]
|
||||
// 避免重复
|
||||
duplicate := false
|
||||
for _, existing := range randExtensions {
|
||||
if existing == ext {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate {
|
||||
randExtensions = append(randExtensions, ext)
|
||||
}
|
||||
}
|
||||
wf.extensions = randExtensions
|
||||
|
||||
// 随机化一些参数
|
||||
wf.parameters["MAX_TEXTURE_SIZE"] = 2048 + rand.Intn(4096) // 2048-6144
|
||||
wf.parameters["MAX_VIEWPORT_DIMS"] = []int{wf.parameters["MAX_TEXTURE_SIZE"].(int), wf.parameters["MAX_TEXTURE_SIZE"].(int)}
|
||||
wf.parameters["MAX_RENDERBUFFER_SIZE"] = wf.parameters["MAX_TEXTURE_SIZE"].(int)
|
||||
|
||||
// 随机化纹理单元数量
|
||||
wf.parameters["MAX_TEXTURE_IMAGE_UNITS"] = 8 + rand.Intn(16) // 8-24
|
||||
wf.parameters["MAX_FRAGMENT_UNIFORM_VECTORS"] = 128 + rand.Intn(256) // 128-384
|
||||
|
||||
return wf.Generate()
|
||||
}
|
||||
371
internal/utils/useragent/useragent.go
Normal file
371
internal/utils/useragent/useragent.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlatformType 定义平台类型
|
||||
type PlatformType string
|
||||
|
||||
const (
|
||||
PlatformPC PlatformType = "pc"
|
||||
PlatformMac PlatformType = "mac"
|
||||
PlatformIPad PlatformType = "ipad"
|
||||
PlatformIPhone PlatformType = "iphone"
|
||||
PlatformAndroid PlatformType = "android"
|
||||
PlatformPhone PlatformType = "phone"
|
||||
PlatformAll PlatformType = "all"
|
||||
)
|
||||
|
||||
// BrowserType 定义浏览器类型
|
||||
type BrowserType string
|
||||
|
||||
const (
|
||||
BrowserChrome BrowserType = "chrome"
|
||||
BrowserFirefox BrowserType = "firefox"
|
||||
BrowserSafari BrowserType = "safari"
|
||||
BrowserEdge BrowserType = "edge"
|
||||
)
|
||||
|
||||
// BrowserVersion 浏览器版本信息
|
||||
type BrowserVersion struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
}
|
||||
|
||||
// UserAgentComponent User-Agent组件
|
||||
type UserAgentComponent struct {
|
||||
Platform PlatformType
|
||||
Browser BrowserType
|
||||
Version BrowserVersion
|
||||
OSVersion string
|
||||
Device string
|
||||
Engine string
|
||||
EngineVersion string
|
||||
}
|
||||
|
||||
// 浏览器版本范围 - 更新到最新版本
|
||||
var chromeVersions = []BrowserVersion{
|
||||
{140, 0, 7339}, {139, 0, 7278}, {138, 0, 7156}, {137, 0, 7012},
|
||||
{136, 0, 6842}, {135, 0, 6721}, {134, 0, 6523}, {133, 0, 6413},
|
||||
{132, 0, 6234}, {131, 0, 6128}, {130, 0, 5921}, {129, 0, 5845},
|
||||
{128, 0, 5734}, {127, 0, 5618}, {126, 0, 5523}, {125, 0, 5421},
|
||||
{124, 0, 5328}, {123, 0, 5214}, {122, 0, 5129}, {121, 0, 5018},
|
||||
{120, 0, 4912}, {119, 0, 4823}, {118, 0, 4734}, {117, 0, 4621},
|
||||
{116, 0, 4512}, {115, 0, 4423}, {114, 0, 4318}, {113, 0, 4217},
|
||||
{112, 0, 4123}, {111, 0, 4028}, {110, 0, 3921}, {109, 0, 3814},
|
||||
}
|
||||
|
||||
var firefoxVersions = []BrowserVersion{
|
||||
{140, 0, 0}, {139, 0, 0}, {138, 0, 0}, {137, 0, 0}, {136, 0, 0},
|
||||
{135, 0, 0}, {134, 0, 0}, {133, 0, 0}, {132, 0, 0}, {131, 0, 0},
|
||||
{130, 0, 0}, {129, 0, 0}, {128, 0, 0}, {127, 0, 0}, {126, 0, 0},
|
||||
{125, 0, 0}, {124, 0, 0}, {123, 0, 0}, {122, 0, 0}, {121, 0, 0},
|
||||
{120, 0, 0}, {119, 0, 0}, {118, 0, 0}, {117, 0, 0}, {116, 0, 0},
|
||||
{115, 0, 0}, {114, 0, 0}, {113, 0, 0}, {112, 0, 0}, {111, 0, 0},
|
||||
{110, 0, 0}, {109, 0, 0},
|
||||
}
|
||||
|
||||
var safariVersions = []BrowserVersion{
|
||||
{18, 2, 0}, {18, 1, 0}, {18, 0, 0}, {17, 6, 0}, {17, 5, 0},
|
||||
{17, 4, 0}, {17, 3, 0}, {17, 2, 0}, {17, 1, 0}, {17, 0, 0},
|
||||
{16, 6, 0}, {16, 5, 0}, {16, 4, 0}, {16, 3, 0}, {16, 2, 0},
|
||||
{16, 1, 0}, {16, 0, 0}, {15, 6, 0}, {15, 5, 0}, {15, 4, 0},
|
||||
}
|
||||
|
||||
var edgeVersions = []BrowserVersion{
|
||||
{140, 0, 7339}, {139, 0, 7278}, {138, 0, 7156}, {137, 0, 7012},
|
||||
{136, 0, 6842}, {135, 0, 6721}, {134, 0, 6523}, {133, 0, 6413},
|
||||
{132, 0, 6234}, {131, 0, 6128}, {130, 0, 5921}, {129, 0, 5845},
|
||||
{128, 0, 5734}, {127, 0, 5618}, {126, 0, 5523}, {125, 0, 5421},
|
||||
{124, 0, 5328}, {123, 0, 5214}, {122, 0, 5129}, {121, 0, 5018},
|
||||
{120, 0, 4912}, {119, 0, 4823}, {118, 0, 4734}, {117, 0, 4621},
|
||||
{116, 0, 4512}, {115, 0, 4423}, {114, 0, 4318}, {113, 0, 4217},
|
||||
}
|
||||
|
||||
// 平台和设备信息
|
||||
var windowsVersions = []string{
|
||||
"Windows NT 10.0", "Windows NT 11.0",
|
||||
}
|
||||
|
||||
var macVersions = []string{
|
||||
"Mac OS X 10_15_7", "Mac OS X 11_7_10", "Mac OS X 12_6_8", "Mac OS X 13_5_2",
|
||||
}
|
||||
|
||||
var iosVersions = []string{
|
||||
"17_4", "17_3", "17_2", "17_1", "17_0", "16_6", "16_5", "16_4", "16_3", "16_2",
|
||||
}
|
||||
|
||||
var androidVersions = []string{
|
||||
"14", "13", "12", "11", "10",
|
||||
}
|
||||
|
||||
var androidDevices = []string{
|
||||
"SM-G998B", "SM-G991B", "SM-G975F", "SM-S906N", "Pixel 7", "Pixel 6", "Pixel 5",
|
||||
"OnePlus 9", "OnePlus 8", "Xiaomi 12", "Xiaomi 11", "OPPO Find X5", "vivo X80",
|
||||
}
|
||||
|
||||
var iphoneDevices = []string{
|
||||
"iPhone15,3", "iPhone15,2", "iPhone14,7", "iPhone14,6", "iPhone14,5", "iPhone14,4",
|
||||
"iPhone14,3", "iPhone14,2", "iPhone13,4", "iPhone13,3", "iPhone13,2", "iPhone13,1",
|
||||
"iPhone12,8", "iPhone12,5", "iPhone12,3", "iPhone12,1", "iPhone11,8", "iPhone11,6",
|
||||
"iPhone11,4", "iPhone11,2", "iPhone10,6", "iPhone10,5", "iPhone10,4", "iPhone10,3",
|
||||
"iPhone10,2", "iPhone9,4", "iPhone9,3", "iPhone9,2", "iPhone9,1", "iPhone8,4",
|
||||
}
|
||||
|
||||
// Initialize random seed
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// GetRandomBrowserVersion 获取随机浏览器版本
|
||||
func GetRandomBrowserVersion(browser BrowserType) BrowserVersion {
|
||||
switch browser {
|
||||
case BrowserChrome:
|
||||
return chromeVersions[rand.Intn(len(chromeVersions))]
|
||||
case BrowserFirefox:
|
||||
return firefoxVersions[rand.Intn(len(firefoxVersions))]
|
||||
case BrowserSafari:
|
||||
return safariVersions[rand.Intn(len(safariVersions))]
|
||||
case BrowserEdge:
|
||||
return edgeVersions[rand.Intn(len(edgeVersions))]
|
||||
default:
|
||||
return chromeVersions[rand.Intn(len(chromeVersions))]
|
||||
}
|
||||
}
|
||||
|
||||
// FormatVersion 格式化版本号
|
||||
func FormatVersion(version BrowserVersion) string {
|
||||
if version.Patch > 0 {
|
||||
return fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d", version.Major, version.Minor)
|
||||
}
|
||||
|
||||
// GetRandomPlatformInfo 获取随机平台信息
|
||||
func GetRandomPlatformInfo(platform PlatformType) string {
|
||||
switch platform {
|
||||
case PlatformPC:
|
||||
return windowsVersions[rand.Intn(len(windowsVersions))]
|
||||
case PlatformMac:
|
||||
return macVersions[rand.Intn(len(macVersions))]
|
||||
case PlatformIPad:
|
||||
return iosVersions[rand.Intn(len(iosVersions))]
|
||||
case PlatformIPhone:
|
||||
return iosVersions[rand.Intn(len(iosVersions))]
|
||||
case PlatformAndroid:
|
||||
return androidVersions[rand.Intn(len(androidVersions))]
|
||||
default:
|
||||
return windowsVersions[0]
|
||||
}
|
||||
}
|
||||
|
||||
// GetRandomDevice 获取随机设备信息
|
||||
func GetRandomDevice(platform PlatformType) string {
|
||||
switch platform {
|
||||
case PlatformAndroid:
|
||||
return androidDevices[rand.Intn(len(androidDevices))]
|
||||
case PlatformIPhone:
|
||||
return iphoneDevices[rand.Intn(len(iphoneDevices))]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// BuildChromeUserAgent 构建Chrome User-Agent
|
||||
func BuildChromeUserAgent(component UserAgentComponent) string {
|
||||
switch component.Platform {
|
||||
case PlatformPC:
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
case PlatformMac:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
case PlatformAndroid:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36",
|
||||
component.OSVersion, component.Device, FormatVersion(component.Version))
|
||||
case PlatformIPhone:
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) CriOS/%s Mobile Safari/537.36",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
default:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Unknown) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36",
|
||||
FormatVersion(component.Version))
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFirefoxUserAgent 构建Firefox User-Agent
|
||||
func BuildFirefoxUserAgent(component UserAgentComponent) string {
|
||||
switch component.Platform {
|
||||
case PlatformPC:
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; Win64; x64; rv:%d.0) Gecko/20100101 Firefox/%d.0",
|
||||
component.OSVersion, component.Version.Major, component.Version.Major)
|
||||
case PlatformMac:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s; rv:%d.0) Gecko/20100101 Firefox/%d.0",
|
||||
component.OSVersion, component.Version.Major, component.Version.Major)
|
||||
case PlatformAndroid:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Android %s; Mobile; rv:%d.0) Gecko/%d.0 Firefox/%d.0",
|
||||
component.OSVersion, component.Version.Major, component.Version.Major, component.Version.Major)
|
||||
case PlatformIPhone:
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X; rv:%d.0) Gecko/%d.0 Firefox/%d.0",
|
||||
component.OSVersion, component.Version.Major, component.Version.Major, component.Version.Major)
|
||||
default:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Unknown; rv:%d.0) Gecko/20100101 Firefox/%d.0",
|
||||
component.Version.Major, component.Version.Major)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSafariUserAgent 构建Safari User-Agent
|
||||
func BuildSafariUserAgent(component UserAgentComponent) string {
|
||||
switch component.Platform {
|
||||
case PlatformMac:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Safari/605.1.15",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
case PlatformIPad:
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPad; CPU OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
case PlatformIPhone:
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1",
|
||||
component.OSVersion, FormatVersion(component.Version))
|
||||
default:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Unknown) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Safari/605.1.15",
|
||||
FormatVersion(component.Version))
|
||||
}
|
||||
}
|
||||
|
||||
// BuildEdgeUserAgent 构建Edge User-Agent
|
||||
func BuildEdgeUserAgent(component UserAgentComponent) string {
|
||||
switch component.Platform {
|
||||
case PlatformPC:
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36 Edg/%s",
|
||||
component.OSVersion, FormatVersion(component.Version), FormatVersion(component.Version))
|
||||
case PlatformMac:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36 Edg/%s",
|
||||
component.OSVersion, FormatVersion(component.Version), FormatVersion(component.Version))
|
||||
default:
|
||||
return fmt.Sprintf("Mozilla/5.0 (Unknown) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36 Edg/%s",
|
||||
FormatVersion(component.Version), FormatVersion(component.Version))
|
||||
}
|
||||
}
|
||||
|
||||
// BuildUserAgent 构建User-Agent字符串
|
||||
func BuildUserAgent(component UserAgentComponent) string {
|
||||
switch component.Browser {
|
||||
case BrowserChrome:
|
||||
return BuildChromeUserAgent(component)
|
||||
case BrowserFirefox:
|
||||
return BuildFirefoxUserAgent(component)
|
||||
case BrowserSafari:
|
||||
return BuildSafariUserAgent(component)
|
||||
case BrowserEdge:
|
||||
return BuildEdgeUserAgent(component)
|
||||
default:
|
||||
return BuildChromeUserAgent(component)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRandomBrowser 获取随机浏览器类型
|
||||
func GetRandomBrowser() BrowserType {
|
||||
browsers := []BrowserType{BrowserChrome, BrowserFirefox, BrowserSafari, BrowserEdge}
|
||||
return browsers[rand.Intn(len(browsers))]
|
||||
}
|
||||
|
||||
// GetRandomUserAgent 获取指定平台的随机User-Agent
|
||||
func GetRandomUserAgent(platform PlatformType) string {
|
||||
component := UserAgentComponent{
|
||||
Platform: platform,
|
||||
Browser: GetRandomBrowser(),
|
||||
OSVersion: GetRandomPlatformInfo(platform),
|
||||
Device: GetRandomDevice(platform),
|
||||
}
|
||||
|
||||
component.Version = GetRandomBrowserVersion(component.Browser)
|
||||
|
||||
return BuildUserAgent(component)
|
||||
}
|
||||
|
||||
// GetRandomUserAgentFromPlatforms 从指定平台列表中获取随机User-Agent
|
||||
func GetRandomUserAgentFromPlatforms(platforms []PlatformType) string {
|
||||
if len(platforms) == 0 {
|
||||
return GetRandomUserAgent(PlatformPC)
|
||||
}
|
||||
|
||||
selectedPlatform := platforms[rand.Intn(len(platforms))]
|
||||
return GetRandomUserAgent(selectedPlatform)
|
||||
}
|
||||
|
||||
// GetAllRandomUserAgent 获取所有平台的随机User-Agent(按权重分配)
|
||||
func GetAllRandomUserAgent() string {
|
||||
platforms := []PlatformType{PlatformPC, PlatformMac, PlatformIPad, PlatformIPhone, PlatformAndroid}
|
||||
|
||||
// 权重分配:PC 35%, Mac 25%, iPad 10%, iPhone 10%, Android 20%
|
||||
weights := []int{35, 25, 10, 10, 20}
|
||||
|
||||
totalWeight := 0
|
||||
for _, weight := range weights {
|
||||
totalWeight += weight
|
||||
}
|
||||
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
cumulativeWeight := 0
|
||||
|
||||
for i, weight := range weights {
|
||||
cumulativeWeight += weight
|
||||
if randomWeight < cumulativeWeight {
|
||||
return GetRandomUserAgent(platforms[i])
|
||||
}
|
||||
}
|
||||
|
||||
return GetRandomUserAgent(PlatformPC)
|
||||
}
|
||||
|
||||
// GetUserAgentByPlatform 根据平台枚举获取User-Agent
|
||||
func GetUserAgentByPlatform(platform PlatformType) string {
|
||||
switch platform {
|
||||
case PlatformPC:
|
||||
return GetRandomUserAgent(PlatformPC)
|
||||
case PlatformMac:
|
||||
return GetRandomUserAgent(PlatformMac)
|
||||
case PlatformIPad:
|
||||
return GetRandomUserAgent(PlatformIPad)
|
||||
case PlatformAndroid:
|
||||
return GetRandomUserAgent(PlatformAndroid)
|
||||
case PlatformPhone:
|
||||
if rand.Int() > 50 {
|
||||
return GetRandomUserAgent(PlatformIPhone)
|
||||
} else {
|
||||
return GetRandomUserAgent(PlatformAndroid)
|
||||
}
|
||||
default:
|
||||
return GetAllRandomUserAgent()
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAgentByBrowser 根据浏览器类型获取随机User-Agent
|
||||
func GetUserAgentByBrowser(browser BrowserType) string {
|
||||
platforms := []PlatformType{PlatformPC, PlatformMac, PlatformIPad, PlatformIPhone, PlatformAndroid}
|
||||
|
||||
// 根据浏览器类型排除不兼容的平台
|
||||
var compatiblePlatforms []PlatformType
|
||||
switch browser {
|
||||
case BrowserSafari:
|
||||
compatiblePlatforms = []PlatformType{PlatformMac, PlatformIPad, PlatformIPhone}
|
||||
case BrowserEdge:
|
||||
compatiblePlatforms = []PlatformType{PlatformPC, PlatformMac}
|
||||
default:
|
||||
compatiblePlatforms = platforms
|
||||
}
|
||||
|
||||
selectedPlatform := compatiblePlatforms[rand.Intn(len(compatiblePlatforms))]
|
||||
component := UserAgentComponent{
|
||||
Platform: selectedPlatform,
|
||||
Browser: browser,
|
||||
OSVersion: GetRandomPlatformInfo(selectedPlatform),
|
||||
Device: GetRandomDevice(selectedPlatform),
|
||||
Version: GetRandomBrowserVersion(browser),
|
||||
}
|
||||
|
||||
return BuildUserAgent(component)
|
||||
}
|
||||
Reference in New Issue
Block a user