mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
feat: support gift code operation (#90)
This commit is contained in:
parent
5adc0fd815
commit
de5d863686
@ -136,6 +136,27 @@ func RedeemListAPI(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, GetRedeemData(db))
|
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) {
|
func InvitationPaginationAPI(c *gin.Context) {
|
||||||
db := utils.GetDBFromContext(c)
|
db := utils.GetDBFromContext(c)
|
||||||
|
|
||||||
|
@ -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, `
|
rows, err := globals.QueryDb(db, `
|
||||||
SELECT code, quota, type, used, updated_at FROM invitation
|
SELECT invitation.code, invitation.quota, invitation.type, invitation.used,
|
||||||
ORDER BY id DESC LIMIT ? OFFSET ?
|
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)
|
`, pagination, page*pagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PaginationForm{
|
return PaginationForm{
|
||||||
@ -35,14 +40,16 @@ func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
|
|||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var invitation InvitationData
|
var invitation InvitationData
|
||||||
var date []uint8
|
var createdAt []uint8
|
||||||
if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &date); err != nil {
|
var updatedAt []uint8
|
||||||
|
if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &createdAt, &updatedAt, &invitation.Username); err != nil {
|
||||||
return PaginationForm{
|
return PaginationForm{
|
||||||
Status: false,
|
Status: false,
|
||||||
Message: err.Error(),
|
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)
|
invitations = append(invitations, invitation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,36 @@ func GetRedeemData(db *sql.DB) []RedeemData {
|
|||||||
return data
|
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 {
|
func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {
|
||||||
arr := make([]string, 0)
|
arr := make([]string, 0)
|
||||||
idx := 0
|
idx := 0
|
||||||
|
@ -20,6 +20,7 @@ func Register(app *gin.RouterGroup) {
|
|||||||
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
|
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
|
||||||
|
|
||||||
app.GET("/admin/redeem/list", RedeemListAPI)
|
app.GET("/admin/redeem/list", RedeemListAPI)
|
||||||
|
app.GET("/admin/redeem/segment", RedeemSegmentAPI)
|
||||||
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
|
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
|
||||||
|
|
||||||
app.GET("/admin/user/list", UserPaginationAPI)
|
app.GET("/admin/user/list", UserPaginationAPI)
|
||||||
|
@ -45,7 +45,9 @@ type InvitationData struct {
|
|||||||
Quota float32 `json:"quota"`
|
Quota float32 `json:"quota"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Used bool `json:"used"`
|
Used bool `json:"used"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedeemData struct {
|
type RedeemData struct {
|
||||||
|
@ -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(
|
export async function generateRedeem(
|
||||||
quota: number,
|
quota: number,
|
||||||
number: number,
|
number: number,
|
||||||
|
@ -47,6 +47,8 @@ export type InvitationData = {
|
|||||||
quota: number;
|
quota: number;
|
||||||
type: string;
|
type: string;
|
||||||
used: boolean;
|
used: boolean;
|
||||||
|
username: string;
|
||||||
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,6 +71,9 @@ export type Redeem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RedeemResponse = Redeem[];
|
export type RedeemResponse = Redeem[];
|
||||||
|
export type RedeemSegmentResponse = CommonResponse & {
|
||||||
|
data: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type InvitationGenerateResponse = {
|
export type InvitationGenerateResponse = {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
|
@ -135,7 +135,6 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid hsl(var(--border-hover));
|
border: 1px solid hsl(var(--border-hover));
|
||||||
transition: 0.25s linear;
|
transition: 0.25s linear;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: hsl(var(--border-active));
|
border-color: hsl(var(--border-active));
|
||||||
|
@ -175,7 +175,9 @@ function InvitationTable() {
|
|||||||
<TableHead>{t("admin.quota")}</TableHead>
|
<TableHead>{t("admin.quota")}</TableHead>
|
||||||
<TableHead>{t("admin.type")}</TableHead>
|
<TableHead>{t("admin.type")}</TableHead>
|
||||||
<TableHead>{t("admin.used")}</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>
|
<TableHead>{t("admin.action")}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@ -183,11 +185,15 @@ function InvitationTable() {
|
|||||||
{(data.data || []).map((invitation, idx) => (
|
{(data.data || []).map((invitation, idx) => (
|
||||||
<TableRow key={idx} className={`whitespace-nowrap`}>
|
<TableRow key={idx} className={`whitespace-nowrap`}>
|
||||||
<TableCell>{invitation.code}</TableCell>
|
<TableCell>{invitation.code}</TableCell>
|
||||||
<TableCell>{invitation.quota}</TableCell>
|
<TableCell>
|
||||||
|
<Badge variant={`outline`}>{invitation.quota}</Badge>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge>{invitation.type}</Badge>
|
<Badge>{invitation.type}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
|
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
|
||||||
|
<TableCell>{invitation.username || "-"}</TableCell>
|
||||||
|
<TableCell>{invitation.created_at}</TableCell>
|
||||||
<TableCell>{invitation.updated_at}</TableCell>
|
<TableCell>{invitation.updated_at}</TableCell>
|
||||||
<TableCell className={`flex gap-2`}>
|
<TableCell className={`flex gap-2`}>
|
||||||
<TemporaryButton
|
<TemporaryButton
|
||||||
|
@ -26,6 +26,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
|
|||||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||||
import { saveAsFile } from "@/utils/dom.ts";
|
import { saveAsFile } from "@/utils/dom.ts";
|
||||||
import { useEffectAsync } from "@/utils/hook.ts";
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
|
import { Badge } from "@/components/ui/badge.tsx";
|
||||||
|
|
||||||
function GenerateDialog({ sync }: { sync: () => void }) {
|
function GenerateDialog({ sync }: { sync: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -157,7 +158,11 @@ function RedeemTable() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((redeem, idx) => (
|
{data.map((redeem, idx) => (
|
||||||
<TableRow key={idx} className={`whitespace-nowrap`}>
|
<TableRow key={idx} className={`whitespace-nowrap`}>
|
||||||
<TableCell>{redeem.quota}</TableCell>
|
<TableCell>
|
||||||
|
<Badge variant={`outline`}>
|
||||||
|
{redeem.quota}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{redeem.total}</TableCell>
|
<TableCell>{redeem.total}</TableCell>
|
||||||
<TableCell>{redeem.used}</TableCell>
|
<TableCell>{redeem.used}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -482,7 +482,10 @@
|
|||||||
"operate-success-prompt": "您的操作已成功执行。",
|
"operate-success-prompt": "您的操作已成功执行。",
|
||||||
"operate-failed": "操作失败",
|
"operate-failed": "操作失败",
|
||||||
"operate-failed-prompt": "操作失败,原因:{{reason}}",
|
"operate-failed-prompt": "操作失败,原因:{{reason}}",
|
||||||
|
"created-at": "创建时间",
|
||||||
"updated-at": "更新时间",
|
"updated-at": "更新时间",
|
||||||
|
"used-at": "领取时间",
|
||||||
|
"used-username": "领取用户",
|
||||||
"used-true": "已使用",
|
"used-true": "已使用",
|
||||||
"used-false": "未使用",
|
"used-false": "未使用",
|
||||||
"generate": "批量生成",
|
"generate": "批量生成",
|
||||||
|
@ -48,9 +48,9 @@
|
|||||||
"fast": "Fast",
|
"fast": "Fast",
|
||||||
"english-model": "English Model",
|
"english-model": "English Model",
|
||||||
"badges": {
|
"badges": {
|
||||||
"non-billing": "FREE PASS",
|
"non-billing": "free",
|
||||||
"times-billing": "{{price}}/time",
|
"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": {
|
"market": {
|
||||||
@ -672,9 +672,12 @@
|
|||||||
"unban-action-desc": "Are you sure you want to unblock this user?",
|
"unban-action-desc": "Are you sure you want to unblock this user?",
|
||||||
"billing": "Income",
|
"billing": "Income",
|
||||||
"chatnio-format-only": "This format is unique to Chat Nio",
|
"chatnio-format-only": "This format is unique to Chat Nio",
|
||||||
"exit": "Log out of the background",
|
"exit": "Exit",
|
||||||
"view": "View",
|
"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": {
|
"mask": {
|
||||||
"title": "Mask Settings",
|
"title": "Mask Settings",
|
||||||
|
@ -674,7 +674,10 @@
|
|||||||
"chatnio-format-only": "このフォーマットはChat Nioに固有です",
|
"chatnio-format-only": "このフォーマットはChat Nioに固有です",
|
||||||
"exit": "バックグラウンドからログアウト",
|
"exit": "バックグラウンドからログアウト",
|
||||||
"view": "確認",
|
"view": "確認",
|
||||||
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。"
|
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
|
||||||
|
"created-at": "作成日時",
|
||||||
|
"used-at": "乗車時間",
|
||||||
|
"used-username": "ユーザーを請求する"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
"title": "プリセット設定",
|
"title": "プリセット設定",
|
||||||
|
@ -674,7 +674,10 @@
|
|||||||
"chatnio-format-only": "Этот формат уникален для Chat Nio",
|
"chatnio-format-only": "Этот формат уникален для Chat Nio",
|
||||||
"exit": "Выйти из фонового режима",
|
"exit": "Выйти из фонового режима",
|
||||||
"view": "проверить",
|
"view": "проверить",
|
||||||
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр."
|
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
|
||||||
|
"created-at": "Время создания",
|
||||||
|
"used-at": "Время награждения",
|
||||||
|
"used-username": "Получить пользователя"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
"title": "Настройки маски",
|
"title": "Настройки маски",
|
||||||
|
@ -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 {
|
func ConvertSqlTime(t time.Time) string {
|
||||||
return t.Format("2006-01-02 15:04:05")
|
return t.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user