diff --git a/app/category_management_dialog.py b/app/category_management_dialog.py new file mode 100644 index 0000000..d991e12 --- /dev/null +++ b/app/category_management_dialog.py @@ -0,0 +1,268 @@ +""" +分类管理对话框 +提供新增、重命名、删除分类的功能 +""" + +from typing import Optional +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem) +from PyQt5.QtCore import Qt +from qfluentwidgets import (BodyLabel, PushButton, PrimaryPushButton, LineEdit, + InfoBar, InfoBarPosition) +from .tools.finance_manager import FinanceManager + + +class CategoryManagementDialog(QDialog): + """分类管理对话框""" + + def __init__(self, parent=None, finance_manager: Optional[FinanceManager] = None, account_id: Optional[str] = None): + super().__init__(parent) + self.finance_manager = finance_manager + self.account_id = account_id + self.setWindowTitle("分类管理") + self.setGeometry(100, 100, 500, 400) + self.init_ui() + + def init_ui(self): + """初始化UI""" + main_layout = QVBoxLayout() + + # 标签 + title_label = BodyLabel("选择分类进行管理:") + main_layout.addWidget(title_label) + + # 分类列表 + self.category_list = QListWidget() + self.category_list.itemSelectionChanged.connect(self.on_category_selected) + main_layout.addWidget(self.category_list) + + # 加载分类 + self.load_categories() + + # 按钮区域 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + # 新增按钮 + add_btn = PrimaryPushButton("新增") + add_btn.clicked.connect(self.on_add_category) + btn_layout.addWidget(add_btn) + + # 重命名按钮 + self.rename_btn = PushButton("重命名") + self.rename_btn.clicked.connect(self.on_rename_category) + self.rename_btn.setEnabled(False) + btn_layout.addWidget(self.rename_btn) + + # 删除按钮 + self.delete_btn = PushButton("删除") + self.delete_btn.clicked.connect(self.on_delete_category) + self.delete_btn.setEnabled(False) + btn_layout.addWidget(self.delete_btn) + + # 关闭按钮 + close_btn = PushButton("关闭") + close_btn.clicked.connect(self.accept) + btn_layout.addWidget(close_btn) + + main_layout.addLayout(btn_layout) + self.setLayout(main_layout) + + def load_categories(self): + """加载分类列表""" + if not self.finance_manager or not self.account_id: + return + + self.category_list.clear() + categories = self.finance_manager.get_categories(self.account_id) + for category in categories: + item = QListWidgetItem(category) + self.category_list.addItem(item) + + def on_category_selected(self): + """分类被选择""" + has_selection = self.category_list.currentItem() is not None + self.rename_btn.setEnabled(has_selection) + self.delete_btn.setEnabled(has_selection) + + def on_add_category(self): + """新增分类""" + # 弹出输入对话框 + from PyQt5.QtWidgets import QDialog as QStdDialog + from PyQt5.QtWidgets import QLabel + + dialog = QStdDialog(self) + dialog.setWindowTitle("新增分类") + dialog.setGeometry(150, 150, 400, 150) + + layout = QVBoxLayout(dialog) + layout.addWidget(BodyLabel("分类名称:")) + + input_edit = LineEdit() + input_edit.setPlaceholderText("例如:食品、交通、娱乐等") + layout.addWidget(input_edit) + + # 按钮 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + def on_create(): + category_name = input_edit.text().strip() + if not category_name: + InfoBar.warning( + title="提示", + content="分类名称不能为空", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + return + + if self.finance_manager.add_category(self.account_id, category_name): + InfoBar.success( + title="成功", + content=f"分类 '{category_name}' 创建成功", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + self.load_categories() + dialog.accept() + else: + InfoBar.warning( + title="提示", + content="分类已存在", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + + cancel_btn = PushButton("取消") + cancel_btn.clicked.connect(dialog.reject) + btn_layout.addWidget(cancel_btn) + + create_btn = PrimaryPushButton("创建") + create_btn.clicked.connect(on_create) + btn_layout.addWidget(create_btn) + + layout.addLayout(btn_layout) + dialog.exec() + + def on_rename_category(self): + """重命名分类""" + current_item = self.category_list.currentItem() + if not current_item: + return + + old_name = current_item.text() + + # 弹出输入对话框 + from PyQt5.QtWidgets import QDialog as QStdDialog + + dialog = QStdDialog(self) + dialog.setWindowTitle("重命名分类") + dialog.setGeometry(150, 150, 400, 150) + + layout = QVBoxLayout(dialog) + layout.addWidget(BodyLabel(f"原分类名: {old_name}")) + layout.addWidget(BodyLabel("新分类名:")) + + input_edit = LineEdit() + input_edit.setText(old_name) + input_edit.selectAll() + layout.addWidget(input_edit) + + # 按钮 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + def on_rename(): + new_name = input_edit.text().strip() + if not new_name: + InfoBar.warning( + title="提示", + content="分类名称不能为空", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + return + + if new_name == old_name: + dialog.accept() + return + + if self.finance_manager.rename_category(self.account_id, old_name, new_name): + InfoBar.success( + title="成功", + content=f"分类已重命名为 '{new_name}'", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + self.load_categories() + dialog.accept() + else: + InfoBar.warning( + title="提示", + content="重命名失败,可能分类已存在", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + + cancel_btn = PushButton("取消") + cancel_btn.clicked.connect(dialog.reject) + btn_layout.addWidget(cancel_btn) + + rename_btn = PrimaryPushButton("重命名") + rename_btn.clicked.connect(on_rename) + btn_layout.addWidget(rename_btn) + + layout.addLayout(btn_layout) + dialog.exec() + + def on_delete_category(self): + """删除分类""" + current_item = self.category_list.currentItem() + if not current_item: + return + + category_name = current_item.text() + + # 确认删除 + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + self, + "确认删除", + f"确定要删除分类 '{category_name}' 吗?\n\n使用该分类的交易记录分类将被清空。", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + if self.finance_manager.delete_category(self.account_id, category_name): + InfoBar.success( + title="成功", + content=f"分类 '{category_name}' 已删除", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + self.load_categories() + else: + InfoBar.warning( + title="错误", + content="删除分类失败", + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) diff --git a/app/finance_interface.py b/app/finance_interface.py index 3e8faf4..1f21f6e 100644 --- a/app/finance_interface.py +++ b/app/finance_interface.py @@ -21,6 +21,7 @@ import json import os from .tools.finance_manager import FinanceManager, TransactionType, Transaction, Account +from .category_management_dialog import CategoryManagementDialog class CreateTransactionDialog(QDialog): @@ -502,11 +503,11 @@ class FinanceInterface(QWidget): title_layout = QHBoxLayout() title_layout.addWidget(SubtitleLabel("交易记录")) - # 新建分类按钮 - new_category_btn = PushButton("新建分类") - new_category_btn.setFixedWidth(90) - new_category_btn.clicked.connect(self.on_create_category_clicked) - title_layout.addWidget(new_category_btn) + # 管理分类按钮 + manage_category_btn = PushButton("管理分类") + manage_category_btn.setFixedWidth(90) + manage_category_btn.clicked.connect(self.on_manage_category_clicked) + title_layout.addWidget(manage_category_btn) title_layout.addStretch() @@ -1224,8 +1225,8 @@ class FinanceInterface(QWidget): if trans_id: self.view_record(trans_id) - def on_create_category_clicked(self): - """新建分类按钮点击""" + def on_manage_category_clicked(self): + """分类管理按钮点击""" account_id = self.get_current_account_id() if not account_id: InfoBar.warning( @@ -1238,71 +1239,27 @@ class FinanceInterface(QWidget): ) return - # 创建自定义对话框 - from PyQt5.QtWidgets import QDialog as QStdDialog, QLabel + # 打开分类管理对话框 + dialog = CategoryManagementDialog(self, self.finance_manager, account_id) + if dialog.exec(): + # 刷新查询页面的分类下拉框 + self.refresh_query_category_dropdown() + + def refresh_query_category_dropdown(self): + """刷新查询页面的分类下拉框""" + account_id = self.get_current_account_id() + if not account_id or not hasattr(self, 'query_category'): + return - category_dialog = QStdDialog(self) - category_dialog.setWindowTitle("新建分类") - category_dialog.setGeometry(100, 100, 400, 150) + current_text = self.query_category.currentText() + self.query_category.clear() + self.query_category.addItem("全部") - layout = QVBoxLayout(category_dialog) - layout.addWidget(BodyLabel("分类名称:")) + categories = self.finance_manager.get_categories(account_id) + for category in categories: + self.query_category.addItem(category) - input_edit = LineEdit() - input_edit.setPlaceholderText("例如:食品、交通、娱乐等") - layout.addWidget(input_edit) - - # 按钮 - btn_layout = QHBoxLayout() - btn_layout.addStretch() - - def on_create(): - category_name = input_edit.text().strip() - if not category_name: - InfoBar.warning( - title="提示", - content="分类名称不能为空", - isClosable=True, - position=InfoBarPosition.TOP, - duration=2000, - parent=self - ) - return - - if self.finance_manager.add_category(account_id, category_name): - InfoBar.success( - title="成功", - content=f"分类 '{category_name}' 创建成功", - isClosable=True, - position=InfoBarPosition.TOP, - duration=2000, - parent=self - ) - - # 更新查询页面的分类下拉框 - if hasattr(self, 'query_category'): - # 检查是否已经存在 - if self.query_category.findText(category_name) < 0: - self.query_category.addItem(category_name) - - category_dialog.accept() - else: - InfoBar.warning( - title="提示", - content="分类已存在", - isClosable=True, - position=InfoBarPosition.TOP, - duration=2000, - parent=self - ) - - cancel_btn = PushButton("取消") - cancel_btn.clicked.connect(category_dialog.reject) - btn_layout.addWidget(cancel_btn) - - create_btn = PrimaryPushButton("创建") - create_btn.clicked.connect(on_create) - btn_layout.addWidget(create_btn) - - layout.addLayout(btn_layout) - category_dialog.exec() + # 恢复之前的选择 + index = self.query_category.findText(current_text) + if index >= 0: + self.query_category.setCurrentIndex(index) diff --git a/app/tools/finance_manager.py b/app/tools/finance_manager.py index 4765868..0048600 100644 --- a/app/tools/finance_manager.py +++ b/app/tools/finance_manager.py @@ -576,6 +576,45 @@ class FinanceManager: return True return False + def rename_category(self, account_id: str, old_name: str, new_name: str) -> bool: + """重命名交易分类""" + account = self.accounts.get(account_id) + if not account: + return False + + if old_name not in account.categories: + return False + + if new_name in account.categories: + return False # 新分类名已存在 + + # 重命名分类 + idx = account.categories.index(old_name) + account.categories[idx] = new_name + + # 更新所有使用旧分类的交易 + account_dir = self._get_account_dir(account_id) + for trans_dir in account_dir.iterdir(): + if not trans_dir.is_dir() or trans_dir.name in ['invoice', 'payment', 'purchase']: + continue + + data_file = trans_dir / 'data.json' + if data_file.exists(): + try: + with open(data_file, 'r', encoding='utf-8') as f: + transaction_data = json.load(f) + + if transaction_data.get('category') == old_name: + transaction_data['category'] = new_name + with open(data_file, 'w', encoding='utf-8') as f: + json.dump(transaction_data, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"更新交易分类出错: {e}") + + account.updated_at = datetime.now().isoformat() + self._save_account_metadata(account) + return True + def delete_category(self, account_id: str, category: str) -> bool: """删除交易分类""" account = self.accounts.get(account_id) @@ -584,6 +623,26 @@ class FinanceManager: if category in account.categories: account.categories.remove(category) + + # 清除所有使用此分类的交易的分类字段 + account_dir = self._get_account_dir(account_id) + for trans_dir in account_dir.iterdir(): + if not trans_dir.is_dir() or trans_dir.name in ['invoice', 'payment', 'purchase']: + continue + + data_file = trans_dir / 'data.json' + if data_file.exists(): + try: + with open(data_file, 'r', encoding='utf-8') as f: + transaction_data = json.load(f) + + if transaction_data.get('category') == category: + transaction_data['category'] = "" + with open(data_file, 'w', encoding='utf-8') as f: + json.dump(transaction_data, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"清除交易分类出错: {e}") + account.updated_at = datetime.now().isoformat() self._save_account_metadata(account) return True diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json index 6e4a1fa..12e7a96 100644 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json +++ b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json @@ -1,13 +1,13 @@ { "id": "28ae4af8-67d9-431a-b2fe-312f587020b3", "date": "2025-01-01", - "amount": 5000, + "amount": 5000.0, "trader": "工作", "notes": "1月工资", "invoice_path": null, "payment_path": null, "purchase_path": null, - "category": "工资", + "category": "NUC", "created_at": "2025-11-25T20:44:13.766681", - "updated_at": "2025-11-25T20:44:13.766682" + "updated_at": "2025-11-25T20:54:10.845738" } \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json index e138ba3..9adea80 100644 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json +++ b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json @@ -1,13 +1,13 @@ { "id": "5cf3dc01-8a1c-4418-86b6-1728f1ae47d1", "date": "2025-01-10", - "amount": -200, + "amount": -200.0, "trader": "超市", "notes": "食材", - "invoice_path": null, + "invoice_path": "accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png", "payment_path": null, "purchase_path": null, - "category": "食品", + "category": "NUC", "created_at": "2025-11-25T20:44:13.766985", - "updated_at": "2025-11-25T20:44:13.766986" + "updated_at": "2025-11-25T20:56:17.767227" } \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png new file mode 100644 index 0000000..5aa13d5 Binary files /dev/null and b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png differ diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json index 620f826..2989261 100644 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json +++ b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json @@ -1,13 +1,13 @@ { "id": "86232779-66fb-4e8e-b31e-477aef8e4ecf", "date": "2025-01-05", - "amount": -1500, + "amount": -1500.0, "trader": "房东", "notes": "房租", "invoice_path": null, "payment_path": null, "purchase_path": null, - "category": "房租", + "category": "NUC", "created_at": "2025-11-25T20:44:13.766850", - "updated_at": "2025-11-25T20:44:13.766853" + "updated_at": "2025-11-25T20:54:13.895024" } \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json index 5852e1a..9d8c53f 100644 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json +++ b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json @@ -3,11 +3,8 @@ "name": "admin", "description": "默认管理账户", "categories": [ - "工资", - "房租", - "食品", "NUC" ], "created_at": "2025-11-25T20:44:13.766008", - "updated_at": "2025-11-25T20:44:37.459188" + "updated_at": "2025-11-25T20:53:48.533051" } \ No newline at end of file diff --git a/test_category_management.py b/test_category_management.py new file mode 100644 index 0000000..722b464 --- /dev/null +++ b/test_category_management.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +分类管理功能测试脚本 +测试新增、重命名、删除分类的功能 +""" + +import sys +import os +import json +from pathlib import Path +from datetime import datetime + +# 添加app目录到路径 +sys.path.insert(0, str(Path(__file__).parent)) + +from app.tools.finance_manager import FinanceManager, Transaction + + +def test_rename_category(): + """测试重命名分类""" + print("=== 测试重命名分类 ===") + + # 创建测试用的FinanceManager + test_data_root = Path("/tmp/test_finance_data_rename") + fm = FinanceManager(str(test_data_root)) + + # 获取admin账户或创建一个测试账户 + account_ids = list(fm.accounts.keys()) + if not account_ids: + account_id = fm.create_account("test_account", "测试账户") + else: + account_id = account_ids[0] + + print(f"使用账户: {account_id}") + + # 添加分类 + print("添加分类: 食品、交通、娱乐") + assert fm.add_category(account_id, "食品"), "添加分类'食品'失败" + assert fm.add_category(account_id, "交通"), "添加分类'交通'失败" + assert fm.add_category(account_id, "娱乐"), "添加分类'娱乐'失败" + + categories = fm.get_categories(account_id) + print(f"当前分类: {categories}") + assert len(categories) == 3, f"期望3个分类,实际{len(categories)}个" + + # 添加一个使用"食品"分类的交易 + trans = Transaction( + date="2024-01-01", + amount=100.0, + trader="超市", + notes="购买食品", + category="食品" + ) + fm.add_transaction(account_id, trans) + print(f"添加交易,分类为'食品': {trans.id}") + + # 重命名分类 + print("重命名分类: 食品 -> 饮食") + assert fm.rename_category(account_id, "食品", "饮食"), "重命名分类失败" + + categories = fm.get_categories(account_id) + print(f"重命名后分类: {categories}") + assert "饮食" in categories, "重命名后分类中没有'饮食'" + assert "食品" not in categories, "重命名后分类中仍有'食品'" + + # 验证交易的分类也被更新了 + # 需要重新加载账户数据 + fm.load_all_accounts() + account = fm.accounts[account_id] + for t in account.transactions: + if t.id == trans.id: + print(f"交易分类已更新为: {t.category}") + assert t.category == "饮食", f"交易分类应该是'饮食',实际是'{t.category}'" + break + + # 测试重命名失败的情况:新分类名已存在 + print("测试重命名失败情况: 新分类名'交通'已存在") + assert not fm.rename_category(account_id, "饮食", "交通"), "应该重命名失败" + + print("✓ 重命名分类测试通过\n") + + +def test_delete_category(): + """测试删除分类""" + print("=== 测试删除分类 ===") + + test_data_root = Path("/tmp/test_finance_data_delete") + fm = FinanceManager(str(test_data_root)) + + account_ids = list(fm.accounts.keys()) + if not account_ids: + account_id = fm.create_account("test_account", "测试账户") + else: + account_id = account_ids[0] + + print(f"使用账户: {account_id}") + + # 添加分类 + print("添加分类: 食品、交通、娱乐") + fm.add_category(account_id, "食品") + fm.add_category(account_id, "交通") + fm.add_category(account_id, "娱乐") + + # 添加使用"食品"分类的交易 + trans1 = Transaction( + date="2024-01-01", + amount=100.0, + trader="超市", + notes="购买食品", + category="食品" + ) + trans2 = Transaction( + date="2024-01-02", + amount=50.0, + trader="出租车", + notes="交通费用", + category="交通" + ) + + fm.add_transaction(account_id, trans1) + fm.add_transaction(account_id, trans2) + + print(f"添加交易1(分类'食品'): {trans1.id}") + print(f"添加交易2(分类'交通'): {trans2.id}") + + # 删除"食品"分类 + print("删除分类: 食品") + assert fm.delete_category(account_id, "食品"), "删除分类失败" + + categories = fm.get_categories(account_id) + print(f"删除后分类: {categories}") + assert "食品" not in categories, "删除后分类中仍有'食品'" + + # 验证使用"食品"分类的交易分类被清空了 + fm.load_all_accounts() + account = fm.accounts[account_id] + for t in account.transactions: + if t.id == trans1.id: + print(f"使用已删除分类的交易,其分类现在为: '{t.category}'") + assert t.category == "", f"交易分类应该被清空,实际是'{t.category}'" + elif t.id == trans2.id: + print(f"使用'交通'分类的交易,其分类仍为: '{t.category}'") + assert t.category == "交通", f"交易分类应该保持'交通',实际是'{t.category}'" + + print("✓ 删除分类测试通过\n") + + +def test_add_category(): + """测试添加分类""" + print("=== 测试添加分类 ===") + + test_data_root = Path("/tmp/test_finance_data_add") + fm = FinanceManager(str(test_data_root)) + + account_ids = list(fm.accounts.keys()) + if not account_ids: + account_id = fm.create_account("test_account", "测试账户") + else: + account_id = account_ids[0] + + print(f"使用账户: {account_id}") + + # 初始应该没有分类 + categories = fm.get_categories(account_id) + print(f"初始分类: {categories}") + + # 添加分类 + print("添加分类: 食品") + assert fm.add_category(account_id, "食品"), "添加分类失败" + + categories = fm.get_categories(account_id) + print(f"添加后分类: {categories}") + assert "食品" in categories, "分类中没有'食品'" + + # 测试添加重复分类 + print("测试添加重复分类") + assert not fm.add_category(account_id, "食品"), "应该返回False" + + categories = fm.get_categories(account_id) + assert len(categories) == 1, f"应该只有1个分类,实际{len(categories)}个" + + print("✓ 添加分类测试通过\n") + + +def main(): + """运行所有测试""" + print("开始测试分类管理功能...\n") + + try: + test_add_category() + test_rename_category() + test_delete_category() + + print("=" * 50) + print("✓ 所有测试通过!") + print("=" * 50) + + except Exception as e: + print(f"\n✗ 测试失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main()