init
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
/user-data/
|
||||
/log/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
config.yaml
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
134
api/task.go
Normal file
134
api/task.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-account-register/libs"
|
||||
"go-account-register/task"
|
||||
paramsTypes "go-account-register/types"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
}
|
||||
|
||||
type CreateRequestParams struct {
|
||||
ID string `form:"id" json:"id" uri:"id" xml:"id"`
|
||||
Name string `form:"name" json:"name" uri:"name" xml:"name"`
|
||||
RunStatus int `form:"runStatus" json:"runStatus" uri:"runStatus" xml:"runStatus" binding:"required"`
|
||||
Type int `form:"type" json:"type" uri:"type" xml:"type" binding:"required"`
|
||||
AccountClassify string `form:"accountClassify" json:"accountClassify" uri:"accountClassify" xml:"accountClassify" binding:"required"`
|
||||
ClassifyId []int `form:"classifyId" json:"classifyId" uri:"classifyId" xml:"classifyId"`
|
||||
Keyword string `form:"keyword" json:"keyword" uri:"keyword" xml:"keyword"`
|
||||
TwitterId string `form:"twitterId" json:"twitterId" uri:"twitterId" xml:"twitterId"`
|
||||
Compensate int `form:"compensate" json:"compensate" uri:"compensate" xml:"compensate"`
|
||||
}
|
||||
|
||||
type StartRequestParams struct {
|
||||
TaskId string `form:"taskId" json:"taskId" uri:"taskId" xml:"taskId" binding:"required"`
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
func (*Task) Create(ctx *gin.Context) {
|
||||
// 参数
|
||||
var res *libs.ErrorInfo
|
||||
var paramsJson CreateRequestParams
|
||||
if err := ctx.ShouldBindJSON(¶msJson); err != nil {
|
||||
res = libs.ErrorCode["ParamsError"]
|
||||
fmt.Println(err)
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": res.Code,
|
||||
"msg": res.Msg,
|
||||
"data": res.Data,
|
||||
})
|
||||
return
|
||||
}
|
||||
var scheduler = task.GetScheduler()
|
||||
var status string
|
||||
if paramsJson.RunStatus == 0 {
|
||||
status = "stop"
|
||||
} else {
|
||||
status = "pending"
|
||||
}
|
||||
taskId := scheduler.AddTask(¶msTypes.TaskParams{
|
||||
ID: paramsJson.ID,
|
||||
Name: paramsJson.Name,
|
||||
Status: status,
|
||||
Type: paramsJson.Type,
|
||||
AccountClassify: paramsJson.AccountClassify,
|
||||
DataClassify: paramsJson.ClassifyId,
|
||||
Keyword: paramsJson.Keyword,
|
||||
TwitterId: paramsJson.TwitterId,
|
||||
Compensate: paramsJson.Compensate,
|
||||
})
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": taskId,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 开始任务
|
||||
func (*Task) Start(ctx *gin.Context) {
|
||||
// 参数
|
||||
var res *libs.ErrorInfo
|
||||
var paramsJson StartRequestParams
|
||||
if err := ctx.ShouldBindJSON(¶msJson); err != nil {
|
||||
res = libs.ErrorCode["ParamsError"]
|
||||
fmt.Println(err)
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": res.Code,
|
||||
"msg": res.Msg,
|
||||
"data": res.Data,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
func (*Task) Stop(ctx *gin.Context) {
|
||||
// 参数
|
||||
var res *libs.ErrorInfo
|
||||
var paramsJson StartRequestParams
|
||||
if err := ctx.ShouldBindJSON(¶msJson); err != nil {
|
||||
res = libs.ErrorCode["ParamsError"]
|
||||
fmt.Println(err)
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": res.Code,
|
||||
"msg": res.Msg,
|
||||
"data": res.Data,
|
||||
})
|
||||
return
|
||||
}
|
||||
var scheduler = task.GetScheduler()
|
||||
scheduler.StopTask(paramsJson.TaskId)
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": "",
|
||||
})
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
func (*Task) Delete(ctx *gin.Context) {
|
||||
// 参数
|
||||
var res *libs.ErrorInfo
|
||||
var paramsJson StartRequestParams
|
||||
if err := ctx.ShouldBindJSON(¶msJson); err != nil {
|
||||
res = libs.ErrorCode["ParamsError"]
|
||||
fmt.Println(err)
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": res.Code,
|
||||
"msg": res.Msg,
|
||||
"data": res.Data,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务信息
|
||||
func (*Task) GetTaskInfo(ctx *gin.Context) {
|
||||
|
||||
}
|
||||
43
cmd/main.go
Normal file
43
cmd/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-account-register/config"
|
||||
"go-account-register/router"
|
||||
"go-account-register/utils"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
appConfig, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("读取配置失败")
|
||||
}
|
||||
app := gin.Default()
|
||||
// 注册路由
|
||||
router.Init(app)
|
||||
utils.Connect()
|
||||
go handleShutdownSignals()
|
||||
|
||||
app.Run(":" + strconv.Itoa(appConfig.Port))
|
||||
}
|
||||
|
||||
// 处理关闭信号的函数
|
||||
func handleShutdownSignals() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
// 阻塞等待信号
|
||||
sig := <-sigChan
|
||||
log.Printf("接收到信号: %v, 关闭所有浏览器...", sig)
|
||||
|
||||
// 强制退出程序
|
||||
os.Exit(0)
|
||||
}
|
||||
57
config/index.go
Normal file
57
config/index.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Database string `yaml:"database"`
|
||||
}
|
||||
|
||||
type Limit struct {
|
||||
CommentNumMin int `yaml:"commentNumMin"`
|
||||
CommentNumMax int `yaml:"commentNumMax"`
|
||||
LikeNumMin int `yaml:"likeNumMin"`
|
||||
LikeNumMax int `yaml:"likeNumMax"`
|
||||
FollowNumMin int `yaml:"followNumMin"`
|
||||
FollowNumMax int `yaml:"followNumMax"`
|
||||
ViweTimeMax int `yaml:"viweTimeMax"`
|
||||
ViweTimeMin int `yaml:"viweTimeMin"`
|
||||
BrowserRunMax int `yaml:"browserRunMax"`
|
||||
}
|
||||
|
||||
type Clash struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Secret string `yaml:"secret"`
|
||||
ExePath string `yaml:"exePath"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Limit Limit `yaml:"limit"`
|
||||
Clash Clash `yaml:"clash"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*AppConfig, error) {
|
||||
// 读取文件内容
|
||||
data, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析 YAML
|
||||
var cfg AppConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
BIN
fonts/AlibabaPuHuiTi-3-115-Black.ttf
Normal file
BIN
fonts/AlibabaPuHuiTi-3-115-Black.ttf
Normal file
Binary file not shown.
57
go.mod
Normal file
57
go.mod
Normal file
@@ -0,0 +1,57 @@
|
||||
module go-account-register
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/go-rod/stealth v0.4.9
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
golang.org/x/image v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
)
|
||||
145
go.sum
Normal file
145
go.sum
Normal file
@@ -0,0 +1,145 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4=
|
||||
github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
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/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM=
|
||||
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||
github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
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/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
65
libs/browser-fingerprint/canvas.go
Normal file
65
libs/browser-fingerprint/canvas.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package browserfingerprint
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GetChangeCanvasJavaScript(rgba [4]int) string {
|
||||
return fmt.Sprintf(`
|
||||
const getImageData = CanvasRenderingContext2D.prototype.getImageData;
|
||||
|
||||
const noisify = (canvas, context) => {
|
||||
if (context) {
|
||||
const shift = {
|
||||
r: %d,
|
||||
g: %d,
|
||||
b: %d,
|
||||
a: %d,
|
||||
};
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
if (width && height) {
|
||||
const imageData = getImageData.apply(context, [0, 0, width, height]);
|
||||
|
||||
for (let i = 0; i < height; i++)
|
||||
for (let j = 0; j < width; j++) {
|
||||
const n = i * (width * 4) + j * 4;
|
||||
imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
|
||||
imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
|
||||
imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
|
||||
imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
|
||||
}
|
||||
|
||||
|
||||
context.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
|
||||
apply(target, self, args) {
|
||||
noisify(self, self.getContext('2d'));
|
||||
|
||||
return Reflect.apply(target, self, args);
|
||||
},
|
||||
});
|
||||
|
||||
HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
|
||||
apply(target, self, args) {
|
||||
noisify(self, self.getContext('2d'));
|
||||
|
||||
return Reflect.apply(target, self, args);
|
||||
},
|
||||
});
|
||||
|
||||
CanvasRenderingContext2D.prototype.getImageData = new Proxy(
|
||||
CanvasRenderingContext2D.prototype.getImageData,
|
||||
{
|
||||
apply(target, self, args) {
|
||||
noisify(self.canvas, self);
|
||||
|
||||
return Reflect.apply(target, self, args);
|
||||
},
|
||||
}
|
||||
);
|
||||
`, rgba[0], rgba[1], rgba[2], rgba[3])
|
||||
}
|
||||
42
libs/browser-fingerprint/index.go
Normal file
42
libs/browser-fingerprint/index.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package browserfingerprint
|
||||
|
||||
import (
|
||||
paramsTypes "go-account-register/types"
|
||||
"math/rand"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
type BrowserFingerprint struct {
|
||||
}
|
||||
|
||||
func (b *BrowserFingerprint) SetBrowserFingerprint(page *rod.Page, params *paramsTypes.BrowserFingerprintParams) *rod.Page {
|
||||
var p *rod.Page = page
|
||||
if params.Canvas {
|
||||
p = setCanvas(p)
|
||||
}
|
||||
if params.TimeZone != "" {
|
||||
p = setTimezoneAndLangAndGeo(p, params)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func setCanvas(page *rod.Page) *rod.Page {
|
||||
// 注入噪声脚本
|
||||
// 生成随机 RGBA 偏移值 (Go 端控制)
|
||||
rgba := [4]int{
|
||||
rand.Intn(10) - 5, // R: -5 ~ +4
|
||||
rand.Intn(10) - 5, // G
|
||||
rand.Intn(10) - 5, // B
|
||||
rand.Intn(10) - 5, // A
|
||||
}
|
||||
// 使用更隐蔽的 Canvas 指纹修改方法
|
||||
page.MustEvalOnNewDocument(GetChangeCanvasJavaScript(rgba))
|
||||
return page
|
||||
}
|
||||
|
||||
func setTimezoneAndLangAndGeo(page *rod.Page, params *paramsTypes.BrowserFingerprintParams) *rod.Page {
|
||||
page.MustEvalOnNewDocument(GetChangeTimezoneJavaScript(params.TimeZone, params.Language, params.GeoLocation))
|
||||
return page
|
||||
}
|
||||
98
libs/browser-fingerprint/timezone.go
Normal file
98
libs/browser-fingerprint/timezone.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package browserfingerprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
func GetChangeTimezoneJavaScript(timezone string, lang string, geo string) string {
|
||||
return fmt.Sprintf(`
|
||||
(() => {
|
||||
// 设置全局时区配置
|
||||
window.__rodTZConfig = {
|
||||
timeZone: '%s',
|
||||
language: '%s',
|
||||
geo: '%s'
|
||||
};
|
||||
|
||||
// 覆盖 Date 对象
|
||||
const OriginalDate = window.Date;
|
||||
window.Date = function(...args) {
|
||||
if (new.target) {
|
||||
// 作为构造函数
|
||||
if (args.length === 0) {
|
||||
// 无参数调用时应用时区偏移
|
||||
const now = new OriginalDate();
|
||||
const targetOffset = -480; // %s 的 UTC 偏移(分钟)
|
||||
return new OriginalDate(now.getTime() + (targetOffset - now.getTimezoneOffset()) * 60000);
|
||||
}
|
||||
return new OriginalDate(...args);
|
||||
}
|
||||
// 作为函数调用
|
||||
return OriginalDate();
|
||||
};
|
||||
|
||||
// 复制静态方法和属性
|
||||
Object.defineProperties(Date, Object.getOwnPropertyDescriptors(OriginalDate));
|
||||
|
||||
// 覆盖 Date.now()
|
||||
const origDateNow = Date.now;
|
||||
Date.now = function() {
|
||||
return origDateNow() + (%d * 60000); // 添加时区偏移
|
||||
};
|
||||
|
||||
// 覆盖 getTimezoneOffset
|
||||
const origGetTimezoneOffset = Date.prototype.getTimezoneOffset;
|
||||
Date.prototype.getTimezoneOffset = function() {
|
||||
return %d; // %s 的 UTC 偏移(分钟)
|
||||
};
|
||||
|
||||
// 覆盖 Intl.DateTimeFormat
|
||||
const origDateTimeFormat = Intl.DateTimeFormat;
|
||||
Intl.DateTimeFormat = function(locales, options) {
|
||||
options = options || {};
|
||||
options.timeZone = options.timeZone || window.__rodTZConfig.timeZone;
|
||||
return new origDateTimeFormat(locales, options);
|
||||
};
|
||||
|
||||
// 覆盖 navigator 属性
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
get: () => window.__rodTZConfig.language,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => [window.__rodTZConfig.language],
|
||||
configurable: false
|
||||
});
|
||||
|
||||
// 覆盖控制台时区
|
||||
if (console._timeZone) {
|
||||
console._timeZone = window.__rodTZConfig.timeZone;
|
||||
}
|
||||
})();
|
||||
`,
|
||||
timezone,
|
||||
lang,
|
||||
geo,
|
||||
timezone, // 用于注释
|
||||
getUTCOffsetMinutes(timezone), // 时区偏移分钟数
|
||||
getUTCOffsetMinutes(timezone), // 时区偏移分钟数
|
||||
timezone)
|
||||
}
|
||||
|
||||
// getUTCOffsetMinutes 获取时区的UTC偏移分钟数
|
||||
func getUTCOffsetMinutes(timeZone string) int {
|
||||
// 使用时区名称获取实际偏移
|
||||
loc, err := time.LoadLocation(timeZone)
|
||||
if err != nil {
|
||||
log.Printf("无法加载时区 %s: %v, 使用默认值 -480", timeZone, err)
|
||||
return -480 // 默认太平洋时间
|
||||
}
|
||||
|
||||
// 获取当前时间的偏移
|
||||
_, offset := time.Now().In(loc).Zone()
|
||||
return -offset / 60 // 转换为分钟并取反
|
||||
}
|
||||
95
libs/browser.go
Normal file
95
libs/browser.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package libs
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
type Browser struct{}
|
||||
|
||||
type BrowserInfo struct {
|
||||
Id int
|
||||
Browser *rod.Browser
|
||||
Url string
|
||||
Page *rod.Page
|
||||
}
|
||||
|
||||
// 浏览器指纹数据
|
||||
type Fingerprint struct {
|
||||
Viewport []struct {
|
||||
Height int
|
||||
Width int
|
||||
}
|
||||
Proxy string
|
||||
Lang string
|
||||
}
|
||||
|
||||
var (
|
||||
BrowserPool = make(map[int]*BrowserInfo) // 初始化map
|
||||
poolMutex sync.RWMutex // 添加读写锁保证并发安全
|
||||
)
|
||||
|
||||
var ChromeExample Chrome
|
||||
|
||||
func (*Browser) GetBrowser(id int, params *Fingerprint) *BrowserInfo {
|
||||
poolMutex.RLock()
|
||||
value, ok := BrowserPool[id]
|
||||
poolMutex.RUnlock()
|
||||
|
||||
if ok {
|
||||
return value
|
||||
}
|
||||
|
||||
// 未找到时获取写锁(互斥)
|
||||
poolMutex.Lock()
|
||||
defer poolMutex.Unlock()
|
||||
|
||||
// 双检查:避免在等待锁期间其他goroutine已创建
|
||||
if value, ok := BrowserPool[id]; ok {
|
||||
return value
|
||||
}
|
||||
|
||||
// 创建新实例(补充CancelFunc)
|
||||
var ChromeExample Chrome
|
||||
browser, url, page := ChromeExample.Create(id, params)
|
||||
browserInfo := &BrowserInfo{
|
||||
Id: id,
|
||||
Browser: browser,
|
||||
Url: url,
|
||||
Page: page,
|
||||
}
|
||||
BrowserPool[id] = browserInfo
|
||||
return browserInfo
|
||||
}
|
||||
|
||||
func (*Browser) GetNewBrowser(params *Fingerprint) *BrowserInfo {
|
||||
// 创建新实例(补充CancelFunc)
|
||||
var ChromeExample Chrome
|
||||
browser, url, page := ChromeExample.TemporaryCreate(params)
|
||||
browserInfo := &BrowserInfo{
|
||||
Browser: browser,
|
||||
Url: url,
|
||||
Page: page,
|
||||
}
|
||||
return browserInfo
|
||||
}
|
||||
|
||||
func (*Browser) GetAllBrowser() map[int]*BrowserInfo {
|
||||
return BrowserPool
|
||||
}
|
||||
|
||||
func (*Browser) CancelAllBrowser() {
|
||||
for _, v := range BrowserPool {
|
||||
v.Browser.MustClose()
|
||||
}
|
||||
}
|
||||
|
||||
func (*Browser) CancelBrowser(id int) {
|
||||
browserInfo := BrowserPool[id]
|
||||
if browserInfo == nil {
|
||||
return
|
||||
}
|
||||
browserInfo.Browser.MustClose()
|
||||
delete(BrowserPool, id)
|
||||
}
|
||||
204
libs/chrome.go
Normal file
204
libs/chrome.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package libs
|
||||
|
||||
import (
|
||||
browserfingerprint "go-account-register/libs/browser-fingerprint"
|
||||
paramsTypes "go-account-register/types"
|
||||
"go-account-register/utils/external"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/go-rod/stealth"
|
||||
)
|
||||
|
||||
type Chrome struct{}
|
||||
|
||||
func (c *Chrome) Create(accountId int, params *Fingerprint) (*rod.Browser, string, *rod.Page) {
|
||||
var accountIdStr = strconv.Itoa(accountId)
|
||||
userDataDir := "user-data/" + accountIdStr
|
||||
absPath, _ := filepath.Abs(userDataDir)
|
||||
log.Printf("用户数据目录: %s", absPath)
|
||||
path, _ := launcher.LookPath() // 获取当前系统的浏览器目录
|
||||
// 判断是否传递代理
|
||||
// var proxyHandle utils.ProxyHandle
|
||||
// protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy)
|
||||
|
||||
ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy)))
|
||||
|
||||
l := launcher.New().
|
||||
Bin(path).
|
||||
// Proxy("210.51.27.121:50003"). // 直接传入完整认证URL
|
||||
Delete("use-mock-keychain"). // delete flag "--use-mock-keychain"
|
||||
Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测
|
||||
// Set("incognito"). // 无痕模式
|
||||
Set("user-data-dir", absPath). // 数据持久化目录
|
||||
Set("window-size", "1920,1480"). // 窗口尺寸
|
||||
Set("disable-infobars", "true"). // 隐藏自动化提示栏
|
||||
Set("no-sandbox", "true").
|
||||
Set("excludeSwitches", "enable-automation").
|
||||
Set("enable-gpu"). // 启用 GPU 加速
|
||||
Set("ignore-certificate-errors"). // 忽略证书错误
|
||||
Set("use-fake-ui-for-media-stream"). // 允许媒体流
|
||||
Set("autoplay-policy", "no-user-gesture-required"). // 自动播放
|
||||
Set("ignore-certificate-errors").
|
||||
Set("disable-application-cache").
|
||||
Set("disable-dev-shm-usage").
|
||||
// 禁用webrtc
|
||||
Set("disable-webrtc", "true"). // 核心禁用参数
|
||||
Set("disable-features", "WebRtcHideLocalIpsWithMdns"). // 隐藏本地IP
|
||||
Set("webrtc-ip-handling-policy", "disable_non_proxied_udp"). // 禁用非代理UDP
|
||||
Set("force-webrtc-ip-handling-policy").
|
||||
Headless(false) // 关闭无头模式
|
||||
|
||||
// 修改语言
|
||||
l.Set("lang", "en-US")
|
||||
l.Set("accept-lang", "en-US")
|
||||
|
||||
// 修改时区
|
||||
l.Set("timezone", ipInfo.Timezone)
|
||||
l.Set("disable-geolocation", "true") // 启用地理定位API
|
||||
l.Set("disable-web-security", "true") // 允许跨域(某些网站需要)
|
||||
l.Set("disable-notifications", "true")
|
||||
|
||||
// 设置环境变量 - 核心时区设置
|
||||
l.Env(append(os.Environ(), "TZ="+ipInfo.Timezone)...)
|
||||
l.
|
||||
Set("timezone", ipInfo.Timezone).
|
||||
Set("geolocation", ipInfo.Lat+","+ipInfo.Lon)
|
||||
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(protocol + "://" + ip + ":" + port)
|
||||
// l = l.Proxy(ip + ":" + port)
|
||||
// }
|
||||
// }
|
||||
|
||||
uri := l.MustLaunch()
|
||||
|
||||
browser := rod.New().
|
||||
ControlURL(uri).
|
||||
MustConnect().NoDefaultDevice()
|
||||
browser.MustIgnoreCertErrors(true)
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(username, password)
|
||||
// go browser.MustHandleAuth(username, password)()
|
||||
// }
|
||||
// }
|
||||
page := stealth.MustPage(browser)
|
||||
// page := browser.MustPage()
|
||||
|
||||
go browser.EachEvent(func(e *proto.TargetTargetCreated) {
|
||||
if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
|
||||
return
|
||||
}
|
||||
browser.MustPageFromTargetID(e.TargetInfo.TargetID).MustEvalOnNewDocument(stealth.JS)
|
||||
})()
|
||||
|
||||
// 在页面加载前注入时区覆盖代码
|
||||
var browserFingerprint browserfingerprint.BrowserFingerprint
|
||||
page = browserFingerprint.SetBrowserFingerprint(page, ¶msTypes.BrowserFingerprintParams{
|
||||
Canvas: true,
|
||||
TimeZone: ipInfo.Timezone, // 太平洋时间 (UTC-8)
|
||||
Language: "en-US",
|
||||
GeoLocation: ipInfo.Lat + "," + ipInfo.Lon, // 洛杉矶坐标
|
||||
})
|
||||
page.MustSetViewport(1920, 1480, 1.0, false)
|
||||
|
||||
return browser, uri, page
|
||||
}
|
||||
|
||||
func (c *Chrome) TemporaryCreate(params *Fingerprint) (*rod.Browser, string, *rod.Page) {
|
||||
path, _ := launcher.LookPath() // 获取当前系统的浏览器目录
|
||||
// 判断是否传递代理
|
||||
// var proxyHandle utils.ProxyHandle
|
||||
// protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy)
|
||||
|
||||
ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy)))
|
||||
|
||||
l := launcher.New().
|
||||
Bin(path).
|
||||
// Proxy("210.51.27.121:50003"). // 直接传入完整认证URL
|
||||
Delete("use-mock-keychain"). // delete flag "--use-mock-keychain"
|
||||
Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测
|
||||
// Set("incognito"). // 无痕模式
|
||||
Set("window-size", "1920,1480"). // 窗口尺寸
|
||||
Set("disable-infobars", "true"). // 隐藏自动化提示栏
|
||||
Set("no-sandbox", "true").
|
||||
Set("excludeSwitches", "enable-automation").
|
||||
Set("enable-gpu"). // 启用 GPU 加速
|
||||
Set("ignore-certificate-errors"). // 忽略证书错误
|
||||
Set("use-fake-ui-for-media-stream"). // 允许媒体流
|
||||
Set("autoplay-policy", "no-user-gesture-required"). // 自动播放
|
||||
Set("ignore-certificate-errors").
|
||||
Set("disable-application-cache").
|
||||
Set("disable-dev-shm-usage").
|
||||
// 禁用webrtc
|
||||
Set("disable-webrtc", "true"). // 核心禁用参数
|
||||
Set("disable-features", "WebRtcHideLocalIpsWithMdns"). // 隐藏本地IP
|
||||
Set("webrtc-ip-handling-policy", "disable_non_proxied_udp"). // 禁用非代理UDP
|
||||
Set("force-webrtc-ip-handling-policy").
|
||||
Headless(false) // 关闭无头模式
|
||||
|
||||
// 修改语言
|
||||
l.Set("lang", "en-US")
|
||||
l.Set("accept-lang", "en-US")
|
||||
|
||||
// 修改时区
|
||||
l.Set("timezone", ipInfo.Timezone)
|
||||
l.Set("disable-geolocation", "true") // 启用地理定位API
|
||||
l.Set("disable-web-security", "true") // 允许跨域(某些网站需要)
|
||||
l.Set("disable-notifications", "true")
|
||||
|
||||
// 设置环境变量 - 核心时区设置
|
||||
l.Env(append(os.Environ(), "TZ="+ipInfo.Timezone)...)
|
||||
l.
|
||||
Set("timezone", ipInfo.Timezone).
|
||||
Set("geolocation", ipInfo.Lat+","+ipInfo.Lon)
|
||||
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(protocol + "://" + ip + ":" + port)
|
||||
// l = l.Proxy(ip + ":" + port)
|
||||
// }
|
||||
// }
|
||||
|
||||
uri := l.MustLaunch()
|
||||
|
||||
browser := rod.New().
|
||||
ControlURL(uri).
|
||||
MustConnect().NoDefaultDevice()
|
||||
browser.MustIgnoreCertErrors(true)
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(username, password)
|
||||
// go browser.MustHandleAuth(username, password)()
|
||||
// }
|
||||
// }
|
||||
page := stealth.MustPage(browser)
|
||||
// page := browser.MustPage()
|
||||
|
||||
go browser.EachEvent(func(e *proto.TargetTargetCreated) {
|
||||
if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
|
||||
return
|
||||
}
|
||||
browser.MustPageFromTargetID(e.TargetInfo.TargetID).MustEvalOnNewDocument(stealth.JS)
|
||||
})()
|
||||
|
||||
// 在页面加载前注入时区覆盖代码
|
||||
var browserFingerprint browserfingerprint.BrowserFingerprint
|
||||
page = browserFingerprint.SetBrowserFingerprint(page, ¶msTypes.BrowserFingerprintParams{
|
||||
Canvas: true,
|
||||
TimeZone: ipInfo.Timezone, // 太平洋时间 (UTC-8)
|
||||
Language: "en-US",
|
||||
GeoLocation: ipInfo.Lat + "," + ipInfo.Lon, // 洛杉矶坐标
|
||||
})
|
||||
page.MustSetViewport(1920, 1480, 1.0, false)
|
||||
|
||||
return browser, uri, page
|
||||
}
|
||||
33
libs/error_code.go
Normal file
33
libs/error_code.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package libs
|
||||
|
||||
type ErrorInfo struct {
|
||||
Code int
|
||||
Data any
|
||||
Msg string
|
||||
}
|
||||
|
||||
var ErrorCode = map[string]*ErrorInfo{
|
||||
"LoginFailed": {Code: 0, Data: "", Msg: "账号异常"},
|
||||
"LoginSuccessful": {Code: 200, Data: "", Msg: "登录成功"},
|
||||
"NetworkTimeout": {Code: 1, Data: "", Msg: "网络超时"},
|
||||
"AccountHasBeenTakenOffline": {Code: 2, Data: "", Msg: "账号已下线,请重新登录"},
|
||||
"OtherError": {Code: 3, Data: "", Msg: "其他错误"},
|
||||
"ChangeSuccessful": {Code: 200, Data: "", Msg: "修改成功"},
|
||||
"ChangeFailed": {Code: 0, Data: "", Msg: "修改失败"},
|
||||
"SendPostSuccessful": {Code: 200, Data: "", Msg: "发推成功"},
|
||||
"SendPostFailed": {Code: 200, Data: "", Msg: "发推失败"},
|
||||
"SimulationSuccessful": {Code: 200, Data: "", Msg: "模拟成功"},
|
||||
"FollowSuccessful": {Code: 200, Data: "", Msg: "关注成功"},
|
||||
"FollowFailed": {Code: 200, Data: "", Msg: "关注失败"},
|
||||
"CommentSuccessful": {Code: 200, Data: "", Msg: "评论成功"},
|
||||
"CommentFailed": {Code: 200, Data: "", Msg: "评论失败"},
|
||||
"ParamsError": {Code: 4, Data: "", Msg: "参数错误"},
|
||||
"SearchSuccessful": {Code: 200, Data: "", Msg: "搜索成功"},
|
||||
"SearchFailed": {Code: 200, Data: "", Msg: "搜索失败"},
|
||||
"ChangeLangSuccessful": {Code: 200, Data: "", Msg: "修改语言成功"},
|
||||
"ChangeLangFailed": {Code: 0, Data: "", Msg: "修改语言失败"},
|
||||
"MaintainAccountSuccessful": {Code: 200, Data: "", Msg: "养号运行成功"},
|
||||
"MaintainAccountFailed": {Code: 0, Data: "", Msg: "养号运行失败"},
|
||||
"RegisterFailed": {Code: 0, Data: "", Msg: "注册失败"},
|
||||
"RegisterSuccessful": {Code: 0, Data: "", Msg: "注册成功"},
|
||||
}
|
||||
36
libs/file.go
Normal file
36
libs/file.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package libs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
}
|
||||
|
||||
// 下载图片到临时文件
|
||||
func (*File) DownloadImage(url string) (string, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 创建临时文件
|
||||
tempDir := os.TempDir()
|
||||
tempFile := filepath.Join(tempDir, fmt.Sprintf("upload-%d.jpg", time.Now().UnixNano()))
|
||||
|
||||
out, err := os.Create(tempFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 复制文件内容
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return tempFile, err
|
||||
}
|
||||
34
models/ip.go
Normal file
34
models/ip.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
type Ip struct {
|
||||
ID int64 `gorm:"id;primary_key;auto_increment"`
|
||||
As string `gorm:"as"`
|
||||
Asname string `gorm:"asname"`
|
||||
City string `gorm:"city"`
|
||||
Continent string `gorm:"continent"`
|
||||
ContinentCode string `gorm:"continent_code"`
|
||||
Country string `gorm:"country"`
|
||||
CountryCode string `gorm:"country_code"`
|
||||
Currency string `gorm:"currency"`
|
||||
District string `gorm:"district"`
|
||||
Hosting int64 `gorm:"hosting"`
|
||||
Isp string `gorm:"isp"`
|
||||
Lat string `gorm:"lat"`
|
||||
Lon string `gorm:"lon"`
|
||||
Mobile int64 `gorm:"mobile"`
|
||||
Offset string `gorm:"offset"`
|
||||
Org string `gorm:"org"`
|
||||
Proxy int64 `gorm:"proxy"`
|
||||
Query string `gorm:"query"`
|
||||
Region string `gorm:"region"`
|
||||
RegionName string `gorm:"region_name"`
|
||||
Timezone string `gorm:"timezone"`
|
||||
Zip string `gorm:"zip"`
|
||||
UpdateTime int64 `gorm:"update_time"`
|
||||
CreateTime int64 `gorm:"create_time"`
|
||||
}
|
||||
|
||||
// 实现 TableName 方法指定表名
|
||||
func (Ip) TableName() string {
|
||||
return "j_ip"
|
||||
}
|
||||
14
router/index.go
Normal file
14
router/index.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var task Task
|
||||
|
||||
type Router struct {
|
||||
}
|
||||
|
||||
func Init(app *gin.Engine) {
|
||||
task.TaskRouterInit(app)
|
||||
}
|
||||
19
router/task.go
Normal file
19
router/task.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"go-account-register/api"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Task struct{}
|
||||
|
||||
var apiTask api.Task
|
||||
|
||||
func (t *Task) TaskRouterInit(app *gin.Engine) {
|
||||
task := app.Group("/task")
|
||||
task.POST("/create", apiTask.Create)
|
||||
task.POST("/start", apiTask.Start)
|
||||
task.POST("/stop", apiTask.Stop)
|
||||
task.POST("/delete", apiTask.Delete)
|
||||
}
|
||||
206
task/scheduler.go
Normal file
206
task/scheduler.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
paramsTypes "go-account-register/types"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
TaskQueue map[string]*paramsTypes.Task // 待处理的任务
|
||||
mu sync.Mutex
|
||||
ActiveProcesses map[string]*paramsTypes.Task // 正在运行的任务
|
||||
PausedTasks map[string]*paramsTypes.Task // 暂停的任务
|
||||
}
|
||||
|
||||
var (
|
||||
StopStatus string = "stop" // 暂停状态
|
||||
PendingStatus string = "pending" // 运行状态
|
||||
)
|
||||
|
||||
var MaxTaskNum int = 10
|
||||
|
||||
// 全局单例实例
|
||||
var globalScheduler *Scheduler
|
||||
var once sync.Once
|
||||
|
||||
// 获取全局调度器实例
|
||||
func GetScheduler() *Scheduler {
|
||||
once.Do(func() {
|
||||
globalScheduler = &Scheduler{
|
||||
TaskQueue: make(map[string]*paramsTypes.Task),
|
||||
ActiveProcesses: make(map[string]*paramsTypes.Task),
|
||||
PausedTasks: make(map[string]*paramsTypes.Task),
|
||||
}
|
||||
})
|
||||
return globalScheduler
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
func (s *Scheduler) AddTask(params *paramsTypes.TaskParams) string {
|
||||
s.mu.Lock()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var uuidV4 string
|
||||
is := structs.New(params)
|
||||
if is.Field("ID").IsZero() {
|
||||
uuidV4 = uuid.NewString()
|
||||
} else {
|
||||
uuidV4 = params.ID
|
||||
}
|
||||
task := ¶msTypes.Task{
|
||||
ID: uuidV4,
|
||||
Name: params.Name,
|
||||
Status: params.Status,
|
||||
Cancel: cancel,
|
||||
Ctx: ctx,
|
||||
Message: make(chan string, 1),
|
||||
Type: params.Type,
|
||||
Data: ¶msTypes.TaskData{
|
||||
AccountClassify: params.AccountClassify,
|
||||
DataClassify: params.DataClassify,
|
||||
Keyword: params.Keyword,
|
||||
TwitterId: params.TwitterId,
|
||||
Compensate: params.Compensate,
|
||||
}}
|
||||
switch params.Status {
|
||||
case StopStatus:
|
||||
s.PausedTasks[uuidV4] = task
|
||||
case PendingStatus:
|
||||
s.TaskQueue[uuidV4] = task
|
||||
default:
|
||||
s.TaskQueue[uuidV4] = task
|
||||
}
|
||||
log.Println("创建任务:" + uuidV4)
|
||||
log.Println("状态:" + params.Status)
|
||||
s.mu.Unlock()
|
||||
s.processQueue()
|
||||
return uuidV4
|
||||
}
|
||||
|
||||
// 运行任务
|
||||
func (s *Scheduler) RunTask(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// 查询暂停的任务列表中是否包含
|
||||
v, ok := s.PausedTasks[id]
|
||||
if ok {
|
||||
s.TaskQueue[id] = v
|
||||
delete(s.PausedTasks, id)
|
||||
}
|
||||
s.processQueue()
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
func (s *Scheduler) StopTask(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// 查询任务列表中是否包含
|
||||
v, ok := s.TaskQueue[id]
|
||||
if ok {
|
||||
s.PausedTasks[id] = v
|
||||
v.Status = StopStatus
|
||||
v.Cancel() // 停止上下文
|
||||
// v.Message <- "Error"
|
||||
delete(s.TaskQueue, id)
|
||||
}
|
||||
// 查询运行中的任务列表中是否包含
|
||||
value, ok1 := s.ActiveProcesses[id]
|
||||
if ok1 {
|
||||
s.PausedTasks[id] = value
|
||||
value.Status = StopStatus
|
||||
log.Println("向通道中发送消息")
|
||||
value.Cancel() // 停止上下文
|
||||
// value.Message <- "Error"
|
||||
delete(s.ActiveProcesses, id)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
func (s *Scheduler) RemoveTask(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// 查询任务列表中是否包含
|
||||
_, ok := s.TaskQueue[id]
|
||||
if ok {
|
||||
delete(s.TaskQueue, id)
|
||||
}
|
||||
|
||||
// 查询运行中的任务列表中是否包含
|
||||
_, ok1 := s.ActiveProcesses[id]
|
||||
if ok1 {
|
||||
delete(s.ActiveProcesses, id)
|
||||
}
|
||||
|
||||
// 查询暂停中的任务列表中是否包含
|
||||
_, ok2 := s.PausedTasks[id]
|
||||
if ok2 {
|
||||
delete(s.PausedTasks, id)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理任务
|
||||
func (s *Scheduler) processQueue() {
|
||||
|
||||
s.mu.Lock()
|
||||
log.Println("开始处理任务")
|
||||
defer s.mu.Unlock()
|
||||
for len(s.ActiveProcesses) < MaxTaskNum && len(s.TaskQueue) > 0 {
|
||||
keys := make([]string, 0, len(s.TaskQueue))
|
||||
for k := range s.TaskQueue {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
// 排序键
|
||||
sort.Strings(keys)
|
||||
// 获取第一个键值对
|
||||
firstKey := keys[0]
|
||||
go s.executeTask(s.TaskQueue[firstKey])
|
||||
s.ActiveProcesses[firstKey] = s.TaskQueue[firstKey]
|
||||
delete(s.TaskQueue, firstKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) executeTask(task *paramsTypes.Task) {
|
||||
// 根据类型判断任务
|
||||
// switch task.Type {
|
||||
// case 6:
|
||||
// var taskNurturingClashPlugin plugin.TaskNurturingClashPlugin
|
||||
// go taskNurturingClashPlugin.Run(task)
|
||||
// case 1:
|
||||
// var taskChangeAccoutnClashPlugin plugin.TaskChangeAccoutnClashPlugin
|
||||
// go taskChangeAccoutnClashPlugin.Run(task)
|
||||
// case 3:
|
||||
// var taskFollowClashPlugin plugin.TaskFollowClashPlugin
|
||||
// go taskFollowClashPlugin.Run(task)
|
||||
// case 7:
|
||||
// var taskSendGifPlugin plugin.TaskSendGifPlugin
|
||||
// go taskSendGifPlugin.Run(task)
|
||||
// }
|
||||
|
||||
// 创建一个阻塞获取当前任务
|
||||
select {
|
||||
case message := <-task.Message:
|
||||
if message == "Success" {
|
||||
log.Println("任务成功")
|
||||
// 运行结束
|
||||
s.PausedTasks[task.ID] = task
|
||||
delete(s.ActiveProcesses, task.ID)
|
||||
}
|
||||
if message == "Error" {
|
||||
log.Println("任务失败")
|
||||
// 运行结束
|
||||
s.PausedTasks[task.ID] = task
|
||||
delete(s.ActiveProcesses, task.ID)
|
||||
}
|
||||
case <-task.Ctx.Done():
|
||||
// 运行结束
|
||||
s.PausedTasks[task.ID] = task
|
||||
delete(s.ActiveProcesses, task.ID)
|
||||
}
|
||||
}
|
||||
9
types/fingerprint_type.go
Normal file
9
types/fingerprint_type.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
type BrowserFingerprintParams struct {
|
||||
Canvas bool // 是否随机指纹
|
||||
TimeZone string // 时区名称,如 "America/New_York"
|
||||
Language string // 语言,如 "en-US"
|
||||
UserAgent string // 用户代理字符串
|
||||
GeoLocation string // 地理位置坐标,如 "40.7128,-74.0060"
|
||||
}
|
||||
37
types/task_type.go
Normal file
37
types/task_type.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
import "context"
|
||||
|
||||
// 任务信息
|
||||
type Task struct {
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Message chan string
|
||||
Cancel context.CancelFunc
|
||||
Ctx context.Context
|
||||
Type int
|
||||
Data *TaskData
|
||||
}
|
||||
|
||||
// 添加任务传递的参数
|
||||
type TaskParams struct {
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Type int
|
||||
AccountClassify string
|
||||
DataClassify []int
|
||||
Keyword string
|
||||
TwitterId string
|
||||
Compensate int
|
||||
}
|
||||
|
||||
// 任务携带的数据
|
||||
type TaskData struct {
|
||||
AccountClassify string
|
||||
DataClassify []int
|
||||
Keyword string
|
||||
TwitterId string
|
||||
Compensate int
|
||||
}
|
||||
94
utils/ai_request.go
Normal file
94
utils/ai_request.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type AiReqeust struct{}
|
||||
|
||||
type DeepseekResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"` // 可能是null或复杂结构
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
PromptTokensDetails struct {
|
||||
CachedTokens int `json:"cached_tokens"`
|
||||
} `json:"prompt_tokens_details"`
|
||||
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
|
||||
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
|
||||
} `json:"usage"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
func (*AiReqeust) Deepseek(text string) string {
|
||||
// 定义结构体
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
}
|
||||
// 创建 resty 客户端
|
||||
client := resty.New()
|
||||
|
||||
// 目标URL
|
||||
url := "https://api.deepseek.com/v1/chat/completions"
|
||||
|
||||
request := ChatRequest{
|
||||
Model: "deepseek-chat",
|
||||
Messages: []Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: text,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 发送 POST 请求
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json"). // 必填头
|
||||
SetHeader("Authorization", "Bearer sk-9ca769cb45c249559276b979a2e1a2cd"). // 认证头
|
||||
SetBody(request). // 设置JSON体
|
||||
Post(url)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("请求失败:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response DeepseekResponse
|
||||
if err := json.Unmarshal(resp.Body(), &response); err != nil {
|
||||
fmt.Println("解析失败:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// 提取关键信息
|
||||
if len(response.Choices) > 0 {
|
||||
firstChoice := response.Choices[0]
|
||||
// fmt.Printf("\nAI回复 (%s):\n%s\n",
|
||||
// firstChoice.Message.Role,
|
||||
// firstChoice.Message.Content)
|
||||
return firstChoice.Message.Content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
60
utils/db.go
Normal file
60
utils/db.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-account-register/config"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Connect() (*gorm.DB, error) {
|
||||
appConfig, dberr := config.LoadConfig()
|
||||
if dberr != nil {
|
||||
fmt.Println("读取配置失败")
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
appConfig.Database.Username,
|
||||
appConfig.Database.Password,
|
||||
appConfig.Database.Host,
|
||||
appConfig.Database.Port,
|
||||
appConfig.Database.Database)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sql.DB: %w", err)
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
log.Println("Database connection established")
|
||||
DB = db
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func GetDB() *gorm.DB {
|
||||
if DB == nil {
|
||||
panic("database connection is not initialized")
|
||||
}
|
||||
return DB
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
69
utils/external/index.go
vendored
Normal file
69
utils/external/index.go
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-account-register/models"
|
||||
"go-account-register/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetIpInfo(ip string) models.Ip {
|
||||
db := utils.GetDB()
|
||||
var ipInfo models.Ip
|
||||
if ip == "" {
|
||||
var ipRequest utils.IpRequest
|
||||
res, err := ipRequest.IpApi(ip)
|
||||
if err != nil {
|
||||
}
|
||||
resIp := db.Model(&models.Ip{}).Where("query = ?", res.Query).First(&ipInfo)
|
||||
if resIp.Error != nil {
|
||||
createIp(res)
|
||||
db.Model(&models.Ip{}).Where("query = ?", res.Query).First(&ipInfo)
|
||||
return ipInfo
|
||||
}
|
||||
return ipInfo
|
||||
}
|
||||
resIp := db.Model(&models.Ip{}).Where("query = ?", ip).First(&ipInfo)
|
||||
if resIp.Error != nil {
|
||||
var ipRequest utils.IpRequest
|
||||
res, err := ipRequest.IpApi(ip)
|
||||
if err != nil {
|
||||
}
|
||||
createIp(res)
|
||||
db.Model(&models.Ip{}).Where("query = ?", ip).First(&ipInfo)
|
||||
return ipInfo
|
||||
}
|
||||
|
||||
return ipInfo
|
||||
}
|
||||
|
||||
func createIp(res utils.IPAPIResponse) {
|
||||
db := utils.GetDB()
|
||||
|
||||
db.Model(&models.Ip{}).Create(&models.Ip{
|
||||
As: res.As,
|
||||
Asname: res.Asname,
|
||||
City: res.City,
|
||||
Continent: res.Continent,
|
||||
ContinentCode: res.ContinentCode,
|
||||
Country: res.Country,
|
||||
CountryCode: res.CountryCode,
|
||||
Currency: res.Currency,
|
||||
District: res.District,
|
||||
Hosting: utils.BoolToInt(res.Hosting),
|
||||
Isp: res.Isp,
|
||||
Lat: fmt.Sprint(res.Lat),
|
||||
Lon: fmt.Sprint(res.Lon),
|
||||
Mobile: utils.BoolToInt(res.Mobile),
|
||||
Offset: fmt.Sprint(res.Offset),
|
||||
Org: res.Org,
|
||||
Proxy: utils.BoolToInt(res.Proxy),
|
||||
Query: res.Query,
|
||||
Region: res.Region,
|
||||
RegionName: res.RegionName,
|
||||
Timezone: res.Timezone,
|
||||
Zip: res.Zip,
|
||||
UpdateTime: time.Now().Unix(),
|
||||
CreateTime: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
75
utils/index.go
Normal file
75
utils/index.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RandDateParams struct {
|
||||
Year int
|
||||
Month int
|
||||
Day int
|
||||
}
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const (
|
||||
letterBits = 6 // 52个字母需6位(2^6=64)
|
||||
letterMask = 1<<letterBits - 1 // 二进制掩码(63个1)
|
||||
)
|
||||
|
||||
func BoolToInt(b bool) int64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func RandString(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := 0; i < n; {
|
||||
if idx := int(rand.Int63() & letterMask); idx < len(letters) {
|
||||
b[i] = letters[idx]
|
||||
i++
|
||||
} // 丢弃无效索引,重试
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func RandDate(minAge, maxAge int) RandDateParams {
|
||||
// 创建本地随机生成器(避免全局 rand.Seed 弃用问题)
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// 计算年份范围(当前年-100岁 到 当前年-18岁)
|
||||
currentTime := time.Now()
|
||||
currentYear := currentTime.Year()
|
||||
minYear := currentYear - maxAge
|
||||
maxYear := currentYear - minAge
|
||||
|
||||
// 生成随机年份
|
||||
year := minYear + r.Intn(maxYear-minYear+1)
|
||||
|
||||
// 生成随机月份(1-12)
|
||||
month := r.Intn(12) + 1
|
||||
|
||||
// 动态计算当月最大天数(考虑闰年)
|
||||
daysInMonth := 31
|
||||
switch month {
|
||||
case 2: // 二月特殊处理
|
||||
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
|
||||
daysInMonth = 29 // 闰年
|
||||
} else {
|
||||
daysInMonth = 28 // 平年
|
||||
}
|
||||
case 4, 6, 9, 11: // 30 天的月份
|
||||
daysInMonth = 30
|
||||
}
|
||||
|
||||
// 生成随机日期(1 ~ daysInMonth)
|
||||
day := r.Intn(daysInMonth) + 1
|
||||
|
||||
return RandDateParams{
|
||||
Year: year,
|
||||
Month: month,
|
||||
Day: day,
|
||||
}
|
||||
}
|
||||
84
utils/ip_request.go
Normal file
84
utils/ip_request.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type IpRequest struct {
|
||||
}
|
||||
|
||||
type IPAPIResponse struct {
|
||||
As string `json:"as"`
|
||||
Asname string `json:"asname"`
|
||||
City string `json:"city"`
|
||||
Continent string `json:"continent"`
|
||||
ContinentCode string `json:"continentCode"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Currency string `json:"currency"`
|
||||
District string `json:"district"`
|
||||
Hosting bool `json:"hosting"`
|
||||
Isp string `json:"isp"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Mobile bool `json:"mobile"`
|
||||
Offset float64 `json:"offset"`
|
||||
Org string `json:"org"`
|
||||
Proxy bool `json:"proxy"`
|
||||
Query string `json:"query"`
|
||||
Region string `json:"region"`
|
||||
RegionName string `json:"regionName"`
|
||||
Timezone string `json:"timezone"`
|
||||
Zip string `json:"zip"`
|
||||
}
|
||||
|
||||
func (*IpRequest) IpApi(targetIP string) (IPAPIResponse, error) {
|
||||
// 创建Resty客户端
|
||||
client := resty.New()
|
||||
|
||||
// 构建请求URL
|
||||
// targetIP := "103.142.140.235"
|
||||
var url string = "http://demo.ip-api.com/json?fields=66842623&lang=en"
|
||||
if targetIP != "" {
|
||||
url = fmt.Sprintf("http://demo.ip-api.com/json/%s?fields=66842623&lang=en", targetIP)
|
||||
}
|
||||
|
||||
// 创建响应结构体实例
|
||||
var result IPAPIResponse
|
||||
|
||||
// 发送GET请求(不设置任何请求头)
|
||||
resp, err := client.R().
|
||||
SetResult(&result). // 自动解析JSON到结构体
|
||||
Get(url)
|
||||
|
||||
// 错误处理
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %v\n", err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode() != 200 {
|
||||
fmt.Printf("API返回错误状态码: %d\n", resp.StatusCode())
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (*IpRequest) GetIPAddress() {
|
||||
// 创建Resty客户端
|
||||
client := resty.New()
|
||||
var url string = "http://icanhazip.com/"
|
||||
// 发送GET请求(不设置任何请求头)
|
||||
resp, err := client.R().
|
||||
Get(url)
|
||||
// 错误处理
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %v\n", err)
|
||||
}
|
||||
log.Println(resp)
|
||||
}
|
||||
42
utils/proxy_handle.go
Normal file
42
utils/proxy_handle.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProxyHandle struct {
|
||||
}
|
||||
|
||||
func (ProxyHandle) ParseProxy(proxyStr string) (protocol, username, password, ip, port string, err error) {
|
||||
// 清理字符串中的多余字符(如末尾的 \n)
|
||||
proxyStr = strings.TrimSpace(proxyStr)
|
||||
|
||||
// 解析 URL
|
||||
proxyURL, err := url.Parse(proxyStr)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
|
||||
// 提取协议(http/https)
|
||||
protocol = proxyURL.Scheme
|
||||
|
||||
// 提取 IP 和端口
|
||||
host := proxyURL.Host
|
||||
if strings.Contains(host, ":") {
|
||||
parts := strings.Split(host, ":")
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
} else {
|
||||
ip = host
|
||||
port = "8080" // 默认端口
|
||||
}
|
||||
|
||||
// 提取用户名和密码
|
||||
if proxyURL.User != nil {
|
||||
username = proxyURL.User.Username()
|
||||
password, _ = proxyURL.User.Password()
|
||||
}
|
||||
|
||||
return protocol, username, password, ip, port, nil
|
||||
}
|
||||
34
utils/redis.go
Normal file
34
utils/redis.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Redis 封装结构体
|
||||
type Redis struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
// 初始化Redis连接
|
||||
func InitRedis() (*Redis, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建 Redis 客户端
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379", // Redis 服务器地址
|
||||
Password: "", // 密码,没有则为空
|
||||
DB: 0, // 默认数据库
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
if _, err := rdb.Ping(ctx).Result(); err != nil {
|
||||
return nil, fmt.Errorf("redis连接失败: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Redis连接成功")
|
||||
return &Redis{rdb: rdb}, nil
|
||||
}
|
||||
85
utils/verify_image_plugin.go
Normal file
85
utils/verify_image_plugin.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
type VerifyImagePlugin struct{}
|
||||
|
||||
func (*VerifyImagePlugin) DrawText(originalImage, newImage, text, fontPath string) (string, error) {
|
||||
srcFile, err := os.Open(originalImage) // 替换为你的图片路径
|
||||
if err != nil {
|
||||
log.Fatal("图片打开失败:", err)
|
||||
return "", err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// 解码原始图片
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err != nil {
|
||||
log.Fatal("图片解码失败,只支持jpg、png:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
rgba := image.NewRGBA(img.Bounds())
|
||||
draw.Draw(rgba, rgba.Bounds(), img, image.Point{}, draw.Src)
|
||||
var fontStr string = fontPath
|
||||
if fontPath == "" {
|
||||
fontStr = "fonts/AlibabaPuHuiTi-3-115-Black.ttf"
|
||||
}
|
||||
fontBytes, err := os.ReadFile(fontStr) // 替换为你的字体路径
|
||||
if err != nil {
|
||||
log.Fatal("字体加载失败:", err)
|
||||
return "", err
|
||||
}
|
||||
fontObj, err := freetype.ParseFont(fontBytes)
|
||||
if err != nil {
|
||||
log.Fatal("字体解析失败:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72) // 屏幕分辨率
|
||||
c.SetFont(fontObj) // 设置中文字体
|
||||
c.SetFontSize(40) // 字体大小(磅)
|
||||
c.SetClip(rgba.Bounds()) // 绘制区域
|
||||
c.SetDst(rgba) // 目标画布
|
||||
c.SetSrc(image.NewUniform(color.RGBA{ // 文字颜色
|
||||
R: 255,
|
||||
G: 255,
|
||||
B: 255,
|
||||
A: 255,
|
||||
}))
|
||||
c.SetHinting(font.HintingFull) // 增强小字清晰度
|
||||
|
||||
x := 150 // 水平起始位置
|
||||
y := 250 // 垂直起始位置
|
||||
yBase := y + int(c.PointToFixed(40)>>6) // 关键:修正垂直偏移
|
||||
pt := freetype.Pt(x, yBase)
|
||||
|
||||
if _, err := c.DrawString(text, pt); err != nil {
|
||||
log.Fatal("文字绘制失败:", err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(newImage)
|
||||
if err != nil {
|
||||
log.Fatal("输出文件创建失败:", err)
|
||||
return "", err
|
||||
}
|
||||
defer outFile.Close()
|
||||
if err := png.Encode(outFile, rgba); err != nil {
|
||||
log.Fatal("图片保存失败:", err)
|
||||
return "", err
|
||||
}
|
||||
log.Println("成功生成:output.png")
|
||||
return newImage, nil
|
||||
}
|
||||
Reference in New Issue
Block a user