feat: support initial user quota (#41)

This commit is contained in:
Zhang Minghan 2024-01-19 13:09:58 +08:00
parent 8dfb1185b8
commit 8e678637fb
12 changed files with 159 additions and 19 deletions

View File

@ -23,8 +23,14 @@ export type SearchState = {
query: number;
};
export type SiteState = {
quota: number;
announcement: string;
};
export type SystemProps = {
general: GeneralState;
site: SiteState;
mail: MailState;
search: SearchState;
};
@ -70,6 +76,10 @@ export const initialSystemState: SystemProps = {
docs: "",
file: "",
},
site: {
quota: 0,
announcement: "",
},
mail: {
host: "",
port: 465,

View File

@ -218,6 +218,20 @@ input[type="number"] {
align-items: center;
font-size: 0.9rem;
&.row-layout {
align-items: flex-start !important;
flex-direction: column !important;
& > * {
margin-right: 0 !important;
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
label {
font-size: 0.9rem;
font-weight: normal;

View File

@ -71,11 +71,17 @@ function Paragraph({
function ParagraphItem({
children,
className,
rowLayout,
}: {
children: React.ReactNode;
className?: string;
rowLayout?: boolean;
}) {
return <div className={cn("paragraph-item", className)}>{children}</div>;
return (
<div className={cn("paragraph-item", className, rowLayout && "row-layout")}>
{children}
</div>
);
}
export function ParagraphDescription({ children }: { children: string }) {

View File

@ -507,6 +507,7 @@
"system": {
"general": "常规设置",
"search": "联网搜索",
"site": "站点设置",
"mail": "SMTP 发件设置",
"save": "保存",
"updateRoot": "修改 Root 密码",
@ -533,7 +534,11 @@
"mailFrom": "发件人",
"searchEndpoint": "搜索接入点",
"searchQuery": "最大搜索结果数",
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。"
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。",
"quota": "用户初始点数",
"quotaTip": "用户注册后赠送的点数",
"announcement": "站点公告",
"announcementPlaceholder": "请输入站点公告 (支持 Markdown / HTML 格式)"
},
"logger": {
"title": "服务日志",

View File

@ -433,7 +433,12 @@
"docsTip": "Document link, leave blank for default https://docs.chatnio.net",
"file": "File Parsing Service",
"filePlaceholder": "File parsing service, leave blank for default https://blob.chatnio.net (stability not guaranteed)",
"fileTip": "For file parsing services, please refer to the [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) project to build."
"fileTip": "For file parsing services, please refer to the [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) project to build.",
"site": "Site Ayarları",
"quota": "User Initial Points",
"quotaTip": "Credits given after user registration",
"announcement": "Site Announcement",
"announcementPlaceholder": "Please enter a site announcement (Markdown/HTML format supported)"
},
"user": "User Management",
"invitation-code": "Invitation Code",

View File

@ -433,7 +433,12 @@
"docsTip": "ドキュメントリンク、デフォルトの場合は空白のままhttps://docs.chatnio.net",
"file": "ファイル解析サービス",
"filePlaceholder": "ファイル解析サービス、デフォルトの場合は空白のままhttps://blob.chatnio.net (安定性は保証されていません)",
"fileTip": "ファイル解析サービスについては、[chatnio-blob-service ]( https://github.com/Deeptrain-Community/chatnio-blob-service)プロジェクトを参照して構築してください。"
"fileTip": "ファイル解析サービスについては、[chatnio-blob-service ]( https://github.com/Deeptrain-Community/chatnio-blob-service)プロジェクトを参照して構築してください。",
"site": "サイト設定",
"quota": "ユーザーの初期ポイント",
"quotaTip": "ユーザー登録後に付与されたクレジット",
"announcement": "サイトのお知らせ",
"announcementPlaceholder": "サイトのお知らせを入力してください( Markdown/HTML形式に対応"
},
"user": "ユーザー管理",
"invitation-code": "招待コード",

View File

@ -433,7 +433,12 @@
"docsTip": "Ссылка на документ, оставьте пустым для по умолчанию https://docs.chatnio.net",
"file": "Служба разбора файлов",
"filePlaceholder": "Служба разбора файлов, оставьте пустым для по умолчанию https://blob.chatnio.net (стабильность не гарантируется)",
"fileTip": "Для получения услуг по разбору файлов обратитесь к проекту [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) для сборки."
"fileTip": "Для получения услуг по разбору файлов обратитесь к проекту [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) для сборки.",
"site": "Настройки сайта",
"quota": "Начальные точки пользователя",
"quotaTip": "Кредиты, предоставленные после регистрации пользователя",
"announcement": "Объявление о площадке",
"announcementPlaceholder": "Введите объявление сайта (поддерживается формат Markdown/HTML)"
},
"user": "Управление пользователями",
"invitation-code": "Код приглашения",

View File

@ -23,6 +23,7 @@ import {
MailState,
SearchState,
setConfig,
SiteState,
SystemProps,
updateRootPassword,
} from "@/admin/api/system.ts";
@ -41,6 +42,8 @@ import {
import { DialogTitle } from "@radix-ui/react-dialog";
import Require from "@/components/Require.tsx";
import { Loader2 } from "lucide-react";
import { Textarea } from "@/components/ui/textarea.tsx";
import Tips from "@/components/Tips.tsx";
type CompProps<T> = {
data: T;
@ -366,6 +369,64 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
);
}
function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
const { t } = useTranslation();
// export type SiteState = {
// quota: number;
// announcement: string;
// }
return (
<Paragraph
title={t("admin.system.site")}
configParagraph={true}
isCollapsed={true}
>
<ParagraphItem>
<Label>
{t("admin.system.quota")}
<Tips
className={`inline-block`}
content={t("admin.system.quotaTip")}
/>
</Label>
<NumberInput
value={data.quota}
onValueChange={(value) =>
dispatch({ type: "update:site.quota", value })
}
placeholder={`5`}
min={0}
/>
</ParagraphItem>
<ParagraphItem rowLayout={true}>
<Label>{t("admin.system.announcement")}</Label>
<Textarea
value={data.announcement}
onChange={(e) =>
dispatch({
type: "update:site.announcement",
value: e.target.value,
})
}
placeholder={t("admin.system.announcementPlaceholder")}
/>
</ParagraphItem>
<ParagraphFooter>
<div className={`grow`} />
<Button
size={`sm`}
loading={true}
onClick={async () => await onChange()}
>
{t("admin.system.save")}
</Button>
</ParagraphFooter>
</Paragraph>
);
}
function Search({ data, dispatch, onChange }: CompProps<SearchState>) {
const { t } = useTranslation();
@ -452,6 +513,7 @@ function System() {
</CardHeader>
<CardContent className={`flex flex-col gap-1`}>
<General data={data.general} dispatch={setData} onChange={doSaving} />
<Site data={data.site} dispatch={setData} onChange={doSaving} />
<Mail data={data.mail} dispatch={setData} onChange={doSaving} />
<Search data={data.search} dispatch={setData} onChange={doSaving} />
</CardContent>

View File

@ -12,21 +12,23 @@ export function setKey<T>(state: T, key: string, value: any): T {
}
export const formReducer = <T>() => {
return (state: T, action: any) => {
return (state: T, action: any): T => {
action.payload = action.payload ?? action.value;
switch (action.type) {
case "update":
return { ...state, ...action.payload };
return { ...state, ...action.payload } as T;
case "reset":
return { ...action.payload };
return { ...action.payload } as T;
case "set":
return action.payload;
return action.payload as T;
default:
if (action.type.startsWith("update:")) {
const key = action.type.slice(7);
return setKey(state, key, action.payload);
return setKey(state, key, action.payload) as T;
}
return state;
}
};
};

View File

@ -144,6 +144,7 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
return "", err
}
user.CreateInitialQuota(db)
return user.GenerateToken()
}
@ -193,6 +194,8 @@ func DeepLogin(c *gin.Context, token string) (string, error) {
Username: user.Username,
Password: password,
}
u.CreateInitialQuota(db)
return u.GenerateToken()
}

View File

@ -1,6 +1,16 @@
package auth
import "database/sql"
import (
"chat/channel"
"database/sql"
)
func (u *User) CreateInitialQuota(db *sql.DB) bool {
_, err := db.Exec(`
INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)
`, u.GetID(db), channel.SystemInstance.GetInitialQuota(), 0.)
return err == nil
}
func (u *User) GetQuota(db *sql.DB) float32 {
var quota float32

View File

@ -9,10 +9,11 @@ import (
)
type ApiInfo struct {
Title string `json:"title"`
Logo string `json:"logo"`
File string `json:"file"`
Docs string `json:"docs"`
Title string `json:"title"`
Logo string `json:"logo"`
File string `json:"file"`
Docs string `json:"docs"`
Announcement string `json:"announcement"`
}
type generalState struct {
@ -23,6 +24,11 @@ type generalState struct {
Docs string `json:"docs" mapstructure:"docs"`
}
type siteState struct {
Quota float64 `json:"quota" mapstructure:"quota"`
Announcement string `json:"announcement" mapstructure:"announcement"`
}
type mailState struct {
Host string `json:"host" mapstructure:"host"`
Port int `json:"port" mapstructure:"port"`
@ -38,6 +44,7 @@ type searchState struct {
type SystemConfig struct {
General generalState `json:"general" mapstructure:"general"`
Site siteState `json:"site" mapstructure:"site"`
Mail mailState `json:"mail" mapstructure:"mail"`
Search searchState `json:"search" mapstructure:"search"`
}
@ -65,21 +72,27 @@ func (c *SystemConfig) SaveConfig() error {
func (c *SystemConfig) AsInfo() ApiInfo {
return ApiInfo{
Title: c.General.Title,
Logo: c.General.Logo,
File: c.General.File,
Docs: c.General.Docs,
Title: c.General.Title,
Logo: c.General.Logo,
File: c.General.File,
Docs: c.General.Docs,
Announcement: c.Site.Announcement,
}
}
func (c *SystemConfig) UpdateConfig(data *SystemConfig) error {
c.General = data.General
c.Site = data.Site
c.Mail = data.Mail
c.Search = data.Search
return c.SaveConfig()
}
func (c *SystemConfig) GetInitialQuota() float64 {
return c.Site.Quota
}
func (c *SystemConfig) GetBackend() string {
return c.General.Backend
}