+
量潮财务全流程演示
+
① 录入单据 → ② 确认标准化 → ③ 系统自动分类 → ④ 批量审核 → ⑤ 统计看板
+
+
+
+
+
+
① 录入单据 M1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/finance/seed.py b/examples/finance/seed.py
new file mode 100644
index 0000000..b7c8794
--- /dev/null
+++ b/examples/finance/seed.py
@@ -0,0 +1,225 @@
+"""M4 Demo — 填充演示数据到独立 demo 数据库。
+
+不会触碰主开发库的 quanttide_finance.db。
+通过 --reset 确认后清空 demo 数据重新生成。
+"""
+
+import argparse
+import sys
+from datetime import date
+from pathlib import Path
+from random import choice, randint, seed as random_seed
+
+from sqlalchemy import create_engine, event
+from sqlalchemy.engine import Engine
+from sqlalchemy.orm import sessionmaker
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "packages/fastapi/src"))
+from fastapi_quanttide_finance.models.source_record import SourceRecord
+from fastapi_quanttide_finance.models.normalized_record import NormalizedRecord
+from fastapi_quanttide_finance.models.record_link import RecordLink
+from fastapi_quanttide_finance.models.classification_result import ClassificationResult
+from fastapi_quanttide_finance.database import Base
+
+
+@event.listens_for(Engine, "connect")
+def _set_sqlite_pragma(dbapi_connection, connection_record):
+ cursor = dbapi_connection.cursor()
+ cursor.execute("PRAGMA foreign_keys=ON")
+ cursor.close()
+
+
+DEMO_DIR = Path(__file__).resolve().parent
+DEMO_DB_PATH = DEMO_DIR / "demo.db"
+DB_URL = f"sqlite:///{DEMO_DB_PATH}"
+
+# ------- 演示数据 -------
+
+DEPARTMENTS = ["研发部", "市场部", "行政部", "财务部", "销售部", "采购部"]
+PEOPLE = {
+ "研发部": ["张伟", "李强", "王芳", "刘洋"],
+ "市场部": ["陈静", "赵敏", "周杰"],
+ "行政部": ["吴婷", "郑浩"],
+ "财务部": ["孙丽", "黄伟", "林涛"],
+ "销售部": ["马超", "朱红", "何亮", "徐飞"],
+ "采购部": ["胡明", "郭雪"],
+}
+COUNTERPARTIES = [
+ "京东企业购", "携程商旅", "滴滴企业版", "中国石油", "中国移动",
+ "顺丰速运", "联想集团", "用友网络", "华为技术", "阿里云",
+]
+RECORD_DESCRIPTIONS = [
+ "办公用品采购", "差旅报销", "项目外包服务费", "设备维修费",
+ "培训费用", "交通补贴", "通讯费", "快递费",
+ "软件订阅费", "招待费", "房租水电", "保洁服务",
+]
+AMOUNT_RANGES = { # (min, max) 单位分
+ "expense": (5000, 500000),
+ "income": (10000, 2000000),
+ "transfer": (50000, 1000000),
+ "reimbursement": (10000, 200000),
+ "other": (1000, 100000),
+}
+CLASSIFICATION_CATEGORIES = ["办公用品", "差旅", "采购", "工资", "其他"]
+CATEGORY_WEIGHT = {"办公用品": .3, "差旅": .25, "采购": .2, "工资": .15, "其他": .1}
+
+
+def weighted_choice(options, weights):
+ r = randint(1, 100)
+ cumulative = 0
+ for opt, w in zip(options, weights):
+ cumulative += w * 100
+ if r <= cumulative:
+ return opt
+ return options[-1]
+
+
+def main():
+ parser = argparse.ArgumentParser(description="填充 demo 数据库")
+ parser.add_argument("--reset", action="store_true", help="确认清空 demo 数据后重新生成")
+ args = parser.parse_args()
+
+ random_seed(42)
+
+ engine = create_engine(DB_URL, echo=False)
+ SessionLocal = sessionmaker(bind=engine)
+
+ if args.reset and DEMO_DB_PATH.exists():
+ DEMO_DB_PATH.unlink()
+ print(f"Removed existing demo DB: {DEMO_DB_PATH}")
+
+ if DEMO_DB_PATH.exists():
+ print(f"Demo DB already exists: {DEMO_DB_PATH}")
+ print("Use --reset to regenerate.")
+ sys.exit(0)
+
+ # 建表
+ Base.metadata.create_all(engine)
+ print(f"Created demo DB: {DEMO_DB_PATH}")
+
+ session = SessionLocal()
+
+ # ---- 生成原始记录 ----
+ all_srs = []
+ sr_id = 0
+ for month in [6, 7, 8]:
+ for dept in DEPARTMENTS:
+ num = randint(2, 4)
+ for _ in range(num):
+ sr_id += 1
+ day = randint(1, 28)
+ person = choice(PEOPLE[dept])
+ sr = SourceRecord(
+ source_type="manual",
+ raw_text=f"{dept}{person}提交的{choice(RECORD_DESCRIPTIONS)}",
+ ingestion_status="normalized",
+ )
+ session.add(sr)
+ session.flush()
+ all_srs.append(sr)
+
+ session.commit()
+ print(f"Created {len(all_srs)} SourceRecords")
+
+ # ---- 生成标准化记录 ----
+ all_nrs = []
+ for i, sr in enumerate(all_srs):
+ dept = DEPARTMENTS[i % len(DEPARTMENTS)]
+ person = choice(PEOPLE[dept])
+ month = 6 + (i // (len(DEPARTMENTS) * 3)) # 大致分配到 6-8 月
+ day = 1 + (i % 28)
+ record_type = choice(["expense", "expense", "expense", "reimbursement", "other"])
+ direction = "outflow" if record_type != "income" else choice(["outflow", "inflow"])
+ amt_range = AMOUNT_RANGES.get(record_type, (10000, 100000))
+ amount_cents = randint(*amt_range)
+
+ nr = NormalizedRecord(
+ primary_source_id=sr.id,
+ record_type=record_type,
+ business_date=date(2026, month, day),
+ amount_cents=amount_cents,
+ currency="CNY",
+ direction=direction,
+ department=dept,
+ person=person,
+ counterparty=choice(COUNTERPARTIES),
+ description=choice(RECORD_DESCRIPTIONS),
+ normalization_status="normalized",
+ )
+ session.add(nr)
+ session.flush()
+ all_nrs.append(nr)
+
+ # 建立 RecordLink
+ rl = RecordLink(
+ source_record_id=sr.id,
+ normalized_record_id=nr.id,
+ relation_type="primary",
+ )
+ session.add(rl)
+
+ session.commit()
+ print(f"Created {len(all_nrs)} NormalizedRecords + {len(all_nrs)} RecordLinks")
+
+ # ---- 生成分类 ----
+ total_classifications = 0
+ accepted_total = 0
+ for nr in all_nrs:
+ # 约 85% 的记录有分类(剩余的作为"未分类"展示在统计中)
+ if randint(1, 100) > 85:
+ continue
+ cat = weighted_choice(CLASSIFICATION_CATEGORIES, [
+ CATEGORY_WEIGHT[c] for c in CLASSIFICATION_CATEGORIES
+ ])
+ is_accepted = randint(1, 100) <= 75 # 75% 已审核
+
+ cr = ClassificationResult(
+ normalized_record_id=nr.id,
+ taxonomy="expense_type",
+ category=cat,
+ classifier_kind="manual",
+ confidence=0.95 if is_accepted else 0.70,
+ review_status="accepted" if is_accepted else "candidate",
+ is_active=True,
+ )
+ session.add(cr)
+ total_classifications += 1
+ if is_accepted:
+ accepted_total += 1
+
+ session.commit()
+ print(f"Created {total_classifications} classifications ({accepted_total} accepted, {total_classifications - accepted_total} candidates)")
+
+ # ---- 验证 ----
+ total = session.query(NormalizedRecord).count()
+ sum_amount = session.query(__import__("sqlalchemy").func.sum(NormalizedRecord.amount_cents)).scalar() or 0
+ classified = (
+ session.query(NormalizedRecord)
+ .filter(
+ NormalizedRecord.id.in_(
+ session.query(ClassificationResult.normalized_record_id).filter(
+ ClassificationResult.review_status == "accepted",
+ ClassificationResult.is_active == True,
+ )
+ )
+ )
+ .count()
+ )
+
+ print(f"\n=== Demo Data Summary ===")
+ print(f"Total records: {total}")
+ print(f"Sum amount_cents: {sum_amount:,} (¥{sum_amount/100:,.2f})")
+ print(f"Records with accepted classification: {classified}")
+ print(f"Classification rate: {classified/total*100:.0f}%")
+ print(f"\nDemo DB: {DEMO_DB_PATH}")
+ print("Ready! Start uvicorn and open the demo.")
+ print()
+ print("启动后端时需指定 demo 数据库:")
+ print(" DEMO_DB=1 uvicorn fastapi_quanttide_finance.app:app --reload")
+ print("或手动修改 database.py 中的 DATABASE_URL 指向 demo/demo.db")
+
+ session.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/finance/LICENSE b/packages/finance/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/packages/finance/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/finance/README.md b/packages/finance/README.md
new file mode 100644
index 0000000..e293a18
--- /dev/null
+++ b/packages/finance/README.md
@@ -0,0 +1,219 @@
+# quanttide-finance-toolkit
+
+**量潮财务记录标准化工具箱** —— 把各种原始记录收进系统,标准化、分类、统计。
+
+## 为什么做这个
+
+企业财务里的原始记录来源非常杂:报销单、付款单、银行流水、微信转账截图、聊天消息、口头说明、CSV 导出。
+
+问题不在于“先记账”,而在于**这些记录先天就不统一**:
+
+- 有的只有截图,没有结构化字段
+- 有的有金额,没有清晰分类
+- 有的能看出是支出,但看不出该归到什么费用类型
+- 同一件事可能分散在多条记录里
+
+所以本项目的核心目标不是做一个传统记账 App,而是做两件事:
+
+1. 把各种形态的原始记录标准化进系统
+2. 基于标准化结果和分类结果做统计分析
+
+会计凭证只是可选下游,不是主干。
+
+## 核心流程
+
+```text
+原始输入(图片 / 聊天 / 表单 / API / CSV / 人工录入)
+ ↓
+SourceRecord(保留原始证据和原始语义)
+ │
+ │ ┌─ RecordLink ──┐ (关联表,两端记录存在后创建)
+ │ │ │
+ ↓ ↓ ↓
+NormalizedRecord(抽取并统一成可统计的标准字段)
+ │
+ ├──→ Statistics(基于标准字段聚合、筛选、钻取)
+ │ ↑
+ └──→ ClassificationResult(AI/规则/人工分类,作为叠加维度,可选)
+
+可选下游:
+NormalizedRecord
+ ↓
+Journal(日记账)→ JournalEntry(凭证)→ JournalEntryLine(分录行)
+```
+
+## 典型使用流程(产品视角)
+
+以上数据流在财务部日常使用中表现为 5 步操作:
+
+```
+① 录入单据 ─→ ② 确认标准化 ─→ ③ 系统自动分类 ─→ ④ 批量审核 ─→ ⑤ 统计看板
+ 填金额/描述 预览/编辑/确认 关键词匹配预填 扫一眼批量确认 汇总/趋势/明细
+ 选部门/人员 分为 5 大类 单条可调类别
+```
+
+**每个步骤的职责**:
+
+| 步骤 | 用户做什么 | 系统做什么 | 对应技术层 |
+|------|-----------|-----------|-----------|
+| ① 录入 | 填写金额、描述、日期、部门、人员等结构化字段 | 将原始证据存入 `SourceRecord`,同时写入 `NormalizedRecord` | M1–M2 |
+| ② 确认 | 预览标准化结果,修正字段后点确认 | 确认后 `normalization_status` 标记为 `normalized` | M2 |
+| ③ 自动分类 | 无需操作,观察系统预分类结果 | 根据描述关键词(机票→差旅、A4纸→办公用品等)预填 `category` | M3 |
+| ④ 批量审核 | 逐类浏览,点"确认"批量接受一类;错分的单条拖到正确类别 | 写入 `ClassificationResult`,`review_status=accepted` 后才纳入统计 | M3 |
+| ⑤ 统计 | 查看汇总卡片、部门分布、趋势图、明细表 | 聚合 `NormalizedRecord` 事实字段 + 已接受的分类结果 | M4 |
+
+**重要区分**:数据流(SourceRecord → NormalizedRecord → ClassificationResult)描述的是**数据如何存储**;使用流程描述的是**用户如何操作**。两者对应但并不相同——例如用户录入结构化的金额/描述时,系统同时写入 SourceRecord(存原始证据)和 NormalizedRecord(存结构化事实),不是"用户先填 raw_text、再调标准化 API"。
+
+## 产品定位
+
+- **核心是数据标准化**:先把杂乱记录变成统一结构。
+- **核心也是数据统计**:统计基于标准字段和分类结果,而不是只围绕凭证。
+- **分类是独立步骤**:分类不是原始事实本身,可以由 AI、规则或人工给出,并允许修订。
+- **凭证是可选能力**:只有在需要对接会计系统时,才把标准化记录映射成凭证。
+
+## 核心实体
+
+| 实体 | 作用 | 是否主干 |
+|---|---|---|
+| `SourceRecord` | 保存原始记录、来源、原始文本、附件引用、导入状态 | 是 |
+| `RecordLink` | 关联 SourceRecord 与 NormalizedRecord(支持拆/并关系) | 是 |
+| `NormalizedRecord` | 保存标准化后的事实字段,作为统计基线 | 是 |
+| `ClassificationResult` | 保存分类、标签、置信度、分类来源、审核状态 | 是 |
+| `Journal` / `JournalEntry` / `JournalEntryLine` | 会计凭证层,用于下游记账或系统集成 | 否,可选 |
+
+`SourceDocument` 这个名字过窄,更适合替换为 `SourceRecord`,因为输入不一定是“单据”,也可能是聊天、截图、流水行或导入记录。
+
+## 设计原则
+
+- **原始优先**:原始记录必须保留,方便回溯、复核和重新抽取。
+- **标准化与分类分离**:金额、日期、人员、部门等是标准化事实;费用类型、业务标签等是分类结果。
+- **统计先于凭证**:产品主线是“能统计、能筛选、能钻取”,不是“先做借贷分录”。
+- **允许多轮整理**:同一条记录可以先粗标准化,再补分类,再人工修正。
+- **统一类型约定**:所有 `id` 目标使用 `int`,金额使用 `int` 且单位为“分”。
+
+## 当前版本
+
+**v1.0** — M0–M4 全链路完成,Demo 可运行。
+
+- `packages/fastapi` **M0–M4 完成** — 四个 ORM 模型 + Pydantic Schema + Alembic 迁移 + Normalizer 接口 + CsvRowNormalizer + ManualNormalizer + 分类服务 + 4 统计端点(summary/breakdown/trend/drilldown),三包总计 132 tests。
+- `demo/` **全流程演示已上线** — 覆盖完整 5 步产品流程:结构化录入 → 预览确认 → 自动预分类 → 批量审核 → 统计看板。一键启动脚本 `run_demo.sh` / `run_demo.bat`。
+- `packages/dart` 已发布 [`quanttide_finance`](https://pub.dev/packages/quanttide_finance) `^0.1.1`,提供 `Journal`、`JournalEntry`、`JournalEntryLine` 三个 freezed 不可变模型(可选下游凭证层,非主干)。
+
+## 产品功能矩阵
+
+以下将 5 步产品流程映射到当前的实现状态:
+
+| 步骤 | 用户操作 | 实现状态 | 后端支撑 | Demo 覆盖 |
+|------|---------|---------|---------|-----------|
+| ① 录入单据 | 填写金额、方向、日期、部门、人员、描述 | **已实现** | `POST /source-records` + `POST /normalized-records` | 结构化表单,录入即写两条记录 |
+| ② 确认标准化 | 预览标准化结果,修正字段后点确认 | **已实现** | `PATCH /normalized-records/{id}` 可改任意字段,`normalization_status` 标记已确认 | 可编辑预览面板,确认/放弃按钮 |
+| ③ 自动分类 | 系统根据关键词规则预分类,无需操作 | **已实现** | 前端 `autoClassify()`(非生产逻辑,仅 demo) | 关键词匹配 → 预填 `category` |
+| ④ 批量审核 | 分组浏览,一键确认一类;错分可单条下调改 | **已实现** | `POST /normalized-records/{id}/classifications` 写入,`PATCH /classifications/{id}` 审核通过 | 5 类分组 + 复选框批量确认 + 下拉改分类 |
+| ⑤ 统计看板 | 汇总卡片、部门分布、趋势图、明细表 | **已实现** | `GET /statistics/summary` / `breakdown` / `trend` / `drilldown` | 无过滤栏,自动刷新的 Chart.js 看板 |
+
+**说明**:
+- 分类基于前端关键词规则,是演示辅助,非生产逻辑。生产环境应走后端规则引擎或 AI 分类。
+- 统计 `classified_count` 仅计入 `review_status=accepted` 的分类结果。
+- 所有步骤均可在 `demo/index.html` 中体验完整流程。
+
+## 当前 Dart 版本的定位
+
+以下示例对应当前已发布的 Dart 包 `^0.1.1`。它展示的是凭证层模型,不代表本项目未来的完整主干架构。
+
+```yaml
+dependencies:
+ quanttide_finance: ^0.1.1
+```
+
+```dart
+import 'package:quanttide_finance/quanttide_finance.dart';
+
+void main() {
+ final journal = Journal(
+ id: '1',
+ name: '备用金',
+ createdAt: DateTime.now(),
+ );
+
+ final entry = JournalEntry(
+ id: 'je1',
+ journalId: journal.id,
+ createdAt: DateTime.now(),
+ description: '采购办公用品',
+ lines: [
+ JournalEntryLine(
+ id: 'l1',
+ type: LineType.debit,
+ amount: 1200,
+ createdAt: DateTime.now(),
+ ),
+ JournalEntryLine(
+ id: 'l2',
+ type: LineType.credit,
+ amount: 1200,
+ createdAt: DateTime.now(),
+ ),
+ ],
+ );
+
+ print('${entry.description}: 共 ${entry.lines.length} 行');
+}
+```
+
+## 目标架构
+
+| 路径 | 状态 | 说明 |
+|---|---|---|
+| `packages/dart` | 已有 | 当前是凭证模型;后续可继续承担共享 DTO 或下游模型 |
+| `packages/fastapi` | M4 完成 | 核心模型 + 标准化 + 分类 + 统计 API 就绪;下一阶段 M5(数据安全) |
+
+## 近期计划
+
+1. **M5(数据安全)**:脱敏器 + 外部 API 审计日志 + Taxonomy 输出校验。
+2. **M6(Dart 模型同步)**:`id` → `int`,`amount` → `int`(分),`normalizedRecordId` 对齐。
+
+## 安全与数据治理(规划)
+
+### 脱敏原则
+
+- **脱敏在数据离站前一刻执行**,不污染存储层。原始 `raw_text` 和 `description` 完整入库,仅在发往外部 AI 前做替换。
+- 本地模型(Ollama)调用不脱敏,数据不出域。
+- 脱敏使用类型标记替换(`[AMOUNT]`、`[ID_CARD]`、`[BANK_CARD]`),保留语义信息供 AI 参考,不暴露精确值。
+
+### 分类结果审核
+
+- AI 分类结果写入 `ClassificationResult`,`review_status = candidate`,不参与统计。
+- 人工审核确认后改为 `accepted`,此时才纳入统计口径。
+- 置信度阈值可配置(默认 0.7),低于阈值自动标记待审核。
+
+### Taxonomy 受控词表
+
+- 初期硬编码基础分类列表(`办公用品` / `差旅` / `采购` / `工资` / `其他`)。
+- V2 升级为数据库表 + 管理后台,支持增删改并记录版本。
+- AI 返回的分类标签必须在 taxonomy 范围内,非法值丢弃或标记异常。
+
+### 输入校验
+
+- `raw_text` 上限 65535 字符,`description` 上限 1000 字符。
+- 分类模型上线前使用历史数据做沙箱测试,对比新旧分类差异。
+
+## 待处理事项
+
+| 优先级 | 事项 | 说明 |
+|---|---|---|
+| 🟡 高 | 脱敏粒度细化 | 金额保留相对大小标记(`[AMOUNT:SMALL]` / `[AMOUNT:LARGE]`),保留量级信息辅助 AI 分类 |
+| 🟡 中 | 置信度阈值校准 | 上线后基于实际分布调整默认值 |
+| 🟢 低 | 速率限制 | 外部 API 每分钟调用上限、超时、降级策略 |
+| 🟢 低 | 附件存储 | `evidence_refs` 指向的对象存储方案 |
+| 🟢 低 | 导入格式规范 | CSV / 银行流水导入模板 |
+
+## 许可证
+
+本项目基于 [LICENSE](LICENSE) 发布。
+
+## 链接
+
+- [设计文档](doc/architecture.md) — 架构总览、实体、API、服务、安全、计划
+- [Dart 包文档](packages/dart/README.md)
+- [更新日志](packages/dart/CHANGELOG.md)
+- [报告 Issue](https://github.com/quanttide/quanttide-finance-toolkit/issues)
diff --git a/packages/finance/dart/.dart_tool/package_config.json b/packages/finance/dart/.dart_tool/package_config.json
new file mode 100644
index 0000000..881022c
--- /dev/null
+++ b/packages/finance/dart/.dart_tool/package_config.json
@@ -0,0 +1,412 @@
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "_fe_analyzer_shared",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/_fe_analyzer_shared-96.0.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.9"
+ },
+ {
+ "name": "analyzer",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/analyzer-10.2.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.9"
+ },
+ {
+ "name": "args",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/args-2.7.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "async",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/async-2.13.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "boolean_selector",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "build",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/build-4.0.6",
+ "packageUri": "lib/",
+ "languageVersion": "3.7"
+ },
+ {
+ "name": "build_config",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/build_config-1.3.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.7"
+ },
+ {
+ "name": "build_daemon",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/build_daemon-4.1.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.7"
+ },
+ {
+ "name": "build_runner",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/build_runner-2.15.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.7"
+ },
+ {
+ "name": "built_collection",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/built_collection-5.1.1",
+ "packageUri": "lib/",
+ "languageVersion": "2.12"
+ },
+ {
+ "name": "built_value",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/built_value-8.12.6",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "checked_yaml",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/checked_yaml-2.0.4",
+ "packageUri": "lib/",
+ "languageVersion": "3.8"
+ },
+ {
+ "name": "cli_config",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/cli_config-0.2.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "collection",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/collection-1.19.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "convert",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/convert-3.1.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "coverage",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/coverage-1.15.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "crypto",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.7",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "dart_style",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/dart_style-3.1.7",
+ "packageUri": "lib/",
+ "languageVersion": "3.10"
+ },
+ {
+ "name": "file",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/file-7.0.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "fixnum",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/fixnum-1.1.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "freezed",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/freezed-3.2.5",
+ "packageUri": "lib/",
+ "languageVersion": "3.8"
+ },
+ {
+ "name": "freezed_annotation",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/freezed_annotation-3.1.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "frontend_server_client",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/frontend_server_client-4.0.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "glob",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/glob-2.1.3",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "graphs",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/graphs-2.3.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "http_multi_server",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/http_multi_server-3.2.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.2"
+ },
+ {
+ "name": "http_parser",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.1.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "io",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/io-1.0.5",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "json_annotation",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/json_annotation-4.9.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "json_serializable",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/json_serializable-6.11.4",
+ "packageUri": "lib/",
+ "languageVersion": "3.9"
+ },
+ {
+ "name": "lints",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/lints-6.1.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.8"
+ },
+ {
+ "name": "logging",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/logging-1.3.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "matcher",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.17",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "meta",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/meta-1.18.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.5"
+ },
+ {
+ "name": "mime",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/mime-2.0.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.2"
+ },
+ {
+ "name": "node_preamble",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/node_preamble-2.0.2",
+ "packageUri": "lib/",
+ "languageVersion": "2.12"
+ },
+ {
+ "name": "package_config",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/package_config-2.2.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "path",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "pool",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/pool-1.5.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "pub_semver",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/pub_semver-2.2.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "pubspec_parse",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/pubspec_parse-1.5.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.6"
+ },
+ {
+ "name": "shelf",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/shelf-1.4.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "shelf_packages_handler",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/shelf_packages_handler-3.0.2",
+ "packageUri": "lib/",
+ "languageVersion": "2.17"
+ },
+ {
+ "name": "shelf_static",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/shelf_static-1.1.3",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "shelf_web_socket",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/shelf_web_socket-3.0.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.5"
+ },
+ {
+ "name": "source_gen",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/source_gen-4.2.3",
+ "packageUri": "lib/",
+ "languageVersion": "3.9"
+ },
+ {
+ "name": "source_helper",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/source_helper-1.3.12",
+ "packageUri": "lib/",
+ "languageVersion": "3.9"
+ },
+ {
+ "name": "source_map_stack_trace",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/source_map_stack_trace-2.1.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "source_maps",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/source_maps-0.10.13",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "source_span",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "stack_trace",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.12.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "stream_channel",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.4",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "stream_transform",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/stream_transform-2.1.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "string_scanner",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.4.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "term_glyph",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.2",
+ "packageUri": "lib/",
+ "languageVersion": "3.1"
+ },
+ {
+ "name": "test",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/test-1.31.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.10"
+ },
+ {
+ "name": "test_api",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.7.12",
+ "packageUri": "lib/",
+ "languageVersion": "3.10"
+ },
+ {
+ "name": "test_core",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/test_core-0.6.18",
+ "packageUri": "lib/",
+ "languageVersion": "3.10"
+ },
+ {
+ "name": "typed_data",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.4.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.5"
+ },
+ {
+ "name": "vm_service",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/vm_service-15.2.0",
+ "packageUri": "lib/",
+ "languageVersion": "3.5"
+ },
+ {
+ "name": "watcher",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/watcher-1.2.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "web",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/web-1.1.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "web_socket",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/web_socket-1.0.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "web_socket_channel",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/web_socket_channel-3.0.3",
+ "packageUri": "lib/",
+ "languageVersion": "3.3"
+ },
+ {
+ "name": "webkit_inspection_protocol",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/webkit_inspection_protocol-1.2.1",
+ "packageUri": "lib/",
+ "languageVersion": "3.0"
+ },
+ {
+ "name": "yaml",
+ "rootUri": "file:///home/linli/.pub-cache/hosted/pub.flutter-io.cn/yaml-3.1.3",
+ "packageUri": "lib/",
+ "languageVersion": "3.4"
+ },
+ {
+ "name": "quanttide_finance",
+ "rootUri": "../",
+ "packageUri": "lib/",
+ "languageVersion": "3.11"
+ }
+ ],
+ "generator": "pub",
+ "generatorVersion": "3.12.0",
+ "flutterRoot": "file:///home/linli/development/flutter",
+ "flutterVersion": "3.44.0",
+ "pubCache": "file:///home/linli/.pub-cache"
+}
diff --git a/packages/finance/dart/.dart_tool/package_graph.json b/packages/finance/dart/.dart_tool/package_graph.json
new file mode 100644
index 0000000..d805604
--- /dev/null
+++ b/packages/finance/dart/.dart_tool/package_graph.json
@@ -0,0 +1,669 @@
+{
+ "roots": [
+ "quanttide_finance"
+ ],
+ "packages": [
+ {
+ "name": "quanttide_finance",
+ "version": "0.2.0",
+ "dependencies": [
+ "freezed_annotation",
+ "json_annotation"
+ ],
+ "devDependencies": [
+ "build_runner",
+ "freezed",
+ "json_serializable",
+ "lints",
+ "test"
+ ]
+ },
+ {
+ "name": "lints",
+ "version": "6.1.0",
+ "dependencies": []
+ },
+ {
+ "name": "json_annotation",
+ "version": "4.9.0",
+ "dependencies": [
+ "meta"
+ ]
+ },
+ {
+ "name": "freezed_annotation",
+ "version": "3.1.0",
+ "dependencies": [
+ "collection",
+ "json_annotation",
+ "meta"
+ ]
+ },
+ {
+ "name": "meta",
+ "version": "1.18.2",
+ "dependencies": []
+ },
+ {
+ "name": "pubspec_parse",
+ "version": "1.5.0",
+ "dependencies": [
+ "checked_yaml",
+ "collection",
+ "json_annotation",
+ "pub_semver",
+ "yaml"
+ ]
+ },
+ {
+ "name": "pub_semver",
+ "version": "2.2.0",
+ "dependencies": [
+ "collection"
+ ]
+ },
+ {
+ "name": "path",
+ "version": "1.9.1",
+ "dependencies": []
+ },
+ {
+ "name": "async",
+ "version": "2.13.1",
+ "dependencies": [
+ "collection",
+ "meta"
+ ]
+ },
+ {
+ "name": "collection",
+ "version": "1.19.1",
+ "dependencies": []
+ },
+ {
+ "name": "yaml",
+ "version": "3.1.3",
+ "dependencies": [
+ "collection",
+ "source_span",
+ "string_scanner"
+ ]
+ },
+ {
+ "name": "checked_yaml",
+ "version": "2.0.4",
+ "dependencies": [
+ "json_annotation",
+ "source_span",
+ "yaml"
+ ]
+ },
+ {
+ "name": "string_scanner",
+ "version": "1.4.1",
+ "dependencies": [
+ "source_span"
+ ]
+ },
+ {
+ "name": "source_span",
+ "version": "1.10.2",
+ "dependencies": [
+ "collection",
+ "path",
+ "term_glyph"
+ ]
+ },
+ {
+ "name": "term_glyph",
+ "version": "1.2.2",
+ "dependencies": []
+ },
+ {
+ "name": "freezed",
+ "version": "3.2.5",
+ "dependencies": [
+ "analyzer",
+ "build",
+ "build_config",
+ "collection",
+ "dart_style",
+ "freezed_annotation",
+ "json_annotation",
+ "meta",
+ "pub_semver",
+ "source_gen"
+ ]
+ },
+ {
+ "name": "watcher",
+ "version": "1.2.1",
+ "dependencies": [
+ "async",
+ "path"
+ ]
+ },
+ {
+ "name": "package_config",
+ "version": "2.2.0",
+ "dependencies": [
+ "path"
+ ]
+ },
+ {
+ "name": "glob",
+ "version": "2.1.3",
+ "dependencies": [
+ "async",
+ "collection",
+ "file",
+ "path",
+ "string_scanner"
+ ]
+ },
+ {
+ "name": "crypto",
+ "version": "3.0.7",
+ "dependencies": [
+ "typed_data"
+ ]
+ },
+ {
+ "name": "convert",
+ "version": "3.1.2",
+ "dependencies": [
+ "typed_data"
+ ]
+ },
+ {
+ "name": "file",
+ "version": "7.0.1",
+ "dependencies": [
+ "meta",
+ "path"
+ ]
+ },
+ {
+ "name": "typed_data",
+ "version": "1.4.0",
+ "dependencies": [
+ "collection"
+ ]
+ },
+ {
+ "name": "build_config",
+ "version": "1.3.0",
+ "dependencies": [
+ "checked_yaml",
+ "json_annotation",
+ "path",
+ "pubspec_parse"
+ ]
+ },
+ {
+ "name": "args",
+ "version": "2.7.0",
+ "dependencies": []
+ },
+ {
+ "name": "logging",
+ "version": "1.3.0",
+ "dependencies": []
+ },
+ {
+ "name": "json_serializable",
+ "version": "6.11.4",
+ "dependencies": [
+ "analyzer",
+ "async",
+ "build",
+ "build_config",
+ "dart_style",
+ "json_annotation",
+ "meta",
+ "path",
+ "pub_semver",
+ "pubspec_parse",
+ "source_gen",
+ "source_helper"
+ ]
+ },
+ {
+ "name": "build",
+ "version": "4.0.6",
+ "dependencies": [
+ "analyzer",
+ "crypto",
+ "glob",
+ "logging",
+ "package_config",
+ "path"
+ ]
+ },
+ {
+ "name": "source_helper",
+ "version": "1.3.12",
+ "dependencies": [
+ "analyzer",
+ "source_gen"
+ ]
+ },
+ {
+ "name": "dart_style",
+ "version": "3.1.7",
+ "dependencies": [
+ "analyzer",
+ "args",
+ "collection",
+ "package_config",
+ "path",
+ "pub_semver",
+ "source_span",
+ "yaml"
+ ]
+ },
+ {
+ "name": "analyzer",
+ "version": "10.2.0",
+ "dependencies": [
+ "_fe_analyzer_shared",
+ "collection",
+ "convert",
+ "crypto",
+ "glob",
+ "meta",
+ "package_config",
+ "path",
+ "pub_semver",
+ "source_span",
+ "watcher",
+ "yaml"
+ ]
+ },
+ {
+ "name": "_fe_analyzer_shared",
+ "version": "96.0.0",
+ "dependencies": [
+ "meta",
+ "source_span"
+ ]
+ },
+ {
+ "name": "source_gen",
+ "version": "4.2.3",
+ "dependencies": [
+ "analyzer",
+ "async",
+ "build",
+ "dart_style",
+ "glob",
+ "path",
+ "pub_semver",
+ "source_span",
+ "yaml"
+ ]
+ },
+ {
+ "name": "test",
+ "version": "1.31.1",
+ "dependencies": [
+ "analyzer",
+ "async",
+ "boolean_selector",
+ "collection",
+ "coverage",
+ "http_multi_server",
+ "io",
+ "matcher",
+ "node_preamble",
+ "package_config",
+ "path",
+ "pool",
+ "shelf",
+ "shelf_packages_handler",
+ "shelf_static",
+ "shelf_web_socket",
+ "source_span",
+ "stack_trace",
+ "stream_channel",
+ "test_api",
+ "test_core",
+ "typed_data",
+ "web_socket_channel",
+ "webkit_inspection_protocol",
+ "yaml"
+ ]
+ },
+ {
+ "name": "webkit_inspection_protocol",
+ "version": "1.2.1",
+ "dependencies": [
+ "logging"
+ ]
+ },
+ {
+ "name": "web_socket_channel",
+ "version": "3.0.3",
+ "dependencies": [
+ "async",
+ "crypto",
+ "stream_channel",
+ "web",
+ "web_socket"
+ ]
+ },
+ {
+ "name": "test_core",
+ "version": "0.6.18",
+ "dependencies": [
+ "analyzer",
+ "args",
+ "async",
+ "boolean_selector",
+ "collection",
+ "coverage",
+ "frontend_server_client",
+ "glob",
+ "io",
+ "meta",
+ "package_config",
+ "path",
+ "pool",
+ "source_map_stack_trace",
+ "source_maps",
+ "source_span",
+ "stack_trace",
+ "stream_channel",
+ "test_api",
+ "vm_service",
+ "yaml"
+ ]
+ },
+ {
+ "name": "test_api",
+ "version": "0.7.12",
+ "dependencies": [
+ "async",
+ "boolean_selector",
+ "collection",
+ "meta",
+ "source_span",
+ "stack_trace",
+ "stream_channel",
+ "string_scanner",
+ "term_glyph"
+ ]
+ },
+ {
+ "name": "stream_channel",
+ "version": "2.1.4",
+ "dependencies": [
+ "async"
+ ]
+ },
+ {
+ "name": "stack_trace",
+ "version": "1.12.1",
+ "dependencies": [
+ "path"
+ ]
+ },
+ {
+ "name": "shelf_web_socket",
+ "version": "3.0.0",
+ "dependencies": [
+ "shelf",
+ "stream_channel",
+ "web_socket_channel"
+ ]
+ },
+ {
+ "name": "shelf_static",
+ "version": "1.1.3",
+ "dependencies": [
+ "convert",
+ "http_parser",
+ "mime",
+ "path",
+ "shelf"
+ ]
+ },
+ {
+ "name": "shelf_packages_handler",
+ "version": "3.0.2",
+ "dependencies": [
+ "path",
+ "shelf",
+ "shelf_static"
+ ]
+ },
+ {
+ "name": "shelf",
+ "version": "1.4.2",
+ "dependencies": [
+ "async",
+ "collection",
+ "http_parser",
+ "path",
+ "stack_trace",
+ "stream_channel"
+ ]
+ },
+ {
+ "name": "pool",
+ "version": "1.5.2",
+ "dependencies": [
+ "async",
+ "stack_trace"
+ ]
+ },
+ {
+ "name": "node_preamble",
+ "version": "2.0.2",
+ "dependencies": []
+ },
+ {
+ "name": "matcher",
+ "version": "0.12.17",
+ "dependencies": [
+ "async",
+ "meta",
+ "stack_trace",
+ "term_glyph",
+ "test_api"
+ ]
+ },
+ {
+ "name": "io",
+ "version": "1.0.5",
+ "dependencies": [
+ "meta",
+ "path",
+ "string_scanner"
+ ]
+ },
+ {
+ "name": "http_multi_server",
+ "version": "3.2.2",
+ "dependencies": [
+ "async"
+ ]
+ },
+ {
+ "name": "coverage",
+ "version": "1.15.0",
+ "dependencies": [
+ "args",
+ "cli_config",
+ "glob",
+ "logging",
+ "meta",
+ "package_config",
+ "path",
+ "source_maps",
+ "stack_trace",
+ "vm_service",
+ "yaml"
+ ]
+ },
+ {
+ "name": "boolean_selector",
+ "version": "2.1.2",
+ "dependencies": [
+ "source_span",
+ "string_scanner"
+ ]
+ },
+ {
+ "name": "web_socket",
+ "version": "1.0.1",
+ "dependencies": [
+ "web"
+ ]
+ },
+ {
+ "name": "web",
+ "version": "1.1.1",
+ "dependencies": []
+ },
+ {
+ "name": "vm_service",
+ "version": "15.2.0",
+ "dependencies": []
+ },
+ {
+ "name": "source_maps",
+ "version": "0.10.13",
+ "dependencies": [
+ "source_span"
+ ]
+ },
+ {
+ "name": "source_map_stack_trace",
+ "version": "2.1.2",
+ "dependencies": [
+ "path",
+ "source_maps",
+ "stack_trace"
+ ]
+ },
+ {
+ "name": "frontend_server_client",
+ "version": "4.0.0",
+ "dependencies": [
+ "async",
+ "path"
+ ]
+ },
+ {
+ "name": "mime",
+ "version": "2.0.0",
+ "dependencies": []
+ },
+ {
+ "name": "http_parser",
+ "version": "4.1.2",
+ "dependencies": [
+ "collection",
+ "source_span",
+ "string_scanner",
+ "typed_data"
+ ]
+ },
+ {
+ "name": "cli_config",
+ "version": "0.2.0",
+ "dependencies": [
+ "args",
+ "yaml"
+ ]
+ },
+ {
+ "name": "build_runner",
+ "version": "2.15.0",
+ "dependencies": [
+ "analyzer",
+ "args",
+ "async",
+ "build",
+ "build_config",
+ "build_daemon",
+ "built_collection",
+ "built_value",
+ "collection",
+ "convert",
+ "crypto",
+ "dart_style",
+ "glob",
+ "graphs",
+ "http_multi_server",
+ "io",
+ "json_annotation",
+ "logging",
+ "meta",
+ "mime",
+ "package_config",
+ "path",
+ "pool",
+ "pub_semver",
+ "shelf",
+ "shelf_web_socket",
+ "stream_transform",
+ "watcher",
+ "web_socket_channel",
+ "yaml"
+ ]
+ },
+ {
+ "name": "stream_transform",
+ "version": "2.1.1",
+ "dependencies": []
+ },
+ {
+ "name": "graphs",
+ "version": "2.3.2",
+ "dependencies": [
+ "collection"
+ ]
+ },
+ {
+ "name": "built_value",
+ "version": "8.12.6",
+ "dependencies": [
+ "built_collection",
+ "collection",
+ "fixnum",
+ "meta"
+ ]
+ },
+ {
+ "name": "built_collection",
+ "version": "5.1.1",
+ "dependencies": []
+ },
+ {
+ "name": "build_daemon",
+ "version": "4.1.1",
+ "dependencies": [
+ "built_collection",
+ "built_value",
+ "crypto",
+ "http_multi_server",
+ "logging",
+ "path",
+ "pool",
+ "shelf",
+ "shelf_web_socket",
+ "stream_transform",
+ "watcher",
+ "web_socket_channel"
+ ]
+ },
+ {
+ "name": "fixnum",
+ "version": "1.1.1",
+ "dependencies": []
+ }
+ ],
+ "configVersion": 1
+}
\ No newline at end of file
diff --git a/packages/finance/dart/.dart_tool/pub/bin/test/test.dart-3.12.0.snapshot b/packages/finance/dart/.dart_tool/pub/bin/test/test.dart-3.12.0.snapshot
new file mode 100644
index 0000000..fdcd1b5
Binary files /dev/null and b/packages/finance/dart/.dart_tool/pub/bin/test/test.dart-3.12.0.snapshot differ
diff --git a/packages/finance/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjEx b/packages/finance/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjEx
new file mode 100644
index 0000000..d63d437
Binary files /dev/null and b/packages/finance/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjEx differ
diff --git a/packages/finance/dart/.gitignore b/packages/finance/dart/.gitignore
new file mode 100644
index 0000000..3cceda5
--- /dev/null
+++ b/packages/finance/dart/.gitignore
@@ -0,0 +1,7 @@
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
+.dart_tool/
+
+# Avoid committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
+pubspec.lock
diff --git a/packages/finance/dart/CHANGELOG.md b/packages/finance/dart/CHANGELOG.md
new file mode 100644
index 0000000..b3f0226
--- /dev/null
+++ b/packages/finance/dart/CHANGELOG.md
@@ -0,0 +1,26 @@
+# CHANGELOG
+
+## [0.2.0] - 2026-06-01
+
+### Added
+
+- SourceRecordDto — 原始记录 DTO(@JsonSerializable + @JsonKey snake_case)
+- NormalizedRecordDto — 标准化记录 DTO(同上)
+- 5 枚举 — SourceType, IngestionStatus, RecordType, Direction, NormalizationStatus(@JsonEnum + @JsonValue 显式映射 + unknown 兜底)
+- 枚举 wire-value 对齐测试,与 doc/entities.md 值表一致
+
+## [0.1.1] - 2026-05-29
+
+### Fixed
+
+- 添加 LICENSE 文件,满足 pub.dev 发布要求
+
+## [0.1.0] - 2026-05-29
+
+### Added
+
+- Journal — 日记账实体(id, name, createdAt)
+- JournalEntry — 凭证实体(id, journalId, createdAt, description, lines)
+- JournalEntryLine — 分录行(id, type, amount, description, createdAt),支持多行
+- LineType 枚举(debit / credit)
+- 基于 freezed 的不可变模型,支持 copyWith 与 JSON 序列化
diff --git a/packages/finance/dart/LICENSE b/packages/finance/dart/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/packages/finance/dart/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/finance/dart/README.md b/packages/finance/dart/README.md
new file mode 100644
index 0000000..e27029c
--- /dev/null
+++ b/packages/finance/dart/README.md
@@ -0,0 +1,45 @@
+# quanttide_finance
+
+量潮财务领域模型包 —— 日记账与凭证的核心实体。
+
+## 说明
+
+本包是本项目早期发布的 Dart 模型库,提供基于 `freezed` 的不可变财务实体。当前覆盖的是**下游凭证层**(Journal / JournalEntry / JournalEntryLine),并非主干标准化模型。
+
+项目主干(SourceRecord → NormalizedRecord → ClassificationResult → Statistics)正在 FastAPI 上构建中,参见根目录 README。
+
+## 模型
+
+| 实体 | 说明 |
+|---|---|
+| `Journal` | 日记账(id, name, createdAt) |
+| `JournalEntry` | 凭证(id, journalId, createdAt, description, lines) |
+| `JournalEntryLine` | 分录行(id, type, amount, description, createdAt) |
+| `LineType` | 枚举:`debit` / `credit` |
+
+所有模型支持 `copyWith`、`toJson` / `fromJson`。
+
+## 使用
+
+```dart
+import 'package:quanttide_finance/quanttide_finance.dart';
+
+void main() {
+ final journal = Journal(id: '1', name: '备用金', createdAt: DateTime.now());
+
+ final entry = JournalEntry(
+ id: 'je1',
+ journalId: journal.id,
+ createdAt: DateTime.now(),
+ description: '采购办公用品',
+ lines: [
+ JournalEntryLine(id: 'l1', type: LineType.debit, amount: 1200, createdAt: DateTime.now()),
+ JournalEntryLine(id: 'l2', type: LineType.credit, amount: 1200, createdAt: DateTime.now()),
+ ],
+ );
+}
+```
+
+## 发布
+
+版本 `0.1.1` 已发布到 pub.dev。
diff --git a/packages/finance/dart/analysis_options.yaml b/packages/finance/dart/analysis_options.yaml
new file mode 100644
index 0000000..82c7ca7
--- /dev/null
+++ b/packages/finance/dart/analysis_options.yaml
@@ -0,0 +1,18 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+analyzer:
+ errors:
+ invalid_annotation_target: ignore
diff --git a/packages/finance/dart/lib/quanttide_finance.dart b/packages/finance/dart/lib/quanttide_finance.dart
new file mode 100644
index 0000000..383f49c
--- /dev/null
+++ b/packages/finance/dart/lib/quanttide_finance.dart
@@ -0,0 +1,7 @@
+export 'src/models/journal.dart';
+export 'src/models/journal_entry.dart';
+
+export 'src/dto/enums.dart';
+export 'src/dto/source_record.dart';
+export 'src/dto/normalized_record.dart';
+export 'src/dto/classification_result.dart';
diff --git a/packages/finance/dart/lib/src/dto/classification_result.dart b/packages/finance/dart/lib/src/dto/classification_result.dart
new file mode 100644
index 0000000..8f96059
--- /dev/null
+++ b/packages/finance/dart/lib/src/dto/classification_result.dart
@@ -0,0 +1,36 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'enums.dart';
+
+part 'classification_result.freezed.dart';
+part 'classification_result.g.dart';
+
+@freezed
+class ClassificationResultDto with _$ClassificationResultDto {
+ const factory ClassificationResultDto({
+ required int id,
+ @JsonKey(name: 'normalized_record_id') required int normalizedRecordId,
+ @Default('expense_type') String taxonomy,
+ required String category,
+ @JsonKey(name: 'tags') Map