v1.2.35: Admin页面显示28字段,登录/AI/配号次数统计,修复OCR/短信/资讯API等问题

This commit is contained in:
甲辰生产 2026-03-31 21:02:18 +08:00
parent 190d11347f
commit 83c8410627
7 changed files with 186 additions and 24 deletions

View File

@ -1 +1 @@
1.2.31
1.2.35

View File

@ -454,6 +454,10 @@ def get_seek_match(
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 更新用户配号(寻号)次数
current_user.f99_96_search_count = (current_user.f99_96_search_count or 0) + 1
db.commit()
# 获取用户所有藏品
collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id,

View File

@ -113,6 +113,9 @@ async def recognize_image(
# 生成OSS临时路径: temp/{year}/{month}/{day}/{uuid}.{ext}
oss_key, temp_id = get_oss_path("temp", filename=image.filename)
# 初始化temp_path为空
temp_path = None
# 上传到OSS
try:
image_url = upload_to_oss(image_data, oss_key)
@ -159,7 +162,7 @@ async def recognize_image(
if response.status_code != 200:
# 识别失败,删除临时文件
if os.path.exists(temp_path):
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
raise HTTPException(status_code=500, detail=f"OCR API 调用失败:{response.text[:200]}")

View File

@ -8,10 +8,10 @@ from typing import Optional
# 阿里云短信配置
SMS_CONFIG = {
"access_key_id": os.getenv("SMS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"),
"access_key_secret": os.getenv("SMS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"),
"sign_name": "苏州算力",
"template_code": "SMS_501590956",
"access_key_id": os.getenv("SMS_ACCESS_KEY_ID", "LTAI5tQAx5niD7JQVqGE5acE"),
"access_key_secret": os.getenv("SMS_ACCESS_KEY_SECRET", "QsQFAEKBkaNynIoKyvdIi3BUyWVZu1"),
"sign_name": os.getenv("SMS_SIGN_NAME", "苏州算力"),
"template_code": os.getenv("SMS_TEMPLATE_CODE", "SMS_501590956"),
}
# 验证码缓存生产环境建议用Redis

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v1.2.30</title>
<title>甲辰收藏 v1.2.35</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

View File

@ -126,7 +126,16 @@ export default function Admin() {
const updateData = {
username: editingUser.username,
email: editingUser.email,
role: editingUser.role
role: editingUser.role,
user_code: editingUser.user_code,
level: editingUser.level,
loginCount: editingUser.loginCount,
aiCount: editingUser.aiCount,
searchCount: editingUser.searchCount,
points: editingUser.points,
balance: editingUser.balance,
totalAmount: editingUser.totalAmount,
phoneVerified: editingUser.phoneVerified
}
//
@ -186,9 +195,9 @@ export default function Admin() {
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{users.map((user) => (
<div key={user.id} style={{ background: 'rgba(30, 41, 59, 0.8)', borderRadius: '12px', padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold' }}>{user.username}</div>
<div style={{
padding: '2px 8px', borderRadius: '4px',
@ -198,9 +207,15 @@ export default function Admin() {
}}>
{user.role === 'admin' ? '👑 管理员' : (user.role === 'editor' ? '📝 信息员' : '👤 用户')}
</div>
<div style={{ background: 'rgba(59, 130, 246, 0.2)', padding: '2px 8px', borderRadius: '4px', color: '#60a5fa', fontSize: '12px' }}>
编码: {user.user_code || '-'}
</div>
<div style={{ color: '#64748b', fontSize: '13px', marginTop: '4px' }}>
📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'}
<div style={{ background: 'rgba(168, 85, 247, 0.2)', padding: '2px 8px', borderRadius: '4px', color: '#a855f7', fontSize: '12px' }}>
等级: {user.level || '青铜'}
</div>
</div>
<div style={{ color: '#64748b', fontSize: '13px', marginTop: '8px' }}>
📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'} | 📱已认证: {user.phoneVerified ? '✓' : '✗'}
</div>
</div>
<div style={{ textAlign: 'right' }}>
@ -208,6 +223,30 @@ export default function Admin() {
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collectionCount || 0}</div>
</div>
</div>
{/* 更多字段 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<div style={{ background: 'rgba(16, 185, 129, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#10b981' }}>
🔑 登录: {user.loginCount || 0}
</div>
<div style={{ background: 'rgba(59, 130, 246, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#3b82f6' }}>
🤖 AI识别: {user.aiCount || 0}
</div>
<div style={{ background: 'rgba(20, 184, 166, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#14b8a6' }}>
🎯 配号: {user.searchCount || 0}
</div>
<div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#f59e0b' }}>
🔍 寻号: {user.searchCount || 0}
</div>
<div style={{ background: 'rgba(168, 85, 247, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#a855f7' }}>
💎 积分: {user.points || 0}
</div>
<div style={{ background: 'rgba(236, 72, 153, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#ec4899' }}>
💰 余额: {user.balance || 0}
</div>
<div style={{ background: 'rgba(34, 197, 94, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#22c55e' }}>
📊 累计: {user.totalAmount || 0}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<div style={{ color: '#64748b', fontSize: '12px' }}>
注册时间{user.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '-'}
@ -393,6 +432,122 @@ export default function Admin() {
</div>
</div>
{/* 第三部分:扩展信息 */}
<div style={{ marginBottom: '24px' }}>
<h3 style={{ color: '#a855f7', fontSize: '15px', fontWeight: 'bold', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid rgba(168, 85, 247, 0.3)' }}>📊 扩展信息</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>用户编码</label>
<input
type="text"
value={editingUser.user_code || ''}
onChange={(e) => setEditingUser({ ...editingUser, user_code: e.target.value })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>会员等级</label>
<select
value={editingUser.level || '青铜'}
onChange={(e) => setEditingUser({ ...editingUser, level: e.target.value })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
>
<option value="青铜">青铜</option>
<option value="白银">白银</option>
<option value="黄金">黄金</option>
<option value="铂金">铂金</option>
<option value="钻石">钻石</option>
</select>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>登录次数</label>
<input
type="number"
value={editingUser.loginCount || 0}
onChange={(e) => setEditingUser({ ...editingUser, loginCount: parseInt(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>AI识别次数</label>
<input
type="number"
value={editingUser.aiCount || 0}
onChange={(e) => setEditingUser({ ...editingUser, aiCount: parseInt(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>配号次数</label>
<input
type="number"
value={editingUser.searchCount || 0}
onChange={(e) => setEditingUser({ ...editingUser, searchCount: parseInt(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>寻号次数</label>
<input
type="number"
value={editingUser.searchCount || 0}
onChange={(e) => setEditingUser({ ...editingUser, searchCount: parseInt(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>积分</label>
<input
type="number"
value={editingUser.points || 0}
onChange={(e) => setEditingUser({ ...editingUser, points: parseInt(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>余额</label>
<input
type="number"
step="0.01"
value={editingUser.balance || 0}
onChange={(e) => setEditingUser({ ...editingUser, balance: parseFloat(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>累计金额</label>
<input
type="number"
step="0.01"
value={editingUser.totalAmount || 0}
onChange={(e) => setEditingUser({ ...editingUser, totalAmount: parseFloat(e.target.value) || 0 })}
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}
/>
</div>
</div>
<div style={{ marginTop: '12px' }}>
<label style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: '13px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={editingUser.phoneVerified || false}
onChange={(e) => setEditingUser({ ...editingUser, phoneVerified: e.target.checked })}
style={{ marginRight: '8px' }}
/>
手机号已认证
</label>
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => setEditingUser(null)}

View File

@ -79,7 +79,7 @@ function UserManagement({ token, API_BASE }) {
const handleUpdateUser = async () => {
try {
const res = await fetch(`${API_BASE}/api/admin/users/${editingUser.f99_90_id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(editingUser) })
const res = await fetch(`${API_BASE}/api/admin/users/${editingUser.id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(editingUser) })
if (res.ok) { alert('更新成功'); setEditingUser(null); fetchUsers() }
} catch (e) { alert('更新失败') }
}
@ -89,7 +89,7 @@ function UserManagement({ token, API_BASE }) {
try { await fetch(`${API_BASE}/api/admin/users/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); fetchUsers() } catch (e) { console.error(e) }
}
const filteredUsers = users.filter(u => u.f01_01_name?.includes(search) || u.phone?.includes(search) || u.user_code?.includes(search))
const filteredUsers = users.filter(u => u.username?.includes(search) || u.phone?.includes(search) || u.user_code?.includes(search))
return (
<div>
@ -101,21 +101,21 @@ function UserManagement({ token, API_BASE }) {
{loading ? <div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div> : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '12px' }}>
{filteredUsers.map(u => (
<div key={u.f99_90_id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
<div key={u.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: '16px' }}>{u.f01_01_name?.[0] || '?'}</div>
<div><div style={{ color: '#fff', fontWeight: 'bold' }}>{u.f01_01_name}</div><div style={{ color: '#64748b', fontSize: '12px' }}>{u.user_code || '-'}</div></div>
<div style={{ width: '40px', height: '40px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: '16px' }}>{u.username?.[0] || '?'}</div>
<div><div style={{ color: '#fff', fontWeight: 'bold' }}>{u.username}</div><div style={{ color: '#64748b', fontSize: '12px' }}>{u.user_code || '-'}</div></div>
</div>
<span style={{ background: u.role === 'admin' ? '#f59e0b' : '#10b981', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '12px' }}>{u.role === 'admin' ? '管理员' : '用户'}</span>
</div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '8px' }}>
<div>📱 {u.phone || '-'}</div><div>🏷 等级: {u.f99_94_level || '青铜'} | 藏品: {u.f99_97_collection_count || 0}</div>
<div>🔑 登录: {u.f99_98_login_count || 0} | AI: {u.f99_95_ai_count || 0}</div>
<div>📱 {u.phone || '-'}</div><div>🏷 等级: {u.level || '青铜'} | 藏品: {u.collectionCount || 0}</div>
<div>🔑 登录: {u.loginCount || 0} | AI: {u.aiCount || 0}</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setEditingUser(u)} style={{ flex: 1, padding: '6px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>编辑</button>
<button onClick={() => handleDeleteUser(u.f99_90_id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
<button onClick={() => handleDeleteUser(u.id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
</div>
</div>
))}
@ -144,8 +144,8 @@ function UserManagement({ token, API_BASE }) {
<h3 style={{ color: '#fbbf24', marginTop: 0 }}>编辑用户</h3>
<input type="text" placeholder="手机号" value={editingUser.phone || ''} onChange={(e) => setEditingUser({...editingUser, phone: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<select value={editingUser.role} onChange={(e) => setEditingUser({...editingUser, role: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px' }}><option value="user">普通用户</option><option value="admin">管理员</option></select>
<input type="number" placeholder="积分" value={editingUser.f99_100_points || 0} onChange={(e) => setEditingUser({...editingUser, f99_100_points: parseInt(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<input type="number" placeholder="余额" value={editingUser.f01_11_balance || 0} onChange={(e) => setEditingUser({...editingUser, f01_11_balance: parseFloat(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
<input type="number" placeholder="积分" value={editingUser.points || 0} onChange={(e) => setEditingUser({...editingUser, points: parseInt(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<input type="number" placeholder="余额" value={editingUser.balance || 0} onChange={(e) => setEditingUser({...editingUser, balance: parseFloat(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={handleUpdateUser} style={{ flex: 1, padding: '10px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>保存</button>
<button onClick={() => setEditingUser(null)} style={{ flex: 1, padding: '10px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>取消</button>
@ -173,9 +173,9 @@ function Statistics({ token, API_BASE }) {
const totalUsers = Array.isArray(usersData) ? usersData.length : 0
const totalCollections = collectionsData.total || 0
const levelCount = {}
if (Array.isArray(usersData)) { usersData.forEach(u => { const level = u.f99_94_level || '青铜'; levelCount[level] = (levelCount[level] || 0) + 1 }) }
if (Array.isArray(usersData)) { usersData.forEach(u => { const level = u.level || '青铜'; levelCount[level] = (levelCount[level] || 0) + 1 }) }
const statusCount = { in_collection: collectionsData.byStatus?.find(s => s.status === 'in_collection')?.count || 0, sold: collectionsData.byStatus?.find(s => s.status === 'sold')?.count || 0 }
const totalBalance = Array.isArray(usersData) ? usersData.reduce((sum, u) => sum + (u.f01_11_balance || 0), 0) : 0
const totalBalance = Array.isArray(usersData) ? usersData.reduce((sum, u) => sum + (u.balance || 0), 0) : 0
setStats({ users: { total: totalUsers, levels: levelCount }, collections: { total: totalCollections, status: statusCount }, balance: { total: totalBalance } })
} catch (e) { console.error(e) }
setLoading(false)