重构对话框组件,使用 React.forwardRef 优化 DialogOverlayDialogContent 的实现,提升代码可读性和可维护性。同时,更新 ReferralSettingsPageSettingsPage 以使用新的 API 路径,确保数据获取和保存逻辑的一致性,增强用户体验。

This commit is contained in:
乘风
2026-02-09 19:48:36 +08:00
parent 1c4a086124
commit 0e716cbc6e
6 changed files with 322 additions and 248 deletions

View File

@@ -11,42 +11,43 @@ function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>
return <DialogPrimitive.Portal {...props} />
}
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
className={cn('fixed inset-0 z-50 bg-black/50', className)}
{...props}
/>
)
}
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/50', className)}
{...props}
/>
))
DialogOverlay.displayName = 'DialogOverlay'
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }
>(({ className, children, showCloseButton = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = 'DialogContent'
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />

View File

@@ -31,10 +31,10 @@ export function ReferralSettingsPage() {
const [saving, setSaving] = useState(false)
useEffect(() => {
get<{ success?: boolean; data?: ReferralConfig }>('/api/db/config/full?key=referral_config')
.then((data) => {
const c = (data as { data?: ReferralConfig; config?: ReferralConfig })?.data ?? (data as { config?: ReferralConfig })?.config
if (c) {
get<{ success?: boolean; data?: ReferralConfig }>('/api/admin/referral-settings')
.then((res) => {
const c = (res as { data?: ReferralConfig })?.data
if (c && typeof c === 'object') {
setConfig({
distributorShare: c.distributorShare ?? 90,
minWithdrawAmount: c.minWithdrawAmount ?? 10,
@@ -51,19 +51,14 @@ export function ReferralSettingsPage() {
const handleSave = async () => {
setSaving(true)
try {
const safeConfig = {
const body = {
distributorShare: Number(config.distributorShare) || 0,
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
bindingDays: Number(config.bindingDays) || 0,
userDiscount: Number(config.userDiscount) || 0,
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
}
const body = {
key: 'referral_config',
value: safeConfig,
description: '分销 / 推广规则配置',
}
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
const res = await post<{ success?: boolean; error?: string }>('/api/admin/referral-settings', body)
if (!res || (res as { success?: boolean }).success === false) {
alert('保存失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : ''))
return

View File

@@ -9,6 +9,14 @@ import {
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
@@ -23,7 +31,6 @@ import {
Gift,
X,
Plus,
Smartphone,
} from 'lucide-react'
import { get, post } from '@/api/client'
@@ -43,14 +50,6 @@ interface LocalSettings {
authorInfo: AuthorInfo
}
interface MpConfig {
appId: string
apiDomain: string
buyerDiscount: number
referralBindDays: number
minWithdraw: number
}
interface FeatureConfig {
matchEnabled: boolean
referralEnabled: boolean
@@ -74,14 +73,6 @@ const defaultSettings: LocalSettings = {
authorInfo: { ...defaultAuthor },
}
const defaultMp: MpConfig = {
appId: 'wxb8bbb2b10dec74aa',
apiDomain: 'https://soul.quwanzhi.com',
buyerDiscount: 5,
referralBindDays: 30,
minWithdraw: 10,
}
const defaultFeatures: FeatureConfig = {
matchEnabled: true,
referralEnabled: true,
@@ -89,54 +80,6 @@ const defaultFeatures: FeatureConfig = {
aboutEnabled: true,
}
function parseConfigResponse(raw: unknown): {
freeChapters?: string[]
mpConfig?: Partial<MpConfig>
features?: Partial<FeatureConfig>
sectionPrice?: number
baseBookPrice?: number
distributorShare?: number
authorInfo?: AuthorInfo
} {
if (!raw || typeof raw !== 'object') return {}
const o = raw as Record<string, unknown>
const out: ReturnType<typeof parseConfigResponse> = {}
if (Array.isArray(o.freeChapters)) out.freeChapters = o.freeChapters as string[]
if (o.mpConfig && typeof o.mpConfig === 'object') out.mpConfig = o.mpConfig as Partial<MpConfig>
if (o.features && typeof o.features === 'object') out.features = o.features as Partial<FeatureConfig>
if (typeof o.sectionPrice === 'number') out.sectionPrice = o.sectionPrice
if (typeof o.baseBookPrice === 'number') out.baseBookPrice = o.baseBookPrice
if (typeof o.distributorShare === 'number') out.distributorShare = o.distributorShare
if (o.authorInfo && typeof o.authorInfo === 'object') out.authorInfo = o.authorInfo as AuthorInfo
return out
}
function mergeFromConfigList(list: unknown[]): ReturnType<typeof parseConfigResponse> {
const out: ReturnType<typeof parseConfigResponse> = {}
for (const item of list) {
if (!item || typeof item !== 'object') continue
const row = item as { configKey?: string; configValue?: string }
const key = row.configKey
let val: unknown
try {
val = typeof row.configValue === 'string' ? JSON.parse(row.configValue) : row.configValue
} catch {
val = row.configValue
}
if (key === 'feature_config' && val && typeof val === 'object') out.features = val as Partial<FeatureConfig>
if (key === 'mp_config' && val && typeof val === 'object') out.mpConfig = val as Partial<MpConfig>
if (key === 'free_chapters' && Array.isArray(val)) out.freeChapters = val as string[]
if (key === 'site_settings' && val && typeof val === 'object') {
const s = val as Record<string, unknown>
if (typeof s.sectionPrice === 'number') out.sectionPrice = s.sectionPrice
if (typeof s.baseBookPrice === 'number') out.baseBookPrice = s.baseBookPrice
if (typeof s.distributorShare === 'number') out.distributorShare = s.distributorShare
if (s.authorInfo && typeof s.authorInfo === 'object') out.authorInfo = s.authorInfo as AuthorInfo
}
}
return out
}
export function SettingsPage() {
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
const [freeChapters, setFreeChapters] = useState<string[]>([
@@ -148,53 +91,47 @@ export function SettingsPage() {
'appendix-3',
])
const [newFreeChapter, setNewFreeChapter] = useState('')
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMp)
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
const [isSaving, setIsSaving] = useState(false)
const [loading, setLoading] = useState(true)
const [dialogOpen, setDialogOpen] = useState(false)
const [dialogTitle, setDialogTitle] = useState('')
const [dialogMessage, setDialogMessage] = useState('')
const [dialogIsError, setDialogIsError] = useState(false)
const [featureSwitchSaving, setFeatureSwitchSaving] = useState(false)
const showResult = (title: string, message: string, isError = false) => {
setDialogTitle(title)
setDialogMessage(message)
setDialogIsError(isError)
setDialogOpen(true)
}
useEffect(() => {
const load = async () => {
try {
const [configRes, appConfigRes] = await Promise.all([
get<{ success?: boolean; data?: unknown } | Record<string, unknown>>('/api/db/config/full'),
get<Record<string, unknown>>('/api/config').catch(() => null),
])
let parsed = parseConfigResponse(
(configRes as { data?: unknown })?.data ?? configRes,
)
const data = (configRes as { data?: unknown })?.data
if (Array.isArray(data)) parsed = { ...parsed, ...mergeFromConfigList(data) }
if (parsed.freeChapters?.length) setFreeChapters(parsed.freeChapters)
if (parsed.mpConfig && Object.keys(parsed.mpConfig).length)
setMpConfig((prev) => ({ ...prev, ...parsed.mpConfig }))
if (parsed.features && Object.keys(parsed.features).length)
setFeatureConfig((prev) => ({ ...prev, ...parsed.features }))
if (
appConfigRes?.authorInfo &&
typeof appConfigRes.authorInfo === 'object'
) {
const res = await get<{
success?: boolean
freeChapters?: string[]
featureConfig?: Partial<FeatureConfig>
siteSettings?: { sectionPrice?: number; baseBookPrice?: number; distributorShare?: number; authorInfo?: AuthorInfo }
}>('/api/admin/settings')
if (!res || (res as { success?: boolean }).success === false) return
if (Array.isArray(res.freeChapters) && res.freeChapters.length) setFreeChapters(res.freeChapters)
if (res.featureConfig && Object.keys(res.featureConfig).length)
setFeatureConfig((prev) => ({ ...prev, ...res.featureConfig }))
if (res.siteSettings && typeof res.siteSettings === 'object') {
const s = res.siteSettings
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, ...(appConfigRes.authorInfo as AuthorInfo) },
}))
}
if (
typeof parsed.sectionPrice === 'number' ||
typeof parsed.baseBookPrice === 'number' ||
typeof parsed.distributorShare === 'number' ||
(parsed.authorInfo && Object.keys(parsed.authorInfo).length)
) {
setLocalSettings((prev) => ({
...prev,
...(typeof parsed.sectionPrice === 'number' && { sectionPrice: parsed.sectionPrice }),
...(typeof parsed.baseBookPrice === 'number' && { baseBookPrice: parsed.baseBookPrice }),
...(typeof parsed.distributorShare === 'number' && { distributorShare: parsed.distributorShare }),
...(parsed.authorInfo && { authorInfo: { ...prev.authorInfo, ...parsed.authorInfo } }),
...(typeof s.sectionPrice === 'number' && { sectionPrice: s.sectionPrice }),
...(typeof s.baseBookPrice === 'number' && { baseBookPrice: s.baseBookPrice }),
...(typeof s.distributorShare === 'number' && { distributorShare: s.distributorShare }),
...(s.authorInfo && typeof s.authorInfo === 'object' && { authorInfo: { ...prev.authorInfo, ...s.authorInfo } }),
}))
}
} catch (e) {
console.error('Load config error:', e)
console.error('Load settings error:', e)
} finally {
setLoading(false)
}
@@ -202,37 +139,58 @@ export function SettingsPage() {
load()
}, [])
const saveFeatureConfigOnly = async (
nextConfig: FeatureConfig,
onFailRevert: () => void,
) => {
setFeatureSwitchSaving(true)
try {
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
featureConfig: nextConfig,
})
if (!res || (res as { success?: boolean }).success === false) {
onFailRevert()
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
return
}
showResult('已保存', '功能开关已更新,相关入口将随之显示或隐藏。')
} catch (error) {
console.error('Save feature config error:', error)
onFailRevert()
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
} finally {
setFeatureSwitchSaving(false)
}
}
const handleFeatureSwitch = (field: keyof FeatureConfig, checked: boolean) => {
const prev = featureConfig
const next = { ...prev, [field]: checked }
setFeatureConfig(next)
saveFeatureConfigOnly(next, () => setFeatureConfig(prev))
}
const handleSave = async () => {
setIsSaving(true)
try {
await post('/api/db/config', {
key: 'free_chapters',
value: freeChapters,
description: '免费章节ID列表',
}).catch(() => {})
await post('/api/db/config', {
key: 'mp_config',
value: mpConfig,
description: '小程序配置',
}).catch(() => {})
await post('/api/db/config', {
key: 'feature_config',
value: featureConfig,
description: '功能开关配置',
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
freeChapters,
featureConfig,
siteSettings: {
sectionPrice: localSettings.sectionPrice,
baseBookPrice: localSettings.baseBookPrice,
distributorShare: localSettings.distributorShare,
authorInfo: localSettings.authorInfo,
},
})
const verifyRes = await get<{ features?: FeatureConfig }>('/api/db/config/full').catch(() => ({}))
const verifyData = Array.isArray((verifyRes as { data?: unknown })?.data)
? mergeFromConfigList((verifyRes as { data: unknown[] }).data)
: parseConfigResponse((verifyRes as { data?: unknown })?.data ?? verifyRes)
if (verifyData.features)
setFeatureConfig((prev) => ({ ...prev, ...verifyData.features }))
alert(
'设置已保存!\n\n找伙伴功能' +
(verifyData.features?.matchEnabled ? '✅ 开启' : '❌ 关闭'),
)
if (!res || (res as { success?: boolean }).success === false) {
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
return
}
showResult('已保存', '设置已保存成功。')
} catch (error) {
console.error('Save settings error:', error)
alert('保存失败: ' + (error instanceof Error ? error.message : String(error)))
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
} finally {
setIsSaving(false)
}
@@ -523,9 +481,8 @@ export function SettingsPage() {
<Switch
id="match-enabled"
checked={featureConfig.matchEnabled}
onCheckedChange={(checked) =>
setFeatureConfig((prev) => ({ ...prev, matchEnabled: checked }))
}
disabled={featureSwitchSaving}
onCheckedChange={(checked) => handleFeatureSwitch('matchEnabled', checked)}
/>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
@@ -541,9 +498,8 @@ export function SettingsPage() {
<Switch
id="referral-enabled"
checked={featureConfig.referralEnabled}
onCheckedChange={(checked) =>
setFeatureConfig((prev) => ({ ...prev, referralEnabled: checked }))
}
disabled={featureSwitchSaving}
onCheckedChange={(checked) => handleFeatureSwitch('referralEnabled', checked)}
/>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
@@ -559,9 +515,8 @@ export function SettingsPage() {
<Switch
id="search-enabled"
checked={featureConfig.searchEnabled}
onCheckedChange={(checked) =>
setFeatureConfig((prev) => ({ ...prev, searchEnabled: checked }))
}
disabled={featureSwitchSaving}
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
/>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
@@ -577,9 +532,8 @@ export function SettingsPage() {
<Switch
id="about-enabled"
checked={featureConfig.aboutEnabled}
onCheckedChange={(checked) =>
setFeatureConfig((prev) => ({ ...prev, aboutEnabled: checked }))
}
disabled={featureSwitchSaving}
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
/>
</div>
</div>
@@ -590,72 +544,31 @@ export function SettingsPage() {
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Smartphone className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300">AppID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.appId}
onChange={(e) => setMpConfig((prev) => ({ ...prev, appId: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">API域名</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.apiDomain}
onChange={(e) => setMpConfig((prev) => ({ ...prev, apiDomain: e.target.value }))}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-gray-300"> (%)</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.buyerDiscount}
onChange={(e) =>
setMpConfig((prev) => ({ ...prev, buyerDiscount: Number(e.target.value) }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.referralBindDays}
onChange={(e) =>
setMpConfig((prev) => ({ ...prev, referralBindDays: Number(e.target.value) }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"> ()</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.minWithdraw}
onChange={(e) =>
setMpConfig((prev) => ({ ...prev, minWithdraw: Number(e.target.value) }))
}
/>
</div>
</div>
</CardContent>
</Card>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent
className="bg-[#0f2137] border-gray-700 text-white"
showCloseButton={true}
>
<DialogHeader>
<DialogTitle className={dialogIsError ? 'text-red-400' : 'text-[#38bdac]'}>
{dialogTitle}
</DialogTitle>
<DialogDescription className="text-gray-400 whitespace-pre-wrap pt-2">
{dialogMessage}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
<Button
onClick={() => setDialogOpen(false)}
className={dialogIsError ? 'bg-gray-600 hover:bg-gray-500' : 'bg-[#38bdac] hover:bg-[#2da396]'}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -122,6 +122,167 @@ func DBConfigGet(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
}
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回免费章节、功能开关、站点/作者与价格
func AdminSettingsGet(c *gin.Context) {
db := database.DB()
out := gin.H{
"success": true,
"freeChapters": []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"},
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
}
keys := []string{"free_chapters", "feature_config", "site_settings"}
for _, k := range keys {
var row model.SystemConfig
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
continue
}
var val interface{}
if err := json.Unmarshal(row.ConfigValue, &val); err != nil {
continue
}
switch k {
case "free_chapters":
if arr, ok := val.([]interface{}); ok && len(arr) > 0 {
ss := make([]string, 0, len(arr))
for _, x := range arr {
if s, ok := x.(string); ok {
ss = append(ss, s)
}
}
if len(ss) > 0 {
out["freeChapters"] = ss
}
}
case "feature_config":
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
out["featureConfig"] = m
}
case "site_settings":
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
out["siteSettings"] = m
}
}
}
c.JSON(http.StatusOK, out)
}
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格(不包含小程序配置,该配置已移除)
func AdminSettingsPost(c *gin.Context) {
var body struct {
FreeChapters []string `json:"freeChapters"`
FeatureConfig map[string]interface{} `json:"featureConfig"`
SiteSettings map[string]interface{} `json:"siteSettings"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
return
}
db := database.DB()
saveKey := func(key, desc string, value interface{}) error {
valBytes, err := json.Marshal(value)
if err != nil {
return err
}
var row model.SystemConfig
err = db.Where("config_key = ?", key).First(&row).Error
if err != nil {
row = model.SystemConfig{ConfigKey: key, ConfigValue: valBytes, Description: &desc}
return db.Create(&row).Error
}
row.ConfigValue = valBytes
if desc != "" {
row.Description = &desc
}
return db.Save(&row).Error
}
if body.FreeChapters != nil {
if err := saveKey("free_chapters", "免费章节ID列表", body.FreeChapters); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存免费章节失败: " + err.Error()})
return
}
}
if body.FeatureConfig != nil {
if err := saveKey("feature_config", "功能开关配置", body.FeatureConfig); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存功能开关失败: " + err.Error()})
return
}
}
if body.SiteSettings != nil {
if err := saveKey("site_settings", "站点与作者配置", body.SiteSettings); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存站点设置失败: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "设置已保存"})
}
// AdminReferralSettingsGet GET /api/admin/referral-settings 推广设置页专用:仅返回 referral_config
func AdminReferralSettingsGet(c *gin.Context) {
db := database.DB()
defaultConfig := gin.H{
"distributorShare": float64(90),
"minWithdrawAmount": float64(10),
"bindingDays": float64(30),
"userDiscount": float64(5),
"enableAutoWithdraw": false,
}
var row model.SystemConfig
if err := db.Where("config_key = ?", "referral_config").First(&row).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultConfig})
return
}
var val map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &val); err != nil || len(val) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultConfig})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": val})
}
// AdminReferralSettingsPost POST /api/admin/referral-settings 推广设置页专用:仅保存 referral_config请求体为完整配置对象
func AdminReferralSettingsPost(c *gin.Context) {
var body struct {
DistributorShare float64 `json:"distributorShare"`
MinWithdrawAmount float64 `json:"minWithdrawAmount"`
BindingDays float64 `json:"bindingDays"`
UserDiscount float64 `json:"userDiscount"`
EnableAutoWithdraw bool `json:"enableAutoWithdraw"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
return
}
val := gin.H{
"distributorShare": body.DistributorShare,
"minWithdrawAmount": body.MinWithdrawAmount,
"bindingDays": body.BindingDays,
"userDiscount": body.UserDiscount,
"enableAutoWithdraw": body.EnableAutoWithdraw,
}
valBytes, err := json.Marshal(val)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
db := database.DB()
desc := "分销 / 推广规则配置"
var row model.SystemConfig
if err := db.Where("config_key = ?", "referral_config").First(&row).Error; err != nil {
row = model.SystemConfig{ConfigKey: "referral_config", ConfigValue: valBytes, Description: &desc}
err = db.Create(&row).Error
} else {
row.ConfigValue = valBytes
row.Description = &desc
err = db.Save(&row).Error
}
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "推广设置已保存"})
}
// DBConfigPost POST /api/db/config
func DBConfigPost(c *gin.Context) {
var body struct {

View File

@@ -59,6 +59,10 @@ func Setup(cfg *config.Config) *gin.Engine {
admin.DELETE("/referral", handler.AdminReferral)
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
admin.GET("/settings", handler.AdminSettingsGet)
admin.POST("/settings", handler.AdminSettingsPost)
admin.GET("/referral-settings", handler.AdminReferralSettingsGet)
admin.POST("/referral-settings", handler.AdminReferralSettingsPost)
}
// ----- 鉴权 -----

Binary file not shown.