tb-order-sync 0.3.1 → 0.4.1
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.
- package/.env.example +3 -2
- package/CHANGELOG.md +36 -0
- package/README.md +110 -103
- package/build.py +11 -4
- package/cli/commands.py +66 -6
- package/cli/dashboard.py +40 -9
- package/cli/setup.py +87 -35
- package/config/settings.py +2 -2
- package/connectors/tencent_docs.py +247 -137
- package/models/state_models.py +4 -1
- package/models/task_models.py +15 -0
- package/package.json +5 -2
- package/services/daemon_service.py +134 -0
- package/services/gross_profit_service.py +15 -1
- package/services/refund_match_service.py +52 -9
- package/services/scheduler_service.py +27 -2
- package/services/state_service.py +25 -0
- package/sync_service.spec +2 -0
- package/utils/retry.py +20 -2
- package//345/277/253/351/200/237/345/274/200/345/247/213.txt +31 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""Tencent Docs (腾讯文档) sheet connector.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
- Style/formatting API availability is uncertain — update_row_style is best-effort.
|
|
3
|
+
Uses the official Online Sheet v3 APIs:
|
|
4
|
+
- GET /openapi/spreadsheet/v3/files/{fileId}/{sheetId}/{range}
|
|
5
|
+
- POST /openapi/spreadsheet/v3/files/{fileId}/batchUpdate
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- Access token refresh is still external. Supply a valid access token in config.
|
|
9
|
+
- The sheet model exposes text color formatting. For "整行标红", this connector
|
|
10
|
+
rewrites the full row with red text formatting.
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
13
|
from __future__ import annotations
|
|
@@ -25,18 +24,15 @@ from utils.retry import default_retry
|
|
|
25
24
|
|
|
26
25
|
logger = get_logger(__name__)
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# 写入单元格数据: PUT /openapi/v2/files/{fileID}/sheets/{sheetID}/content
|
|
38
|
-
# 这些路径需要与腾讯文档开放平台文档核实
|
|
39
|
-
# ────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
_BASE_URL = "https://docs.qq.com"
|
|
28
|
+
# Tencent Docs v3 range query limit:
|
|
29
|
+
# rows <= 1000, cols <= 200, total cells <= 10000.
|
|
30
|
+
# Current project only needs up to column I/L, so keep the query window narrow.
|
|
31
|
+
_MAX_QUERY_ROWS = 200
|
|
32
|
+
_MAX_QUERY_COLS = 20
|
|
33
|
+
_MAX_BATCH_REQUESTS = 5
|
|
34
|
+
_BATCH_SLEEP_SECONDS = 0.5
|
|
35
|
+
_DEFAULT_TEXT_COLOR = {"red": 0, "green": 0, "blue": 0, "alpha": 255}
|
|
40
36
|
|
|
41
37
|
|
|
42
38
|
class TencentDocsConnector(BaseSheetConnector):
|
|
@@ -60,33 +56,23 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
60
56
|
)
|
|
61
57
|
|
|
62
58
|
def _build_headers(self) -> dict[str, str]:
|
|
63
|
-
|
|
59
|
+
headers = {
|
|
64
60
|
"Content-Type": "application/json",
|
|
61
|
+
"Accept": "application/json",
|
|
65
62
|
"Access-Token": self._access_token,
|
|
66
|
-
|
|
67
|
-
# 可能是 "Access-Token" 或 "Authorization: Bearer <token>"
|
|
68
|
-
# 需根据实际文档确认
|
|
63
|
+
"Client-Id": self._client_id,
|
|
69
64
|
}
|
|
65
|
+
if self._open_id:
|
|
66
|
+
headers["Open-Id"] = self._open_id
|
|
67
|
+
else:
|
|
68
|
+
logger.warning("Tencent connector initialized without Open-Id; official APIs require it")
|
|
69
|
+
return headers
|
|
70
70
|
|
|
71
|
-
# ── Token refresh (placeholder) ────────────────────────────────────────
|
|
72
71
|
def refresh_token(self) -> None:
|
|
73
|
-
"""
|
|
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
|
-
# }
|
|
72
|
+
"""Token refresh remains external in this MVP."""
|
|
86
73
|
raise NotImplementedError("Token refresh not yet implemented — supply valid access_token in .env")
|
|
87
74
|
|
|
88
|
-
|
|
89
|
-
@default_retry(max_attempts=3)
|
|
75
|
+
@default_retry(max_attempts=5)
|
|
90
76
|
def read_rows(
|
|
91
77
|
self,
|
|
92
78
|
file_id: str,
|
|
@@ -95,70 +81,63 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
95
81
|
start_row: int = 0,
|
|
96
82
|
end_row: Optional[int] = None,
|
|
97
83
|
) -> list[list[Any]]:
|
|
98
|
-
"""Read rows from a Tencent Docs sheet.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
84
|
+
"""Read rows from a Tencent Docs sheet using the official v3 range API."""
|
|
85
|
+
if end_row is not None and end_row <= start_row:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
max_col_letter = col_index_to_letter(_MAX_QUERY_COLS - 1)
|
|
89
|
+
all_rows: list[list[Any]] = []
|
|
90
|
+
next_row = start_row
|
|
91
|
+
|
|
92
|
+
while True:
|
|
93
|
+
chunk_end = next_row + _MAX_QUERY_ROWS
|
|
94
|
+
if end_row is not None:
|
|
95
|
+
chunk_end = min(chunk_end, end_row)
|
|
96
|
+
|
|
97
|
+
range_ref = f"A{next_row + 1}:{max_col_letter}{chunk_end}"
|
|
98
|
+
url = f"/openapi/spreadsheet/v3/files/{file_id}/{sheet_id}/{range_ref}"
|
|
99
|
+
logger.info("Reading rows from file=%s sheet=%s range=%s", file_id, sheet_id, range_ref)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
data = self._unwrap_response(self._http.get(url))
|
|
103
|
+
except RuntimeError as exc:
|
|
104
|
+
if all_rows and "range' invalid" in str(exc):
|
|
105
|
+
logger.info("Stop reading at invalid range boundary: %s", range_ref)
|
|
106
|
+
break
|
|
107
|
+
raise
|
|
108
|
+
grid_data = data.get("gridData", data)
|
|
109
|
+
rows = self._grid_data_to_rows(grid_data)
|
|
110
|
+
all_rows.extend(rows)
|
|
111
|
+
|
|
112
|
+
requested_rows = chunk_end - next_row
|
|
113
|
+
if end_row is not None and chunk_end >= end_row:
|
|
114
|
+
break
|
|
115
|
+
if len(rows) < requested_rows:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
next_row = chunk_end
|
|
119
|
+
|
|
120
|
+
logger.info("Read %d rows from file=%s sheet=%s", len(all_rows), file_id, sheet_id)
|
|
121
|
+
return all_rows
|
|
122
|
+
|
|
123
|
+
@default_retry(max_attempts=5)
|
|
134
124
|
def write_cells(
|
|
135
125
|
self,
|
|
136
126
|
file_id: str,
|
|
137
127
|
sheet_id: str,
|
|
138
128
|
updates: list[CellUpdate],
|
|
139
129
|
) -> None:
|
|
140
|
-
"""Write cell values
|
|
141
|
-
|
|
142
|
-
TODO / NEED_VERIFY: 确认写入 API 的 endpoint 和 request body 格式。
|
|
143
|
-
"""
|
|
130
|
+
"""Write cell values via the official v3 batchUpdate API."""
|
|
144
131
|
if not updates:
|
|
145
132
|
return
|
|
146
133
|
|
|
147
|
-
|
|
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
|
-
|
|
134
|
+
url = f"/openapi/spreadsheet/v3/files/{file_id}/batchUpdate"
|
|
135
|
+
payload = self._build_write_payload(sheet_id, updates)
|
|
154
136
|
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
|
-
|
|
137
|
+
self._unwrap_response(self._http.post(url, json=payload))
|
|
159
138
|
logger.info("Successfully wrote %d cells", len(updates))
|
|
160
139
|
|
|
161
|
-
@default_retry(max_attempts=
|
|
140
|
+
@default_retry(max_attempts=5)
|
|
162
141
|
def batch_update(
|
|
163
142
|
self,
|
|
164
143
|
file_id: str,
|
|
@@ -166,16 +145,16 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
166
145
|
updates: list[CellUpdate],
|
|
167
146
|
batch_size: int = 100,
|
|
168
147
|
) -> None:
|
|
169
|
-
"""Write cells in batches
|
|
148
|
+
"""Write cells in batches while respecting Tencent Docs batch limits."""
|
|
170
149
|
total = len(updates)
|
|
171
|
-
|
|
172
|
-
|
|
150
|
+
chunk_size = max(1, min(batch_size, _MAX_BATCH_REQUESTS))
|
|
151
|
+
for i in range(0, total, chunk_size):
|
|
152
|
+
batch = updates[i : i + chunk_size]
|
|
173
153
|
self.write_cells(file_id, sheet_id, batch)
|
|
174
|
-
if i +
|
|
175
|
-
time.sleep(
|
|
176
|
-
logger.info("Batch update complete: %d cells in %d batches", total, (total +
|
|
154
|
+
if i + chunk_size < total:
|
|
155
|
+
time.sleep(_BATCH_SLEEP_SECONDS)
|
|
156
|
+
logger.info("Batch update complete: %d cells in %d batches", total, (total + chunk_size - 1) // chunk_size)
|
|
177
157
|
|
|
178
|
-
# ── Column management ──────────────────────────────────────────────────
|
|
179
158
|
def ensure_column(
|
|
180
159
|
self,
|
|
181
160
|
file_id: str,
|
|
@@ -183,20 +162,14 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
183
162
|
col_letter: str,
|
|
184
163
|
header_name: str,
|
|
185
164
|
) -> None:
|
|
186
|
-
"""Ensure column header exists; write it if missing.
|
|
187
|
-
|
|
188
|
-
TODO / NEED_VERIFY: 可能需要先读取 header row,检查列是否存在,
|
|
189
|
-
如果不存在则需要插入列或写入 header。
|
|
190
|
-
"""
|
|
165
|
+
"""Ensure column header exists; write it if missing."""
|
|
191
166
|
header = self.get_header(file_id, sheet_id)
|
|
192
167
|
from config.mappings import col_letter_to_index
|
|
193
|
-
idx = col_letter_to_index(col_letter)
|
|
194
168
|
|
|
169
|
+
idx = col_letter_to_index(col_letter)
|
|
195
170
|
if idx < len(header) and header[idx] == header_name:
|
|
196
|
-
logger.debug("Column %s already has header '%s'", col_letter, header_name)
|
|
197
171
|
return
|
|
198
172
|
|
|
199
|
-
# Write the header cell
|
|
200
173
|
self.write_cells(file_id, sheet_id, [CellUpdate(row=0, col=idx, value=header_name)])
|
|
201
174
|
logger.info("Ensured column %s header = '%s'", col_letter, header_name)
|
|
202
175
|
|
|
@@ -211,7 +184,7 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
211
184
|
return [str(v) if v is not None else "" for v in rows[0]]
|
|
212
185
|
return []
|
|
213
186
|
|
|
214
|
-
|
|
187
|
+
@default_retry(max_attempts=5)
|
|
215
188
|
def update_row_style(
|
|
216
189
|
self,
|
|
217
190
|
file_id: str,
|
|
@@ -219,35 +192,172 @@ class TencentDocsConnector(BaseSheetConnector):
|
|
|
219
192
|
row_index: int,
|
|
220
193
|
bg_color: Optional[str] = None,
|
|
221
194
|
) -> None:
|
|
222
|
-
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"""
|
|
228
|
-
if bg_color is None:
|
|
229
|
-
logger.debug("update_row_style called with no color, skipping row=%d", row_index)
|
|
195
|
+
"""Rewrite a row with red or default text formatting."""
|
|
196
|
+
header = self.get_header(file_id, sheet_id)
|
|
197
|
+
rows = self.read_rows(file_id, sheet_id, start_row=row_index, end_row=row_index + 1)
|
|
198
|
+
if not rows:
|
|
199
|
+
logger.warning("No row found for style update: row=%d file=%s sheet=%s", row_index, file_id, sheet_id)
|
|
230
200
|
return
|
|
231
201
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
)
|
|
202
|
+
row_values = list(rows[0])
|
|
203
|
+
width = max(len(header), len(row_values))
|
|
204
|
+
if width == 0:
|
|
205
|
+
return
|
|
206
|
+
if len(row_values) < width:
|
|
207
|
+
row_values.extend([""] * (width - len(row_values)))
|
|
208
|
+
|
|
209
|
+
text_color = self._hex_to_rgba(bg_color) if bg_color else _DEFAULT_TEXT_COLOR
|
|
210
|
+
payload = {
|
|
211
|
+
"requests": [
|
|
212
|
+
{
|
|
213
|
+
"updateRangeRequest": {
|
|
214
|
+
"sheetId": sheet_id,
|
|
215
|
+
"gridData": {
|
|
216
|
+
"startRow": row_index,
|
|
217
|
+
"startColumn": 0,
|
|
218
|
+
"rows": [
|
|
219
|
+
{
|
|
220
|
+
"values": [
|
|
221
|
+
self._build_cell_data(value, text_color=text_color)
|
|
222
|
+
for value in row_values
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
url = f"/openapi/spreadsheet/v3/files/{file_id}/batchUpdate"
|
|
232
|
+
self._unwrap_response(self._http.post(url, json=payload))
|
|
233
|
+
logger.info("Updated row style for row=%d file=%s sheet=%s", row_index, file_id, sheet_id)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _grid_data_to_rows(grid_data: dict[str, Any]) -> list[list[Any]]:
|
|
237
|
+
rows: list[list[Any]] = []
|
|
238
|
+
for row_data in grid_data.get("rows", []):
|
|
239
|
+
rows.append([
|
|
240
|
+
TencentDocsConnector._extract_cell_value(cell)
|
|
241
|
+
for cell in row_data.get("values", [])
|
|
242
|
+
])
|
|
243
|
+
return rows
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _extract_cell_value(cell: dict[str, Any]) -> Any:
|
|
247
|
+
if not isinstance(cell, dict):
|
|
248
|
+
return None
|
|
249
|
+
cell_value = cell.get("cellValue") or {}
|
|
250
|
+
if "number" in cell_value:
|
|
251
|
+
return cell_value["number"]
|
|
252
|
+
if "text" in cell_value:
|
|
253
|
+
return cell_value["text"]
|
|
254
|
+
if "link" in cell_value:
|
|
255
|
+
link = cell_value["link"] or {}
|
|
256
|
+
return link.get("text") or link.get("url") or ""
|
|
257
|
+
if "location" in cell_value:
|
|
258
|
+
location = cell_value["location"] or {}
|
|
259
|
+
return location.get("name") or ""
|
|
260
|
+
if "time" in cell_value:
|
|
261
|
+
return cell_value["time"]
|
|
262
|
+
if "select" in cell_value:
|
|
263
|
+
select = cell_value["select"] or {}
|
|
264
|
+
return select.get("text") or ""
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _unwrap_response(resp: httpx.Response) -> dict[str, Any]:
|
|
269
|
+
if resp.status_code == 429:
|
|
270
|
+
raise RuntimeError("Tencent Docs API failed: HTTP 429 Requests Over Limit. Please Retry Later.")
|
|
271
|
+
resp.raise_for_status()
|
|
272
|
+
payload = resp.json()
|
|
273
|
+
if not isinstance(payload, dict):
|
|
274
|
+
raise RuntimeError(f"Unexpected Tencent Docs response: {payload!r}")
|
|
275
|
+
|
|
276
|
+
if "ret" in payload and payload.get("ret") not in (0, None):
|
|
277
|
+
raise RuntimeError(
|
|
278
|
+
TencentDocsConnector._friendly_api_error(
|
|
279
|
+
code=payload.get("ret"),
|
|
280
|
+
message=payload.get("msg"),
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
if "code" in payload and payload.get("code") not in (0, None):
|
|
284
|
+
raise RuntimeError(
|
|
285
|
+
TencentDocsConnector._friendly_api_error(
|
|
286
|
+
code=payload.get("code"),
|
|
287
|
+
message=payload.get("message"),
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
data = payload.get("data")
|
|
292
|
+
if isinstance(data, dict):
|
|
293
|
+
return data
|
|
294
|
+
return payload
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _friendly_api_error(code: Any, message: Any) -> str:
|
|
298
|
+
code_text = str(code)
|
|
299
|
+
message_text = str(message or "")
|
|
300
|
+
base = f"Tencent Docs API failed: code={code_text} message={message_text}"
|
|
301
|
+
|
|
302
|
+
if code_text == "400007":
|
|
303
|
+
return base + "。腾讯文档接口限流,请稍后重试。"
|
|
304
|
+
if code_text == "400001":
|
|
305
|
+
return base + "。请求参数不合法,请检查表格 ID、sheet ID、列范围或表格结构。"
|
|
306
|
+
if code_text in {"401", "401001"}:
|
|
307
|
+
return base + "。认证失败,请检查 Access Token、Client ID、Open ID。"
|
|
308
|
+
if code_text in {"403", "403001"}:
|
|
309
|
+
return base + "。权限不足,请确认应用授权和文档访问权限。"
|
|
310
|
+
return base
|
|
240
311
|
|
|
241
|
-
# ── Internal helpers ───────────────────────────────────────────────────
|
|
242
312
|
@staticmethod
|
|
243
|
-
def _build_write_payload(updates: list[CellUpdate]) -> dict[str, Any]:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
313
|
+
def _build_write_payload(sheet_id: str, updates: list[CellUpdate]) -> dict[str, Any]:
|
|
314
|
+
return {
|
|
315
|
+
"requests": [
|
|
316
|
+
{
|
|
317
|
+
"updateRangeRequest": {
|
|
318
|
+
"sheetId": sheet_id,
|
|
319
|
+
"gridData": {
|
|
320
|
+
"startRow": update.row,
|
|
321
|
+
"startColumn": update.col,
|
|
322
|
+
"rows": [
|
|
323
|
+
{
|
|
324
|
+
"values": [
|
|
325
|
+
TencentDocsConnector._build_cell_data(update.value)
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for update in updates
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _build_cell_data(value: Any, text_color: Optional[dict[str, int]] = None) -> dict[str, Any]:
|
|
338
|
+
cell_data: dict[str, Any] = {"cellValue": TencentDocsConnector._build_cell_value(value)}
|
|
339
|
+
if text_color is not None:
|
|
340
|
+
cell_data["cellFormat"] = {"textFormat": {"color": text_color}}
|
|
341
|
+
return cell_data
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _build_cell_value(value: Any) -> dict[str, Any]:
|
|
345
|
+
if isinstance(value, bool):
|
|
346
|
+
return {"text": str(value).lower()}
|
|
347
|
+
if isinstance(value, (int, float)):
|
|
348
|
+
return {"number": value}
|
|
349
|
+
if value is None:
|
|
350
|
+
return {"text": ""}
|
|
351
|
+
return {"text": str(value)}
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def _hex_to_rgba(color: str) -> dict[str, int]:
|
|
355
|
+
value = color.strip().lstrip("#")
|
|
356
|
+
if len(value) != 6:
|
|
357
|
+
raise ValueError(f"Unsupported color value: {color}")
|
|
358
|
+
return {
|
|
359
|
+
"red": int(value[0:2], 16),
|
|
360
|
+
"green": int(value[2:4], 16),
|
|
361
|
+
"blue": int(value[4:6], 16),
|
|
362
|
+
"alpha": 255,
|
|
363
|
+
}
|
package/models/state_models.py
CHANGED
|
@@ -21,9 +21,12 @@ class SyncState(BaseModel):
|
|
|
21
21
|
|
|
22
22
|
last_run_at: Optional[datetime] = None
|
|
23
23
|
|
|
24
|
-
# A
|
|
24
|
+
# A 表毛利任务: row_index -> fingerprint
|
|
25
25
|
a_table_fingerprints: dict[str, str] = Field(default_factory=dict)
|
|
26
26
|
|
|
27
|
+
# A 表退款任务: 整体扫描 hash(基于单号 + 当前退款状态)
|
|
28
|
+
a_table_refund_scan_hash: str = ""
|
|
29
|
+
|
|
27
30
|
# B 表: 退款单号集合 hash
|
|
28
31
|
b_table_refund_hash: str = ""
|
|
29
32
|
|
package/models/task_models.py
CHANGED
|
@@ -45,3 +45,18 @@ class TaskResult(BaseModel):
|
|
|
45
45
|
self.success = success
|
|
46
46
|
if error_message:
|
|
47
47
|
self.error_message = error_message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RunSummary(BaseModel):
|
|
51
|
+
"""Summary of one manual or scheduled execution round."""
|
|
52
|
+
|
|
53
|
+
trigger: str = "manual"
|
|
54
|
+
success: bool = True
|
|
55
|
+
started_at: datetime = Field(default_factory=datetime.now)
|
|
56
|
+
finished_at: Optional[datetime] = None
|
|
57
|
+
task_count: int = 0
|
|
58
|
+
rows_read: int = 0
|
|
59
|
+
rows_changed: int = 0
|
|
60
|
+
rows_error: int = 0
|
|
61
|
+
tasks: list[TaskResult] = Field(default_factory=list)
|
|
62
|
+
message: Optional[str] = None
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tb-order-sync",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Tencent Docs
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Tencent Docs order sync service with gross-profit automation, refund marking, startup self-check, autostart daemon, and tb CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tb": "bin/tb.js"
|
|
7
7
|
},
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"main.py",
|
|
17
17
|
"requirements.txt",
|
|
18
18
|
"README.md",
|
|
19
|
+
"快速开始.txt",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"LICENSE",
|
|
21
22
|
".env.example",
|
|
@@ -42,6 +43,8 @@
|
|
|
42
43
|
"refund-workflow",
|
|
43
44
|
"gross-profit",
|
|
44
45
|
"daemon",
|
|
46
|
+
"autostart",
|
|
47
|
+
"self-check",
|
|
45
48
|
"scheduler",
|
|
46
49
|
"rich-cli",
|
|
47
50
|
"tencent-docs",
|