@@ -141,6 +152,20 @@ onMounted(() => {
margin: 12px 16px;
}
+.preview p span {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ padding: 4px 6px;
+ margin: 0 4px;
+ font-size: 14px;
+ transition: .5s;
+ cursor: pointer;
+}
+
+.preview p span:hover {
+ background: rgba(0, 0, 0, 0.4);
+}
+
.time {
color: var(--card-text-secondary);
font-size: 16px;
diff --git a/app/src/views/LoginView.vue b/app/src/views/LoginView.vue
index a3fed22..e9b28c0 100644
--- a/app/src/views/LoginView.vue
+++ b/app/src/views/LoginView.vue
@@ -9,7 +9,7 @@ const message = ref("登录中...");
onMounted(async () => {
const url = new URL(location.href);
const client = url.searchParams.get("token");
- if (!client) location.href = "https://deeptrain.net/login?app=chatnio";
+ if (!client) location.href = "https://deeptrain.lightxi.com/login?app=chatnio";
try {
const res = await axios.post("/login", {
diff --git a/app/src/views/SettingsView.vue b/app/src/views/SettingsView.vue
new file mode 100644
index 0000000..826f028
--- /dev/null
+++ b/app/src/views/SettingsView.vue
@@ -0,0 +1,494 @@
+
+
+
+
+
+
+
账户
+
+
![]()
+
{{ username }}
+
+
+
+
+
配额
+
+
+ Deeptrain 钱包
+ {{ info.balance }} ¥
+
+
+
+ GPT-4 配额
+ {{ info.gpt4 }} 次
+
+
+
+ DALL-E 配额
+ {{ info.dalle }} 次
+
+
+
购买
+
+
+
+
礼包
+
tip: 首次领取后,刷新后即可领取配额
+
+ 实名认证礼包
+
+
+
+
+
+ 青少年礼包
+
+
+
+
+
+
+
+
+
diff --git a/auth/cert.go b/auth/cert.go
new file mode 100644
index 0000000..28eba0b
--- /dev/null
+++ b/auth/cert.go
@@ -0,0 +1,31 @@
+package auth
+
+import (
+ "chat/utils"
+ "encoding/json"
+ "github.com/spf13/viper"
+)
+
+type CertResponse struct {
+ Status bool `json:"status" required:"true"`
+ Cert bool `json:"cert"`
+ Teenager bool `json:"teenager"`
+}
+
+func Cert(username string) *CertResponse {
+ res, err := utils.Post("https://api.deeptrain.net/app/cert", map[string]string{
+ "Content-Type": "application/json",
+ }, map[string]interface{}{
+ "password": viper.GetString("auth.access"),
+ "user": username,
+ "hash": utils.Sha2Encrypt(username + viper.GetString("auth.salt")),
+ })
+
+ if err != nil || res == nil || res.(map[string]interface{})["status"] == false {
+ return nil
+ }
+
+ converter, _ := json.Marshal(res)
+ resp, _ := utils.Unmarshal[CertResponse](converter)
+ return &resp
+}
diff --git a/auth/controller.go b/auth/controller.go
new file mode 100644
index 0000000..e2d8416
--- /dev/null
+++ b/auth/controller.go
@@ -0,0 +1,101 @@
+package auth
+
+import (
+ "chat/utils"
+ "database/sql"
+ "github.com/gin-gonic/gin"
+)
+
+type BuyForm struct {
+ Type string `json:"type" binding:"required"`
+ Quota int `json:"quota" binding:"required"`
+}
+
+func GetUserByCtx(c *gin.Context) *User {
+ user := c.MustGet("user").(string)
+ if len(user) == 0 {
+ c.JSON(200, gin.H{
+ "status": false,
+ "error": "user not found",
+ })
+ return nil
+ }
+
+ return &User{
+ Username: user,
+ }
+}
+
+func PackageAPI(c *gin.Context) {
+ user := GetUserByCtx(c)
+ if user == nil {
+ return
+ }
+
+ db := utils.GetDBFromContext(c)
+ c.JSON(200, gin.H{
+ "status": true,
+ "data": RefreshPackage(db, user),
+ })
+}
+
+func GetUsageAPI(c *gin.Context) {
+ user := GetUserByCtx(c)
+ if user == nil {
+ return
+ }
+
+ db := utils.GetDBFromContext(c)
+ c.JSON(200, gin.H{
+ "status": true,
+ "data": UsageAPI(db, user),
+ "balance": GetBalance(user.Username),
+ })
+}
+
+func PayResponse(c *gin.Context, db *sql.DB, user *User, state bool) {
+ if state {
+ c.JSON(200, gin.H{
+ "status": true,
+ "data": UsageAPI(db, user),
+ })
+ } else {
+ c.JSON(200, gin.H{
+ "status": false,
+ "error": "not enough money",
+ })
+ }
+}
+
+func BuyAPI(c *gin.Context) {
+ user := GetUserByCtx(c)
+ if user == nil {
+ return
+ }
+
+ db := utils.GetDBFromContext(c)
+ var form BuyForm
+ if err := c.ShouldBindJSON(&form); err != nil {
+ c.JSON(200, gin.H{
+ "status": false,
+ "error": err.Error(),
+ })
+ return
+ }
+
+ if form.Quota <= 0 || form.Quota > 50000 {
+ c.JSON(200, gin.H{
+ "status": false,
+ "error": "invalid quota range (1 ~ 50000)",
+, })
+ return
+ }
+
+ if form.Type == "dalle" {
+ PayResponse(c, db, user, BuyDalle(db, user, form.Quota))
+ } else if form.Type == "gpt4" {
+ PayResponse(c, db, user, BuyGPT4(db, user, form.Quota))
+ } else {
+ c.JSON(200, gin.H{"status": false, "error": "unknown type"})
+ }
+}
diff --git a/auth/package.go b/auth/package.go
new file mode 100644
index 0000000..ca9530e
--- /dev/null
+++ b/auth/package.go
@@ -0,0 +1,67 @@
+package auth
+
+import "database/sql"
+
+type GiftResponse struct {
+ Cert bool `json:"cert"`
+ Teenager bool `json:"teenager"`
+}
+
+func NewPackage(db *sql.DB, user *User, _t string) bool {
+ id := user.GetID(db)
+
+ var count int
+ if err := db.QueryRow(`SELECT COUNT(*) FROM package where user_id = ? AND type = ?`, id, _t).Scan(&count); err != nil {
+ return false
+ }
+
+ if count > 0 {
+ return false
+ }
+
+ _ = db.QueryRow(`INSERT INTO package (user_id, type) VALUES (?, ?)`, id, _t)
+ return true
+}
+
+func NewCertPackage(db *sql.DB, user *User) bool {
+ res := NewPackage(db, user, "cert")
+ if !res {
+ return false
+ }
+
+ IncreaseGPT4(db, user, 1)
+ IncreaseDalle(db, user, 20)
+
+ return true
+}
+
+func NewTeenagerPackage(db *sql.DB, user *User) bool {
+ res := NewPackage(db, user, "teenager")
+ if !res {
+ return false
+ }
+
+ IncreaseGPT4(db, user, 3)
+ IncreaseDalle(db, user, 100)
+
+ return true
+}
+
+func RefreshPackage(db *sql.DB, user *User) *GiftResponse {
+ resp := Cert(user.Username)
+ if resp == nil || resp.Status == false {
+ return nil
+ }
+
+ if resp.Cert {
+ NewCertPackage(db, user)
+ }
+ if resp.Teenager {
+ NewTeenagerPackage(db, user)
+ }
+
+ return &GiftResponse{
+ Cert: resp.Cert,
+ Teenager: resp.Teenager,
+ }
+}
diff --git a/auth/payment.go b/auth/payment.go
new file mode 100644
index 0000000..35b2fde
--- /dev/null
+++ b/auth/payment.go
@@ -0,0 +1,64 @@
+package auth
+
+import (
+ "chat/utils"
+ "encoding/json"
+ "github.com/spf13/viper"
+)
+
+type BalanceResponse struct {
+ Status bool `json:"status" required:"true"`
+ Balance float32 `json:"balance"`
+}
+
+type PaymentResponse struct {
+ Status bool `json:"status" required:"true"`
+ Type bool `json:"type"`
+}
+
+func GenerateOrder() string {
+ return utils.Sha2Encrypt(utils.GenerateChar(32))
+}
+
+func GetBalance(username string) float32 {
+ order := GenerateOrder()
+ res, err := utils.Post("https://api.deeptrain.net/app/balance", map[string]string{
+ "Content-Type": "application/json",
+ }, map[string]interface{}{
+ "password": viper.GetString("auth.access"),
+ "user": username,
+ "hash": utils.Sha2Encrypt(username + viper.GetString("auth.salt")),
+ "order": order,
+ "sign": utils.Sha2Encrypt(username + order + viper.GetString("auth.sign")),
+ })
+
+ if err != nil || res == nil || res.(map[string]interface{})["status"] == false {
+ return 0.
+ }
+
+ converter, _ := json.Marshal(res)
+ resp, _ := utils.Unmarshal[BalanceResponse](converter)
+ return resp.Balance
+}
+
+func Pay(username string, amount float32) bool {
+ order := GenerateOrder()
+ res, err := utils.Post("https://api.deeptrain.net/app/payment", map[string]string{
+ "Content-Type": "application/json",
+ }, map[string]interface{}{
+ "password": viper.GetString("auth.access"),
+ "user": username,
+ "hash": utils.Sha2Encrypt(username + viper.GetString("auth.salt")),
+ "order": order,
+ "amount": amount,
+ "sign": utils.Sha2Encrypt(username + order + viper.GetString("auth.sign")),
+ })
+
+ if err != nil || res == nil || res.(map[string]interface{})["status"] == false {
+ return false
+ }
+
+ converter, _ := json.Marshal(res)
+ resp, _ := utils.Unmarshal[PaymentResponse](converter)
+ return resp.Type
+}
diff --git a/auth/usage.go b/auth/usage.go
new file mode 100644
index 0000000..094dd9a
--- /dev/null
+++ b/auth/usage.go
@@ -0,0 +1,100 @@
+package auth
+
+import (
+ "database/sql"
+)
+
+func ReduceUsage(db *sql.DB, user *User, _t string) bool {
+ id := user.GetID(db)
+ var count int
+ if err := db.QueryRow(`SELECT balance FROM usages where user_id = ? AND type = ?`, id, _t).Scan(&count); err != nil {
+ count = 0
+ }
+
+ if count <= 0 {
+ return false
+ }
+
+ if _, err := db.Exec(`UPDATE usages SET balance = ? WHERE user_id = ? AND type = ?`, count-1, id, _t); err != nil {
+ return false
+ }
+
+ return true
+}
+
+func ReduceDalle(db *sql.DB, user *User) bool {
+ return ReduceUsage(db, user, "dalle")
+}
+
+func ReduceGPT4(db *sql.DB, user *User) bool {
+ return ReduceUsage(db, user, "gpt4")
+}
+
+func IncreaseUsage(db *sql.DB, user *User, _t string, value int) {
+ id := user.GetID(db)
+ var count int
+ if err := db.QueryRow(`SELECT balance FROM usages where user_id = ? AND type = ?`, id, _t).Scan(&count); err != nil {
+ count = 0
+ }
+
+ _ = db.QueryRow(`INSERT INTO usages (user_id, type, balance) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE balance = ?`, id, _t, count+value, count+value).Scan()
+}
+
+func IncreaseDalle(db *sql.DB, user *User, value int) {
+ IncreaseUsage(db, user, "dalle", value)
+}
+
+func IncreaseGPT4(db *sql.DB, user *User, value int) {
+ IncreaseUsage(db, user, "gpt4", value)
+}
+
+func GetUsage(db *sql.DB, user *User, _t string) int {
+ id := user.GetID(db)
+ var count int
+ if err := db.QueryRow(`SELECT balance FROM usages where user_id = ? AND type = ?`, id, _t).Scan(&count); err != nil {
+ return 0
+ }
+ return count
+}
+
+func GetDalleUsage(db *sql.DB, user *User) int {
+ return GetUsage(db, user, "dalle")
+}
+
+func GetGPT4Usage(db *sql.DB, user *User) int {
+ return GetUsage(db, user, "gpt4")
+}
+
+func UsageAPI(db *sql.DB, user *User) map[string]int {
+ return map[string]int{
+ "dalle": GetDalleUsage(db, user),
+ "gpt4": GetGPT4Usage(db, user),
+ }
+}
+
+func BuyDalle(db *sql.DB, user *User, value int) bool {
+ // 1 dalle usage = ¥0.1
+ if !Pay(user.Username, float32(value)*0.1) {
+ return false
+ }
+
+ IncreaseDalle(db, user, value)
+ return true
+}
+
+func CountGPT4Prize(value int) float32 {
+ if value <= 20 {
+ return float32(value) * 0.5
+ }
+
+ return 20*0.5 + float32(value-20)*0.4
+}
+
+func BuyGPT4(db *sql.DB, user *User, value int) bool {
+ if !Pay(user.Username, CountGPT4Prize(value)) {
+ return false
+ }
+
+ IncreaseGPT4(db, user, value)
+ return true
+}
diff --git a/connection/database.go b/connection/database.go
index 7d4edaa..1b29e2b 100644
--- a/connection/database.go
+++ b/connection/database.go
@@ -28,9 +28,8 @@ func ConnectMySQL() *sql.DB {
CreateUserTable(db)
CreateConversationTable(db)
- CreateSubscriptionTable(db)
CreatePackageTable(db)
- CreatePaymentLogTable(db)
+ CreateUsageTable(db)
return db
}
@@ -49,43 +48,33 @@ func CreateUserTable(db *sql.DB) {
}
}
-func CreatePaymentLogTable(db *sql.DB) {
- _, err := db.Exec(`
- CREATE TABLE IF NOT EXISTS payment_log (
- id INT PRIMARY KEY AUTO_INCREMENT,
- user_id INT,
- amount DECIMAL(12,2) DEFAULT 0,
- description VARCHAR(3600),
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- `)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-func CreateSubscriptionTable(db *sql.DB) {
- _, err := db.Exec(`
- CREATE TABLE IF NOT EXISTS subscription (
- id INT PRIMARY KEY AUTO_INCREMENT,
- user_id INT,
- plan_id INT,
- expired_at DATETIME,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- `)
- if err != nil {
- log.Fatal(err)
- }
-}
-
func CreatePackageTable(db *sql.DB) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS package (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
- money DECIMAL(12,2) DEFAULT 0,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ type VARCHAR(255),
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES auth(id),
+ UNIQUE KEY (user_id, type)
+ );
+ `)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func CreateUsageTable(db *sql.DB) {
+ _, err := db.Exec(`
+ CREATE TABLE IF NOT EXISTS usages (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ user_id INT,
+ type VARCHAR(255),
+ balance INT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY (user_id, type),
+ FOREIGN KEY (user_id) REFERENCES auth(id)
);
`)
if err != nil {
diff --git a/conversation/conversation.go b/conversation/conversation.go
index a01f684..b135e96 100644
--- a/conversation/conversation.go
+++ b/conversation/conversation.go
@@ -8,25 +8,36 @@ import (
)
type Conversation struct {
- UserID int64 `json:"user_id"`
- Id int64 `json:"id"`
- Name string `json:"name"`
- Message []types.ChatGPTMessage `json:"message"`
+ UserID int64 `json:"user_id"`
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Message []types.ChatGPTMessage `json:"message"`
+ EnableGPT4 bool `json:"enable_gpt4"`
}
type FormMessage struct {
Message string `json:"message" binding:"required"`
+ GPT4 bool `json:"gpt4"`
}
func NewConversation(db *sql.DB, id int64) *Conversation {
return &Conversation{
- UserID: id,
- Id: GetConversationLengthByUserID(db, id) + 1,
- Name: "new chat",
- Message: []types.ChatGPTMessage{},
+ UserID: id,
+ Id: GetConversationLengthByUserID(db, id) + 1,
+ Name: "new chat",
+ Message: []types.ChatGPTMessage{},
+ EnableGPT4: false,
}
}
+func (c *Conversation) IsEnableGPT4() bool {
+ return c.EnableGPT4
+}
+
+func (c *Conversation) SetEnableGPT4(enable bool) {
+ c.EnableGPT4 = enable
+}
+
func (c *Conversation) GetName() string {
return c.Name
}
@@ -116,6 +127,7 @@ func (c *Conversation) AddMessageFromUserForm(data []byte) (string, error) {
}
c.AddMessageFromUser(form.Message)
+ c.SetEnableGPT4(form.GPT4)
return form.Message, nil
}
diff --git a/main.go b/main.go
index 68bfe1c..cabcdc5 100644
--- a/main.go
+++ b/main.go
@@ -27,6 +27,9 @@ func main() {
app.GET("/chat", api.ChatAPI)
app.POST("/login", auth.LoginAPI)
app.POST("/state", auth.StateAPI)
+ app.GET("/package", auth.PackageAPI)
+ app.GET("/usage", auth.GetUsageAPI)
+ app.POST("/buy", auth.BuyAPI)
app.GET("/conversation/list", conversation.ListAPI)
app.GET("/conversation/load", conversation.LoadAPI)
app.GET("/conversation/delete", conversation.DeleteAPI)
diff --git a/middleware/throttle.go b/middleware/throttle.go
index 776b803..6f3ec8e 100644
--- a/middleware/throttle.go
+++ b/middleware/throttle.go
@@ -26,9 +26,14 @@ func (l *Limiter) RateLimit(ctx *gin.Context, rds *redis.Client, ip string, path
}
var limits = map[string]Limiter{
- "/login": {Duration: 10, Count: 5},
- "/anonymous": {Duration: 60, Count: 15},
- "/user": {Duration: 1, Count: 1},
+ "/login": {Duration: 10, Count: 5},
+ "/anonymous": {Duration: 60, Count: 15},
+ "/user": {Duration: 1, Count: 1},
+ "/package": {Duration: 1, Count: 2},
+ "/usage": {Duration: 1, Count: 2},
+ "/buy": {Duration: 1, Count: 2},
+ "/chat": {Duration: 1, Count: 5},
+ "/conversation": {Duration: 1, Count: 5},
}
func GetPrefixMap[T comparable](s string, p map[string]T) *T {