release: Elysia ToDo v1.0.0

鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
This commit is contained in:
祀梦
2026-03-14 22:21:26 +08:00
commit 2979197b1c
104 changed files with 21737 additions and 0 deletions

618
tests/test_accounts.py Normal file
View File

@@ -0,0 +1,618 @@
"""
资产总览功能 - 全面测试脚本
测试覆盖:账户 CRUD、余额更新、变更历史、分期计划 CRUD、还款操作
"""
import requests
import sys
from datetime import date
BASE_URL = "http://localhost:23994/api"
passed = 0
failed = 0
errors = []
def test(name, condition, detail=""):
global passed, failed
if condition:
passed += 1
print(f" [PASS] {name}")
else:
failed += 1
errors.append(name)
print(f" [FAIL] {name} {detail}")
def section(title):
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}")
# ============================================================
section("1. 账户 CRUD 测试")
# ============================================================
# 1.1 创建存款账户 - 微信
print("\n--- 创建存款账户 ---")
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "微信",
"account_type": "savings",
"balance": 5800.50,
"icon": "wechat",
"color": "#67C23A",
"is_active": True,
"description": "日常零钱"
})
test("创建微信账户", r.status_code == 201, f"status={r.status_code}")
wechat = r.json()
test("微信账户ID存在", wechat.get("id") is not None)
test("微信余额正确", wechat.get("balance") == 5800.50)
test("微信类型正确", wechat.get("account_type") == "savings")
wechat_id = wechat["id"]
# 1.2 创建存款账户 - 支付宝
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "支付宝",
"account_type": "savings",
"balance": 12300.00,
"icon": "alipay",
"color": "#1677FF",
"is_active": True,
"description": "工资卡"
})
test("创建支付宝账户", r.status_code == 201, f"status={r.status_code}")
alipay = r.json()
alipay_id = alipay["id"]
# 1.3 创建存款账户 - 银行卡
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "招商银行",
"account_type": "savings",
"balance": 45600.00,
"icon": "bank",
"color": "#FF6B6B",
"is_active": True
})
test("创建招商银行账户", r.status_code == 201, f"status={r.status_code}")
bank = r.json()
bank_id = bank["id"]
# 1.4 创建欠款账户 - 花呗
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "花呗",
"account_type": "debt",
"balance": 3000.00,
"icon": "credit-card",
"color": "#FFB347",
"is_active": True,
"description": "分3期每月12号还"
})
test("创建花呗账户", r.status_code == 201, f"status={r.status_code}")
huabei = r.json()
huabei_id = huabei["id"]
# 1.5 创建欠款账户 - 白条
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "白条",
"account_type": "debt",
"balance": 2000.00,
"icon": "ticket",
"color": "#E6A23C",
"is_active": True,
"description": "分6期每月15号还"
})
test("创建白条账户", r.status_code == 201, f"status={r.status_code}")
baitiao = r.json()
baitiao_id = baitiao["id"]
# 1.6 创建一个已禁用的账户
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "已注销信用卡",
"account_type": "debt",
"balance": 0,
"icon": "credit-card",
"color": "#909399",
"is_active": False,
"description": "测试禁用状态"
})
test("创建已禁用账户", r.status_code == 201)
disabled_id = r.json()["id"]
# ============================================================
section("2. 获取账户列表测试")
# ============================================================
print("\n--- 获取所有账户 ---")
r = requests.get(f"{BASE_URL}/accounts")
test("获取账户列表 200", r.status_code == 200, f"status={r.status_code}")
accounts = r.json()
test("账户总数正确", len(accounts) >= 5, f"实际数量: {len(accounts)}")
savings = [a for a in accounts if a["account_type"] == "savings" and a["is_active"]]
debt = [a for a in accounts if a["account_type"] == "debt" and a["is_active"]]
test("活跃存款账户数量", len(savings) == 3, f"实际: {len(savings)}")
test("活跃欠款账户数量", len(debt) == 2, f"实际: {len(debt)}")
# 2.1 获取单个账户
print("\n--- 获取单个账户 ---")
r = requests.get(f"{BASE_URL}/accounts/{wechat_id}")
test("获取单个微信账户 200", r.status_code == 200)
test("账户名称正确", r.json().get("name") == "微信")
# 2.2 获取不存在的账户
r = requests.get(f"{BASE_URL}/accounts/99999")
test("获取不存在账户 404", r.status_code == 404, f"status={r.status_code}")
# ============================================================
section("3. 更新账户测试")
# ============================================================
print("\n--- 更新账户信息 ---")
r = requests.put(f"{BASE_URL}/accounts/{wechat_id}", json={
"description": "日常零钱+红包"
})
test("更新微信描述 200", r.status_code == 200, f"status={r.status_code}")
test("描述更新成功", r.json().get("description") == "日常零钱+红包")
# 3.1 确认 balance 字段被忽略
r = requests.put(f"{BASE_URL}/accounts/{wechat_id}", json={
"balance": 99999 # 应该被忽略
})
test("更新忽略 balance", r.json().get("balance") == 5800.50,
f"余额不应被修改, 实际: {r.json().get('balance')}")
# ============================================================
section("4. 余额更新与变更历史测试")
# ============================================================
print("\n--- 更新微信余额 ---")
r = requests.post(f"{BASE_URL}/accounts/{wechat_id}/balance", json={
"new_balance": 6800.50,
"note": "收到红包"
})
test("更新微信余额 200", r.status_code == 200, f"status={r.status_code}")
test("微信余额更新成功", r.json().get("balance") == 6800.50)
print("\n--- 再次更新微信余额 ---")
r = requests.post(f"{BASE_URL}/accounts/{wechat_id}/balance", json={
"new_balance": 5300.50,
"note": "日常消费"
})
test("微信消费后余额", r.json().get("balance") == 5300.50)
print("\n--- 更新支付宝余额 ---")
r = requests.post(f"{BASE_URL}/accounts/{alipay_id}/balance", json={
"new_balance": 15300.00,
"note": "工资到账"
})
test("支付宝工资到账 200", r.status_code == 200)
test("支付宝余额正确", r.json().get("balance") == 15300.00)
print("\n--- 更新招商银行余额 ---")
r = requests.post(f"{BASE_URL}/accounts/{bank_id}/balance", json={
"new_balance": 40000.00,
"note": "取现"
})
test("招商银行取现 200", r.status_code == 200)
print("\n--- 更新花呗余额 ---")
r = requests.post(f"{BASE_URL}/accounts/{huabei_id}/balance", json={
"new_balance": 2500.00,
"note": "提前还款500"
})
test("花呗提前还款 200", r.status_code == 200)
test("花呗余额更新", r.json().get("balance") == 2500.00)
# ============================================================
section("5. 变更历史测试")
# ============================================================
print("\n--- 查看微信变更历史 ---")
r = requests.get(f"{BASE_URL}/accounts/{wechat_id}/history", params={
"page": 1,
"page_size": 10
})
test("获取微信历史 200", r.status_code == 200, f"status={r.status_code}")
history = r.json()
test("历史总数正确", history.get("total") == 2, f"实际: {history.get('total')}")
test("分页参数正确", history.get("page") == 1 and history.get("page_size") == 10)
if history.get("records"):
first_record = history["records"][0]
test("最新记录是消费", first_record.get("change_amount") == -1500.0,
f"实际: {first_record.get('change_amount')}")
test("消费前余额正确", first_record.get("balance_before") == 6800.50)
test("消费后余额正确", first_record.get("balance_after") == 5300.50)
test("消费备注正确", first_record.get("note") == "日常消费")
test("记录有创建时间", first_record.get("created_at") is not None)
print("\n--- 查看支付宝变更历史 ---")
r = requests.get(f"{BASE_URL}/accounts/{alipay_id}/history")
history_alipay = r.json()
test("支付宝历史 200", r.status_code == 200)
test("支付宝历史有记录", history_alipay.get("total") == 1)
print("\n--- 查看从未变更的账户历史 ---")
r = requests.get(f"{BASE_URL}/accounts/{baitiao_id}/history")
test("白条历史 200", r.status_code == 200)
test("白条无变更记录", r.json().get("total") == 0)
# ============================================================
section("6. 分期还款计划 CRUD 测试")
# ============================================================
# 6.1 为花呗创建分期计划3期每月12号每期1000
print("\n--- 创建花呗分期计划 ---")
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": huabei_id,
"total_amount": 3000.00,
"total_periods": 3,
"current_period": 1,
"payment_day": 12,
"payment_amount": 1000.00,
"start_date": "2026-03-12",
"is_completed": False
})
test("创建花呗分期 201", r.status_code == 201, f"status={r.status_code}")
huabei_inst = r.json()
test("花呗分期ID存在", huabei_inst.get("id") is not None)
test("花呗下次还款日期已计算", huabei_inst.get("next_payment_date") is not None)
test("花呗距今天数已计算", huabei_inst.get("days_until_payment") is not None)
test("花呗剩余期数", huabei_inst.get("remaining_periods") == 3)
test("花呗账户名称关联", huabei_inst.get("account_name") == "花呗")
huabei_inst_id = huabei_inst["id"]
# 6.2 为白条创建分期计划6期每月15号每期333.33
print("\n--- 创建白条分期计划 ---")
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": baitiao_id,
"total_amount": 2000.00,
"total_periods": 6,
"current_period": 3,
"payment_day": 15,
"payment_amount": 333.33,
"start_date": "2026-01-15",
"is_completed": False
})
test("创建白条分期 201", r.status_code == 201, f"status={r.status_code}")
baitiao_inst = r.json()
test("白条分期第3期", baitiao_inst.get("current_period") == 3)
test("白条剩余期数", baitiao_inst.get("remaining_periods") == 4)
baitiao_inst_id = baitiao_inst["id"]
# 6.3 验证不能给存款账户创建分期
print("\n--- 验证存款账户不能创建分期 ---")
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": wechat_id,
"total_amount": 1000,
"total_periods": 1,
"current_period": 1,
"payment_day": 1,
"payment_amount": 1000,
"start_date": "2026-04-01",
"is_completed": False
})
test("存款账户不能分期 400", r.status_code == 400, f"status={r.status_code}")
# 6.4 验证不存在的账户
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": 99999,
"total_amount": 1000,
"total_periods": 1,
"current_period": 1,
"payment_day": 1,
"payment_amount": 1000,
"start_date": "2026-04-01",
"is_completed": False
})
test("不存在账户不能分期 404", r.status_code == 404, f"status={r.status_code}")
# ============================================================
section("7. 获取分期计划列表测试")
# ============================================================
print("\n--- 获取所有分期计划 ---")
r = requests.get(f"{BASE_URL}/debt-installments")
test("获取分期列表 200", r.status_code == 200, f"status={r.status_code}")
installments = r.json()
test("分期计划总数 >= 2", len(installments) >= 2, f"实际: {len(installments)}")
# 验证排序:未完成的排前面,临近的排前面
if len(installments) >= 2:
first_active = next((i for i in installments if not i["is_completed"]), None)
test("列表第一个是未完成的", first_active is not None)
if first_active:
test("第一个有还款日期", first_active.get("next_payment_date") is not None)
test("第一个有距今天数", first_active.get("days_until_payment") is not None)
# 验证每个计划都有计算字段
for inst in installments:
test(f"分期#{inst['id']}有计算字段",
inst.get("next_payment_date") is not None and
inst.get("days_until_payment") is not None and
inst.get("remaining_periods") is not None,
f"id={inst['id']}")
# ============================================================
section("8. 更新分期计划测试")
# ============================================================
print("\n--- 更新花呗分期计划 ---")
r = requests.put(f"{BASE_URL}/debt-installments/{huabei_inst_id}", json={
"total_amount": 3500.00,
"payment_amount": 1166.67
})
test("更新花呗分期 200", r.status_code == 200, f"status={r.status_code}")
updated = r.json()
test("花呗总额更新", updated.get("total_amount") == 3500.00)
test("花呗每期金额更新", updated.get("payment_amount") == 1166.67)
# 恢复
requests.put(f"{BASE_URL}/debt-installments/{huabei_inst_id}", json={
"total_amount": 3000.00,
"payment_amount": 1000.00
})
# ============================================================
section("9. 标记还款测试(核心流程)")
# ============================================================
print("\n--- 花呗还款第1期 ---")
r = requests.patch(f"{BASE_URL}/debt-installments/{huabei_inst_id}/pay")
test("花呗还款第1期 200", r.status_code == 200, f"status={r.status_code}")
paid = r.json()
test("当前期数变为2", paid.get("current_period") == 2, f"实际: {paid.get('current_period')}")
test("未完成", paid.get("is_completed") == False)
test("剩余期数变为2", paid.get("remaining_periods") == 2)
print("\n--- 花呗还款第2期 ---")
r = requests.patch(f"{BASE_URL}/debt-installments/{huabei_inst_id}/pay")
test("花呗还款第2期 200", r.status_code == 200)
paid = r.json()
test("当前期数变为3", paid.get("current_period") == 3)
test("剩余期数变为1", paid.get("remaining_periods") == 1)
print("\n--- 花呗还款第3期最后一期---")
r = requests.patch(f"{BASE_URL}/debt-installments/{huabei_inst_id}/pay")
test("花呗还款第3期 200", r.status_code == 200)
paid = r.json()
test("标记为已完成", paid.get("is_completed") == True)
test("当前期数等于总期数", paid.get("current_period") == 3)
test("剩余期数为0", paid.get("remaining_periods") == 0)
print("\n--- 已完成的分期不能继续还款 ---")
r = requests.patch(f"{BASE_URL}/debt-installments/{huabei_inst_id}/pay")
test("已完成分期拒绝还款 400", r.status_code == 400, f"status={r.status_code}")
# ============================================================
section("10. 删除操作测试")
# ============================================================
# 10.1 删除分期计划
print("\n--- 删除白条分期计划 ---")
r = requests.delete(f"{BASE_URL}/debt-installments/{baitiao_inst_id}")
test("删除白条分期 200", r.status_code == 200, f"status={r.status_code}")
# 验证删除后列表中没有了
r = requests.get(f"{BASE_URL}/debt-installments")
ids = [i["id"] for i in r.json()]
test("白条分期已从列表移除", baitiao_inst_id not in ids)
# 10.2 删除已禁用账户
print("\n--- 删除已禁用账户 ---")
r = requests.delete(f"{BASE_URL}/accounts/{disabled_id}")
test("删除已禁用账户 200", r.status_code == 200)
# 10.3 验证删除后
r = requests.get(f"{BASE_URL}/accounts")
ids = [a["id"] for a in r.json()]
test("已禁用账户已移除", disabled_id not in ids)
# 10.4 重新创建一个花呗分期用来测试级联删除
print("\n--- 测试级联删除 ---")
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": huabei_id,
"total_amount": 1000.00,
"total_periods": 1,
"current_period": 1,
"payment_day": 25,
"payment_amount": 1000.00,
"start_date": "2026-03-25",
"is_completed": False
})
cascade_inst_id = r.json()["id"]
r = requests.delete(f"{BASE_URL}/accounts/{huabei_id}")
test("级联删除花呗账户 200", r.status_code == 200)
# 验证分期计划也被删除了
r = requests.get(f"{BASE_URL}/debt-installments")
ids = [i["id"] for i in r.json()]
test("花呗关联分期也被删除", cascade_inst_id not in ids)
# 验证历史记录也被删除
r = requests.get(f"{BASE_URL}/accounts/{huabei_id}/history")
test("花呗账户删除后历史不可访问", r.status_code == 404, f"status={r.status_code}")
# ============================================================
section("11. 边界条件测试")
# ============================================================
print("\n--- 余额更新到0 ---")
r = requests.post(f"{BASE_URL}/accounts/{baitiao_id}/balance", json={
"new_balance": 0,
"note": "还清"
})
test("余额归零 200", r.status_code == 200)
test("余额为0", r.json().get("balance") == 0)
print("\n--- 余额更新到大额 ---")
r = requests.post(f"{BASE_URL}/accounts/{baitiao_id}/balance", json={
"new_balance": 999999.99,
"note": "测试大额"
})
test("大额余额 200", r.status_code == 200)
test("大额余额正确", r.json().get("balance") == 999999.99)
# 恢复
requests.post(f"{BASE_URL}/accounts/{baitiao_id}/balance", json={
"new_balance": 2000.00,
"note": "恢复"
})
print("\n--- 参数校验:缺失必填字段 ---")
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "",
})
test("空名称被拒绝 422", r.status_code == 422, f"status={r.status_code}")
print("\n--- 参数校验:无效类型 ---")
r = requests.post(f"{BASE_URL}/accounts", json={
"name": "测试账户",
"account_type": "invalid_type",
})
test("无效类型被拒绝 422", r.status_code == 422, f"status={r.status_code}")
print("\n--- 分期参数校验 ---")
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": baitiao_id,
"total_amount": 0,
"total_periods": 1,
"current_period": 1,
"payment_day": 1,
"payment_amount": 0,
"start_date": "2026-04-01",
"is_completed": False
})
test("零金额分期被拒绝 422", r.status_code == 422, f"status={r.status_code}")
# ============================================================
section("12. 账户列表的分期信息附加测试")
# ============================================================
print("\n--- 获取白条账户(验证分期信息附加) ---")
# 先给白条创建一个分期
r = requests.post(f"{BASE_URL}/debt-installments", json={
"account_id": baitiao_id,
"total_amount": 2000.00,
"total_periods": 6,
"current_period": 2,
"payment_day": 15,
"payment_amount": 333.33,
"start_date": "2026-01-15",
"is_completed": False
})
test("白条分期重建 201", r.status_code == 201)
r = requests.get(f"{BASE_URL}/accounts")
accounts = r.json()
baitiao_acc = next((a for a in accounts if a["id"] == baitiao_id), None)
test("白条账户有分期附加信息", baitiao_acc is not None and baitiao_acc.get("installments") is not None)
# 存款账户不应有分期信息
wechat_acc = next((a for a in accounts if a["id"] == wechat_id), None)
if wechat_acc:
test("微信账户有分期字段(空列表)", wechat_acc.get("installments") is not None)
test("微信分期列表为空", len(wechat_acc.get("installments", [])) == 0)
# ============================================================
section("13. 历史分页测试")
# ============================================================
print("\n--- 历史分页 ---")
# 先给招商银行做多次变更
for i, (bal, note) in enumerate([
(45000, "存入"),
(42000, "消费"),
(50000, "转入"),
(48000, "理财"),
(52000, "收益"),
]):
requests.post(f"{BASE_URL}/accounts/{bank_id}/balance", json={
"new_balance": bal,
"note": note
})
r = requests.get(f"{BASE_URL}/accounts/{bank_id}/history", params={"page": 1, "page_size": 3})
test("历史分页第1页 200", r.status_code == 200)
page1 = r.json()
test("每页3条", len(page1.get("records", [])) == 3, f"实际: {len(page1.get('records', []))}")
test("总记录6条", page1.get("total") == 6, f"实际: {page1.get('total')}")
r = requests.get(f"{BASE_URL}/accounts/{bank_id}/history", params={"page": 2, "page_size": 3})
test("历史分页第2页 200", r.status_code == 200)
page2 = r.json()
test("第2页3条", len(page2.get("records", [])) == 3, f"实际: {len(page2.get('records', []))}")
# 验证第1页和第2页没有重复
page1_ids = {r["id"] for r in page1["records"]}
page2_ids = {r["id"] for r in page2["records"]}
test("两页记录不重复", len(page1_ids & page2_ids) == 0)
# ============================================================
section("14. 花呗还款日期计算验证")
# ============================================================
print("\n--- 验证还款日期计算 ---")
today = date.today()
r = requests.get(f"{BASE_URL}/debt-installments")
for inst in r.json():
if inst["is_completed"]:
continue
next_date_str = inst.get("next_payment_date")
days_until = inst.get("days_until_payment")
if next_date_str:
next_date = date.fromisoformat(next_date_str)
expected_days = (next_date - today).days
test(f"分期#{inst['id']}天数计算正确", days_until == expected_days,
f"计算: {days_until}, 预期: {expected_days}")
# ============================================================
section("15. 保留测试数据(跳过清理)")
# ============================================================
print("\n--- 保留测试数据供页面展示 ---")
r = requests.get(f"{BASE_URL}/accounts")
remaining_accounts = len(r.json())
test("账户数据已保留", remaining_accounts > 0, f"保留: {remaining_accounts} 个账户")
r = requests.get(f"{BASE_URL}/debt-installments")
remaining_installs = len(r.json())
test("分期数据已保留", remaining_installs >= 0, f"保留: {remaining_installs} 个分期")
# ============================================================
# 最终报告
# ============================================================
print(f"\n{'='*60}")
print(f" 测试报告")
print(f"{'='*60}")
print(f" 通过: {passed}")
print(f" 失败: {failed}")
print(f" 总计: {passed + failed}")
if errors:
print(f"\n 失败项:")
for e in errors:
print(f" - {e}")
print(f"{'='*60}")
if failed > 0:
sys.exit(1)
else:
print("\n 全部测试通过!")
sys.exit(0)