tb-order-sync 0.3.0 → 0.4.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.
- package/.env.example +3 -2
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +111 -104
- package/build.py +10 -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 +21 -5
- 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 +1 -0
- package/utils/retry.py +20 -2
- package//345/277/253/351/200/237/345/274/200/345/247/213.txt +31 -0
package/cli/dashboard.py
CHANGED
|
@@ -37,7 +37,7 @@ class DashboardApp:
|
|
|
37
37
|
choice = self._ask(
|
|
38
38
|
"选择操作",
|
|
39
39
|
default="1",
|
|
40
|
-
choices={"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "0"},
|
|
40
|
+
choices={"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"},
|
|
41
41
|
)
|
|
42
42
|
if not self._handle_choice(choice):
|
|
43
43
|
break
|
|
@@ -54,6 +54,8 @@ class DashboardApp:
|
|
|
54
54
|
def _build_screen(self) -> Group:
|
|
55
55
|
daemon_status = self._daemon.status()
|
|
56
56
|
state = self._state_svc.load(quiet=True)
|
|
57
|
+
last_run = self._state_svc.load_last_run(quiet=True)
|
|
58
|
+
autostart_status = self._daemon.autostart_status()
|
|
57
59
|
|
|
58
60
|
title = Text("多表格同步与退款标记服务", style="bold white")
|
|
59
61
|
subtitle = Text("Scheduler Console", style="bold #8ecae6")
|
|
@@ -71,13 +73,13 @@ class DashboardApp:
|
|
|
71
73
|
box=box.ROUNDED,
|
|
72
74
|
)
|
|
73
75
|
daemon_panel = Panel(
|
|
74
|
-
self._build_daemon_table(daemon_status),
|
|
76
|
+
self._build_daemon_table(daemon_status, autostart_status),
|
|
75
77
|
title="[bold #023047]守护进程[/bold #023047]",
|
|
76
78
|
border_style="#90be6d" if daemon_status.running else "#f4a261",
|
|
77
79
|
box=box.ROUNDED,
|
|
78
80
|
)
|
|
79
81
|
state_panel = Panel(
|
|
80
|
-
self._build_state_table(state),
|
|
82
|
+
self._build_state_table(state, last_run),
|
|
81
83
|
title="[bold #023047]同步状态[/bold #023047]",
|
|
82
84
|
border_style="#ffb703",
|
|
83
85
|
box=box.ROUNDED,
|
|
@@ -126,22 +128,27 @@ class DashboardApp:
|
|
|
126
128
|
table.add_row("Dry Run", "开启" if self._settings.dry_run else "关闭")
|
|
127
129
|
return table
|
|
128
130
|
|
|
129
|
-
def _build_daemon_table(self, status) -> Table:
|
|
131
|
+
def _build_daemon_table(self, status, autostart_status) -> Table:
|
|
130
132
|
table = Table(box=None, show_header=False, pad_edge=False)
|
|
131
133
|
table.add_column(style="bold white")
|
|
132
134
|
table.add_column(style="#023047")
|
|
133
135
|
table.add_row("状态", "[green]运行中[/green]" if status.running else "[yellow]未运行[/yellow]")
|
|
136
|
+
table.add_row("登录自启", "[green]已启用[/green]" if autostart_status.enabled else "[yellow]未启用[/yellow]")
|
|
134
137
|
table.add_row("PID", str(status.pid or "-"))
|
|
135
138
|
table.add_row("启动时间", status.started_at or "-")
|
|
136
139
|
table.add_row("日志文件", status.log_file.name)
|
|
137
140
|
table.add_row("PID 文件", status.pid_file.name)
|
|
138
141
|
return table
|
|
139
142
|
|
|
140
|
-
def _build_state_table(self, state) -> Table:
|
|
143
|
+
def _build_state_table(self, state, last_run) -> Table:
|
|
141
144
|
table = Table(box=None, show_header=False, pad_edge=False)
|
|
142
145
|
table.add_column(style="bold white")
|
|
143
146
|
table.add_column(style="#023047")
|
|
144
147
|
table.add_row("上次运行", self._fmt_time(state.last_run_at))
|
|
148
|
+
if last_run is not None:
|
|
149
|
+
table.add_row("最近结果", "[green]成功[/green]" if last_run.success else "[red]失败[/red]")
|
|
150
|
+
table.add_row("最近变更", str(last_run.rows_changed))
|
|
151
|
+
table.add_row("最近异常", str(last_run.rows_error))
|
|
145
152
|
table.add_row("A 表指纹", str(len(state.a_table_fingerprints)))
|
|
146
153
|
table.add_row("退款快照", str(len(state.b_table_refund_set)))
|
|
147
154
|
table.add_row("退款哈希", state.b_table_refund_hash[:12] if state.b_table_refund_hash else "-")
|
|
@@ -176,6 +183,9 @@ class DashboardApp:
|
|
|
176
183
|
table.add_row("9", "配置向导", "打开交互式 setup")
|
|
177
184
|
table.add_row("10", "配置检查", "检查 .env 完整性")
|
|
178
185
|
table.add_row("11", "前台调度", "当前终端直接运行 scheduler")
|
|
186
|
+
table.add_row("12", "启用登录自启", "登录系统后自动拉起后台调度")
|
|
187
|
+
table.add_row("13", "停用登录自启", "移除当前用户的自启配置")
|
|
188
|
+
table.add_row("14", "查看自启状态", "检查当前用户的登录自启状态")
|
|
179
189
|
table.add_row("0", "退出", "返回系统")
|
|
180
190
|
return table
|
|
181
191
|
|
|
@@ -204,6 +214,12 @@ class DashboardApp:
|
|
|
204
214
|
self._run_setup(check=True)
|
|
205
215
|
elif choice == "11":
|
|
206
216
|
self._run_foreground_scheduler()
|
|
217
|
+
elif choice == "12":
|
|
218
|
+
self._daemon_action("autostart-enable")
|
|
219
|
+
elif choice == "13":
|
|
220
|
+
self._daemon_action("autostart-disable")
|
|
221
|
+
elif choice == "14":
|
|
222
|
+
self._daemon_action("autostart-status")
|
|
207
223
|
return True
|
|
208
224
|
|
|
209
225
|
def _run_task(self, task: str, *, dry_run: bool = False) -> None:
|
|
@@ -227,18 +243,33 @@ class DashboardApp:
|
|
|
227
243
|
str(item.rows_changed),
|
|
228
244
|
str(item.rows_error),
|
|
229
245
|
)
|
|
230
|
-
|
|
246
|
+
body: Group | Table = table
|
|
247
|
+
failures = [item for item in results if item.error_message]
|
|
248
|
+
if failures:
|
|
249
|
+
failure_table = Table(box=box.SIMPLE_HEAVY, expand=True)
|
|
250
|
+
failure_table.add_column("任务", style="bold red")
|
|
251
|
+
failure_table.add_column("失败原因", style="white")
|
|
252
|
+
for item in failures:
|
|
253
|
+
failure_table.add_row(item.task_name.value, item.error_message or "")
|
|
254
|
+
body = Group(table, Panel(failure_table, title="失败详情", border_style="red", box=box.ROUNDED))
|
|
255
|
+
self._pause_with_panel(Panel(body, title="执行结果", border_style="#219ebc", box=box.ROUNDED))
|
|
231
256
|
|
|
232
257
|
def _daemon_action(self, action: str) -> None:
|
|
233
|
-
if action
|
|
258
|
+
if action in {"start", "autostart-enable"} and not self._ensure_config():
|
|
234
259
|
return
|
|
235
260
|
|
|
236
261
|
if action == "start":
|
|
237
262
|
status = self._daemon.start()
|
|
238
263
|
elif action == "stop":
|
|
239
264
|
status = self._daemon.stop(force=True)
|
|
240
|
-
|
|
265
|
+
elif action == "restart":
|
|
241
266
|
status = self._daemon.restart()
|
|
267
|
+
elif action == "autostart-enable":
|
|
268
|
+
status = self._daemon.enable_autostart()
|
|
269
|
+
elif action == "autostart-disable":
|
|
270
|
+
status = self._daemon.disable_autostart()
|
|
271
|
+
else:
|
|
272
|
+
status = self._daemon.autostart_status()
|
|
242
273
|
|
|
243
274
|
self._pause_with_panel(Panel(status.message, title="守护结果", border_style="#90be6d", box=box.ROUNDED))
|
|
244
275
|
|
|
@@ -282,7 +313,7 @@ class DashboardApp:
|
|
|
282
313
|
def _is_config_ready(self) -> bool:
|
|
283
314
|
fields = [
|
|
284
315
|
self._settings.tencent_client_id,
|
|
285
|
-
self._settings.
|
|
316
|
+
self._settings.tencent_open_id,
|
|
286
317
|
self._settings.tencent_access_token,
|
|
287
318
|
self._settings.tencent_a_file_id,
|
|
288
319
|
self._settings.tencent_a_sheet_id,
|
package/cli/setup.py
CHANGED
|
@@ -15,6 +15,7 @@ import webbrowser
|
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Callable, Optional, Sequence
|
|
18
|
+
from urllib.parse import parse_qs, urlparse
|
|
18
19
|
|
|
19
20
|
try:
|
|
20
21
|
from rich.console import Console
|
|
@@ -47,6 +48,7 @@ ENV_EXAMPLE_PATH = PROJECT_ROOT / ".env.example"
|
|
|
47
48
|
|
|
48
49
|
# Column letter validator
|
|
49
50
|
_COL_RE = re.compile(r"^[A-Z]{1,3}$")
|
|
51
|
+
_TENCENT_FILE_RE = re.compile(r"/(?:sheet|doc|slide|mind|form|pdf)/([^/?#]+)")
|
|
50
52
|
|
|
51
53
|
TENCENT_DOCS_GUIDE_URL = "https://docs.qq.com/open/document/app/"
|
|
52
54
|
TENCENT_DEVELOPER_CONSOLE_URL = "https://docs.qq.com/open/developers/"
|
|
@@ -98,6 +100,28 @@ def _mask_secret(value: str) -> str:
|
|
|
98
100
|
return value[:4] + "****"
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
def parse_tencent_sheet_reference(raw: str) -> tuple[str, str]:
|
|
104
|
+
"""Parse a Tencent Docs sheet URL or raw file id.
|
|
105
|
+
|
|
106
|
+
Returns `(file_id, sheet_id)` where `sheet_id` may be empty.
|
|
107
|
+
"""
|
|
108
|
+
value = raw.strip()
|
|
109
|
+
if not value:
|
|
110
|
+
return "", ""
|
|
111
|
+
|
|
112
|
+
if not value.startswith(("http://", "https://")):
|
|
113
|
+
return value, ""
|
|
114
|
+
|
|
115
|
+
parsed = urlparse(value)
|
|
116
|
+
match = _TENCENT_FILE_RE.search(parsed.path)
|
|
117
|
+
if not match:
|
|
118
|
+
return "", ""
|
|
119
|
+
|
|
120
|
+
file_id = match.group(1).strip()
|
|
121
|
+
sheet_id = parse_qs(parsed.query).get("tab", [""])[0].strip()
|
|
122
|
+
return file_id, sheet_id
|
|
123
|
+
|
|
124
|
+
|
|
101
125
|
# ── Setup Wizard ───────────────────────────────────────────────────────────
|
|
102
126
|
class SetupWizard:
|
|
103
127
|
def __init__(self, console: Optional[Console] = None) -> None:
|
|
@@ -265,8 +289,7 @@ class SetupWizard:
|
|
|
265
289
|
error_msg="Client ID 不能为空",
|
|
266
290
|
)
|
|
267
291
|
self.values["TENCENT_CLIENT_SECRET"] = self._prompt(
|
|
268
|
-
"Client Secret", "TENCENT_CLIENT_SECRET", secret=True,
|
|
269
|
-
error_msg="Client Secret 不能为空",
|
|
292
|
+
"Client Secret(当前运行可留空)", "TENCENT_CLIENT_SECRET", secret=True,
|
|
270
293
|
)
|
|
271
294
|
self.values["TENCENT_OPEN_ID"] = self._prompt(
|
|
272
295
|
"Open ID(可选,部分接口需要)", "TENCENT_OPEN_ID", secret=True,
|
|
@@ -278,29 +301,33 @@ class SetupWizard:
|
|
|
278
301
|
|
|
279
302
|
def _step_sheet_ids(self) -> None:
|
|
280
303
|
self.console.print(f"\n[bold cyan]📊 {STEP_SHEETS}[/bold cyan]")
|
|
281
|
-
self.console.print("
|
|
304
|
+
self.console.print(" 可直接粘贴腾讯文档完整链接,系统会自动拆出 File ID / Sheet ID\n")
|
|
282
305
|
self._show_sheet_id_guide()
|
|
283
306
|
self.console.print("")
|
|
284
307
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
def prompt_sheet_target(name: str, file_key: str, sheet_key: str) -> None:
|
|
309
|
+
while True:
|
|
310
|
+
self.console.print(f" [bold]{name}[/bold]")
|
|
311
|
+
ref = self._prompt(
|
|
312
|
+
f"{name}链接或 File ID", file_key, validator=_not_empty,
|
|
313
|
+
error_msg="请输入腾讯文档链接或 File ID",
|
|
314
|
+
)
|
|
315
|
+
file_id, sheet_id = parse_tencent_sheet_reference(ref)
|
|
316
|
+
if not file_id:
|
|
317
|
+
self.console.print(" [red]无法从链接中解析 File ID,请重新输入完整链接或直接填 File ID[/red]\n")
|
|
318
|
+
continue
|
|
319
|
+
self.values[file_key] = file_id
|
|
320
|
+
if sheet_id:
|
|
321
|
+
self.console.print(f" [green]已自动解析 {name} Sheet ID: {sheet_id}[/green]")
|
|
322
|
+
self.values[sheet_key] = self._prompt(
|
|
323
|
+
f"{name} Sheet ID", sheet_key, default=sheet_id, validator=_not_empty,
|
|
324
|
+
error_msg="Sheet ID 不能为空",
|
|
325
|
+
)
|
|
326
|
+
break
|
|
327
|
+
|
|
328
|
+
prompt_sheet_target("A 表(订单表)", "TENCENT_A_FILE_ID", "TENCENT_A_SHEET_ID")
|
|
329
|
+
self.console.print("")
|
|
330
|
+
prompt_sheet_target("B 表(退款表)", "TENCENT_B_FILE_ID", "TENCENT_B_SHEET_ID")
|
|
304
331
|
|
|
305
332
|
def _step_feishu_creds(self) -> None:
|
|
306
333
|
self.console.print(f"\n[bold cyan]🐦 {STEP_FEISHU}[/bold cyan]")
|
|
@@ -372,7 +399,7 @@ class SetupWizard:
|
|
|
372
399
|
validator=_is_bool_str, error_msg="请输入 true 或 false",
|
|
373
400
|
)
|
|
374
401
|
self.values["ENABLE_STYLE_UPDATE"] = self._prompt(
|
|
375
|
-
"是否启用行样式更新 (true / false)", "ENABLE_STYLE_UPDATE", default="
|
|
402
|
+
"是否启用行样式更新 (true / false)", "ENABLE_STYLE_UPDATE", default="true",
|
|
376
403
|
validator=_is_bool_str, error_msg="请输入 true 或 false",
|
|
377
404
|
)
|
|
378
405
|
|
|
@@ -414,7 +441,7 @@ class SetupWizard:
|
|
|
414
441
|
|
|
415
442
|
# Business text
|
|
416
443
|
self.values["REFUND_STATUS_TEXT"] = self._prompt(
|
|
417
|
-
"退款状态文案", "REFUND_STATUS_TEXT", default="
|
|
444
|
+
"退款状态文案", "REFUND_STATUS_TEXT", default="已退款",
|
|
418
445
|
)
|
|
419
446
|
self.values["DATA_ERROR_TEXT"] = self._prompt(
|
|
420
447
|
"数据异常文案", "DATA_ERROR_TEXT", default="数据异常",
|
|
@@ -520,9 +547,20 @@ class SetupWizard:
|
|
|
520
547
|
# ── Connection test ────────────────────────────────────────────────────
|
|
521
548
|
|
|
522
549
|
def _test_connection(self) -> bool:
|
|
523
|
-
"""Try reading
|
|
550
|
+
"""Try reading both A/B sheets to verify credentials and document access."""
|
|
524
551
|
self.console.print(f"\n[bold cyan]🔌 {STEP_TEST}[/bold cyan]")
|
|
525
|
-
self.console.print("
|
|
552
|
+
self.console.print(" 正在执行启动自检:状态目录 + 腾讯文档 A/B 表读取...\n")
|
|
553
|
+
|
|
554
|
+
state_dir = Path(self.values.get("STATE_DIR", "state"))
|
|
555
|
+
try:
|
|
556
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
557
|
+
probe = state_dir / ".write_test"
|
|
558
|
+
probe.write_text("ok", encoding="utf-8")
|
|
559
|
+
probe.unlink(missing_ok=True)
|
|
560
|
+
self.console.print(f" [bold green]✅ 状态目录可写: {state_dir}[/bold green]")
|
|
561
|
+
except Exception as exc:
|
|
562
|
+
self.console.print(f" [bold red]❌ 状态目录不可写: {state_dir} ({exc})[/bold red]")
|
|
563
|
+
return False
|
|
526
564
|
|
|
527
565
|
try:
|
|
528
566
|
from connectors.tencent_docs import TencentDocsConnector
|
|
@@ -533,19 +571,32 @@ class SetupWizard:
|
|
|
533
571
|
access_token=self.values.get("TENCENT_ACCESS_TOKEN", ""),
|
|
534
572
|
open_id=self.values.get("TENCENT_OPEN_ID", ""),
|
|
535
573
|
)
|
|
536
|
-
|
|
574
|
+
a_rows = conn.read_rows(
|
|
537
575
|
self.values.get("TENCENT_A_FILE_ID", ""),
|
|
538
576
|
self.values.get("TENCENT_A_SHEET_ID", ""),
|
|
539
577
|
start_row=0,
|
|
540
578
|
end_row=2,
|
|
541
579
|
)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
self.
|
|
580
|
+
b_rows = conn.read_rows(
|
|
581
|
+
self.values.get("TENCENT_B_FILE_ID", ""),
|
|
582
|
+
self.values.get("TENCENT_B_SHEET_ID", ""),
|
|
583
|
+
start_row=0,
|
|
584
|
+
end_row=2,
|
|
585
|
+
)
|
|
586
|
+
self.console.print(f" [bold green]✅ A 表可读:{len(a_rows)} 行[/bold green]")
|
|
587
|
+
self.console.print(f" [bold green]✅ B 表可读:{len(b_rows)} 行[/bold green]")
|
|
588
|
+
if a_rows:
|
|
589
|
+
self.console.print(f" A 表表头: {a_rows[0][:5]}...")
|
|
590
|
+
if b_rows:
|
|
591
|
+
self.console.print(f" B 表表头: {b_rows[0][:5]}...")
|
|
592
|
+
self.console.print(" [bold green]✅ 启动自检通过,可直接运行任务[/bold green]")
|
|
545
593
|
return True
|
|
546
594
|
except Exception as exc:
|
|
547
595
|
self.console.print(f" [bold red]❌ 连接失败: {exc}[/bold red]")
|
|
548
|
-
|
|
596
|
+
if "400007" in str(exc) or "Requests Over Limit" in str(exc):
|
|
597
|
+
self.console.print(" [dim]当前更像是腾讯文档接口限流,请稍等后重新运行 tb check。[/dim]")
|
|
598
|
+
else:
|
|
599
|
+
self.console.print(" [dim]请检查 Access Token、Open ID、表格 ID、文档权限和网络,稍后可重新运行 setup/check[/dim]")
|
|
549
600
|
return False
|
|
550
601
|
|
|
551
602
|
# ── Check mode ─────────────────────────────────────────────────────────
|
|
@@ -567,8 +618,8 @@ class SetupWizard:
|
|
|
567
618
|
|
|
568
619
|
required = {
|
|
569
620
|
"TENCENT_CLIENT_ID": "腾讯文档 Client ID",
|
|
570
|
-
"TENCENT_CLIENT_SECRET": "腾讯文档 Client Secret",
|
|
571
621
|
"TENCENT_ACCESS_TOKEN": "腾讯文档 Access Token",
|
|
622
|
+
"TENCENT_OPEN_ID": "腾讯文档 Open ID",
|
|
572
623
|
"TENCENT_A_FILE_ID": "A 表 File ID",
|
|
573
624
|
"TENCENT_A_SHEET_ID": "A 表 Sheet ID",
|
|
574
625
|
"TENCENT_B_FILE_ID": "B 表 File ID",
|
|
@@ -576,7 +627,7 @@ class SetupWizard:
|
|
|
576
627
|
}
|
|
577
628
|
|
|
578
629
|
optional = {
|
|
579
|
-
"
|
|
630
|
+
"TENCENT_CLIENT_SECRET": "腾讯文档 Client Secret",
|
|
580
631
|
"FEISHU_APP_ID": "飞书 App ID",
|
|
581
632
|
"FEISHU_APP_SECRET": "飞书 App Secret",
|
|
582
633
|
"FEISHU_C_FILE_TOKEN": "飞书 C 表 Token",
|
|
@@ -628,7 +679,8 @@ class SetupWizard:
|
|
|
628
679
|
|
|
629
680
|
if all_ok:
|
|
630
681
|
self.console.print("\n[bold green]✅ 必填配置项均已设置[/bold green]")
|
|
631
|
-
|
|
682
|
+
self.console.print("[dim]接下来会做一次启动自检:状态目录写入 + 腾讯文档 A/B 表读取[/dim]")
|
|
683
|
+
if self._prompt_bool("是否执行启动自检?", default=True):
|
|
632
684
|
self.values = dict(values) # type: ignore
|
|
633
685
|
self._test_connection()
|
|
634
686
|
else:
|
|
@@ -655,7 +707,7 @@ class SetupWizard:
|
|
|
655
707
|
self._step_column_mapping()
|
|
656
708
|
|
|
657
709
|
# Ensure business text defaults
|
|
658
|
-
self.values.setdefault("REFUND_STATUS_TEXT", "
|
|
710
|
+
self.values.setdefault("REFUND_STATUS_TEXT", "已退款")
|
|
659
711
|
self.values.setdefault("DATA_ERROR_TEXT", "数据异常")
|
|
660
712
|
|
|
661
713
|
self._show_summary()
|
package/config/settings.py
CHANGED
|
@@ -73,7 +73,7 @@ class Settings(BaseSettings):
|
|
|
73
73
|
write_batch_size: int = 100
|
|
74
74
|
retry_times: int = 3
|
|
75
75
|
dry_run: bool = False
|
|
76
|
-
enable_style_update: bool =
|
|
76
|
+
enable_style_update: bool = True
|
|
77
77
|
|
|
78
78
|
# ── 列映射(可覆盖) ─────────────────────────────────
|
|
79
79
|
a_col_product_price: str = "C"
|
|
@@ -86,7 +86,7 @@ class Settings(BaseSettings):
|
|
|
86
86
|
b_col_order_no: str = "A"
|
|
87
87
|
|
|
88
88
|
# ── 业务文案 ──────────────────────────────────────────
|
|
89
|
-
refund_status_text: str = "
|
|
89
|
+
refund_status_text: str = "已退款"
|
|
90
90
|
data_error_text: str = "数据异常"
|
|
91
91
|
|
|
92
92
|
|