mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 13:00:14 +09:00
update payment and order
This commit is contained in:
parent
2b2005b7d6
commit
dc1469941a
@ -35,7 +35,7 @@ func GetChatGPTResponse(message []types.ChatGPTMessage, token int) (string, erro
|
||||
}
|
||||
|
||||
if res.(map[string]interface{})["choices"] == nil {
|
||||
return "empty", nil
|
||||
return res.(map[string]interface{})["error"].(map[string]interface{})["message"].(string), nil
|
||||
}
|
||||
data := res.(map[string]interface{})["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"]
|
||||
return data.(string), nil
|
||||
|
18
api/chat.go
18
api/chat.go
@ -24,12 +24,21 @@ func SendSegmentMessage(conn *websocket.Conn, message types.ChatGPTSegmentRespon
|
||||
_ = conn.WriteMessage(websocket.TextMessage, []byte(utils.ToJson(message)))
|
||||
}
|
||||
|
||||
func TextChat(conn *websocket.Conn, instance *conversation.Conversation) string {
|
||||
func TextChat(db *sql.DB, user *auth.User, conn *websocket.Conn, instance *conversation.Conversation) string {
|
||||
keyword, segment := ChatWithWeb(conversation.CopyMessage(instance.GetMessageSegment(12)), true)
|
||||
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{Keyword: keyword, End: false})
|
||||
|
||||
msg := ""
|
||||
StreamRequest("gpt-3.5-turbo-16k-0613", segment, 2000, func(resp string) {
|
||||
|
||||
if instance.IsEnableGPT4() && !auth.ReduceGPT4(db, user) {
|
||||
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
|
||||
Message: "You have run out of GPT-4 usage. Please buy more.",
|
||||
End: true,
|
||||
})
|
||||
return "You have run out of GPT-4 usage. Please buy more."
|
||||
}
|
||||
|
||||
StreamRequest(instance.IsEnableGPT4(), segment, 2000, func(resp string) {
|
||||
msg += resp
|
||||
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
|
||||
Message: resp,
|
||||
@ -38,6 +47,9 @@ func TextChat(conn *websocket.Conn, instance *conversation.Conversation) string
|
||||
})
|
||||
if msg == "" {
|
||||
msg = "There was something wrong... Please try again later."
|
||||
if instance.IsEnableGPT4() {
|
||||
auth.IncreaseGPT4(db, user, 1)
|
||||
}
|
||||
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
|
||||
Message: msg,
|
||||
End: false,
|
||||
@ -150,7 +162,7 @@ func ChatAPI(c *gin.Context) {
|
||||
cache := c.MustGet("cache").(*redis.Client)
|
||||
msg = ImageChat(conn, instance, user, db, cache)
|
||||
} else {
|
||||
msg = TextChat(conn, instance)
|
||||
msg = TextChat(db, user, conn, instance)
|
||||
}
|
||||
instance.SaveResponse(db, msg)
|
||||
}
|
||||
|
@ -61,7 +61,10 @@ func GetImageWithUserLimit(user *auth.User, prompt string, db *sql.DB, cache *re
|
||||
}
|
||||
|
||||
if res == "5" {
|
||||
return "", fmt.Errorf("you have reached your limit of 5 images per day")
|
||||
if auth.ReduceDalle(db, user) {
|
||||
return GetImageWithCache(context.Background(), prompt, cache)
|
||||
}
|
||||
return "", fmt.Errorf("you have reached your limit of 5 free images per day, please buy more dalle usage or wait until tomorrow")
|
||||
} else {
|
||||
cache.Set(context.Background(), GetLimitFormat(user.GetID(db)), fmt.Sprintf("%d", utils.ToInt(res)+1), time.Hour*24)
|
||||
return GetImageWithCache(context.Background(), prompt, cache)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"chat/utils"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
"io"
|
||||
"log"
|
||||
@ -50,26 +51,28 @@ func processLine(buf []byte) []string {
|
||||
return resp
|
||||
}
|
||||
|
||||
func StreamRequest(model string, messages []types.ChatGPTMessage, token int, callback func(string)) {
|
||||
func NativeStreamRequest(model string, endpoint string, apikeys string, messages []types.ChatGPTMessage, token int, callback func(string)) {
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", viper.GetString("openai.user_endpoint")+"/chat/completions", utils.ConvertBody(types.ChatGPTRequest{
|
||||
req, err := http.NewRequest("POST", endpoint+"/chat/completions", utils.ConvertBody(types.ChatGPTRequest{
|
||||
Model: model,
|
||||
Messages: messages,
|
||||
MaxToken: token,
|
||||
Stream: true,
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+GetRandomKey(viper.GetString("openai.user")))
|
||||
req.Header.Set("Authorization", "Bearer "+GetRandomKey(apikeys))
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
@ -89,3 +92,11 @@ func StreamRequest(model string, messages []types.ChatGPTMessage, token int, cal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StreamRequest(enableGPT4 bool, messages []types.ChatGPTMessage, token int, callback func(string)) {
|
||||
if enableGPT4 {
|
||||
NativeStreamRequest("gpt-4", viper.GetString("openai.gpt4_endpoint"), viper.GetString("openai.gpt4"), messages, token, callback)
|
||||
} else {
|
||||
NativeStreamRequest("gpt-3.5-turbo-16k-0613", viper.GetString("openai.user_endpoint"), viper.GetString("openai.user"), messages, token, callback)
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,13 @@ const router = createRouter({ //@ts-ignore
|
||||
meta: {
|
||||
title: "Login | Chat Nio",
|
||||
}
|
||||
}, {
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: () => import("../src/views/SettingsView.vue"),
|
||||
meta: {
|
||||
title: "Settings | Chat Nio",
|
||||
}
|
||||
}
|
||||
],
|
||||
});
|
||||
|
@ -11,12 +11,13 @@ import Delete from "./components/icons/delete.vue";
|
||||
import { deleteConversation } from "./assets/script/api";
|
||||
import {ref} from "vue";
|
||||
import Close from "./components/icons/close.vue";
|
||||
import Notification from "./components/Notification.vue";
|
||||
|
||||
const current = manager.getCurrent();
|
||||
const sidebar = ref(false), padding = ref(false);
|
||||
|
||||
function goto() {
|
||||
window.location.href = "https://deeptrain.net/login?app=chatnio";
|
||||
window.location.href = "https://deeptrain.lightxi.com/login?app=chatnio";
|
||||
}
|
||||
|
||||
function toggle(n: boolean) {
|
||||
@ -37,6 +38,7 @@ function toggleConversation(id: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Notification />
|
||||
<div class="sidebar conversation-container mobile" v-if="mobile" :class="{'active': sidebar, 'padding': padding}">
|
||||
<div class="operation-wrapper" v-if="mobile">
|
||||
<div class="grow" />
|
||||
@ -88,10 +90,10 @@ function toggleConversation(id: number) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div class="user" v-if="auth">
|
||||
<router-link to="/settings" class="user" v-if="auth">
|
||||
<img class="avatar" :src="'https://api.deeptrain.net/avatar/' + username" alt="" @click="setSidebar(true)">
|
||||
<span class="username">{{ username }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="login" v-else>
|
||||
<button @click="goto">
|
||||
<login />
|
||||
@ -105,7 +107,7 @@ function toggleConversation(id: number) {
|
||||
</div>
|
||||
<div class="copyright">
|
||||
<a href="https://github.com/zmh-program/chatnio" target="_blank"><github /> chatnio</a>
|
||||
<a href="https://deeptrain.net" target="_blank">© 2023 Deeptrain Team</a>
|
||||
<a href="https://deeptrain.lightxi.com" target="_blank">© 2023 Deeptrain Team</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -4,6 +4,9 @@ import axios from "axios";
|
||||
import { auth, token } from "./auth";
|
||||
import { ws_api } from "./conf";
|
||||
import { gpt4 } from "./shared";
|
||||
import {notify} from "./notify";
|
||||
|
||||
var state = true;
|
||||
|
||||
export type Message = {
|
||||
content: string;
|
||||
@ -37,6 +40,10 @@ export class Connection {
|
||||
this.state = false;
|
||||
this.connection.onopen = () => {
|
||||
this.state = true;
|
||||
if (!state) {
|
||||
notify("服务器连接已恢复", 1000);
|
||||
state = true;
|
||||
}
|
||||
this.send({
|
||||
token: token.value,
|
||||
id: this.id,
|
||||
@ -44,6 +51,10 @@ export class Connection {
|
||||
}
|
||||
this.connection.onclose = () => {
|
||||
this.state = false;
|
||||
if (state) {
|
||||
notify("服务器连接已断开,正在尝试重连中...", 3000);
|
||||
state = false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.init();
|
||||
}, 3000);
|
||||
@ -130,6 +141,7 @@ export class Conversation {
|
||||
})
|
||||
const status = this.connection?.send({
|
||||
message: content,
|
||||
gpt4: gpt4.value,
|
||||
});
|
||||
if (status) {
|
||||
this.addDynamicMessageFromAI(message, keyword, end);
|
||||
|
26
app/src/assets/script/notify.ts
Normal file
26
app/src/assets/script/notify.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {ref} from "vue";
|
||||
|
||||
type Notification = {
|
||||
content: string;
|
||||
expire: number;
|
||||
leave?: boolean;
|
||||
}
|
||||
|
||||
export const notifications = ref<Notification[]>([]);
|
||||
|
||||
export function notify(content: string, expire: number = 5000): void {
|
||||
if (!notifications.value) notifications.value = [];
|
||||
notifications.value.push({content, expire: (new Date()).getTime() + expire});
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (!notifications.value) return;
|
||||
const now = (new Date()).getTime();
|
||||
// leave animation: 0.5s
|
||||
notifications.value = notifications.value.filter((notification) => {
|
||||
if (notification.expire < now) {
|
||||
notification.leave = true;
|
||||
}
|
||||
return notification.expire > now - 500;
|
||||
});
|
||||
}, 800);
|
85
app/src/components/Notification.vue
Normal file
85
app/src/components/Notification.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { notifications } from "../assets/script/notify";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notification-wrapper">
|
||||
<div class="notification" v-for="(notification, index) in notifications" :key="index" :class="{'leave': notification.leave}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="icon">
|
||||
<path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z"></path><path d="M11 11h2v6h-2zm0-4h2v2h-2z"></path>
|
||||
</svg>
|
||||
<div class="content">{{ notification.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
pointer-events: none;
|
||||
z-index: 64;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notification {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
vertical-align: center;
|
||||
justify-content: center;
|
||||
margin: 12px auto;
|
||||
width: max-content;
|
||||
min-width: 160px;
|
||||
text-align: center;
|
||||
height: min-content;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
background: #202121;
|
||||
border: 1px solid #2d2d2f;
|
||||
color: #ddddde;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
animation: FadeInAnimation 0.5s cubic-bezier(0.18, 0.89, 0.32, 1.28) both;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
fill: #ddddde;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.leave {
|
||||
animation: FadeOutAnimation 0.5s cubic-bezier(0.18, 0.89, 0.32, 1.28) both;
|
||||
}
|
||||
|
||||
@keyframes FadeInAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes FadeOutAnimation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
}
|
||||
</style>
|
3
app/src/components/icons/back.vue
Normal file
3
app/src/components/icons/back.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"></path></svg>
|
||||
</template>
|
3
app/src/components/icons/buy.vue
Normal file
3
app/src/components/icons/buy.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="10.5" cy="19.5" r="1.5"></circle><circle cx="17.5" cy="19.5" r="1.5"></circle><path d="m14 13.99 4-5h-3v-4h-2v4h-3l4 5z"></path><path d="M17.31 15h-6.64L6.18 4.23A2 2 0 0 0 4.33 3H2v2h2.33l4.75 11.38A1 1 0 0 0 10 17h8a1 1 0 0 0 .93-.64L21.76 9h-2.14z"></path></svg>
|
||||
</template>
|
3
app/src/components/icons/chart.vue
Normal file
3
app/src/components/icons/chart.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M2 2h20v2H2z"></path><rect x="5" y="6" width="6" height="16" rx="1"></rect><rect x="13" y="6" width="6" height="12" rx="1"></rect></svg>
|
||||
</template>
|
3
app/src/components/icons/draw.vue
Normal file
3
app/src/components/icons/draw.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.4 2.096a10.08 10.08 0 0 0-8.937 3.331A10.054 10.054 0 0 0 2.096 13.4c.53 3.894 3.458 7.207 7.285 8.246a9.982 9.982 0 0 0 2.618.354l.142-.001a3.001 3.001 0 0 0 2.516-1.426 2.989 2.989 0 0 0 .153-2.879l-.199-.416a1.919 1.919 0 0 1 .094-1.912 2.004 2.004 0 0 1 2.576-.755l.412.197c.412.198.85.299 1.301.299A3.022 3.022 0 0 0 22 12.14a9.935 9.935 0 0 0-.353-2.76c-1.04-3.826-4.353-6.754-8.247-7.284zm5.158 10.909-.412-.197c-1.828-.878-4.07-.198-5.135 1.494-.738 1.176-.813 2.576-.204 3.842l.199.416a.983.983 0 0 1-.051.961.992.992 0 0 1-.844.479h-.112a8.061 8.061 0 0 1-2.095-.283c-3.063-.831-5.403-3.479-5.826-6.586-.321-2.355.352-4.623 1.893-6.389a8.002 8.002 0 0 1 7.16-2.664c3.107.423 5.755 2.764 6.586 5.826.198.73.293 1.474.282 2.207-.012.807-.845 1.183-1.441.894z"></path><circle cx="7.5" cy="14.5" r="1.5"></circle><circle cx="7.5" cy="10.5" r="1.5"></circle><circle cx="10.5" cy="7.5" r="1.5"></circle><circle cx="14.5" cy="7.5" r="1.5"></circle></svg>
|
||||
</template>
|
3
app/src/components/icons/gift.vue
Normal file
3
app/src/components/icons/gift.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 12H4v8a2 2 0 0 0 2 2h5V12H5zm13 0h-5v10h5a2 2 0 0 0 2-2v-8h-2zm.791-5A4.92 4.92 0 0 0 19 5.5C19 3.57 17.43 2 15.5 2c-1.622 0-2.705 1.482-3.404 3.085C11.407 3.57 10.269 2 8.5 2 6.57 2 5 3.57 5 5.5c0 .596.079 1.089.209 1.5H2v4h9V9h2v2h9V7h-3.209zM7 5.5C7 4.673 7.673 4 8.5 4c.888 0 1.714 1.525 2.198 3H8c-.374 0-1 0-1-1.5zM15.5 4c.827 0 1.5.673 1.5 1.5C17 7 16.374 7 16 7h-2.477c.51-1.576 1.251-3 1.977-3z"></path></svg>
|
||||
</template>
|
3
app/src/components/icons/sale.vue
Normal file
3
app/src/components/icons/sale.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 15c-1.84 0-2-.86-2-1H8c0 .92.66 2.55 3 2.92V18h2v-1.08c2-.34 3-1.63 3-2.92 0-1.12-.52-3-4-3-2 0-2-.63-2-1s.7-1 2-1 1.39.64 1.4 1h2A3 3 0 0 0 13 7.12V6h-2v1.09C9 7.42 8 8.71 8 10c0 1.12.52 3 4 3 2 0 2 .68 2 1s-.62 1-2 1z"></path><path d="M5 2H2v2h2v17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V4h2V2H5zm13 18H6V4h12z"></path></svg>
|
||||
</template>
|
11
app/src/components/icons/wallet.vue
Normal file
11
app/src/components/icons/wallet.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M95.5 104h320a87.73 87.73 0 0111.18.71 66 66 0 00-77.51-55.56L86 94.08h-.3a66 66 0 00-41.07 26.13A87.57 87.57 0 0195.5 104zM415.5 128h-320a64.07 64.07 0 00-64 64v192a64.07 64.07 0 0064 64h320a64.07 64.07 0 0064-64V192a64.07 64.07 0 00-64-64zM368 320a32 32 0 1132-32 32 32 0 01-32 32z"
|
||||
/>
|
||||
<path
|
||||
d="M32 259.5V160c0-21.67 12-58 53.65-65.87C121 87.5 156 87.5 156 87.5s23 16 4 16-18.5 24.5 0 24.5 0 23.5 0 23.5L85.5 236z"
|
||||
fill="rgba(255, 255, 255, 0.1)"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -8,6 +8,7 @@ import { auth, username } from "../assets/script/auth";
|
||||
import Loading from "../components/icons/loading.vue";
|
||||
import Bing from "../components/icons/bing.vue";
|
||||
import { manager } from "../assets/script/shared";
|
||||
import router from "../../router";
|
||||
|
||||
const state = manager.getState(), length = manager.getLength(), current = manager.getCurrent();
|
||||
manager.setRefresh(function refreshScrollbar() {
|
||||
@ -44,6 +45,14 @@ onMounted(() => {
|
||||
if (e.key === "Enter") await send();
|
||||
});
|
||||
});
|
||||
|
||||
function settings() {
|
||||
router.push('/settings');
|
||||
}
|
||||
|
||||
function login() {
|
||||
location.href = "https://deeptrain.lightxi.com/login?app=chatnio";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -80,6 +89,8 @@ onMounted(() => {
|
||||
<p>🧐 ChatNio 是一个 AI 聊天网站,它可以与您进行对话并提供各种功能。</p>
|
||||
<p>🎃 您可以向它提问问题、寻求建议,或者闲聊。</p>
|
||||
<p>🎈 欢迎开始与 ChatNio 展开交流!</p>
|
||||
<p v-if="auth">🔨 点击头像即可进入<span @click="settings">设置</span>页</p>
|
||||
<p v-else>✨ <span @click="login">登录</span>后可享更多功能,上下文对话、历史记录等。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
@ -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;
|
||||
|
@ -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", {
|
||||
|
494
app/src/views/SettingsView.vue
Normal file
494
app/src/views/SettingsView.vue
Normal file
@ -0,0 +1,494 @@
|
||||
<script setup lang="ts">
|
||||
import Back from "../components/icons/back.vue";
|
||||
import Wallet from "../components/icons/wallet.vue";
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import Chart from "../components/icons/chart.vue";
|
||||
import Draw from "../components/icons/draw.vue";
|
||||
import axios from "axios";
|
||||
import {username} from "../assets/script/auth";
|
||||
import Gift from "../components/icons/gift.vue";
|
||||
import Sale from "../components/icons/sale.vue";
|
||||
import Buy from "../components/icons/buy.vue";
|
||||
import {notify} from "../assets/script/notify";
|
||||
|
||||
const info = reactive<Record<string, any>>({
|
||||
balance: 0,
|
||||
gpt4: 0,
|
||||
dalle: 0,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const packages = reactive<Record<string, any>>({
|
||||
cert: false,
|
||||
teenager: false,
|
||||
});
|
||||
|
||||
const form = reactive<Record<string, any>>({
|
||||
type: "dalle",
|
||||
count: 1,
|
||||
});
|
||||
|
||||
function count(value: number, type: string): string {
|
||||
if (type === "dalle") {
|
||||
return (value * 0.1).toFixed(2);
|
||||
} else {
|
||||
if (value <= 20) return (value * 0.5).toFixed(2);
|
||||
return (20 * 0.5 + (value - 20) * 0.4).toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
axios.get("/package")
|
||||
.then((res) => {
|
||||
packages.cert = res.data.data.cert;
|
||||
packages.teenager = res.data.data.teenager;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
axios.get("/usage")
|
||||
.then((res) => {
|
||||
info.balance = res.data.balance;
|
||||
info.gpt4 = res.data.data.gpt4;
|
||||
info.dalle = res.data.data.dalle;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
})
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("token");
|
||||
location.reload();
|
||||
}
|
||||
|
||||
watch(form, () => {
|
||||
if (form.count < 0) form.count = 0;
|
||||
if (form.count > 50000) form.count = 50000;
|
||||
})
|
||||
|
||||
function payment() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
axios.post("/buy", {
|
||||
type: form.type,
|
||||
quota: form.count,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status) {
|
||||
info.balance = res.data.balance;
|
||||
info.gpt4 = res.data.data.gpt4;
|
||||
info.dalle = res.data.data.dalle;
|
||||
notify("购买成功!稍后将刷新页面更新配额!");
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify("购买失败!请检查您的账户余额");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
notify("购买失败!请检查您的网络连接");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="nav">
|
||||
<router-link to="/" class="router">
|
||||
<back class="back" />
|
||||
</router-link>
|
||||
<div class="grow" />
|
||||
<span class="title">设置</span>
|
||||
<div class="grow" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="subtitle">账户</div>
|
||||
<div class="row user">
|
||||
<img :src="'https://api.deeptrain.net/avatar/' + username" alt="">
|
||||
<span class="value">{{ username }}</span>
|
||||
<button @click="logout">登出</button>
|
||||
</div>
|
||||
<div class="split" />
|
||||
|
||||
<div class="subtitle">配额</div>
|
||||
<div class="row">
|
||||
<wallet />
|
||||
<span class="text">Deeptrain 钱包</span>
|
||||
<span class="value">{{ info.balance }} ¥</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<chart />
|
||||
<span class="text">GPT-4 配额</span>
|
||||
<span class="value">{{ info.gpt4 }} 次</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<draw />
|
||||
<span class="text">DALL-E 配额</span>
|
||||
<span class="value">{{ info.dalle }} 次</span>
|
||||
</div>
|
||||
<div class="split" />
|
||||
<div class="subtitle">购买</div>
|
||||
<div class="buy-form">
|
||||
<div class="buy-type">
|
||||
<div class="buy-card" :class="{'select': form.type === 'dalle'}" @click="form.type = 'dalle'">
|
||||
<draw />
|
||||
<p>DALL-E</p>
|
||||
<span>0.10</span>
|
||||
</div>
|
||||
<div class="buy-card" :class="{'select': form.type === 'gpt4'}" @click="form.type = 'gpt4'">
|
||||
<chart />
|
||||
<p>GPT-4</p>
|
||||
<span>0.50</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sale" v-if="form.type === 'gpt4'">
|
||||
<sale />
|
||||
限时优惠:单次购买 20 份额以上的 GPT-4 配额,享受 20% 折扣
|
||||
</div>
|
||||
<div class="buy-quota">
|
||||
<input type="number" v-model="form.count" class="buy-input" />
|
||||
<p class="pay-info">{{ count(form.count, form.type) }}</p>
|
||||
</div>
|
||||
<div class="buy-action" @click="payment">
|
||||
<buy />
|
||||
购买
|
||||
</div>
|
||||
</div>
|
||||
<div class="split" />
|
||||
|
||||
<div class="subtitle">礼包</div>
|
||||
<div class="tips">tip: 首次领取后,刷新后即可领取配额</div>
|
||||
<div class="row gift">
|
||||
<span class="text">实名认证礼包</span>
|
||||
<span class="value" :class="{'success': packages.cert}">
|
||||
<gift />
|
||||
</span>
|
||||
</div>
|
||||
<div class="row gift">
|
||||
<span class="text">青少年礼包</span>
|
||||
<span class="value" :class="{'success': packages.teenager}">
|
||||
<gift />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.body {
|
||||
width: calc(100% - 72px);
|
||||
flex-grow: 1;
|
||||
padding: 24px 36px;
|
||||
height: max-content;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.split {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.buy-type {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.buy-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
gap: 4px;
|
||||
padding: 12px 36px;
|
||||
border-radius: 8px;
|
||||
background: rgb(43, 50, 69);
|
||||
user-select: none;
|
||||
transition: .25s;
|
||||
cursor: pointer;
|
||||
border: 1px solid #3C445C;
|
||||
}
|
||||
|
||||
.buy-card p {
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
letter-spacing: 1px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.buy-card span {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.buy-card span::before {
|
||||
content: "¥";
|
||||
margin-right: 2px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.buy-card.select {
|
||||
background-color: #004182;
|
||||
border-color: #57ABFF;
|
||||
}
|
||||
|
||||
.buy-card svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.buy-quota {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 480px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pay-info {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
font-size: 20px;
|
||||
background: rgb(2, 90, 175);
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.pay-info::before {
|
||||
content: "¥";
|
||||
margin-right: 2px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.buy-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 12px 0;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
background: #1b8cff;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
transition: .5s;
|
||||
cursor: pointer;
|
||||
border: 1px solid #3C445C;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.buy-action:hover {
|
||||
background: #007cfc;
|
||||
border-color: #57ABFF;
|
||||
}
|
||||
|
||||
.buy-action svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #fff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.sale {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 4px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sale svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #eee;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.buy-input {
|
||||
flex-grow: 1;
|
||||
height: 36px;
|
||||
margin: 6px 2px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
background: rgb(43, 50, 69);
|
||||
border: 1px solid #3C445C;
|
||||
color: var(--card-text);
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.buy-input:hover {
|
||||
border: 1px solid #57ABFF;
|
||||
}
|
||||
|
||||
.buy-input::placeholder {
|
||||
color: var(--card-text-hover);
|
||||
}
|
||||
|
||||
.buy-input::-webkit-outer-spin-button,
|
||||
.buy-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.buy-input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.user {
|
||||
vertical-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.user .value {
|
||||
margin-left: 8px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.user button {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: .5s;
|
||||
cursor: pointer;
|
||||
margin-right: -2px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.user button:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 0 0 0 16px;
|
||||
fill: var(--card-text);
|
||||
transition: .5s;
|
||||
cursor: pointer;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
fill: var(--card-text-hover);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 2px 4px;
|
||||
font-size: 24px;
|
||||
color: #fff !important;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
top: 4px;
|
||||
left: -2px;
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
user-select: none;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
margin: 12px 0 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: calc(100% - 32px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 12px 0;
|
||||
padding: 16px 26px;
|
||||
gap: 6px;
|
||||
user-select: none;
|
||||
background: var(--card-input);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.row.gift .value {
|
||||
margin-right: -2px;
|
||||
}
|
||||
.row.gift .value.success svg {
|
||||
fill: #67C23A;
|
||||
}
|
||||
|
||||
.row svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--card-text);
|
||||
flex-shrink: 0;
|
||||
transform: translateY(2px);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 18px;
|
||||
color: var(--card-text-hover);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--card-text);
|
||||
margin-right: 6px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
31
auth/cert.go
Normal file
31
auth/cert.go
Normal file
@ -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
|
||||
}
|
101
auth/controller.go
Normal file
101
auth/controller.go
Normal file
@ -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"})
|
||||
}
|
||||
}
|
67
auth/package.go
Normal file
67
auth/package.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
64
auth/payment.go
Normal file
64
auth/payment.go
Normal file
@ -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
|
||||
}
|
100
auth/usage.go
Normal file
100
auth/usage.go
Normal file
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -12,10 +12,12 @@ type Conversation struct {
|
||||
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 {
|
||||
@ -24,9 +26,18 @@ func NewConversation(db *sql.DB, id int64) *Conversation {
|
||||
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
|
||||
}
|
||||
|
||||
|
3
main.go
3
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)
|
||||
|
@ -29,6 +29,11 @@ var limits = map[string]Limiter{
|
||||
"/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 {
|
||||
|
Loading…
Reference in New Issue
Block a user