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))
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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": "批量生成",
|
||||
|
@ -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",
|
||||
|
@ -674,7 +674,10 @@
|
||||
"chatnio-format-only": "このフォーマットはChat Nioに固有です",
|
||||
"exit": "バックグラウンドからログアウト",
|
||||
"view": "確認",
|
||||
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。"
|
||||
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
|
||||
"created-at": "作成日時",
|
||||
"used-at": "乗車時間",
|
||||
"used-username": "ユーザーを請求する"
|
||||
},
|
||||
"mask": {
|
||||
"title": "プリセット設定",
|
||||
|
@ -674,7 +674,10 @@
|
||||
"chatnio-format-only": "Этот формат уникален для Chat Nio",
|
||||
"exit": "Выйти из фонового режима",
|
||||
"view": "проверить",
|
||||
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр."
|
||||
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
|
||||
"created-at": "Время создания",
|
||||
"used-at": "Время награждения",
|
||||
"used-username": "Получить пользователя"
|
||||
},
|
||||
"mask": {
|
||||
"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 {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user