update payment and order

This commit is contained in:
Zhang Minghan 2023-08-30 16:48:22 +08:00
parent 2b2005b7d6
commit dc1469941a
28 changed files with 1137 additions and 59 deletions

View File

@ -35,7 +35,7 @@ func GetChatGPTResponse(message []types.ChatGPTMessage, token int) (string, erro
} }
if res.(map[string]interface{})["choices"] == nil { 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"] data := res.(map[string]interface{})["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"]
return data.(string), nil return data.(string), nil

View File

@ -24,12 +24,21 @@ func SendSegmentMessage(conn *websocket.Conn, message types.ChatGPTSegmentRespon
_ = conn.WriteMessage(websocket.TextMessage, []byte(utils.ToJson(message))) _ = 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) keyword, segment := ChatWithWeb(conversation.CopyMessage(instance.GetMessageSegment(12)), true)
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{Keyword: keyword, End: false}) SendSegmentMessage(conn, types.ChatGPTSegmentResponse{Keyword: keyword, End: false})
msg := "" 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 msg += resp
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{ SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
Message: resp, Message: resp,
@ -38,6 +47,9 @@ func TextChat(conn *websocket.Conn, instance *conversation.Conversation) string
}) })
if msg == "" { if msg == "" {
msg = "There was something wrong... Please try again later." msg = "There was something wrong... Please try again later."
if instance.IsEnableGPT4() {
auth.IncreaseGPT4(db, user, 1)
}
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{ SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
Message: msg, Message: msg,
End: false, End: false,
@ -150,7 +162,7 @@ func ChatAPI(c *gin.Context) {
cache := c.MustGet("cache").(*redis.Client) cache := c.MustGet("cache").(*redis.Client)
msg = ImageChat(conn, instance, user, db, cache) msg = ImageChat(conn, instance, user, db, cache)
} else { } else {
msg = TextChat(conn, instance) msg = TextChat(db, user, conn, instance)
} }
instance.SaveResponse(db, msg) instance.SaveResponse(db, msg)
} }

View File

@ -61,7 +61,10 @@ func GetImageWithUserLimit(user *auth.User, prompt string, db *sql.DB, cache *re
} }
if res == "5" { 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 { } else {
cache.Set(context.Background(), GetLimitFormat(user.GetID(db)), fmt.Sprintf("%d", utils.ToInt(res)+1), time.Hour*24) cache.Set(context.Background(), GetLimitFormat(user.GetID(db)), fmt.Sprintf("%d", utils.ToInt(res)+1), time.Hour*24)
return GetImageWithCache(context.Background(), prompt, cache) return GetImageWithCache(context.Background(), prompt, cache)

View File

@ -5,6 +5,7 @@ import (
"chat/utils" "chat/utils"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt"
"github.com/spf13/viper" "github.com/spf13/viper"
"io" "io"
"log" "log"
@ -50,26 +51,28 @@ func processLine(buf []byte) []string {
return resp 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} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{} 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, Model: model,
Messages: messages, Messages: messages,
MaxToken: token, MaxToken: token,
Stream: true, Stream: true,
})) }))
if err != nil { if err != nil {
fmt.Println(err)
return return
} }
req.Header.Set("Content-Type", "application/json") 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) res, err := client.Do(req)
if err != nil { if err != nil {
log.Fatal(err) fmt.Println(err)
return
} }
defer res.Body.Close() 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)
}
}

View File

@ -19,6 +19,13 @@ const router = createRouter({ //@ts-ignore
meta: { meta: {
title: "Login | Chat Nio", title: "Login | Chat Nio",
} }
}, {
path: "/settings",
name: "settings",
component: () => import("../src/views/SettingsView.vue"),
meta: {
title: "Settings | Chat Nio",
}
} }
], ],
}); });

View File

@ -11,12 +11,13 @@ import Delete from "./components/icons/delete.vue";
import { deleteConversation } from "./assets/script/api"; import { deleteConversation } from "./assets/script/api";
import {ref} from "vue"; import {ref} from "vue";
import Close from "./components/icons/close.vue"; import Close from "./components/icons/close.vue";
import Notification from "./components/Notification.vue";
const current = manager.getCurrent(); const current = manager.getCurrent();
const sidebar = ref(false), padding = ref(false); const sidebar = ref(false), padding = ref(false);
function goto() { 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) { function toggle(n: boolean) {
@ -37,6 +38,7 @@ function toggleConversation(id: number) {
</script> </script>
<template> <template>
<Notification />
<div class="sidebar conversation-container mobile" v-if="mobile" :class="{'active': sidebar, 'padding': padding}"> <div class="sidebar conversation-container mobile" v-if="mobile" :class="{'active': sidebar, 'padding': padding}">
<div class="operation-wrapper" v-if="mobile"> <div class="operation-wrapper" v-if="mobile">
<div class="grow" /> <div class="grow" />
@ -88,10 +90,10 @@ function toggleConversation(id: number) {
</div> </div>
</div> </div>
<div class="grow" /> <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)"> <img class="avatar" :src="'https://api.deeptrain.net/avatar/' + username" alt="" @click="setSidebar(true)">
<span class="username">{{ username }}</span> <span class="username">{{ username }}</span>
</div> </router-link>
<div class="login" v-else> <div class="login" v-else>
<button @click="goto"> <button @click="goto">
<login /> <login />
@ -105,7 +107,7 @@ function toggleConversation(id: number) {
</div> </div>
<div class="copyright"> <div class="copyright">
<a href="https://github.com/zmh-program/chatnio" target="_blank"><github /> chatnio</a> <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> </div>
</template> </template>

View File

@ -4,6 +4,9 @@ import axios from "axios";
import { auth, token } from "./auth"; import { auth, token } from "./auth";
import { ws_api } from "./conf"; import { ws_api } from "./conf";
import { gpt4 } from "./shared"; import { gpt4 } from "./shared";
import {notify} from "./notify";
var state = true;
export type Message = { export type Message = {
content: string; content: string;
@ -37,6 +40,10 @@ export class Connection {
this.state = false; this.state = false;
this.connection.onopen = () => { this.connection.onopen = () => {
this.state = true; this.state = true;
if (!state) {
notify("服务器连接已恢复", 1000);
state = true;
}
this.send({ this.send({
token: token.value, token: token.value,
id: this.id, id: this.id,
@ -44,6 +51,10 @@ export class Connection {
} }
this.connection.onclose = () => { this.connection.onclose = () => {
this.state = false; this.state = false;
if (state) {
notify("服务器连接已断开,正在尝试重连中...", 3000);
state = false;
}
setTimeout(() => { setTimeout(() => {
this.init(); this.init();
}, 3000); }, 3000);
@ -130,6 +141,7 @@ export class Conversation {
}) })
const status = this.connection?.send({ const status = this.connection?.send({
message: content, message: content,
gpt4: gpt4.value,
}); });
if (status) { if (status) {
this.addDynamicMessageFromAI(message, keyword, end); this.addDynamicMessageFromAI(message, keyword, end);

View 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);

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -8,6 +8,7 @@ import { auth, username } from "../assets/script/auth";
import Loading from "../components/icons/loading.vue"; import Loading from "../components/icons/loading.vue";
import Bing from "../components/icons/bing.vue"; import Bing from "../components/icons/bing.vue";
import { manager } from "../assets/script/shared"; import { manager } from "../assets/script/shared";
import router from "../../router";
const state = manager.getState(), length = manager.getLength(), current = manager.getCurrent(); const state = manager.getState(), length = manager.getLength(), current = manager.getCurrent();
manager.setRefresh(function refreshScrollbar() { manager.setRefresh(function refreshScrollbar() {
@ -44,6 +45,14 @@ onMounted(() => {
if (e.key === "Enter") await send(); if (e.key === "Enter") await send();
}); });
}); });
function settings() {
router.push('/settings');
}
function login() {
location.href = "https://deeptrain.lightxi.com/login?app=chatnio";
}
</script> </script>
<template> <template>
@ -80,6 +89,8 @@ onMounted(() => {
<p>🧐 ChatNio 是一个 AI 聊天网站它可以与您进行对话并提供各种功能</p> <p>🧐 ChatNio 是一个 AI 聊天网站它可以与您进行对话并提供各种功能</p>
<p>🎃 您可以向它提问问题寻求建议或者闲聊</p> <p>🎃 您可以向它提问问题寻求建议或者闲聊</p>
<p>🎈 欢迎开始与 ChatNio 展开交流</p> <p>🎈 欢迎开始与 ChatNio 展开交流</p>
<p v-if="auth">🔨 点击头像即可进入<span @click="settings">设置</span></p>
<p v-else> <span @click="login">登录</span>后可享更多功能上下文对话历史记录等</p>
</div> </div>
</div> </div>
<div class="input-wrapper"> <div class="input-wrapper">
@ -141,6 +152,20 @@ onMounted(() => {
margin: 12px 16px; 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 { .time {
color: var(--card-text-secondary); color: var(--card-text-secondary);
font-size: 16px; font-size: 16px;

View File

@ -9,7 +9,7 @@ const message = ref("登录中...");
onMounted(async () => { onMounted(async () => {
const url = new URL(location.href); const url = new URL(location.href);
const client = url.searchParams.get("token"); 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 { try {
const res = await axios.post("/login", { const res = await axios.post("/login", {

View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -28,9 +28,8 @@ func ConnectMySQL() *sql.DB {
CreateUserTable(db) CreateUserTable(db)
CreateConversationTable(db) CreateConversationTable(db)
CreateSubscriptionTable(db)
CreatePackageTable(db) CreatePackageTable(db)
CreatePaymentLogTable(db) CreateUsageTable(db)
return 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) { func CreatePackageTable(db *sql.DB) {
_, err := db.Exec(` _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS package ( CREATE TABLE IF NOT EXISTS package (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT, user_id INT,
money DECIMAL(12,2) DEFAULT 0, type VARCHAR(255),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 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 { if err != nil {

View File

@ -12,10 +12,12 @@ type Conversation struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Message []types.ChatGPTMessage `json:"message"` Message []types.ChatGPTMessage `json:"message"`
EnableGPT4 bool `json:"enable_gpt4"`
} }
type FormMessage struct { type FormMessage struct {
Message string `json:"message" binding:"required"` Message string `json:"message" binding:"required"`
GPT4 bool `json:"gpt4"`
} }
func NewConversation(db *sql.DB, id int64) *Conversation { 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, Id: GetConversationLengthByUserID(db, id) + 1,
Name: "new chat", Name: "new chat",
Message: []types.ChatGPTMessage{}, 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 { func (c *Conversation) GetName() string {
return c.Name return c.Name
} }
@ -116,6 +127,7 @@ func (c *Conversation) AddMessageFromUserForm(data []byte) (string, error) {
} }
c.AddMessageFromUser(form.Message) c.AddMessageFromUser(form.Message)
c.SetEnableGPT4(form.GPT4)
return form.Message, nil return form.Message, nil
} }

View File

@ -27,6 +27,9 @@ func main() {
app.GET("/chat", api.ChatAPI) app.GET("/chat", api.ChatAPI)
app.POST("/login", auth.LoginAPI) app.POST("/login", auth.LoginAPI)
app.POST("/state", auth.StateAPI) 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/list", conversation.ListAPI)
app.GET("/conversation/load", conversation.LoadAPI) app.GET("/conversation/load", conversation.LoadAPI)
app.GET("/conversation/delete", conversation.DeleteAPI) app.GET("/conversation/delete", conversation.DeleteAPI)

View File

@ -29,6 +29,11 @@ var limits = map[string]Limiter{
"/login": {Duration: 10, Count: 5}, "/login": {Duration: 10, Count: 5},
"/anonymous": {Duration: 60, Count: 15}, "/anonymous": {Duration: 60, Count: 15},
"/user": {Duration: 1, Count: 1}, "/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 { func GetPrefixMap[T comparable](s string, p map[string]T) *T {