This commit is contained in:
2026-06-16 13:12:43 +08:00
parent 496759ccdf
commit 1b15561e0a
6 changed files with 156 additions and 7 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-C5wDengw.js"></script> <script type="module" crossorigin src="/assets/index-DFBUW3T1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
+18 -1
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Button, Card, Form, Input, message, Space, Typography, Upload, Button, Card, Form, Input, message, Space, Switch, Typography, Upload,
} from 'antd'; } from 'antd';
import { import {
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined,
@@ -69,6 +69,7 @@ const AdminSettings: React.FC = () => {
'站点信息': configs.filter(c => c.key.startsWith('site_')), '站点信息': configs.filter(c => c.key.startsWith('site_')),
'协议配置': configs.filter(c => c.key === 'user_agreement_url' || c.key === 'privacy_policy_url'), '协议配置': configs.filter(c => c.key === 'user_agreement_url' || c.key === 'privacy_policy_url'),
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')), 'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
}; };
const getFieldDescription = (config: SystemConfig): string => { const getFieldDescription = (config: SystemConfig): string => {
@@ -80,6 +81,9 @@ const AdminSettings: React.FC = () => {
seo_title: '搜索引擎结果中显示的标题', seo_title: '搜索引擎结果中显示的标题',
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内', seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
seo_keywords: '用逗号分隔的关键词列表', seo_keywords: '用逗号分隔的关键词列表',
user_register_credits: '用户注册时赠送的初始积分,默认100',
user_login_credits: '用户每日登录赠送的积分数量',
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
}; };
return descMap[config.key] || config.description || ''; return descMap[config.key] || config.description || '';
}; };
@@ -91,6 +95,19 @@ const AdminSettings: React.FC = () => {
if (config.key === 'seo_keywords') { if (config.key === 'seo_keywords') {
return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />; return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />;
} }
if (config.key === 'user_login_credits_enabled') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Switch defaultChecked={config.value === 'true'} />
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
{config.value === 'true' ? '已启用' : '已禁用'}
</Typography.Text>
</div>
);
}
if (config.key === 'user_register_credits' || config.key === 'user_login_credits') {
return <Input type="number" min={0} placeholder={config.description} size="large" />;
}
return <Input placeholder={config.description} size="large" />; return <Input placeholder={config.description} size="large" />;
}; };
+95 -3
View File
@@ -3,10 +3,10 @@ import {
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography,
} from 'antd'; } from 'antd';
import { import {
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword } from '../api'; import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword, getSystemConfigs, updateSystemConfig } from '../api';
import type { AdminUser } from '../types'; import type { AdminUser, SystemConfig } from '../types';
import { formatDate } from '../utils/formatDate'; import { formatDate } from '../utils/formatDate';
const AdminUsers: React.FC = () => { const AdminUsers: React.FC = () => {
@@ -29,6 +29,10 @@ const AdminUsers: React.FC = () => {
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [creditConfigs, setCreditConfigs] = useState<SystemConfig[]>([]);
const [configForm] = Form.useForm();
const [configSaving, setConfigSaving] = useState(false);
const load = async () => { const load = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -41,6 +45,40 @@ const AdminUsers: React.FC = () => {
useEffect(() => { load(); }, [page, pageSize, search]); useEffect(() => { load(); }, [page, pageSize, search]);
useEffect(() => {
const loadCreditConfigs = async () => {
try {
const configs = await getSystemConfigs();
const credit = configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits'));
setCreditConfigs(credit);
const formValues: Record<string, string> = {};
credit.forEach(c => { formValues[c.key] = c.value; });
configForm.setFieldsValue(formValues);
} catch { /* auth error handled by client */ }
};
loadCreditConfigs();
}, []);
const handleSaveCreditConfigs = async () => {
try {
const values = await configForm.validateFields();
setConfigSaving(true);
for (const config of creditConfigs) {
const newVal = values[config.key];
if (newVal !== undefined && newVal !== config.value) {
await updateSystemConfig(config.id, newVal ?? '');
}
}
message.success('积分配置已保存');
const configs = await getSystemConfigs();
setCreditConfigs(configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')));
setConfigSaving(false);
} catch (e: any) {
setConfigSaving(false);
message.error(e?.message || '保存失败');
}
};
const handlePageChange = (p: number, ps: number) => { const handlePageChange = (p: number, ps: number) => {
setPage(p); setPage(p);
setPageSize(ps); setPageSize(ps);
@@ -224,6 +262,60 @@ const AdminUsers: React.FC = () => {
return ( return (
<div> <div>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div style={{
width: 44, height: 44, borderRadius: 10,
background: 'rgba(99,102,241,0.08)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20, color: '#6366f1',
}}>
<SettingOutlined />
</div>
<div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
</div>
<Form form={configForm} layout="vertical">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 20 }}>
<Form.Item
name="user_register_credits"
label={<span style={{ fontWeight: 500 }}></span>}
extra="用户注册时赠送的初始积分"
>
<InputNumber min={0} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item
name="user_login_credits"
label={<span style={{ fontWeight: 500 }}></span>}
extra="用户每日首次登录赠送的积分"
>
<InputNumber min={0} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item
name="user_login_credits_enabled"
label={<span style={{ fontWeight: 500 }}></span>}
extra="是否开启每日登录赠送积分功能"
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Switch defaultChecked={creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true'} />
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
{creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true' ? '已启用' : '已禁用'}
</Typography.Text>
</div>
</Form.Item>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSaveCreditConfigs} loading={configSaving} size="large" style={{ borderRadius: 8 }}>
</Button>
</div>
</Form>
</Card>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}> <Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
{/* Search bar */} {/* Search bar */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
+37 -1
View File
@@ -78,6 +78,38 @@ async def _assign_default_frontend_menus(db: AsyncSession, user: User) -> None:
user.allowed_menus = [m.path for m in default_menus if m.path] user.allowed_menus = [m.path for m in default_menus if m.path]
async def _get_register_credits(db: AsyncSession) -> int:
result = await db.execute(
select(SystemConfig.value).where(SystemConfig.key == "user_register_credits").limit(1)
)
value = result.scalar_one_or_none()
return int(value) if value else 100
async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
enabled_result = await db.execute(
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits_enabled").limit(1)
)
enabled = enabled_result.scalar_one_or_none() == "true"
if not enabled:
return
credits_result = await db.execute(
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits").limit(1)
)
credits = int(credits_result.scalar_one_or_none() or "0")
if credits <= 0:
return
today = datetime.now().date()
if user.last_login_at:
last_login_date = user.last_login_at.date()
if last_login_date >= today:
return
user.credits += credits
@router.post( @router.post(
"/login", "/login",
summary="客户端密码登录", summary="客户端密码登录",
@@ -100,6 +132,7 @@ async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
detail="该账号不允许在此登录", detail="该账号不允许在此登录",
) )
await _handle_daily_login_credits(db, user)
user.last_login_at = datetime.now() user.last_login_at = datetime.now()
await db.flush() await db.flush()
return _token_response(user, req.remember_me) return _token_response(user, req.remember_me)
@@ -130,6 +163,7 @@ async def sms_login(req: SmsLoginRequest, db: AsyncSession = Depends(get_db)):
detail="该账号不允许在此登录", detail="该账号不允许在此登录",
) )
await _handle_daily_login_credits(db, user)
user.last_login_at = datetime.now() user.last_login_at = datetime.now()
await db.flush() await db.flush()
return _token_response(user, req.remember_me) return _token_response(user, req.remember_me)
@@ -155,13 +189,15 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
detail="该手机号已注册", detail="该手机号已注册",
) )
register_credits = await _get_register_credits(db)
user = User( user = User(
id=generate_id(), id=generate_id(),
username=req.phone, username=req.phone,
phone=req.phone, phone=req.phone,
hashed_password=hash_password(req.password), hashed_password=hash_password(req.password),
password_set_at=datetime.now(), password_set_at=datetime.now(),
credits=100, credits=register_credits,
is_admin=False, is_admin=False,
user_type="frontend", user_type="frontend",
) )
+4
View File
@@ -151,6 +151,10 @@ async def _seed_data():
("payment_alipay_private_key", "", "支付宝私钥"), ("payment_alipay_private_key", "", "支付宝私钥"),
# Text credit config # Text credit config
("text_credits_per_1000_tokens", "1", "每1000 token消耗文本积分"), ("text_credits_per_1000_tokens", "1", "每1000 token消耗文本积分"),
# User credits config
("user_register_credits", "100", "用户注册赠送积分"),
("user_login_credits", "0", "用户每日登录赠送积分"),
("user_login_credits_enabled", "false", "启用每日登录赠送积分"),
] ]
for key, value, desc in configs: for key, value, desc in configs:
existing = await db.execute( existing = await db.execute(