tb-order-sync 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ from config.settings import get_settings
2
+ from config.mappings import ColumnMapping, get_column_mapping
3
+
4
+ __all__ = ["get_settings", "ColumnMapping", "get_column_mapping"]
@@ -0,0 +1,63 @@
1
+ """Column mapping utilities.
2
+
3
+ Converts between column letters (A, B, ...) and 0-based indices,
4
+ and provides a typed mapping object built from Settings.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from functools import lru_cache
11
+
12
+ from config.settings import get_settings
13
+
14
+
15
+ def col_letter_to_index(letter: str) -> int:
16
+ """Convert column letter(s) to 0-based index. A->0, B->1, ..., Z->25, AA->26."""
17
+ letter = letter.upper().strip()
18
+ result = 0
19
+ for ch in letter:
20
+ result = result * 26 + (ord(ch) - ord("A") + 1)
21
+ return result - 1
22
+
23
+
24
+ def col_index_to_letter(index: int) -> str:
25
+ """Convert 0-based index to column letter(s). 0->A, 1->B, ..., 25->Z, 26->AA."""
26
+ result = ""
27
+ idx = index + 1
28
+ while idx > 0:
29
+ idx, remainder = divmod(idx - 1, 26)
30
+ result = chr(65 + remainder) + result
31
+ return result
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ColumnMapping:
36
+ """Resolved column indices for A / B tables."""
37
+
38
+ # A 表
39
+ a_product_price: int
40
+ a_packaging_price: int
41
+ a_freight: int
42
+ a_customer_quote: int
43
+ a_gross_profit: int
44
+ a_order_no: int
45
+ a_refund_status: int
46
+
47
+ # B 表
48
+ b_order_no: int
49
+
50
+
51
+ @lru_cache(maxsize=1)
52
+ def get_column_mapping() -> ColumnMapping:
53
+ s = get_settings()
54
+ return ColumnMapping(
55
+ a_product_price=col_letter_to_index(s.a_col_product_price),
56
+ a_packaging_price=col_letter_to_index(s.a_col_packaging_price),
57
+ a_freight=col_letter_to_index(s.a_col_freight),
58
+ a_customer_quote=col_letter_to_index(s.a_col_customer_quote),
59
+ a_gross_profit=col_letter_to_index(s.a_col_gross_profit),
60
+ a_order_no=col_letter_to_index(s.a_col_order_no),
61
+ a_refund_status=col_letter_to_index(s.a_col_refund_status),
62
+ b_order_no=col_letter_to_index(s.b_col_order_no),
63
+ )
@@ -0,0 +1,95 @@
1
+ """Application settings via pydantic-settings + .env file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from enum import Enum
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+
11
+ from pydantic import Field
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+
14
+ def _get_project_root() -> Path:
15
+ """Resolve project root, compatible with PyInstaller frozen exe."""
16
+ if getattr(sys, "frozen", False):
17
+ # Running as packaged exe — use exe's directory as root
18
+ return Path(sys.executable).resolve().parent
19
+ return Path(__file__).resolve().parent.parent
20
+
21
+
22
+ PROJECT_ROOT = _get_project_root()
23
+
24
+
25
+ class AppEnv(str, Enum):
26
+ DEV = "dev"
27
+ STAGING = "staging"
28
+ PROD = "prod"
29
+
30
+
31
+ class SyncMode(str, Enum):
32
+ INCREMENTAL = "incremental"
33
+ FULL = "full"
34
+
35
+
36
+ class Settings(BaseSettings):
37
+ """All configuration knobs, sourced from .env / environment variables."""
38
+
39
+ model_config = SettingsConfigDict(
40
+ env_file=os.environ.get("DOTENV_PATH", str(PROJECT_ROOT / ".env")),
41
+ env_file_encoding="utf-8",
42
+ case_sensitive=False,
43
+ extra="ignore",
44
+ )
45
+
46
+ # ── 基础 ──────────────────────────────────────────────
47
+ app_env: AppEnv = AppEnv.DEV
48
+ log_level: str = "INFO"
49
+ state_dir: str = str(PROJECT_ROOT / "state")
50
+
51
+ # ── 腾讯文档 ──────────────────────────────────────────
52
+ tencent_client_id: str = ""
53
+ tencent_client_secret: str = ""
54
+ tencent_open_id: str = ""
55
+ tencent_access_token: str = ""
56
+ tencent_a_file_id: str = ""
57
+ tencent_a_sheet_id: str = ""
58
+ tencent_b_file_id: str = ""
59
+ tencent_b_sheet_id: str = ""
60
+
61
+ # ── 飞书(预留) ─────────────────────────────────────
62
+ feishu_app_id: str = ""
63
+ feishu_app_secret: str = ""
64
+ feishu_c_file_token: str = ""
65
+ feishu_c_sheet_id: str = ""
66
+
67
+ # ── 运行 ──────────────────────────────────────────────
68
+ gross_profit_mode: SyncMode = SyncMode.INCREMENTAL
69
+ refund_match_mode: SyncMode = SyncMode.INCREMENTAL
70
+ c_sync_mode: SyncMode = SyncMode.INCREMENTAL
71
+ task_interval_minutes: int = 10
72
+ startup_jitter_seconds: int = 15
73
+ write_batch_size: int = 100
74
+ retry_times: int = 3
75
+ dry_run: bool = False
76
+ enable_style_update: bool = False
77
+
78
+ # ── 列映射(可覆盖) ─────────────────────────────────
79
+ a_col_product_price: str = "C"
80
+ a_col_packaging_price: str = "D"
81
+ a_col_freight: str = "E"
82
+ a_col_customer_quote: str = "F"
83
+ a_col_gross_profit: str = "G"
84
+ a_col_order_no: str = "H"
85
+ a_col_refund_status: str = "I"
86
+ b_col_order_no: str = "A"
87
+
88
+ # ── 业务文案 ──────────────────────────────────────────
89
+ refund_status_text: str = "进入退款流程"
90
+ data_error_text: str = "数据异常"
91
+
92
+
93
+ @lru_cache(maxsize=1)
94
+ def get_settings() -> Settings:
95
+ return Settings()
File without changes
@@ -0,0 +1,98 @@
1
+ """Abstract base class for all sheet connectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Optional, Sequence
7
+
8
+
9
+ class BaseSheetConnector(ABC):
10
+ """Unified interface for reading/writing cloud spreadsheets.
11
+
12
+ All platform-specific connectors (Tencent Docs, Feishu, etc.)
13
+ must implement this interface so that services remain platform-agnostic.
14
+ """
15
+
16
+ @abstractmethod
17
+ def read_rows(
18
+ self,
19
+ file_id: str,
20
+ sheet_id: str,
21
+ *,
22
+ start_row: int = 0,
23
+ end_row: Optional[int] = None,
24
+ ) -> list[list[Any]]:
25
+ """Read rows from a sheet. Returns list of rows, each row is a list of cell values.
26
+
27
+ Row indices are 0-based (row 0 = first row in sheet, typically the header).
28
+ """
29
+ ...
30
+
31
+ @abstractmethod
32
+ def write_cells(
33
+ self,
34
+ file_id: str,
35
+ sheet_id: str,
36
+ updates: list[CellUpdate],
37
+ ) -> None:
38
+ """Write individual cell values."""
39
+ ...
40
+
41
+ @abstractmethod
42
+ def batch_update(
43
+ self,
44
+ file_id: str,
45
+ sheet_id: str,
46
+ updates: list[CellUpdate],
47
+ batch_size: int = 100,
48
+ ) -> None:
49
+ """Write cells in batches to respect API rate limits."""
50
+ ...
51
+
52
+ @abstractmethod
53
+ def ensure_column(
54
+ self,
55
+ file_id: str,
56
+ sheet_id: str,
57
+ col_letter: str,
58
+ header_name: str,
59
+ ) -> None:
60
+ """Ensure a column exists with the given header. Create if missing."""
61
+ ...
62
+
63
+ @abstractmethod
64
+ def get_header(
65
+ self,
66
+ file_id: str,
67
+ sheet_id: str,
68
+ ) -> list[str]:
69
+ """Return the header row (first row) as a list of strings."""
70
+ ...
71
+
72
+ def update_row_style(
73
+ self,
74
+ file_id: str,
75
+ sheet_id: str,
76
+ row_index: int,
77
+ bg_color: Optional[str] = None,
78
+ ) -> None:
79
+ """Optional: set background color for an entire row.
80
+
81
+ Default implementation is a no-op. Connectors may override
82
+ if the platform API supports style manipulation.
83
+ """
84
+ pass
85
+
86
+
87
+ class CellUpdate:
88
+ """A single cell write instruction."""
89
+
90
+ __slots__ = ("row", "col", "value")
91
+
92
+ def __init__(self, row: int, col: int, value: Any) -> None:
93
+ self.row = row
94
+ self.col = col
95
+ self.value = value
96
+
97
+ def __repr__(self) -> str:
98
+ return f"CellUpdate(row={self.row}, col={self.col}, value={self.value!r})"
@@ -0,0 +1,96 @@
1
+ """Feishu (飞书) sheet connector — structural skeleton.
2
+
3
+ This connector implements the BaseSheetConnector interface for Feishu/Lark spreadsheets.
4
+ Currently a placeholder — all methods raise NotImplementedError with guidance.
5
+
6
+ API Reference (TODO / NEED_VERIFY):
7
+ 飞书开放平台电子表格: https://open.feishu.cn/document/server-docs/docs/sheets-v3/overview
8
+
9
+ Implementation priority for Phase 2:
10
+ 1. Authentication (app_id + app_secret → tenant_access_token)
11
+ 2. read_rows — GET /open-apis/sheets/v2/spreadsheets/{spreadsheetToken}/values/{range}
12
+ 3. write_cells — PUT /open-apis/sheets/v2/spreadsheets/{spreadsheetToken}/values
13
+ 4. batch_update — same as write_cells with batch splitting
14
+ 5. get_header / ensure_column
15
+ 6. update_row_style (if needed)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Optional
21
+
22
+ from connectors.base import BaseSheetConnector, CellUpdate
23
+ from utils.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class FeishuSheetsConnector(BaseSheetConnector):
29
+ """Connector for 飞书电子表格.
30
+
31
+ TODO: Implement when Feishu integration is needed.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ app_id: str,
37
+ app_secret: str,
38
+ ) -> None:
39
+ self._app_id = app_id
40
+ self._app_secret = app_secret
41
+ self._tenant_token: Optional[str] = None
42
+ # TODO: Initialize httpx client with base_url = "https://open.feishu.cn"
43
+
44
+ def _ensure_token(self) -> None:
45
+ """Obtain or refresh tenant_access_token.
46
+
47
+ TODO / NEED_VERIFY:
48
+ POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
49
+ body: { "app_id": ..., "app_secret": ... }
50
+ """
51
+ raise NotImplementedError("Feishu token acquisition not yet implemented")
52
+
53
+ def read_rows(
54
+ self,
55
+ file_id: str,
56
+ sheet_id: str,
57
+ *,
58
+ start_row: int = 0,
59
+ end_row: Optional[int] = None,
60
+ ) -> list[list[Any]]:
61
+ """TODO: GET /open-apis/sheets/v2/spreadsheets/{file_id}/values/{sheet_id}!A:ZZ"""
62
+ raise NotImplementedError("FeishuSheetsConnector.read_rows not yet implemented")
63
+
64
+ def write_cells(
65
+ self,
66
+ file_id: str,
67
+ sheet_id: str,
68
+ updates: list[CellUpdate],
69
+ ) -> None:
70
+ """TODO: PUT /open-apis/sheets/v2/spreadsheets/{file_id}/values"""
71
+ raise NotImplementedError("FeishuSheetsConnector.write_cells not yet implemented")
72
+
73
+ def batch_update(
74
+ self,
75
+ file_id: str,
76
+ sheet_id: str,
77
+ updates: list[CellUpdate],
78
+ batch_size: int = 100,
79
+ ) -> None:
80
+ raise NotImplementedError("FeishuSheetsConnector.batch_update not yet implemented")
81
+
82
+ def ensure_column(
83
+ self,
84
+ file_id: str,
85
+ sheet_id: str,
86
+ col_letter: str,
87
+ header_name: str,
88
+ ) -> None:
89
+ raise NotImplementedError("FeishuSheetsConnector.ensure_column not yet implemented")
90
+
91
+ def get_header(
92
+ self,
93
+ file_id: str,
94
+ sheet_id: str,
95
+ ) -> list[str]:
96
+ raise NotImplementedError("FeishuSheetsConnector.get_header not yet implemented")
@@ -0,0 +1,253 @@
1
+ """Tencent Docs (腾讯文档) sheet connector.
2
+
3
+ API Reference (needs verification):
4
+ 腾讯文档开放平台: https://docs.qq.com/open/wiki/
5
+
6
+ TODO / NEED_VERIFY:
7
+ - OAuth2 token refresh flow: the current implementation assumes a pre-obtained
8
+ access_token passed via config. Production should implement refresh_token cycle.
9
+ - Exact API endpoints and request/response schemas are marked inline.
10
+ - Rate limit specifics (QPS, batch size caps) need confirmation from docs.
11
+ - Style/formatting API availability is uncertain — update_row_style is best-effort.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from typing import Any, Optional
18
+
19
+ import httpx
20
+
21
+ from connectors.base import BaseSheetConnector, CellUpdate
22
+ from config.mappings import col_index_to_letter
23
+ from utils.logger import get_logger
24
+ from utils.retry import default_retry
25
+
26
+ logger = get_logger(__name__)
27
+
28
+ # ── TODO / NEED_VERIFY ─────────────────────────────────────────────────────
29
+ # Base URL for Tencent Docs Open API.
30
+ # 腾讯文档智能表格(SmartSheet)和在线表格(Sheet)的API路径可能不同,
31
+ # 需要根据实际文档类型确认。
32
+ # 以下为在线表格(Sheet)的推测路径:
33
+ _BASE_URL = "https://docs.qq.com/openapi/v2"
34
+
35
+ # TODO: 确认实际的 API 路径格式
36
+ # 读取单元格数据: GET /openapi/v2/files/{fileID}/sheets/{sheetID}/content
37
+ # 写入单元格数据: PUT /openapi/v2/files/{fileID}/sheets/{sheetID}/content
38
+ # 这些路径需要与腾讯文档开放平台文档核实
39
+ # ────────────────────────────────────────────────────────────────────────────
40
+
41
+
42
+ class TencentDocsConnector(BaseSheetConnector):
43
+ """Connector for 腾讯文档 online spreadsheets."""
44
+
45
+ def __init__(
46
+ self,
47
+ client_id: str,
48
+ client_secret: str,
49
+ access_token: str,
50
+ open_id: str = "",
51
+ ) -> None:
52
+ self._client_id = client_id
53
+ self._client_secret = client_secret
54
+ self._access_token = access_token
55
+ self._open_id = open_id
56
+ self._http = httpx.Client(
57
+ base_url=_BASE_URL,
58
+ timeout=30.0,
59
+ headers=self._build_headers(),
60
+ )
61
+
62
+ def _build_headers(self) -> dict[str, str]:
63
+ return {
64
+ "Content-Type": "application/json",
65
+ "Access-Token": self._access_token,
66
+ # TODO / NEED_VERIFY: 腾讯文档 API 的认证 header 名称
67
+ # 可能是 "Access-Token" 或 "Authorization: Bearer <token>"
68
+ # 需根据实际文档确认
69
+ }
70
+
71
+ # ── Token refresh (placeholder) ────────────────────────────────────────
72
+ def refresh_token(self) -> None:
73
+ """TODO: Implement OAuth2 token refresh using client_id + client_secret.
74
+
75
+ 腾讯文档的 token 有效期通常较短,生产环境必须实现自动刷新。
76
+ 当前版本依赖外部提供有效的 access_token。
77
+ """
78
+ # TODO / NEED_VERIFY:
79
+ # POST /oauth/v2/token
80
+ # {
81
+ # "client_id": ...,
82
+ # "client_secret": ...,
83
+ # "grant_type": "refresh_token",
84
+ # "refresh_token": ...
85
+ # }
86
+ raise NotImplementedError("Token refresh not yet implemented — supply valid access_token in .env")
87
+
88
+ # ── Read ───────────────────────────────────────────────────────────────
89
+ @default_retry(max_attempts=3)
90
+ def read_rows(
91
+ self,
92
+ file_id: str,
93
+ sheet_id: str,
94
+ *,
95
+ start_row: int = 0,
96
+ end_row: Optional[int] = None,
97
+ ) -> list[list[Any]]:
98
+ """Read rows from a Tencent Docs sheet.
99
+
100
+ TODO / NEED_VERIFY: 确认实际的读取 API endpoint 和参数格式。
101
+ 以下为推测实现,需根据腾讯文档开放平台文档调整。
102
+ """
103
+ # TODO / NEED_VERIFY: 实际的 API 路径和参数
104
+ # 可能的路径: GET /files/{fileID}/sheets/{sheetID}/content
105
+ # 也可能需要指定 range,如 "A1:Z1000"
106
+ url = f"/files/{file_id}/sheets/{sheet_id}/content"
107
+ params: dict[str, Any] = {}
108
+ if end_row is not None:
109
+ # TODO: 腾讯文档可能使用 A1 notation 的 range 参数
110
+ # 例如 range = "A{start_row+1}:Z{end_row+1}"
111
+ params["range"] = f"A{start_row + 1}:ZZ{end_row + 1}"
112
+ else:
113
+ params["range"] = f"A{start_row + 1}:ZZ"
114
+
115
+ logger.info("Reading rows from file=%s sheet=%s range=%s", file_id, sheet_id, params.get("range"))
116
+
117
+ resp = self._http.get(url, params=params)
118
+ resp.raise_for_status()
119
+ data = resp.json()
120
+
121
+ # TODO / NEED_VERIFY: 解析响应数据结构
122
+ # 腾讯文档返回的数据格式需要确认,以下是推测:
123
+ # data = { "data": { "rows": [[cell, ...], ...] } }
124
+ # 或: data = { "data": [[cell, ...], ...] }
125
+ rows = data.get("data", {}).get("rows", [])
126
+ if not rows and isinstance(data.get("data"), list):
127
+ rows = data["data"]
128
+
129
+ logger.info("Read %d rows from file=%s sheet=%s", len(rows), file_id, sheet_id)
130
+ return rows
131
+
132
+ # ── Write ──────────────────────────────────────────────────────────────
133
+ @default_retry(max_attempts=3)
134
+ def write_cells(
135
+ self,
136
+ file_id: str,
137
+ sheet_id: str,
138
+ updates: list[CellUpdate],
139
+ ) -> None:
140
+ """Write cell values to a Tencent Docs sheet.
141
+
142
+ TODO / NEED_VERIFY: 确认写入 API 的 endpoint 和 request body 格式。
143
+ """
144
+ if not updates:
145
+ return
146
+
147
+ # TODO / NEED_VERIFY: 构建写入请求体
148
+ # 腾讯文档可能支持批量单元格更新,格式推测如下:
149
+ # PUT /files/{fileID}/sheets/{sheetID}/content
150
+ # body = { "data": [ {"range": "G2", "value": 123}, ... ] }
151
+ payload = self._build_write_payload(updates)
152
+ url = f"/files/{file_id}/sheets/{sheet_id}/content"
153
+
154
+ logger.info("Writing %d cells to file=%s sheet=%s", len(updates), file_id, sheet_id)
155
+
156
+ resp = self._http.put(url, json=payload)
157
+ resp.raise_for_status()
158
+
159
+ logger.info("Successfully wrote %d cells", len(updates))
160
+
161
+ @default_retry(max_attempts=3)
162
+ def batch_update(
163
+ self,
164
+ file_id: str,
165
+ sheet_id: str,
166
+ updates: list[CellUpdate],
167
+ batch_size: int = 100,
168
+ ) -> None:
169
+ """Write cells in batches to stay within API rate limits."""
170
+ total = len(updates)
171
+ for i in range(0, total, batch_size):
172
+ batch = updates[i : i + batch_size]
173
+ self.write_cells(file_id, sheet_id, batch)
174
+ if i + batch_size < total:
175
+ time.sleep(0.5) # Basic rate-limit courtesy
176
+ logger.info("Batch update complete: %d cells in %d batches", total, (total + batch_size - 1) // batch_size)
177
+
178
+ # ── Column management ──────────────────────────────────────────────────
179
+ def ensure_column(
180
+ self,
181
+ file_id: str,
182
+ sheet_id: str,
183
+ col_letter: str,
184
+ header_name: str,
185
+ ) -> None:
186
+ """Ensure column header exists; write it if missing.
187
+
188
+ TODO / NEED_VERIFY: 可能需要先读取 header row,检查列是否存在,
189
+ 如果不存在则需要插入列或写入 header。
190
+ """
191
+ header = self.get_header(file_id, sheet_id)
192
+ from config.mappings import col_letter_to_index
193
+ idx = col_letter_to_index(col_letter)
194
+
195
+ if idx < len(header) and header[idx] == header_name:
196
+ logger.debug("Column %s already has header '%s'", col_letter, header_name)
197
+ return
198
+
199
+ # Write the header cell
200
+ self.write_cells(file_id, sheet_id, [CellUpdate(row=0, col=idx, value=header_name)])
201
+ logger.info("Ensured column %s header = '%s'", col_letter, header_name)
202
+
203
+ def get_header(
204
+ self,
205
+ file_id: str,
206
+ sheet_id: str,
207
+ ) -> list[str]:
208
+ """Read the first row as header."""
209
+ rows = self.read_rows(file_id, sheet_id, start_row=0, end_row=1)
210
+ if rows:
211
+ return [str(v) if v is not None else "" for v in rows[0]]
212
+ return []
213
+
214
+ # ── Style (optional) ───────────────────────────────────────────────────
215
+ def update_row_style(
216
+ self,
217
+ file_id: str,
218
+ sheet_id: str,
219
+ row_index: int,
220
+ bg_color: Optional[str] = None,
221
+ ) -> None:
222
+ """Optional: set row background color.
223
+
224
+ TODO / NEED_VERIFY: 腾讯文档是否支持通过 API 设置单元格/行样式。
225
+ 如果不支持,此方法将仅记录日志并跳过。
226
+ 当前实现为 best-effort placeholder。
227
+ """
228
+ if bg_color is None:
229
+ logger.debug("update_row_style called with no color, skipping row=%d", row_index)
230
+ return
231
+
232
+ # TODO / NEED_VERIFY: 样式 API 路径和格式
233
+ # 可能的路径: PUT /files/{fileID}/sheets/{sheetID}/styles
234
+ # body = { "range": "A{row}:Z{row}", "style": {"backgroundColor": "#FF0000"} }
235
+ logger.info(
236
+ "Style update requested for row=%d bg=%s (file=%s) — "
237
+ "TODO: verify Tencent Docs style API support",
238
+ row_index, bg_color, file_id,
239
+ )
240
+
241
+ # ── Internal helpers ───────────────────────────────────────────────────
242
+ @staticmethod
243
+ def _build_write_payload(updates: list[CellUpdate]) -> dict[str, Any]:
244
+ """Build the API request body for a batch cell write.
245
+
246
+ TODO / NEED_VERIFY: 确认腾讯文档的批量写入 request body 格式。
247
+ """
248
+ cells = []
249
+ for u in updates:
250
+ col_letter = col_index_to_letter(u.col)
251
+ cell_ref = f"{col_letter}{u.row + 1}" # A1 notation, 1-based
252
+ cells.append({"range": cell_ref, "value": u.value})
253
+ return {"data": cells}
package/main.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point: prefer `tb ...`; `python main.py ...` remains supported."""
2
+
3
+ from cli.commands import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,12 @@
1
+ from models.records import OrderRecord, RefundRecord
2
+ from models.task_models import SyncTaskConfig, TaskResult
3
+ from models.state_models import SyncState, RowFingerprint
4
+
5
+ __all__ = [
6
+ "OrderRecord",
7
+ "RefundRecord",
8
+ "SyncTaskConfig",
9
+ "TaskResult",
10
+ "SyncState",
11
+ "RowFingerprint",
12
+ ]
@@ -0,0 +1,38 @@
1
+ """Core business data models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class OrderRecord(BaseModel):
11
+ """A 表一行订单记录的内部表示。"""
12
+
13
+ row_index: int = Field(..., description="原始表格行号 (0-based, 含表头)")
14
+ order_no: str = ""
15
+ product_price: Optional[str] = None
16
+ packaging_price: Optional[str] = None
17
+ freight: Optional[str] = None
18
+ customer_quote: Optional[str] = None
19
+ gross_profit: Optional[str] = None
20
+ refund_status: Optional[str] = None
21
+ raw_data: list[Any] = Field(default_factory=list)
22
+ source_platform: str = "tencent_docs"
23
+ source_sheet: str = ""
24
+
25
+
26
+ class RefundRecord(BaseModel):
27
+ """B 表一行退款记录的内部表示。"""
28
+
29
+ row_index: int
30
+ order_no: str = ""
31
+ refund_order_no: Optional[str] = None
32
+ shop: Optional[str] = None
33
+ customer_wechat: Optional[str] = None
34
+ product: Optional[str] = None
35
+ reason: Optional[str] = None
36
+ amount: Optional[str] = None
37
+ refund_status: Optional[str] = None
38
+ raw_data: list[Any] = Field(default_factory=list)