
diff --git a/README.md b/README.md
index d421442c..1b6191b9 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,6 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
-## 最新版本一键部署脚本
-目前仅支持 Ubuntu 和 Centos 系统。
-```shell
-bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.3-8b588904ef.sh)"
-```
-
## 功能截图
### PC 端聊天界面
@@ -69,11 +63,30 @@ bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.3-8b588904ef.sh)"


-### 7. 体验地址
+### 体验地址
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat)
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
+## 快速部署
+**演示站不提供任何充值点卡售卖或者VIP充值服务。** 如果您体验过后觉得还不错的话,可以花两分钟用下面的一键部署脚本自己部署一套。
+```shell
+bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.3-8b588904ef.sh)"
+```
+目前仅支持 Ubuntu 和 Centos 系统。 部署成功之后可以访问下面地址
+
+* 前端访问地址:http://localhost:8080/chat 使用移动设备访问会自动跳转到移动端页面。
+* 后台管理地址:http://localhost:8080/admin
+* 移动端地址:http://localhost:8080/mobile
+* 初始后台管理账号:admin/admin123
+* 初始前端体验账号:18575670125/12345678
+
+服务启动成功之后不能立刻使用,需要先登录管理后台 -> API-KEY 去添加一个 OpenAI 或者文心一言,科大讯飞等至少一个平台的 API KEY。
+
+
+
+另外,如果您目前还没有 OpenAI 的 API KEY的,推荐您去 https://gpt.bemore.lol 购买,**无需魔法,高速稳定,且价格还远低于 OpenAI 官方**。
+
## 使用须知
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
diff --git a/api/core/types/config.go b/api/core/types/config.go
index 54f3e24d..ca11d3a0 100644
--- a/api/core/types/config.go
+++ b/api/core/types/config.go
@@ -24,6 +24,15 @@ type AppConfig struct {
XXLConfig XXLConfig
AlipayConfig AlipayConfig
HuPiPayConfig HuPiPayConfig
+ SmtpConfig SmtpConfig // 邮件发送配置
+}
+
+type SmtpConfig struct {
+ Host string
+ Port int
+ AppName string // 应用名称
+ From string // 发件人邮箱地址
+ Password string // 发件人邮箱密码
}
type ChatPlusApiConfig struct {
diff --git a/api/handler/admin/reward_handler.go b/api/handler/admin/reward_handler.go
index 61f65696..def4cf87 100644
--- a/api/handler/admin/reward_handler.go
+++ b/api/handler/admin/reward_handler.go
@@ -46,7 +46,7 @@ func (h *RewardHandler) List(c *gin.Context) {
}
r.Id = v.Id
- r.Username = userMap[v.UserId].Mobile
+ r.Username = userMap[v.UserId].Username
r.CreatedAt = v.CreatedAt.Unix()
r.UpdatedAt = v.UpdatedAt.Unix()
rewards = append(rewards, r)
diff --git a/api/handler/admin/user_handler.go b/api/handler/admin/user_handler.go
index 26f3293d..60f18c99 100644
--- a/api/handler/admin/user_handler.go
+++ b/api/handler/admin/user_handler.go
@@ -27,7 +27,7 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
- mobile := h.GetTrim(c, "mobile")
+ username := h.GetTrim(c, "username")
offset := (page - 1) * pageSize
var items []model.User
@@ -35,8 +35,8 @@ func (h *UserHandler) List(c *gin.Context) {
var total int64
session := h.db.Session(&gorm.Session{})
- if mobile != "" {
- session = session.Where("mobile LIKE ?", "%"+mobile+"%")
+ if username != "" {
+ session = session.Where("username LIKE ?", "%"+username+"%")
}
session.Model(&model.User{}).Count(&total)
@@ -95,7 +95,7 @@ func (h *UserHandler) Save(c *gin.Context) {
} else {
salt := utils.RandString(8)
u := model.User{
- Mobile: data.Mobile,
+ Username: data.Mobile,
Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png",
Salt: salt,
diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go
index 6b1bf80e..03512333 100644
--- a/api/handler/chatimpl/chat_handler.go
+++ b/api/handler/chatimpl/chat_handler.go
@@ -80,7 +80,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session = &types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
- Username: user.Mobile,
+ Username: user.Username,
UserId: user.Id,
}
h.App.ChatSession.Put(sessionId, session)
diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go
index 4a940cee..422f133e 100644
--- a/api/handler/payment_handler.go
+++ b/api/handler/payment_handler.go
@@ -204,7 +204,7 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) {
}
order := model.Order{
UserId: user.Id,
- Mobile: user.Mobile,
+ Mobile: user.Username,
ProductId: product.Id,
OrderNo: orderNo,
Subject: product.Name,
diff --git a/api/handler/sms_handler.go b/api/handler/sms_handler.go
index 9ccb4e3d..bd33ae3d 100644
--- a/api/handler/sms_handler.go
+++ b/api/handler/sms_handler.go
@@ -8,6 +8,7 @@ import (
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
+ "strings"
)
const CodeStorePrefix = "/verify/codes/"
@@ -16,21 +17,27 @@ type SmsHandler struct {
BaseHandler
redis *redis.Client
sms *service.AliYunSmsService
+ smtp *service.SmtpService
captcha *service.CaptchaService
}
-func NewSmsHandler(app *core.AppServer, client *redis.Client, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
- handler := &SmsHandler{redis: client, sms: sms, captcha: captcha}
+func NewSmsHandler(
+ app *core.AppServer,
+ client *redis.Client,
+ sms *service.AliYunSmsService,
+ smtp *service.SmtpService,
+ captcha *service.CaptchaService) *SmsHandler {
+ handler := &SmsHandler{redis: client, sms: sms, captcha: captcha, smtp: smtp}
handler.App = app
return handler
}
-// SendCode 发送验证码短信
+// SendCode 发送验证码
func (h *SmsHandler) SendCode(c *gin.Context) {
var data struct {
- Mobile string `json:"mobile"`
- Key string `json:"key"`
- Dots string `json:"dots"`
+ Receiver string `json:"receiver"` // 接收者
+ Key string `json:"key"`
+ Dots string `json:"dots"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -43,14 +50,19 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
}
code := utils.RandomNumber(6)
- err := h.sms.SendVerifyCode(data.Mobile, code)
+ var err error
+ if strings.Contains(data.Receiver, "@") { // email
+ err = h.smtp.SendVerifyCode(data.Receiver, code)
+ } else {
+ err = h.sms.SendVerifyCode(data.Receiver, code)
+ }
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 存储验证码,等待后面注册验证
- _, err = h.redis.Set(c, CodeStorePrefix+data.Mobile, code, 0).Result()
+ _, err = h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0).Result()
if err != nil {
resp.ERROR(c, "验证码保存失败")
return
diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go
index 152924f9..8f90af5b 100644
--- a/api/handler/user_handler.go
+++ b/api/handler/user_handler.go
@@ -39,7 +39,7 @@ func NewUserHandler(
func (h *UserHandler) Register(c *gin.Context) {
// parameters process
var data struct {
- Mobile string `json:"mobile"`
+ Username string `json:"username"`
Password string `json:"password"`
Code string `json:"code"`
InviteCode string `json:"invite_code"`
@@ -49,21 +49,16 @@ func (h *UserHandler) Register(c *gin.Context) {
return
}
data.Password = strings.TrimSpace(data.Password)
-
- if len(data.Mobile) < 10 {
- resp.ERROR(c, "请输入合法的手机号")
- return
- }
if len(data.Password) < 8 {
resp.ERROR(c, "密码长度不能少于8个字符")
return
}
// 检查验证码
- key := CodeStorePrefix + data.Mobile
+ key := CodeStorePrefix + data.Username
code, err := h.redis.Get(c, key).Result()
if err != nil || code != data.Code {
- resp.ERROR(c, "短信验证码错误")
+ resp.ERROR(c, "验证码错误")
return
}
@@ -79,20 +74,20 @@ func (h *UserHandler) Register(c *gin.Context) {
// check if the username is exists
var item model.User
- res := h.db.Where("mobile = ?", data.Mobile).First(&item)
+ res := h.db.Where("username = ?", data.Username).First(&item)
if res.RowsAffected > 0 {
- resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
+ resp.ERROR(c, "该用户名已经被注册")
return
}
salt := utils.RandString(8)
user := model.User{
+ Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(6)),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
- Mobile: data.Mobile,
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
ChatConfig: utils.JsonEncode(types.UserChatConfig{
@@ -105,6 +100,7 @@ func (h *UserHandler) Register(c *gin.Context) {
Calls: h.App.SysConfig.InitChatCalls,
ImgCalls: h.App.SysConfig.InitImgCalls,
}
+
res = h.db.Create(&user)
if res.Error != nil {
resp.ERROR(c, "保存数据失败")
@@ -127,7 +123,7 @@ func (h *UserHandler) Register(c *gin.Context) {
h.db.Create(&model.InviteLog{
InviterId: inviteCode.UserId,
UserId: user.Id,
- Username: user.Mobile,
+ Username: user.Username,
InviteCode: inviteCode.Code,
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}),
})
@@ -157,7 +153,7 @@ func (h *UserHandler) Register(c *gin.Context) {
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var data struct {
- Mobile string `json:"username"`
+ Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&data); err != nil {
@@ -165,7 +161,7 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
var user model.User
- res := h.db.Where("mobile = ?", data.Mobile).First(&user)
+ res := h.db.Where("username = ?", data.Username).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户名不存在")
return
@@ -189,7 +185,7 @@ func (h *UserHandler) Login(c *gin.Context) {
h.db.Create(&model.UserLoginLog{
UserId: user.Id,
- Username: user.Mobile,
+ Username: user.Username,
LoginIp: c.ClientIP(),
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
})
@@ -250,7 +246,7 @@ func (h *UserHandler) Session(c *gin.Context) {
type userProfile struct {
Id uint `json:"id"`
Nickname string `json:"nickname"`
- Mobile string `json:"mobile"`
+ Username string `json:"username"`
Avatar string `json:"avatar"`
ChatConfig types.UserChatConfig `json:"chat_config"`
Calls int `json:"calls"`
@@ -348,9 +344,9 @@ func (h *UserHandler) UpdatePass(c *gin.Context) {
// ResetPass 重置密码
func (h *UserHandler) ResetPass(c *gin.Context) {
var data struct {
- Mobile string
- Code string // 验证码
- Password string // 新密码
+ Username string `json:"username"`
+ Code string `json:"code"` // 验证码
+ Password string `json:"password"` // 新密码
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -358,14 +354,14 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
}
var user model.User
- res := h.db.Where("mobile", data.Mobile).First(&user)
+ res := h.db.Where("username", data.Username).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户不存在!")
return
}
// 检查验证码
- key := CodeStorePrefix + data.Mobile
+ key := CodeStorePrefix + data.Username
code, err := h.redis.Get(c, key).Result()
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
@@ -386,8 +382,8 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
// BindMobile 绑定手机号
func (h *UserHandler) BindMobile(c *gin.Context) {
var data struct {
- Mobile string `json:"mobile"`
- Code string `json:"code"`
+ Username string `json:"username"`
+ Code string `json:"code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -395,7 +391,7 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
}
// 检查验证码
- key := CodeStorePrefix + data.Mobile
+ key := CodeStorePrefix + data.Username
code, err := h.redis.Get(c, key).Result()
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
@@ -404,7 +400,7 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
// 检查手机号是否被其他账号绑定
var item model.User
- res := h.db.Where("mobile = ?", data.Mobile).First(&item)
+ res := h.db.Where("username = ?", data.Username).First(&item)
if res.Error == nil {
resp.ERROR(c, "该手机号已经被其他账号绑定")
return
@@ -416,7 +412,7 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
return
}
- res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
+ res = h.db.Model(&user).UpdateColumn("username", data.Username)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
diff --git a/api/main.go b/api/main.go
index c02816c4..d0508210 100644
--- a/api/main.go
+++ b/api/main.go
@@ -142,6 +142,9 @@ func main() {
fx.Provide(oss.NewUploaderManager),
fx.Provide(mj.NewService),
+ // 邮件服务
+ fx.Provide(service.NewSmtpService),
+
// 微信机器人服务
fx.Provide(wx.NewWeChatBot),
fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
diff --git a/api/service/aliyun_sms_service.go b/api/service/aliyun_sms_service.go
index 582b34c5..d3414ee6 100644
--- a/api/service/aliyun_sms_service.go
+++ b/api/service/aliyun_sms_service.go
@@ -49,3 +49,5 @@ func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
return nil
}
+
+var _ SmsService = &AliYunSmsService{}
diff --git a/api/service/smtp_sms_service.go b/api/service/smtp_sms_service.go
new file mode 100644
index 00000000..fe094d49
--- /dev/null
+++ b/api/service/smtp_sms_service.go
@@ -0,0 +1,44 @@
+package service
+
+import (
+ "bytes"
+ "chatplus/core/types"
+ "fmt"
+ "mime"
+ "net/smtp"
+)
+
+type SmtpService struct {
+ config *types.SmtpConfig
+}
+
+func NewSmtpService(appConfig *types.AppConfig) *SmtpService {
+ return &SmtpService{
+ config: &appConfig.SmtpConfig,
+ }
+}
+
+func (s *SmtpService) SendVerifyCode(to string, code int) error {
+ subject := "ChatPlus注册验证码"
+ body := fmt.Sprintf("您正在注册 ChatPlus AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
+
+ // 设置SMTP客户端配置
+ auth := smtp.PlainAuth("", s.config.From, s.config.Password, s.config.Host)
+
+ // 对主题进行MIME编码
+ encodedSubject := mime.QEncoding.Encode("UTF-8", subject)
+ // 组装邮件
+ message := bytes.NewBuffer(nil)
+ message.WriteString(fmt.Sprintf("From: \"%s\" <%s>\r\n", s.config.AppName, s.config.From))
+ message.WriteString(fmt.Sprintf("To: %s\r\n", to))
+ message.WriteString(fmt.Sprintf("Subject: %s\r\n", encodedSubject))
+ message.WriteString("\r\n" + body)
+
+ // 发送邮件
+ // 发送邮件
+ err := smtp.SendMail(s.config.Host+":"+fmt.Sprint(s.config.Port), auth, s.config.From, []string{to}, message.Bytes())
+ if err != nil {
+ return fmt.Errorf("error sending email: %v", err)
+ }
+ return nil
+}
diff --git a/api/service/xxl_job_service.go b/api/service/xxl_job_service.go
index 5ff11462..222cbefa 100644
--- a/api/service/xxl_job_service.go
+++ b/api/service/xxl_job_service.go
@@ -96,7 +96,7 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
for _, u := range users {
// 账号到期,直接清零
if u.ExpiredTime <= currentTime.Unix() {
- logger.Info("账号过期:", u.Mobile)
+ logger.Info("账号过期:", u.Username)
u.Calls = 0
u.Vip = false
} else {
@@ -133,7 +133,7 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
} else {
u.ImgCalls = u.ImgCalls + config.VipMonthImgCalls
}
- logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
+ logger.Infof("%s 点卡结余:%d", u.Username, calls)
}
u.Tokens = 0
// update user
diff --git a/api/store/model/user.go b/api/store/model/user.go
index f907599b..fb2dfcc2 100644
--- a/api/store/model/user.go
+++ b/api/store/model/user.go
@@ -2,7 +2,7 @@ package model
type User struct {
BaseModel
- Mobile string
+ Username string
Nickname string
Password string
Avatar string
diff --git a/api/store/vo/user.go b/api/store/vo/user.go
index a286c328..b8807d25 100644
--- a/api/store/vo/user.go
+++ b/api/store/vo/user.go
@@ -4,7 +4,7 @@ import "chatplus/core/types"
type User struct {
BaseVo
- Mobile string `json:"mobile"`
+ Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐
diff --git a/api/test/test.go b/api/test/test.go
index 2fb3b157..79058077 100644
--- a/api/test/test.go
+++ b/api/test/test.go
@@ -1,10 +1,5 @@
package main
-import (
- "chatplus/utils"
- "fmt"
-)
-
func main() {
- fmt.Println(utils.RandString(64))
+
}
diff --git a/database/update-v3.2.4.sql b/database/update-v3.2.4.sql
index 7644a093..d0324ddc 100644
--- a/database/update-v3.2.4.sql
+++ b/database/update-v3.2.4.sql
@@ -5,4 +5,4 @@ ALTER TABLE `chatgpt_api_keys` DROP INDEX `value`;
ALTER TABLE `chatgpt_mj_jobs` ADD UNIQUE(`task_id`);
ALTER TABLE `chatgpt_api_keys` ADD `use_proxy` TINYINT(1) NULL COMMENT '是否使用代理访问' AFTER `enabled`;
ALTER TABLE `chatgpt_api_keys` ADD `name` VARCHAR(30) NULL COMMENT '名称' AFTER `platform`;
-ALTER TABLE `chatgpt_users` ADD `email` VARCHAR(100) NULL COMMENT '邮箱地址' AFTER `mobile`;
\ No newline at end of file
+ALTER TABLE `chatgpt_users` CHANGE `mobile` `username` VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名';
\ No newline at end of file
diff --git a/web/src/components/ConfigDialog.vue b/web/src/components/ConfigDialog.vue
index 4fed0a0e..5790688b 100644
--- a/web/src/components/ConfigDialog.vue
+++ b/web/src/components/ConfigDialog.vue
@@ -10,7 +10,7 @@