This commit is contained in:
zyj
2025-09-13 10:22:49 +08:00
commit 002419a7ed
28 changed files with 1888 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View 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(&paramsJson); 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(&paramsTypes.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(&paramsJson); 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(&paramsJson); 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(&paramsJson); 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
View 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
View 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
}

Binary file not shown.

57
go.mod Normal file
View 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
View 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=

View 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])
}

View 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
}

View 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
View 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
View 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, &paramsTypes.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, &paramsTypes.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
View 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
View 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
View 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
View 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
View 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
View 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 := &paramsTypes.Task{
ID: uuidV4,
Name: params.Name,
Status: params.Status,
Cancel: cancel,
Ctx: ctx,
Message: make(chan string, 1),
Type: params.Type,
Data: &paramsTypes.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)
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
}