diff --git a/api/config.sample.toml b/api/config.sample.toml
index b1337cb1..80d99d1f 100644
--- a/api/config.sample.toml
+++ b/api/config.sample.toml
@@ -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"
diff --git a/api/core/config.go b/api/core/config.go
index 83f00f6e..2c8a51d2 100644
--- a/api/core/config.go
+++ b/api/core/config.go
@@ -33,7 +33,7 @@ func NewDefaultConfig() *types.AppConfig {
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
- Func: types.FunctionApiConfig{},
+ ApiConfig: types.ChatPlusApiConfig{},
StartWechatBot: false,
}
}
diff --git a/api/core/types/config.go b/api/core/types/config.go
index f3c01c2a..045ea758 100644
--- a/api/core/types/config.go
+++ b/api/core/types/config.go
@@ -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
diff --git a/api/handler/captcha_handler.go b/api/handler/captcha_handler.go
new file mode 100644
index 00000000..e0ffbfe9
--- /dev/null
+++ b/api/handler/captcha_handler.go
@@ -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)
+ }
+
+}
diff --git a/api/handler/sms_handler.go b/api/handler/sms_handler.go
new file mode 100644
index 00000000..c5fe92b1
--- /dev/null
+++ b/api/handler/sms_handler.go
@@ -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)
+}
diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go
index afc35321..1bdb4cf1 100644
--- a/api/handler/user_handler.go
+++ b/api/handler/user_handler.go
@@ -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)
}
diff --git a/api/handler/verify_handler.go b/api/handler/verify_handler.go
deleted file mode 100644
index 1154169d..00000000
--- a/api/handler/verify_handler.go
+++ /dev/null
@@ -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)
-}
diff --git a/api/main.go b/api/main.go
index 8a23e57b..595f8532 100644
--- a/api/main.go
+++ b/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/")
diff --git a/api/service/captcha_service.go b/api/service/captcha_service.go
new file mode 100644
index 00000000..c4b784ac
--- /dev/null
+++ b/api/service/captcha_service.go
@@ -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
+}
diff --git a/api/service/function/tou_tiao.go b/api/service/function/tou_tiao.go
index 54638dad..c0092d07 100644
--- a/api/service/function/tou_tiao.go
+++ b/api/service/function/tou_tiao.go
@@ -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,
diff --git a/api/service/function/weibo_hot.go b/api/service/function/weibo_hot.go
index a7870be5..f8d830aa 100644
--- a/api/service/function/weibo_hot.go
+++ b/api/service/function/weibo_hot.go
@@ -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,
diff --git a/api/service/function/zao_bao.go b/api/service/function/zao_bao.go
index c04e57f1..7218dd79 100644
--- a/api/service/function/zao_bao.go
+++ b/api/service/function/zao_bao.go
@@ -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,
diff --git a/docker/conf/config.toml b/docker/conf/config.toml
index b1337cb1..80d99d1f 100644
--- a/docker/conf/config.toml
+++ b/docker/conf/config.toml
@@ -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"
diff --git a/web/package-lock.json b/web/package-lock.json
index 7ddbdee1..e03388d5 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -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",
diff --git a/web/package.json b/web/package.json
index 898238ed..7974766b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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": {
diff --git a/web/src/components/CaptchaPlus.vue b/web/src/components/CaptchaPlus.vue
new file mode 100644
index 00000000..f9084793
--- /dev/null
+++ b/web/src/components/CaptchaPlus.vue
@@ -0,0 +1,369 @@
+
+
+
+
+
+
![]()
+

+
+ {{ dot.index }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/SendMsg.vue b/web/src/components/SendMsg.vue
index 7abb5b8b..092ccc62 100644
--- a/web/src/components/SendMsg.vue
+++ b/web/src/components/SendMsg.vue
@@ -1,16 +1,40 @@
- {{
- btnText
- }}
-
+
+
+
+
+
+
+ {{ btnText }}
+
+
+
+