diff --git a/backend/app/models/models.py b/backend/app/models/models.py index e857f99..5f9df82 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -55,6 +55,7 @@ class Collection(Base): f02_11_version = Column(String(100), nullable=True, index=True) f02_12_packaging = Column(String(100), nullable=True, index=True) f02_13_rarity = Column(String(50), nullable=True, index=True) # 珍惜度 + f02_14_number_category = Column(String(20), nullable=True, index=True) # 号码分类 # f03 评级信息 f03_20_is_graded = Column(Boolean, default=False, index=True) diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index 7e1b4e2..5d91468 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -38,6 +38,7 @@ def to_camel_case(data: dict) -> dict: 'f02_11_version': 'version', 'f02_12_packaging': 'packaging', 'f02_13_rarity': 'rarity', + 'f02_14_number_category': 'numberCategory', 'f03_20_is_graded': 'isGraded', 'f03_21_grading_company': 'gradingCompany', 'f03_22_grading_score': 'gradingScore', @@ -73,8 +74,8 @@ def generate_code(version: str, user_id: str, db: Session) -> str: max_num = 0 for (code,) in user_codes: - # 只处理纯数字或 4 位数字编码(忽略 TEST001 等特殊编码) - if re.match(r'^\d{4}$', code): + # 处理纯数字编码(支持4位和5位) + if re.match(r'^\d{4,5}$', code): try: num = int(code) if num > max_num: @@ -84,7 +85,12 @@ def generate_code(version: str, user_id: str, db: Session) -> str: # 当前用户最大号 +1 next_num = max_num + 1 - return str(next_num).zfill(4) + + # 如果超过9999,使用5位;否则使用4位 + if next_num > 9999: + return str(next_num).zfill(5) + else: + return str(next_num).zfill(4) @router.get("/next-code") @@ -164,6 +170,7 @@ def get_collections( 'f02_11_version': collection_item.f02_11_version, 'f02_12_packaging': collection_item.f02_12_packaging, 'f02_13_rarity': collection_item.f02_13_rarity, + 'f02_14_number_category': collection_item.f02_14_number_category, 'f03_20_is_graded': collection_item.f03_20_is_graded, 'f03_21_grading_company': collection_item.f03_21_grading_company, 'f03_22_grading_score': collection_item.f03_22_grading_score, @@ -247,6 +254,7 @@ def get_stats( by_grading_company = Counter(c.f03_21_grading_company for c in all_collections if c.f03_20_is_graded and c.f03_21_grading_company).items() by_grading_score = Counter(c.f03_22_grading_score for c in all_collections if c.f03_20_is_graded and c.f03_22_grading_score).items() by_special_mark = Counter(c.f04_30_special_mark for c in all_collections if c.f04_30_special_mark).items() + by_number_category = Counter(c.f02_14_number_category for c in all_collections if c.f02_14_number_category).items() # 总成本: SUM(cost_price + repair_fee + grading_fee) for ALL collections total_cost = sum( @@ -291,6 +299,7 @@ def get_stats( "byGradingCompany": [{"company": c, "count": n} for c, n in by_grading_company], "byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score], "bySpecialMark": [{"mark": m, "count": n} for m, n in by_special_mark], + "byNumberCategory": [{"numberCategory": n, "count": c} for n, c in by_number_category], # 盈亏统计(只统计已售且有价格的藏品) "byProfitLoss": [ {"type": "profit", "label": "盈利", "count": sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price > c.f05_40_cost_price)}, @@ -337,6 +346,7 @@ def get_collection( 'f02_11_version': collection.get('f02_11_version'), 'f02_12_packaging': collection.get('f02_12_packaging'), 'f02_13_rarity': collection.get('f02_13_rarity'), + 'f02_14_number_category': collection.get('f02_14_number_category'), 'f03_20_is_graded': collection.get('f03_20_is_graded'), 'f03_21_grading_company': collection.get('f03_21_grading_company'), 'f03_22_grading_score': collection.get('f03_22_grading_score'), @@ -446,6 +456,7 @@ def create_collection( f02_11_version=collection_data.f02_11_version, f02_12_packaging=collection_data.f02_12_packaging, f02_13_rarity=collection_data.f02_13_rarity, + f02_14_number_category=collection_data.f02_14_number_category, f03_20_is_graded=collection_data.f03_20_is_graded or False, f03_21_grading_company=collection_data.f03_21_grading_company, f03_22_grading_score=collection_data.f03_22_grading_score, diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index b843b8e..0ba1bae 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -58,6 +58,7 @@ class CollectionBase(BaseModel): f02_11_version: Optional[str] = Field(None, alias="version") f02_12_packaging: Optional[str] = Field(None, alias="packaging") f02_13_rarity: Optional[str] = Field(None, alias="rarity") + f02_14_number_category: Optional[str] = Field(None, alias="numberCategory") # f03 评级信息 f03_20_is_graded: Optional[bool] = Field(False, alias="isGraded") @@ -104,6 +105,7 @@ class CollectionUpdate(BaseModel): f02_11_version: Optional[str] = Field(None, alias="version") f02_12_packaging: Optional[str] = Field(None, alias="packaging") f02_13_rarity: Optional[str] = Field(None, alias="rarity") + f02_14_number_category: Optional[str] = Field(None, alias="numberCategory") # f03 评级信息 f03_20_is_graded: Optional[bool] = Field(None, alias="isGraded") diff --git a/config/VERSION b/config/VERSION index cfb0091..92448f4 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.5 +VERSION=1.2.6 diff --git a/docs/部署检查清单.md b/docs/部署检查清单.md index 4166ed4..2f7f656 100644 --- a/docs/部署检查清单.md +++ b/docs/部署检查清单.md @@ -58,3 +58,15 @@ curl -X POST http://localhost:3000/api/auth/send-verification-code \ | 验证码失败 | 检查SMS_*配置是否完整 | | 导入错误 | 使用python3.11启动 | | 404错误 | 重启后端服务 | + +### 7. 用户协议页面检查 + + + + + +确认返回HTML内容 + +### 8. 前端静态文件检查 + +确认用户协议文件存在 diff --git a/frontend/index.html b/frontend/index.html index 757b50b..0aeae80 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.5 + 甲辰收藏 v1.2.6 diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx index e9a9491..f09e0cb 100644 --- a/frontend/src/pages/Add.jsx +++ b/frontend/src/pages/Add.jsx @@ -1,6 +1,7 @@ // 添加藏品页面 - 支持 AI 识别/手工录入/批量录入 import React, { useState, useRef } from 'react' import { APP_VERSION } from '../config/version' +const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}]; // 字段转换函数 const convertField = (obj) => { @@ -12,7 +13,7 @@ const convertField = (obj) => { packaging: 'f02_12_packaging', rarity: 'f02_13_rarity', isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company', gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star', - specialMark: 'f04_30_special_mark', serialFeature: 'f04_31_serial_feature', + specialMark: 'f04_30_special_mark', serialFeature: 'f04_31_serial_feature', numberCategory: 'f02_14_number_category', issuer: 'f04_32_issuer', issueYear: 'f04_33_issue_year', material: 'f04_34_material', denomination: 'f04_35_denomination', issueQuantity: 'f04_36_issue_quantity', @@ -61,11 +62,11 @@ const Input = ({ form, handleChange, label, field, type = 'text', options = null } const getDefaultForm = () => ({ - name: '龙钞', code: '', category: '自持', rarity: '通货', prefixSerial: '', + name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '', version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张', material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false, gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '', - serialFeature: '', issuer: '中国人民银行', issueYear: '2024', + serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024', costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: '' }) @@ -146,6 +147,23 @@ export default function Add() { if (key === 'goalPrice' && value) { setForm(prev => ({ ...prev, status: 'sold' })) } + // 冠字号变化时自动分类 + if (key === 'prefixSerial') { + const serial = value + let cat = '' + const match = serial.match(/J(\d{9})/) + const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9) + if (digits) { + if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347' + else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347' + else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247' + else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47' + else if (digits.includes('7') && !digits.includes('4')) cat = '无4' + else if (digits.includes('4')) cat = '带4' + else cat = '其他' + } + setForm(prev => ({ ...prev, numberCategory: cat })) + } // 版别变化时同步发行年份 if (key === 'version') { const yearMatch = value.match(/(20\d{2})/) @@ -244,7 +262,26 @@ export default function Add() { setSaving(true) setError('') const token = localStorage.getItem('token') - const formData = convertField(form) + // 保存前自动根据冠字号设置号码分类 + const formWithCategory = { ...form } + if (formWithCategory.prefixSerial && !formWithCategory.numberCategory) { + const serial = formWithCategory.prefixSerial + const match = serial.match(/J(\d{9})/) + const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9) + if (digits) { + let cat = '' + if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347' + else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347' + else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247' + else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47' + else if (digits.includes('7') && !digits.includes('4')) cat = '无4' + else if (digits.includes('4')) cat = '带4' + else cat = '其他' + formWithCategory.numberCategory = cat + } + } + + const formData = convertField(formWithCategory) try { // 1. 保存藏品信息(先检查是否重复) const res = await fetch('/api/collections', { @@ -259,6 +296,11 @@ export default function Add() { const result = await res.json() let collectionId = result.f99_90_id || result.id + // 检查是否有错误(重复编号等) + if (result.error) { + throw new Error(result.error.message || '保存失败') + } + // 检查是否有重复警告 if (result.warning && result.warning.code === 'DUPLICATE_SERIAL') { const { warning } = result @@ -536,6 +578,7 @@ export default function Add() {
基本信息
+ @@ -573,6 +616,7 @@ export default function Add() {
+ diff --git a/frontend/src/pages/Detail.jsx b/frontend/src/pages/Detail.jsx index 31c1a8d..7df0297 100644 --- a/frontend/src/pages/Detail.jsx +++ b/frontend/src/pages/Detail.jsx @@ -230,6 +230,7 @@ export default function Detail() {
+ diff --git a/frontend/src/pages/Edit.jsx b/frontend/src/pages/Edit.jsx index b9a8f08..802fc66 100644 --- a/frontend/src/pages/Edit.jsx +++ b/frontend/src/pages/Edit.jsx @@ -46,6 +46,7 @@ const categoryOptions = [ { value: '其他', label: '其他' } ] +const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}]; const rarityOptions = [ { value: '通货', label: '通货' }, { value: '特色', label: '特色' }, @@ -68,7 +69,7 @@ export default function Edit() { const editId = params.get('id') const [form, setForm] = useState({ - name: '', code: '', category: '自持', rarity: '通货', prefixSerial: '', + name: '', code: '', category: '自持', rarity: '通货', numberCategory: '', prefixSerial: '', version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张', material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false, gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '', @@ -96,6 +97,7 @@ export default function Edit() { code: data.code || '', category: data.category || '自持', rarity: data.rarity || '通货', + numberCategory: data.numberCategory || '', prefixSerial: data.prefixSerial || '', version: data.version || '2024 龙', denomination: data.denomination || '', @@ -146,6 +148,23 @@ export default function Edit() { const handleChange = (key, value) => { setForm({ ...form, [key]: value }) if (key === 'targetPrice' && value) setForm(prev => ({ ...prev, status: 'sold' })) + // 冠字号变化时自动分类 + if (key === 'prefixSerial') { + const serial = value + let cat = '' + const match = serial.match(/J(\d{9})/) + const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9) + if (digits) { + if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347' + else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347' + else if (digits.includes('4')) cat = '带4' + else if (digits.includes('7')) cat = '无4' + else if (!digits.includes('4') && !digits.includes('7')) cat = '无47' + else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247' + else cat = '其他' + } + setForm(prev => ({ ...prev, numberCategory: cat })) + } // 版别变化时同步发行年份 if (key === 'version') { const yearMatch = value.match(/(20\d{2})/) @@ -238,6 +257,7 @@ export default function Edit() { status: 'f01_04_status', remark: 'f01_05_remark', prefixSerial: 'f02_10_prefix_serial', version: 'f02_11_version', packaging: 'f02_12_packaging', rarity: 'f02_13_rarity', + numberCategory: 'f02_14_number_category', isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company', gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star', specialMark: 'f04_30_special_mark', serialFeature: 'f04_31_serial_feature', @@ -398,6 +418,7 @@ export default function Edit() {
+ diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx index ccffac6..1624077 100644 --- a/frontend/src/pages/List.jsx +++ b/frontend/src/pages/List.jsx @@ -94,6 +94,7 @@ export default function List() { category: 'category', packaging: 'packaging', rarity: 'rarity', + numberCategory: 'numberCategory', version: 'version', gradingCompany: 'gradingCompany', gradingScore: 'gradingScore', @@ -278,6 +279,11 @@ export default function List() { const rarityOrder = { '通货': 1, '特色': 2, '少见': 3, '稀有': 4, '珍品': 5, '孤品': 6 } aVal = rarityOrder[aVal] || 0 bVal = rarityOrder[bVal] || 0 + } else if (sortField === 'numberCategory') { + // 号码分类按自定义顺序排序 + const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '无4': 5, '带4': 6, '其他': 7 } + aVal = numberCategoryOrder[aVal] || 99 + bVal = numberCategoryOrder[bVal] || 99 } if (aVal == null) return 1 if (bVal == null) return -1 @@ -317,11 +323,25 @@ export default function List() { return colors[category] || '#64748b' } + const getNumberCategoryColor = (cat) => { + const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' }; + return colors[cat] || '#64748b'; + }; + const getRarityColor = (rarity) => { const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' } return colors[rarity] || '#64748b' } + const formatPrefixSerial = (serial) => { + if (!serial) return '-' + // 提取J开头的10位(1位J + 9位数字) + const match = serial.match(/J(\d{9})/) + if (match) return 'J' + match[1] + // 如果没有J开头,取前10位 + return serial.substring(0, 10) + } + const getPackagingColor = (packaging) => { const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' } return colors[packaging] || '#64748b' @@ -365,26 +385,29 @@ export default function List() {
{item.code || '-'} - {item.prefixSerial || '-'} + {formatPrefixSerial(item.prefixSerial)} {item.packaging && {item.packaging}} + {item.numberCategory && {item.numberCategory}}
{item.status && {getStatusText(item.status)}} {item.category && {getCategoryText(item.category)}} - {item.rarity && {item.rarity}}
{/* 第2行:版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */} -
-
+
+
{item.version && {item.version}} {item.isGraded && 已评级} {item.gradingCompany && {item.gradingCompany.substring(0,4)}} - {item.gradingScore && {item.gradingScore}} + {item.gradingScore && {item.gradingScore}} {item.threeStar && 三星} {item.specialMark && {item.specialMark}}
- {item.remark && {item.remark}} +
+ {item.rarity && {item.rarity}} + {item.remark && {item.remark}} +
{/* 第3行:成本 + 修复 + 评级 | 目标 + 出售 */}
@@ -405,7 +428,7 @@ export default function List() {
-
我的藏品 ({filteredCollections.length})
+
我的藏品 ({filteredCollections.length})
{filter && filterType && (
)} @@ -479,7 +502,7 @@ export default function List() { {/* 排序表头按钮 */}
- 排序: + 排序: {[ { key: 'code', label: '编号' }, { key: 'prefixSerial', label: '冠字号' }, @@ -487,6 +510,7 @@ export default function List() { { key: 'goalPrice', label: '售价' }, { key: 'category', label: '持仓类型' }, { key: 'rarity', label: '珍惜度' }, + { key: 'numberCategory', label: '号码分类' }, { key: 'gradingScore', label: '评级分数' } ].map(item => (
{ @@ -523,9 +547,9 @@ export default function List() {
goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer' }}>
🐉
{item.code || '-'}
-
{item.prefixSerial || '-'}
-
- {item.gradingScore && {item.gradingScore}} +
{formatPrefixSerial(item.prefixSerial)}
+
+ {item.gradingScore && {item.gradingScore}} {item.threeStar && ⭐⭐⭐}
diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index 59be7b9..c76bed7 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -10,6 +10,7 @@ export default function Stats() { byGrading: [], byPackaging: [], byRarity: [], + byNumberCategory: [], byVersion: [], byGradingCompany: [], byGradingScore: [], @@ -49,6 +50,7 @@ export default function Stats() { byGrading: data.byGrading || [], byPackaging: data.byPackaging || [], byRarity: data.byRarity || [], + byNumberCategory: data.byNumberCategory || [], byVersion: data.byVersion || [], byGradingCompany: data.byGradingCompany || [], byGradingScore: data.byGradingScore || [], @@ -94,10 +96,15 @@ export default function Stats() { // 颜色配置 const colors = { packaging: { '标十': '#22c55e', '标百': '#3b82f6', '单张': '#f59e0b', '裸钞': '#64748b' }, - rarity: { '通货': '#64748b', '特色': '#22c55e', '少见': '#3b82f6', '稀有': '#8b5cf6', '珍品': '#f59e0b', '孤品': '#ef4444' }, + rarity: { '通货': '#64748b', '特色': '#22c55e', '少见': '#3b82f6', '稀有': '#8b5cf6', '珍品': '#ef4444', '孤品': '#fbbf24' }, status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' }, category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' }, - profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' } + profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' }, + numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' }, + version: {}, + gradingCompany: {}, + gradingScore: {}, + specialMark: {} } // 标签映射 @@ -117,8 +124,14 @@ export default function Stats() { } } + const colorPalette = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316', '#14b8a6', '#a855f7'] const getColor = (type, value) => { - return colors[type]?.[value] || '#64748b' + if (colors[type]?.[value]) return colors[type][value] + // 动态生成颜色 + const key = String(value) + let hash = 0 + for (let i = 0; i < key.length; i++) hash = key.charCodeAt(i) + ((hash << 5) - hash) + return colorPalette[Math.abs(hash) % colorPalette.length] } const getLabel = (type, value) => { @@ -130,11 +143,23 @@ export default function Stats() { return Number(val).toLocaleString('zh-CN') } + const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '无4', '带4', '其他'] + const getSortedData = (data, type) => { + if (type === 'numberCategory') { + return [...data].sort((a, b) => { + const order = numberCategoryOrder.indexOf(a.numberCategory) + const order2 = numberCategoryOrder.indexOf(b.numberCategory) + return order - order2 + }) + } + return data + } + const DistributionCard = ({ title, data, type, valueKey, labelKey }) => (
{title}
- {data.slice(0, 6).map((item, index) => { + {getSortedData(data, type).slice(0, 6).map((item, index) => { const value = item[valueKey] const label = getLabel(type, item[labelKey] || value) const color = getColor(type, value) @@ -159,13 +184,17 @@ export default function Stats() { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.05)' }} > -
-
-
- {label} +
+
+
+
+ {label} +
+
+
+ {item.count}
-
{item.count}
) })} @@ -246,9 +275,9 @@ export default function Stats() {
- - + +