diff --git a/config/VERSION b/config/VERSION index 22fd10f..e1f6aea 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1,14 +1,5 @@ # 甲辰藏品管理系统 - 版本配置 -# Version Configuration for Zodiac Collection Management System - -# 当前版本号 (语义化版本:主版本。次版本.修订版) -VERSION=1.1.21 - -# 版本代号 (可选) -VERSION_CODENAME="新生" - -# 发布日期 -RELEASE_DATE=2026-03-18 - -# 版本说明 -VERSION_NOTES="性能优化 - Nginx反向代理、图片OSS直连、自动压缩、上传限制20MB" +VERSION=1.2.1 +VERSION_CODENAME="阶段性稳定版本" +RELEASE_DATE=2026-03-20 +VERSION_NOTES="密码强度验证 - 至少8位需包含大写+小写+数字" diff --git a/config/docker-compose-test.yml b/config/docker-compose-test.yml new file mode 100644 index 0000000..d43ead2 --- /dev/null +++ b/config/docker-compose-test.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: jiachenlong-db-test + restart: unless-stopped + environment: + POSTGRES_DB: zodiac + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + image: python:3.11-slim + container_name: jiachenlong-backend-test + restart: unless-stopped + working_dir: /app + command: > + bash -c "pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic python-jose bcrypt python-multipart pillow dashscope alibabacloud-dysmsapi20170525 -q && uvicorn app.main:app --host 0.0.0.0 --port 3000" + ports: + - "3000:3000" + volumes: + - /root/jiachenlong/backend:/app + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/zodiac + - SECRET_KEY=test-secret-key-for-sms + - ACCESS_TOKEN_EXPIRE_MINUTES=60 + - OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG + - OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1 + - OSS_BUCKET=jiachenlong-oss + - OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com + - SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE + - SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1 + - SMS_SIGN_NAME=苏州算力 + - SMS_TEMPLATE_CODE=SMS_501590956 + depends_on: + postgres: + condition: service_healthy + +volumes: + postgres_data: diff --git a/frontend/index.html b/frontend/index.html index 3030ca7..8a47fdb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.1.19 + 甲辰收藏 v1.2.0 diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 91e16b0..653f65f 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -205,7 +205,7 @@ export default function Admin() {
藏品数
-
{user.collection_count || 0}
+
window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collection_count || 0}
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx index e6a2bff..8168df9 100644 --- a/frontend/src/pages/List.jsx +++ b/frontend/src/pages/List.jsx @@ -6,6 +6,9 @@ export default function List() { const [loading, setLoading] = useState(true) const [filter, setFilter] = useState('') const [filterType, setFilterType] = useState('') + const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '') + const urlUserId = urlParams.get('userId') || '' + const [userIdFilter, setUserIdFilter] = useState(urlUserId) const [sortField, setSortField] = useState('createdAt') const [sortOrder, setSortOrder] = useState('desc') const [viewMode, setViewMode] = useState('list') @@ -80,6 +83,11 @@ export default function List() { } // 应用筛选条件 + // 用户ID筛选(从URL获取) + if (userIdFilter) { + list = list.filter(item => item.userId === userIdFilter || item.userId === userIdFilter.replace(/-/g, '')) + } + if (filterType && filter) { const fieldMap = { status: 'status', diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 1d175ee..58549f7 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -12,21 +12,83 @@ export default function Login() { // 注册相关状态 const [showRegister, setShowRegister] = useState(false) - const [registerData, setRegisterData] = useState({ username: '', password: '', confirmPassword: '', email: '' }) + const [registerData, setRegisterData] = useState({ + username: '', + password: '', + confirmPassword: '', + email: '', + phone: '', + verifyCode: '' + }) const [registerLoading, setRegisterLoading] = useState(false) const [registerError, setRegisterError] = useState('') + + // 短信获取相关 + const [sendingCode, setSendingCode] = useState(false) + const [codeCountdown, setCodeCountdown] = useState(0) + const [codeSent, setCodeSent] = useState(false) - // 生成验证码 + // 生成获取 useEffect(() => { generateCaptcha() }, []) + // 获取倒计时 + useEffect(() => { + if (codeCountdown > 0) { + const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000) + return () => clearTimeout(timer) + } + }, [codeCountdown]) + const generateCaptcha = () => { const num1 = Math.floor(Math.random() * 10) const num2 = Math.floor(Math.random() * 10) setCaptcha({ num1, num2, answer: '' }) } + // 发送短信获取 + const handleSendCode = async () => { + if (!registerData.phone) { + setRegisterError('请先输入手机号') + return + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/ + if (!phoneRegex.test(registerData.phone)) { + setRegisterError('请输入正确的手机号') + return + } + + setSendingCode(true) + setRegisterError('') + + try { + const res = await fetch('/api/auth/send-verification-code', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ phone: registerData.phone }) + }) + + const data = await res.json() + + if (data.success) { + setCodeSent(true) + setCodeCountdown(60) + setRegisterError('') + } else { + setRegisterError(data.message || '发送失败') + } + } catch (err) { + setRegisterError('发送失败,请稍后重试') + } finally { + setSendingCode(false) + } + } + // 处理注册 const handleRegister = async () => { // 验证输入 @@ -35,13 +97,13 @@ export default function Login() { return } - if (registerData.username.length < 3) { - setRegisterError('用户名至少 3 个字符') + if (registerData.username.length < 2) { + setRegisterError('用户名至少 2 个字符') return } - if (registerData.password.length < 6) { - setRegisterError('密码至少 6 个字符') + if (registerData.password.length < 8 || !/[A-Z]/.test(registerData.password) || !/[a-z]/.test(registerData.password) || !/[0-9]/.test(registerData.password)) { + setRegisterError('密码至少8位,需包含大写、小写、数字') return } @@ -50,13 +112,33 @@ export default function Login() { return } + // 验证手机号 + if (!registerData.phone) { + setRegisterError('手机号为必填项') + return + } + + const phoneRegex = /^1[3-9]\d{9}$/ + if (!phoneRegex.test(registerData.phone)) { + setRegisterError('请输入正确的手机号') + return + } + + // 验证短信获取 + if (!registerData.verifyCode) { + setRegisterError('请输入短信获取') + return + } + setRegisterLoading(true) setRegisterError('') try { const payload = { username: registerData.username, - password: registerData.password + password: registerData.password, + phone: registerData.phone, + verifyCode: registerData.verifyCode } if (registerData.email) { payload.email = registerData.email @@ -73,12 +155,14 @@ export default function Login() { const data = await res.json() if (!res.ok) { - throw new Error(data.error?.message || '注册失败') + throw new Error(data.detail || data.error?.message || '注册失败') } alert('注册成功!请登录') setShowRegister(false) - setRegisterData({ username: '', password: '', confirmPassword: '', email: '' }) + setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' }) + setCodeSent(false) + setCodeCountdown(0) generateCaptcha() } catch (err) { console.error('注册错误:', err) @@ -95,17 +179,13 @@ export default function Login() { return } - if (username.length < 3) { + if (username.length < 2) { setError(ErrorCodes.E00021.message) return } - if (password.length < 6) { - setError(ErrorCodes.E00022.message) - return - } - // 验证验证码 + // 验证获取 const expectedAnswer = captcha.num1 + captcha.num2 if (!captcha.answer || Number(captcha.answer) !== expectedAnswer) { setError(ErrorCodes.E00012.message) @@ -241,11 +321,11 @@ export default function Login() { />
- {/* 验证码 */} + {/* 获取 */}

注册新账号 🎉 @@ -381,7 +463,7 @@ export default function Login() { type="text" value={registerData.username} onChange={(e) => setRegisterData(prev => ({ ...prev, username: e.target.value }))} - placeholder="用户名(至少 3 个字符)" + placeholder="用户名(至少 2 个字符)" style={{ width: '100%', padding: '14px', @@ -395,6 +477,68 @@ export default function Login() { />

+ {/* 手机号 */} +
+ setRegisterData(prev => ({ ...prev, phone: e.target.value }))} + placeholder="手机号(11位)" + maxLength={11} + style={{ + width: '100%', + padding: '14px', + borderRadius: '8px', + border: '1px solid rgba(255,255,255,0.2)', + background: 'rgba(255,255,255,0.05)', + color: '#fff', + fontSize: '16px', + outline: 'none' + }} + /> +
+ + {/* 短信获取 */} +
+
+ setRegisterData(prev => ({ ...prev, verifyCode: e.target.value }))} + placeholder="短信验证码" + maxLength={6} + style={{ + flex: 1, minWidth: "90px", width: "auto", + padding: '14px', + borderRadius: '8px', + border: '1px solid rgba(255,255,255,0.2)', + background: 'rgba(255,255,255,0.05)', + color: '#fff', + fontSize: '16px', + outline: 'none' + }} + /> + +
+
+ {/* 邮箱(可选) */}
setRegisterData(prev => ({ ...prev, password: e.target.value }))} - placeholder="密码(至少 6 个字符)" + placeholder="密码(至少8位,需大写+小写+数字)" style={{ width: '100%', padding: '14px', @@ -460,11 +604,13 @@ export default function Login() {
- + setForm({ ...form, email: e.target.value })} + placeholder="选填" style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }} />
@@ -213,27 +290,69 @@ export default function Settings() { setForm({ ...form, phone: e.target.value })} + value={newPhone} + onChange={(e) => { + setNewPhone(e.target.value) + setPhoneChanged(true) + setVerifyCode('') + }} + placeholder="11位手机号" + maxLength={11} style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }} />
+ {/* 手机验证码 - 仅在手机号变更时显示 */} + {phoneChanged && ( +
+ +
+ setVerifyCode(e.target.value)} + placeholder="6位验证码" + maxLength={6} + style={{ flex: 1, padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }} + /> + +
+
+ )} +
- + setForm({ ...form, address: e.target.value })} + placeholder="选填" style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }} />
- +