mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: 完成人机交互验证 API 接入,增加短信防刷验证
This commit is contained in:
		@@ -3,9 +3,9 @@ ProxyURL = "http://172.22.11.200:7777"
 | 
			
		||||
MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
 | 
			
		||||
StaticDir = "./static"
 | 
			
		||||
StaticUrl = "http://localhost:5678/static"
 | 
			
		||||
AesEncryptKey = "YOUR_AES_KEY"
 | 
			
		||||
FunApiToken = "YOUR_FUN_API_TOKEN"
 | 
			
		||||
AesEncryptKey = "{YOUR_AES_KEY}"
 | 
			
		||||
StartWechatBot = false
 | 
			
		||||
EnabledMsgService = false
 | 
			
		||||
 | 
			
		||||
[Session]
 | 
			
		||||
  Driver = "cookie"
 | 
			
		||||
@@ -27,9 +27,14 @@ StartWechatBot = false
 | 
			
		||||
  Port = 6379
 | 
			
		||||
  Password = ""
 | 
			
		||||
 | 
			
		||||
[ApiConfig]
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
 | 
			
		||||
[SmsConfig]
 | 
			
		||||
  AccessKey = "YOUR_ACCESS_KEY"
 | 
			
		||||
  AccessSecret = "YOUR_SECRET_KEY"
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
			HttpOnly:  false,
 | 
			
		||||
			SameSite:  http.SameSiteLaxMode,
 | 
			
		||||
		},
 | 
			
		||||
		Func:           types.FunctionApiConfig{},
 | 
			
		||||
		ApiConfig:      types.ChatPlusApiConfig{},
 | 
			
		||||
		StartWechatBot: false,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ type AppConfig struct {
 | 
			
		||||
	StaticDir         string            // 静态资源目录
 | 
			
		||||
	StaticUrl         string            // 静态资源 URL
 | 
			
		||||
	Redis             RedisConfig       // redis 连接信息
 | 
			
		||||
	Func              FunctionApiConfig // function api configs
 | 
			
		||||
	ApiConfig         ChatPlusApiConfig // chatplus api configs
 | 
			
		||||
	AesEncryptKey     string
 | 
			
		||||
	SmsConfig         AliYunSmsConfig // 短信发送配置
 | 
			
		||||
	StartWechatBot    bool            // 是否启动微信机器人
 | 
			
		||||
@@ -86,7 +86,7 @@ type SystemConfig struct {
 | 
			
		||||
	UserInitCalls int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FunctionApiConfig struct {
 | 
			
		||||
type ChatPlusApiConfig struct {
 | 
			
		||||
	ApiURL string
 | 
			
		||||
	AppId  string
 | 
			
		||||
	Token  string
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 今日头条函数实现
 | 
			
		||||
 | 
			
		||||
type CaptchaHandler struct {
 | 
			
		||||
	service *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
 | 
			
		||||
	return &CaptchaHandler{service: s}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CaptchaHandler) Get(c *gin.Context) {
 | 
			
		||||
	data, err := h.service.Get()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check verify the captcha data
 | 
			
		||||
func (h *CaptchaHandler) Check(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Key  string `json:"key"`
 | 
			
		||||
		Dots string `json:"dots"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.service.Check(data) {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CodeStorePrefix = "/verify/codes/"
 | 
			
		||||
 | 
			
		||||
type SmsHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db      *store.LevelDB
 | 
			
		||||
	sms     *service.AliYunSmsService
 | 
			
		||||
	captcha *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	handler := &SmsHandler{db: db, sms: sms, captcha: captcha}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VerifyCode 发送验证码短信
 | 
			
		||||
func (h *SmsHandler) VerifyCode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Key    string `json:"key"`
 | 
			
		||||
		Dots   string `json:"dots"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.captcha.Check(data) {
 | 
			
		||||
		resp.ERROR(c, "验证码错误,请先完人机验证")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := utils.RandomNumber(6)
 | 
			
		||||
	err := h.sms.SendVerifyCode(data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 存储验证码,等待后面注册验证
 | 
			
		||||
	err = h.db.Put(CodeStorePrefix+data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "验证码保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Status check if the message service is enabled
 | 
			
		||||
func (h *SmsHandler) Status(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, h.App.Config.EnabledMsgService)
 | 
			
		||||
}
 | 
			
		||||
@@ -58,12 +58,14 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	var code int
 | 
			
		||||
	err := h.levelDB.Get(key, &code)
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		logger.Info(code)
 | 
			
		||||
		resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	if h.App.Config.EnabledMsgService {
 | 
			
		||||
		var code int
 | 
			
		||||
		err := h.levelDB.Get(key, &code)
 | 
			
		||||
		if err != nil || code != data.Code {
 | 
			
		||||
			logger.Info(code)
 | 
			
		||||
			resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if the username is exists
 | 
			
		||||
@@ -111,7 +113,7 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
	var cfg model.Config
 | 
			
		||||
	h.db.Where("marker = ?", "system").First(&cfg)
 | 
			
		||||
	var config types.SystemConfig
 | 
			
		||||
	err = utils.JsonDecode(cfg.Config, &config)
 | 
			
		||||
	err := utils.JsonDecode(cfg.Config, &config)
 | 
			
		||||
	if err != nil || config.UserInitCalls <= 0 {
 | 
			
		||||
		user.Calls = types.UserInitCalls
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -124,7 +126,9 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码
 | 
			
		||||
	if h.App.Config.EnabledMsgService {
 | 
			
		||||
		_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,150 +0,0 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 短信验证码控制器
 | 
			
		||||
 | 
			
		||||
type VerifyHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	sms *service.AliYunSmsService
 | 
			
		||||
	db  *store.LevelDB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TokenStorePrefix = "/verify/tokens/"
 | 
			
		||||
const CodeStorePrefix = "/verify/codes/"
 | 
			
		||||
const MobileStatPrefix = "/verify/stats/"
 | 
			
		||||
 | 
			
		||||
func NewVerifyHandler(app *core.AppServer, sms *service.AliYunSmsService, db *store.LevelDB) *VerifyHandler {
 | 
			
		||||
	handler := &VerifyHandler{sms: sms, db: db}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type VerifyToken struct {
 | 
			
		||||
	Token     string
 | 
			
		||||
	Timestamp int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CodeStats 验证码发送统计
 | 
			
		||||
type CodeStats struct {
 | 
			
		||||
	Mobile string
 | 
			
		||||
	Count  uint
 | 
			
		||||
	Time   int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Token 生成自验证 token
 | 
			
		||||
func (h *VerifyHandler) Token(c *gin.Context) {
 | 
			
		||||
	// 如果不是通过浏览器访问,则返回错误的 token
 | 
			
		||||
	// TODO: 引入验证码机制防刷机制
 | 
			
		||||
	if c.GetHeader("Sec-Fetch-Mode") != "cors" {
 | 
			
		||||
		token := fmt.Sprintf("%s:%d", utils.RandString(32), time.Now().Unix())
 | 
			
		||||
		encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(token))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "Token 加密出错")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		resp.SUCCESS(c, encrypt)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := VerifyToken{
 | 
			
		||||
		Token:     utils.RandString(32),
 | 
			
		||||
		Timestamp: time.Now().Unix(),
 | 
			
		||||
	}
 | 
			
		||||
	json := utils.JsonEncode(token)
 | 
			
		||||
	encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(json))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Token 加密出错")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = h.db.Put(TokenStorePrefix+token.Token, token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Token 存储失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, encrypt)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendMsg 发送验证码短信
 | 
			
		||||
func (h *VerifyHandler) SendMsg(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Token  string `json:"token"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	decrypt, err := utils.AesDecrypt(h.App.Config.AesEncryptKey, data.Token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Token 解密失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var token VerifyToken
 | 
			
		||||
	err = utils.JsonDecode(string(decrypt), &token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Token 解码失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if time.Now().Unix()-token.Timestamp > 30 {
 | 
			
		||||
		resp.ERROR(c, "Token 已过期,请刷新页面重试")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证当前手机号发送次数,24 小时内相同手机号只允许发送 2 次
 | 
			
		||||
	var stat CodeStats
 | 
			
		||||
	err = h.db.Get(MobileStatPrefix+data.Mobile, &stat)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		stat = CodeStats{
 | 
			
		||||
			Mobile: data.Mobile,
 | 
			
		||||
			Count:  0,
 | 
			
		||||
			Time:   time.Now().Unix(),
 | 
			
		||||
		}
 | 
			
		||||
	} else if stat.Count == 2 {
 | 
			
		||||
		if time.Now().Unix()-stat.Time > 86400 {
 | 
			
		||||
			stat.Count = 0
 | 
			
		||||
			stat.Time = time.Now().Unix()
 | 
			
		||||
		} else {
 | 
			
		||||
			resp.ERROR(c, "触发流量预警,请 24 小时后再操作!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := utils.RandomNumber(6)
 | 
			
		||||
	err = h.sms.SendVerifyCode(data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 每个 token 用完一次立即失效
 | 
			
		||||
	_ = h.db.Delete(TokenStorePrefix + token.Token)
 | 
			
		||||
	// 存储验证码,等待后面注册验证
 | 
			
		||||
	err = h.db.Put(CodeStorePrefix+data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "验证码保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新发送次数
 | 
			
		||||
	stat.Count = stat.Count + 1
 | 
			
		||||
	_ = h.db.Put(MobileStatPrefix+data.Mobile, stat)
 | 
			
		||||
	logger.Infof("%+v", stat)
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								api/main.go
									
									
									
									
									
								
							@@ -117,13 +117,13 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		// 创建函数
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) (function.FuncZaoBao, error) {
 | 
			
		||||
			return function.NewZaoBao(config.Func), nil
 | 
			
		||||
			return function.NewZaoBao(config.ApiConfig), nil
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) (function.FuncWeiboHot, error) {
 | 
			
		||||
			return function.NewWeiboHot(config.Func), nil
 | 
			
		||||
			return function.NewWeiboHot(config.ApiConfig), nil
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) (function.FuncHeadlines, error) {
 | 
			
		||||
			return function.NewHeadLines(config.Func), nil
 | 
			
		||||
			return function.NewHeadLines(config.ApiConfig), nil
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 创建控制器
 | 
			
		||||
@@ -131,8 +131,9 @@ func main() {
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatHandler),
 | 
			
		||||
		fx.Provide(handler.NewUploadHandler),
 | 
			
		||||
		fx.Provide(handler.NewVerifyHandler),
 | 
			
		||||
		fx.Provide(handler.NewSmsHandler),
 | 
			
		||||
		fx.Provide(handler.NewRewardHandler),
 | 
			
		||||
		fx.Provide(handler.NewCaptchaHandler),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewConfigHandler),
 | 
			
		||||
		fx.Provide(admin.NewAdminHandler),
 | 
			
		||||
@@ -143,6 +144,9 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		// 创建服务
 | 
			
		||||
		fx.Provide(service.NewAliYunSmsService),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
 | 
			
		||||
			return service.NewCaptchaService(config.ApiConfig)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 注册路由
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
 | 
			
		||||
@@ -174,10 +178,15 @@ func main() {
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.VerifyHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/verify/")
 | 
			
		||||
			group.GET("token", h.Token)
 | 
			
		||||
			group.POST("sms", h.SendMsg)
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sms/")
 | 
			
		||||
			group.GET("status", h.Status)
 | 
			
		||||
			group.POST("code", h.VerifyCode)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/captcha/")
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
			group.POST("check", h.Check)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/reward/")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CaptchaService struct {
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
 | 
			
		||||
	return &CaptchaService{
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) Get() (interface{}, error) {
 | 
			
		||||
	if s.config.Token == "" {
 | 
			
		||||
		return nil, errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.Data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) Check(data interface{}) bool {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetBodyJsonMarshal(data).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
@@ -13,11 +13,11 @@ import (
 | 
			
		||||
 | 
			
		||||
type FuncHeadlines struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.FunctionApiConfig
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHeadLines(config types.FunctionApiConfig) FuncHeadlines {
 | 
			
		||||
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
 | 
			
		||||
	return FuncHeadlines{
 | 
			
		||||
		name:   "今日头条",
 | 
			
		||||
		config: config,
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,11 @@ import (
 | 
			
		||||
 | 
			
		||||
type FuncWeiboHot struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.FunctionApiConfig
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWeiboHot(config types.FunctionApiConfig) FuncWeiboHot {
 | 
			
		||||
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
 | 
			
		||||
	return FuncWeiboHot{
 | 
			
		||||
		name:   "微博热搜",
 | 
			
		||||
		config: config,
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,11 @@ import (
 | 
			
		||||
 | 
			
		||||
type FuncZaoBao struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.FunctionApiConfig
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewZaoBao(config types.FunctionApiConfig) FuncZaoBao {
 | 
			
		||||
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
 | 
			
		||||
	return FuncZaoBao{
 | 
			
		||||
		name:   "每日早报",
 | 
			
		||||
		config: config,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@ ProxyURL = "http://172.22.11.200:7777"
 | 
			
		||||
MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
 | 
			
		||||
StaticDir = "./static"
 | 
			
		||||
StaticUrl = "http://localhost:5678/static"
 | 
			
		||||
AesEncryptKey = "YOUR_AES_KEY"
 | 
			
		||||
FunApiToken = "YOUR_FUN_API_TOKEN"
 | 
			
		||||
AesEncryptKey = "{YOUR_AES_KEY}"
 | 
			
		||||
StartWechatBot = false
 | 
			
		||||
EnabledMsgService = false
 | 
			
		||||
 | 
			
		||||
[Session]
 | 
			
		||||
  Driver = "cookie"
 | 
			
		||||
@@ -27,9 +27,14 @@ StartWechatBot = false
 | 
			
		||||
  Port = 6379
 | 
			
		||||
  Password = ""
 | 
			
		||||
 | 
			
		||||
[ApiConfig]
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
 | 
			
		||||
[SmsConfig]
 | 
			
		||||
  AccessKey = "YOUR_ACCESS_KEY"
 | 
			
		||||
  AccessSecret = "YOUR_SECRET_KEY"
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -17,6 +17,7 @@
 | 
			
		||||
        "good-storage": "^1.1.1",
 | 
			
		||||
        "highlight.js": "^11.7.0",
 | 
			
		||||
        "json-bigint": "^1.0.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "markdown-it": "^13.0.1",
 | 
			
		||||
        "md-editor-v3": "^2.2.1",
 | 
			
		||||
        "pinia": "^2.1.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
    "sortablejs": "^1.15.0",
 | 
			
		||||
    "vant": "^4.5.0",
 | 
			
		||||
    "vue": "^3.2.13",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "vue-router": "^4.0.15"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										369
									
								
								web/src/components/CaptchaPlus.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								web/src/components/CaptchaPlus.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,369 @@
 | 
			
		||||
<template>
 | 
			
		||||
 | 
			
		||||
    <div class="wg-cap-wrap" :style="{width: width}">
 | 
			
		||||
      <div class="wg-cap-wrap__header">
 | 
			
		||||
        <span>请在下图<em>依次</em>点击:</span>
 | 
			
		||||
        <img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" ">
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="wg-cap-wrap__body">
 | 
			
		||||
        <img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " @click="handleClickPos($event)">
 | 
			
		||||
        <img class="wg-cap-wrap__loading" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+" alt="正在加载中...">
 | 
			
		||||
        <div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`">
 | 
			
		||||
          <span>{{ dot.index }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="wg-cap-wrap__footer">
 | 
			
		||||
        <div class="wg-cap-wrap__ico">
 | 
			
		||||
          <img @click="handleCloseEvent"
 | 
			
		||||
               src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
 | 
			
		||||
               alt="关闭">
 | 
			
		||||
          <img @click="handleRefreshEvent"
 | 
			
		||||
               src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
 | 
			
		||||
               alt="刷新">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="wg-cap-wrap__btn">
 | 
			
		||||
          <button @click="handleConfirmEvent">确认</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  export default {
 | 
			
		||||
    name: 'CaptchaPlus',
 | 
			
		||||
    mounted() {
 | 
			
		||||
      this.$emit('refresh')
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
      value: Boolean,
 | 
			
		||||
      width: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        default: '300px'
 | 
			
		||||
      },
 | 
			
		||||
      calcPosType: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        default: 'dom',
 | 
			
		||||
        validator: value => ['dom', 'screen'].includes(value)
 | 
			
		||||
      },
 | 
			
		||||
      maxDot: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
        default: 5
 | 
			
		||||
        // validator: value => value > 10
 | 
			
		||||
      },
 | 
			
		||||
      imageBase64: String,
 | 
			
		||||
      thumbBase64: String
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
      return {
 | 
			
		||||
        dots: [],
 | 
			
		||||
        imageBase64Code: '',
 | 
			
		||||
        thumbBase64Code: ''
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
      value() {
 | 
			
		||||
        this.dots = []
 | 
			
		||||
        this.imageBase64Code = ''
 | 
			
		||||
        this.thumbBase64Code = ''
 | 
			
		||||
      },
 | 
			
		||||
      imageBase64(val) {
 | 
			
		||||
        this.dots = []
 | 
			
		||||
        this.imageBase64Code = val
 | 
			
		||||
      },
 | 
			
		||||
      thumbBase64(val) {
 | 
			
		||||
        this.dots = []
 | 
			
		||||
        this.thumbBase64Code = val
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 处理关闭事件
 | 
			
		||||
       */
 | 
			
		||||
      handleCloseEvent() {
 | 
			
		||||
        this.$emit('close')
 | 
			
		||||
        // this.dots = []
 | 
			
		||||
        // this.imageBase64Code = ''
 | 
			
		||||
        // this.thumbBase64Code = ''
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 处理刷新事件
 | 
			
		||||
       */
 | 
			
		||||
      handleRefreshEvent() {
 | 
			
		||||
        this.dots = []
 | 
			
		||||
        this.$emit('refresh')
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 处理确认事件
 | 
			
		||||
       */
 | 
			
		||||
      handleConfirmEvent() {
 | 
			
		||||
        this.$emit('confirm', this.dots)
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 处理dot
 | 
			
		||||
       * @param ev
 | 
			
		||||
       */
 | 
			
		||||
      handleClickPos(ev) {
 | 
			
		||||
        if (this.dots.length >= this.maxDot) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const e = ev || window.event
 | 
			
		||||
        e.preventDefault()
 | 
			
		||||
        const dom = e.currentTarget
 | 
			
		||||
 | 
			
		||||
        const {domX, domY} = this.getDomXY(dom)
 | 
			
		||||
        // ===============================================
 | 
			
		||||
        // @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop
 | 
			
		||||
        // const domX = this.calcLocationLeft(dom)
 | 
			
		||||
        // const domY = this.calcLocationTop(dom)
 | 
			
		||||
        // ===============================================
 | 
			
		||||
        let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + document.body.offsetTop
 | 
			
		||||
        let mouseY = (navigator.vendor === 'Netscape') ? e.pageY : e.y + document.body.offsetTop
 | 
			
		||||
 | 
			
		||||
        if (this.calcPosType === 'screen') {
 | 
			
		||||
          mouseX = (navigator.vendor === 'Netscape') ? e.clientX : e.x
 | 
			
		||||
          mouseY = (navigator.vendor === 'Netscape') ? e.clientY : e.y
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 计算点击的相对位置
 | 
			
		||||
        const xPos = mouseX - domX
 | 
			
		||||
        const yPos = mouseY - domY
 | 
			
		||||
 | 
			
		||||
        // 转整形
 | 
			
		||||
        const xp = parseInt(xPos.toString())
 | 
			
		||||
        const yp = parseInt(yPos.toString())
 | 
			
		||||
 | 
			
		||||
        // 减去点的一半
 | 
			
		||||
        this.dots.push({
 | 
			
		||||
          x: xp - 11,
 | 
			
		||||
          y: yp - 11,
 | 
			
		||||
          index: this.dots.length + 1
 | 
			
		||||
        })
 | 
			
		||||
        return false
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 找到元素的屏幕位置
 | 
			
		||||
       * @param el
 | 
			
		||||
       */
 | 
			
		||||
      calcLocationLeft(el) {
 | 
			
		||||
        let tmp = el.offsetLeft
 | 
			
		||||
        let val = el.offsetParent
 | 
			
		||||
        while (val != null) {
 | 
			
		||||
          tmp += val.offsetLeft
 | 
			
		||||
          val = val.offsetParent
 | 
			
		||||
        }
 | 
			
		||||
        return tmp
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 找到元素的屏幕位置
 | 
			
		||||
       * @param el
 | 
			
		||||
       */
 | 
			
		||||
      calcLocationTop(el) {
 | 
			
		||||
        let tmp = el.offsetTop
 | 
			
		||||
        let val = el.offsetParent
 | 
			
		||||
        while (val != null) {
 | 
			
		||||
          tmp += val.offsetTop
 | 
			
		||||
          val = val.offsetParent
 | 
			
		||||
        }
 | 
			
		||||
        return tmp
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * @Description: 找到元素的屏幕位置
 | 
			
		||||
       * @param dom
 | 
			
		||||
       */
 | 
			
		||||
      getDomXY(dom){
 | 
			
		||||
        let x = 0
 | 
			
		||||
        let y = 0
 | 
			
		||||
        if (dom.getBoundingClientRect) {
 | 
			
		||||
          let box = dom.getBoundingClientRect();
 | 
			
		||||
          let D = document.documentElement;
 | 
			
		||||
          x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
 | 
			
		||||
          y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
 | 
			
		||||
        }
 | 
			
		||||
        else{
 | 
			
		||||
          while (dom !== document.body) {
 | 
			
		||||
            x += dom.offsetLeft
 | 
			
		||||
            y += dom.offsetTop
 | 
			
		||||
            dom = dom.offsetParent
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
          domX: x,
 | 
			
		||||
          domY: y
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  .wg-cap-wrap{
 | 
			
		||||
    background: #ffffff;
 | 
			
		||||
 | 
			
		||||
    -webkit-border-radius: 10px;
 | 
			
		||||
    -moz-border-radius: 10px;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
 | 
			
		||||
    -webkit-touch-callout: none;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    -moz-user-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header{
 | 
			
		||||
    height: 50px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
 | 
			
		||||
    display:-webkit-box;
 | 
			
		||||
    display:-webkit-flex;
 | 
			
		||||
    display:-ms-flexbox;
 | 
			
		||||
    display:flex;
 | 
			
		||||
    -webkit-box-align:center;
 | 
			
		||||
    -webkit-align-items:center;
 | 
			
		||||
    -ms-flex-align:center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header span{
 | 
			
		||||
    padding-right: 5px;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header span em{
 | 
			
		||||
    padding: 0 3px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    color: #3e7cff;
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header .wg-cap-wrap__image{
 | 
			
		||||
    -webkit-border-radius: 5px;
 | 
			
		||||
    -moz-border-radius: 5px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header .wg-cap-wrap__thumb{
 | 
			
		||||
    min-width: 150px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__header .wg-cap-wrap__thumb.wg-cap-wrap__hidden{
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wg-cap-wrap__body{
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    display: -moz-box;
 | 
			
		||||
    display: -ms-flexbox;
 | 
			
		||||
    display: -webkit-flex;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    background: #34383e;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    -webkit-border-radius: 5px;
 | 
			
		||||
    -moz-border-radius: 5px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__body .wg-cap-wrap__picture{
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    /*height: 100%;*/
 | 
			
		||||
    /*max-width: 100%;*/
 | 
			
		||||
    /*max-height: 100%;*/
 | 
			
		||||
    /*object-fit: cover;*/
 | 
			
		||||
    /*text-align: center;*/
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__body .wg-cap-wrap__picture.wg-cap-wrap__hidden{
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__body .wg-cap-wrap__loading{
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 9;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    width: 68px;
 | 
			
		||||
    height: 68px;
 | 
			
		||||
    margin-left: -34px;
 | 
			
		||||
    margin-top: -34px;
 | 
			
		||||
    line-height: 68px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__body .wg-cap-wrap__dot{
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    width: 22px;
 | 
			
		||||
    height: 22px;
 | 
			
		||||
    color: #cedffe;
 | 
			
		||||
    background: #3e7cff;
 | 
			
		||||
    border: 2px solid #f7f9fb;
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    -webkit-border-radius: 22px;
 | 
			
		||||
    -moz-border-radius: 22px;
 | 
			
		||||
    border-radius: 22px;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wg-cap-wrap__footer {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    color: #34383e;
 | 
			
		||||
    display:-webkit-box;
 | 
			
		||||
    display:-webkit-flex;
 | 
			
		||||
    display:-ms-flexbox;
 | 
			
		||||
    display:flex;
 | 
			
		||||
    -webkit-box-align:center;
 | 
			
		||||
    -webkit-align-items:center;
 | 
			
		||||
    -ms-flex-align:center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding-top: 15px;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__footer .wg-cap-wrap__ico{
 | 
			
		||||
    flex: 1;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__footer .wg-cap-wrap__ico img{
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 24px;
 | 
			
		||||
    color: #34383e;
 | 
			
		||||
    margin: 0 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__footer .wg-cap-wrap__btn{
 | 
			
		||||
    width: 120px;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__footer .wg-cap-wrap__btn button{
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    letter-spacing: 2px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    padding: 9px 15px;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
    -webkit-border-radius: 5px;
 | 
			
		||||
    -moz-border-radius: 5px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    background-color: #409eff;
 | 
			
		||||
    border: 1px solid #409eff;
 | 
			
		||||
    -webkit-appearance: none;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    transition: .1s;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    -moz-user-select: none;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
  }
 | 
			
		||||
  .wg-cap-wrap__footer .wg-cap-wrap__btn button:hover {
 | 
			
		||||
    background: #66b1ff;
 | 
			
		||||
    border-color: #66b1ff;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,16 +1,40 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-button type="primary" :disabled="!canSend" :size="props.size" @click="sendMsg" plain>{{
 | 
			
		||||
      btnText
 | 
			
		||||
    }}
 | 
			
		||||
  </el-button>
 | 
			
		||||
  <el-container>
 | 
			
		||||
    <el-popover
 | 
			
		||||
        :visible="showCaptcha"
 | 
			
		||||
        :hide-after="0"
 | 
			
		||||
        placement="top"
 | 
			
		||||
        :width="325"
 | 
			
		||||
        trigger="click"
 | 
			
		||||
        content="this is content, this is content, this is content"
 | 
			
		||||
    >
 | 
			
		||||
      <captcha-plus
 | 
			
		||||
          v-if="showCaptcha"
 | 
			
		||||
          :max-dot="maxDot"
 | 
			
		||||
          :image-base64="imageBase64"
 | 
			
		||||
          :thumb-base64="thumbBase64"
 | 
			
		||||
          @close="showCaptcha = false"
 | 
			
		||||
          @refresh="handleRequestCaptCode"
 | 
			
		||||
          @confirm="handleConfirm"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <template #reference>
 | 
			
		||||
        <el-button type="primary" :size="props.size" :disabled="!canSend" @click="loadCaptcha" plain>
 | 
			
		||||
          {{ btnText }}
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-popover>
 | 
			
		||||
  </el-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
// 发送短信验证码组件
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import lodash from 'lodash'
 | 
			
		||||
import {validateMobile} from "@/utils/validate";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import CaptchaPlus from "@/components/CaptchaPlus.vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  mobile: String,
 | 
			
		||||
@@ -18,37 +42,80 @@ const props = defineProps({
 | 
			
		||||
});
 | 
			
		||||
const btnText = ref('发送验证码')
 | 
			
		||||
const canSend = ref(true)
 | 
			
		||||
const showCaptcha = ref(false)
 | 
			
		||||
const maxDot = ref(5)
 | 
			
		||||
const imageBase64 = ref('')
 | 
			
		||||
const thumbBase64 = ref('')
 | 
			
		||||
const captKey = ref('')
 | 
			
		||||
const dots = ref(null)
 | 
			
		||||
 | 
			
		||||
const handleRequestCaptCode = () => {
 | 
			
		||||
 | 
			
		||||
  httpGet('/api/captcha/get').then(res => {
 | 
			
		||||
    const data = res.data
 | 
			
		||||
    console.log(res)
 | 
			
		||||
    imageBase64.value = data.image
 | 
			
		||||
    thumbBase64.value = data.thumb
 | 
			
		||||
    captKey.value = data.key
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error('获取人机验证数据失败:' + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleConfirm = (dots) => {
 | 
			
		||||
  if (lodash.size(dots) <= 0) {
 | 
			
		||||
    return ElMessage.error('请进行人机验证再操作')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let dotArr = []
 | 
			
		||||
  lodash.forEach(dots, (dot) => {
 | 
			
		||||
    dotArr.push(dot.x, dot.y)
 | 
			
		||||
  })
 | 
			
		||||
  dots.value = dotArr.join(',')
 | 
			
		||||
  httpPost('/api/captcha/check', {
 | 
			
		||||
    dots: dots.value,
 | 
			
		||||
    key: captKey.value
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    showCaptcha.value = false
 | 
			
		||||
    sendMsg()
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error('人机验证失败')
 | 
			
		||||
    handleRequestCaptCode()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loadCaptcha = () => {
 | 
			
		||||
  if (!validateMobile(props.mobile)) {
 | 
			
		||||
    return ElMessage.error("请输入合法的手机号")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showCaptcha.value = true
 | 
			
		||||
  handleRequestCaptCode() // 每次点开都刷新验证码
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const sendMsg = () => {
 | 
			
		||||
  if (!canSend.value) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!validateMobile(props.mobile)) {
 | 
			
		||||
    return ElMessage.error("请输入合法的手机号")
 | 
			
		||||
  }
 | 
			
		||||
  canSend.value = false
 | 
			
		||||
  httpGet('/api/verify/token').then(res => {
 | 
			
		||||
    httpPost('/api/verify/sms', {token: res.data, mobile: props.mobile}).then(() => {
 | 
			
		||||
      ElMessage.success('短信发送成功')
 | 
			
		||||
      let time = 120
 | 
			
		||||
      btnText.value = time
 | 
			
		||||
      const handler = setInterval(() => {
 | 
			
		||||
        time = time - 1
 | 
			
		||||
        if (time <= 0) {
 | 
			
		||||
          clearInterval(handler)
 | 
			
		||||
          btnText.value = '重新发送'
 | 
			
		||||
          canSend.value = true
 | 
			
		||||
        } else {
 | 
			
		||||
          btnText.value = time
 | 
			
		||||
        }
 | 
			
		||||
      }, 1000)
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      canSend.value = true
 | 
			
		||||
      ElMessage.error('短信发送失败:' + e.message)
 | 
			
		||||
    })
 | 
			
		||||
  httpPost('/api/sms/code', {mobile: props.mobile, key: captKey.value, dots: dots.value}).then(() => {
 | 
			
		||||
    ElMessage.success('短信发送成功')
 | 
			
		||||
    let time = 120
 | 
			
		||||
    btnText.value = time
 | 
			
		||||
    const handler = setInterval(() => {
 | 
			
		||||
      time = time - 1
 | 
			
		||||
      if (time <= 0) {
 | 
			
		||||
        clearInterval(handler)
 | 
			
		||||
        btnText.value = '重新发送'
 | 
			
		||||
        canSend.value = true
 | 
			
		||||
      } else {
 | 
			
		||||
        btnText.value = time
 | 
			
		||||
      }
 | 
			
		||||
    }, 1000)
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    console.log('failed to fetch token: ' + e.message)
 | 
			
		||||
    canSend.value = true
 | 
			
		||||
    ElMessage.error('短信发送失败:' + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
              </el-input>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="block">
 | 
			
		||||
            <div class="block" v-if="enableMsg">
 | 
			
		||||
              <el-input placeholder="手机号码"
 | 
			
		||||
                        size="large" maxlength="11"
 | 
			
		||||
                        v-model="formData.mobile"
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
              </el-input>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="block">
 | 
			
		||||
            <div class="block" v-if="enableMsg">
 | 
			
		||||
              <el-row :gutter="10">
 | 
			
		||||
                <el-col :span="12">
 | 
			
		||||
                  <el-input placeholder="手机验证码"
 | 
			
		||||
@@ -104,7 +104,7 @@
 | 
			
		||||
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {Checked, Iphone, Lock, UserFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpPost} from "@/utils/http";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import FooterBar from "@/components/FooterBar.vue";
 | 
			
		||||
@@ -120,6 +120,13 @@ const formData = ref({
 | 
			
		||||
  repass: '',
 | 
			
		||||
})
 | 
			
		||||
const formRef = ref(null)
 | 
			
		||||
const enableMsg = ref(false)
 | 
			
		||||
 | 
			
		||||
httpGet('/api/sms/status').then(res => {
 | 
			
		||||
  if (res.data === true) {
 | 
			
		||||
    enableMsg.value = true
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const register = function () {
 | 
			
		||||
  if (formData.value.username.length < 4) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user