feat: support gift code operation (#90)

This commit is contained in:
Zhang Minghan 2024-03-11 16:28:47 +08:00
parent 5adc0fd815
commit de5d863686
15 changed files with 132 additions and 16 deletions

View File

@ -136,6 +136,27 @@ func RedeemListAPI(c *gin.Context) {
c.JSON(http.StatusOK, GetRedeemData(db))
}
func RedeemSegmentAPI(c *gin.Context) {
quota := utils.ParseFloat32(c.Query("quota"))
onlyUnused := utils.ParseBool(c.Query("unused"))
db := utils.GetDBFromContext(c)
data, err := GetRedeemSegment(db, quota, onlyUnused)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
"data": data,
})
}
func InvitationPaginationAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)

View File

@ -22,9 +22,14 @@ func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
}
}
// get used_user from auth table by `user_id`
rows, err := globals.QueryDb(db, `
SELECT code, quota, type, used, updated_at FROM invitation
ORDER BY id DESC LIMIT ? OFFSET ?
SELECT invitation.code, invitation.quota, invitation.type, invitation.used,
invitation.created_at, invitation.updated_at,
COALESCE(auth.username, '-') as username
FROM invitation
LEFT JOIN auth ON auth.id = invitation.used_id
ORDER BY invitation.id DESC LIMIT ? OFFSET ?
`, pagination, page*pagination)
if err != nil {
return PaginationForm{
@ -35,14 +40,16 @@ func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
for rows.Next() {
var invitation InvitationData
var date []uint8
if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &date); err != nil {
var createdAt []uint8
var updatedAt []uint8
if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &createdAt, &updatedAt, &invitation.Username); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
invitation.UpdatedAt = utils.ConvertTime(date).Format("2006-01-02 15:04:05")
invitation.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05")
invitation.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05")
invitations = append(invitations, invitation)
}

View File

@ -31,6 +31,36 @@ func GetRedeemData(db *sql.DB) []RedeemData {
return data
}
func GetRedeemSegment(db *sql.DB, quota float32, onlyUnused bool) ([]string, error) {
var codes []string
var rows *sql.Rows
var err error
if onlyUnused {
rows, err = globals.QueryDb(db, `
SELECT code FROM redeem WHERE quota = ? AND used = 0
`, quota)
} else {
rows, err = globals.QueryDb(db, `
SELECT code FROM redeem WHERE quota = ?
`, quota)
}
if err != nil {
return codes, err
}
for rows.Next() {
var code string
if err := rows.Scan(&code); err != nil {
return codes, err
}
codes = append(codes, code)
}
return codes, nil
}
func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {
arr := make([]string, 0)
idx := 0

View File

@ -20,6 +20,7 @@ func Register(app *gin.RouterGroup) {
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
app.GET("/admin/redeem/list", RedeemListAPI)
app.GET("/admin/redeem/segment", RedeemSegmentAPI)
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
app.GET("/admin/user/list", UserPaginationAPI)

View File

@ -45,7 +45,9 @@ type InvitationData struct {
Quota float32 `json:"quota"`
Type string `json:"type"`
Used bool `json:"used"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Username string `json:"username"`
}
type RedeemData struct {

View File

@ -138,6 +138,18 @@ export async function getRedeemList(): Promise<RedeemResponse> {
}
}
export async function getRedeemSegment(quota: number, only_unused: boolean) {
try {
const response = await axios.get(
`/admin/redeem/segment?quota=${quota}&unused=${only_unused}`,
);
return response.data as RedeemResponse;
} catch (e) {
console.warn(e);
return [];
}
}
export async function generateRedeem(
quota: number,
number: number,

View File

@ -47,6 +47,8 @@ export type InvitationData = {
quota: number;
type: string;
used: boolean;
username: string;
created_at: string;
updated_at: string;
};
@ -69,6 +71,9 @@ export type Redeem = {
};
export type RedeemResponse = Redeem[];
export type RedeemSegmentResponse = CommonResponse & {
data: string[];
};
export type InvitationGenerateResponse = {
status: boolean;

View File

@ -135,7 +135,6 @@
border-radius: var(--radius);
border: 1px solid hsl(var(--border-hover));
transition: 0.25s linear;
cursor: pointer;
&:hover {
border-color: hsl(var(--border-active));

View File

@ -175,7 +175,9 @@ function InvitationTable() {
<TableHead>{t("admin.quota")}</TableHead>
<TableHead>{t("admin.type")}</TableHead>
<TableHead>{t("admin.used")}</TableHead>
<TableHead>{t("admin.updated-at")}</TableHead>
<TableHead>{t("admin.used-username")}</TableHead>
<TableHead>{t("admin.created-at")}</TableHead>
<TableHead>{t("admin.used-at")}</TableHead>
<TableHead>{t("admin.action")}</TableHead>
</TableRow>
</TableHeader>
@ -183,11 +185,15 @@ function InvitationTable() {
{(data.data || []).map((invitation, idx) => (
<TableRow key={idx} className={`whitespace-nowrap`}>
<TableCell>{invitation.code}</TableCell>
<TableCell>{invitation.quota}</TableCell>
<TableCell>
<Badge variant={`outline`}>{invitation.quota}</Badge>
</TableCell>
<TableCell>
<Badge>{invitation.type}</Badge>
</TableCell>
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
<TableCell>{invitation.username || "-"}</TableCell>
<TableCell>{invitation.created_at}</TableCell>
<TableCell>{invitation.updated_at}</TableCell>
<TableCell className={`flex gap-2`}>
<TemporaryButton

View File

@ -26,6 +26,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
import { Textarea } from "@/components/ui/textarea.tsx";
import { saveAsFile } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { Badge } from "@/components/ui/badge.tsx";
function GenerateDialog({ sync }: { sync: () => void }) {
const { t } = useTranslation();
@ -157,7 +158,11 @@ function RedeemTable() {
<TableBody>
{data.map((redeem, idx) => (
<TableRow key={idx} className={`whitespace-nowrap`}>
<TableCell>{redeem.quota}</TableCell>
<TableCell>
<Badge variant={`outline`}>
{redeem.quota}
</Badge>
</TableCell>
<TableCell>{redeem.total}</TableCell>
<TableCell>{redeem.used}</TableCell>
</TableRow>

View File

@ -482,7 +482,10 @@
"operate-success-prompt": "您的操作已成功执行。",
"operate-failed": "操作失败",
"operate-failed-prompt": "操作失败,原因:{{reason}}",
"created-at": "创建时间",
"updated-at": "更新时间",
"used-at": "领取时间",
"used-username": "领取用户",
"used-true": "已使用",
"used-false": "未使用",
"generate": "批量生成",

View File

@ -48,9 +48,9 @@
"fast": "Fast",
"english-model": "English Model",
"badges": {
"non-billing": "FREE PASS",
"non-billing": "free",
"times-billing": "{{price}} / time",
"token-billing": "Input {{input}}/1k tokens Output {{output}}/1k tokens"
"token-billing": "{{input}} / 1k input tokens {{output}} / 1k output tokens"
}
},
"market": {
@ -672,9 +672,12 @@
"unban-action-desc": "Are you sure you want to unblock this user?",
"billing": "Income",
"chatnio-format-only": "This format is unique to Chat Nio",
"exit": "Log out of the background",
"exit": "Exit",
"view": "View",
"broadcast-tip": "Notifications will only show the most recent one and will only be notified once. Site announcements can be set in the system settings. The pop-up window will be displayed on the homepage for the first time and subsequent viewing will be supported."
"broadcast-tip": "Notifications will only show the most recent one and will only be notified once. Site announcements can be set in the system settings. The pop-up window will be displayed on the homepage for the first time and subsequent viewing will be supported.",
"created-at": "Creation Time",
"used-at": "Collection time",
"used-username": "Claim User"
},
"mask": {
"title": "Mask Settings",

View File

@ -674,7 +674,10 @@
"chatnio-format-only": "このフォーマットはChat Nioに固有です",
"exit": "バックグラウンドからログアウト",
"view": "確認",
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。"
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
"created-at": "作成日時",
"used-at": "乗車時間",
"used-username": "ユーザーを請求する"
},
"mask": {
"title": "プリセット設定",

View File

@ -674,7 +674,10 @@
"chatnio-format-only": "Этот формат уникален для Chat Nio",
"exit": "Выйти из фонового режима",
"view": "проверить",
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр."
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
"created-at": "Время создания",
"used-at": "Время награждения",
"used-username": "Получить пользователя"
},
"mask": {
"title": "Настройки маски",

View File

@ -122,6 +122,22 @@ func ParseInt64(value string) int64 {
}
}
func ParseFloat32(value string) float32 {
if res, err := strconv.ParseFloat(value, 32); err == nil {
return float32(res)
} else {
return 0
}
}
func ParseBool(value string) bool {
if res, err := strconv.ParseBool(value); err == nil {
return res
} else {
return false
}
}
func ConvertSqlTime(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
}