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/.env.example
CHANGED
|
@@ -10,6 +10,7 @@ STATE_DIR=state
|
|
|
10
10
|
|
|
11
11
|
# ── 腾讯文档配置 ──────────────────────────────────────────
|
|
12
12
|
TENCENT_CLIENT_ID=
|
|
13
|
+
# 当前运行链路可不填;如后续接 OAuth 自动刷新可再补齐
|
|
13
14
|
TENCENT_CLIENT_SECRET=
|
|
14
15
|
TENCENT_OPEN_ID=
|
|
15
16
|
TENCENT_ACCESS_TOKEN=
|
|
@@ -33,7 +34,7 @@ STARTUP_JITTER_SECONDS=15
|
|
|
33
34
|
WRITE_BATCH_SIZE=100
|
|
34
35
|
RETRY_TIMES=3
|
|
35
36
|
DRY_RUN=false
|
|
36
|
-
ENABLE_STYLE_UPDATE=
|
|
37
|
+
ENABLE_STYLE_UPDATE=true
|
|
37
38
|
|
|
38
39
|
# ── 列映射配置 ────────────────────────────────────────────
|
|
39
40
|
A_COL_PRODUCT_PRICE=C
|
|
@@ -46,5 +47,5 @@ A_COL_REFUND_STATUS=I
|
|
|
46
47
|
B_COL_ORDER_NO=A
|
|
47
48
|
|
|
48
49
|
# ── 业务文案 ──────────────────────────────────────────────
|
|
49
|
-
REFUND_STATUS_TEXT
|
|
50
|
+
REFUND_STATUS_TEXT=已退款
|
|
50
51
|
DATA_ERROR_TEXT=数据异常
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added startup self-check in `tb check`, including state directory write validation and live Tencent Docs A/B sheet read checks.
|
|
10
|
+
- Added Tencent Docs URL parsing in setup so users can paste full sheet links and auto-fill `File ID` / `Sheet ID`.
|
|
11
|
+
- Added login auto-start support via `tb daemon autostart-enable|status|disable` for Windows Task Scheduler and macOS LaunchAgent.
|
|
12
|
+
- Added `state/last_run.json` execution summaries for manual and scheduled runs.
|
|
13
|
+
- Added packaged `快速开始.txt` to distribution outputs for non-technical users.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Changed no-argument startup to enter `setup` automatically when required runtime config is missing.
|
|
18
|
+
- Changed config validation so `TENCENT_CLIENT_SECRET` is now optional for the current live Tencent Docs runtime path.
|
|
19
|
+
- Changed dashboard and daemon status output to surface recent run status and login auto-start state.
|
|
20
|
+
- Changed launcher and README onboarding to emphasize `tb check` as the first runtime self-check after setup.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed Tencent Docs rate-limit handling by retrying `400007 Requests Over Limit` responses with exponential backoff.
|
|
25
|
+
- Fixed repeated batch writes being too aggressive by slowing inter-batch pacing.
|
|
26
|
+
- Fixed user-facing error reporting so failed tasks now include clearer cause hints in CLI output.
|
|
27
|
+
|
|
28
|
+
## [0.3.1] - 2026-03-19
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added README badges for npm, GitHub, and license visibility.
|
|
33
|
+
- Added npm metadata fields for repository, homepage, and issue tracking.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Improved npm package description and keywords to better reflect order sync, refund workflow, and spreadsheet automation.
|
|
38
|
+
- Prepared GitHub release metadata for the public `tb-order-sync` repository.
|
|
39
|
+
|
|
5
40
|
## [0.3.0] - 2026-03-19
|
|
6
41
|
|
|
7
42
|
### Added
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SOULRAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,129 +1,112 @@
|
|
|
1
1
|
# TB Order Sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/tb-order-sync)
|
|
4
|
+
[](https://github.com/SOULRAi/tb-order-sync)
|
|
5
|
+
[](https://github.com/SOULRAi/tb-order-sync/blob/main/LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**多表格同步与退款标记服务**
|
|
8
|
+
面向订单运营场景的轻量自动化服务,负责同步云表格、计算毛利、标记退款状态,并支持后台守护运行。
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
> 支持 `tb` 短命令、Rich 控制台、启动自检、守护进程、登录自启、Windows / macOS 一键启动,以及后续接入飞书 C 表的扩展路径。
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
**快速导航**
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
- [🚀 快速开始](#quick-start)
|
|
15
|
+
- [✨ 核心能力](#capabilities)
|
|
16
|
+
- [🔐 API 获取指引](#api-guide)
|
|
17
|
+
- [🧠 业务规则](#business-rules)
|
|
18
|
+
- [🧩 架构设计](#architecture)
|
|
19
|
+
- [🛡️ 守护进程](#daemon)
|
|
20
|
+
- [⚙️ 配置说明](#configuration)
|
|
21
|
+
- [🧪 测试](#testing)
|
|
22
|
+
|
|
23
|
+
<a id="capabilities"></a>
|
|
24
|
+
## ✨ 核心能力
|
|
25
|
+
|
|
26
|
+
| 能力 | 说明 | 状态 |
|
|
12
27
|
|------|------|------|
|
|
13
|
-
| 毛利自动计算 | 读取 A
|
|
14
|
-
| 退款自动匹配 | B 表退款单号匹配 A
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## 快速开始
|
|
27
|
-
|
|
28
|
-
###
|
|
28
|
+
| 💰 毛利自动计算 | 读取 A 表字段,按固定业务公式回写毛利列 | ✅ 已实现 |
|
|
29
|
+
| 🔁 退款自动匹配 | B 表退款单号匹配 A 表订单,写入退款状态列 | ✅ 已实现 |
|
|
30
|
+
| 🧾 数据异常标记 | 非法数字直接写入 `数据异常`,并记录日志 | ✅ 已实现 |
|
|
31
|
+
| ⚡ 增量同步 | 基于行指纹和退款集合 hash,仅处理变化数据 | ✅ 已实现 |
|
|
32
|
+
| 🧨 全量重建 | 支持忽略缓存,对全表重新扫描和回写 | ✅ 已实现 |
|
|
33
|
+
| 🖥️ Rich 控制台 | 无参启动即进入交互式控制台首页 | ✅ 已实现 |
|
|
34
|
+
| 🩺 启动自检 | 配置检查 + 状态目录写入 + 腾讯文档 A/B 表读取 | ✅ 已实现 |
|
|
35
|
+
| 🛡️ 守护进程 | 支持后台启动、停止、状态检查、日志查看 | ✅ 已实现 |
|
|
36
|
+
| 🔌 登录自启 | 支持 Windows 任务计划 / macOS LaunchAgent | ✅ 已实现 |
|
|
37
|
+
| 📦 一键分发 | 支持双击脚本、npm launcher、PyInstaller 打包 | ✅ 已实现 |
|
|
38
|
+
| 🐦 飞书预留接口 | C 表同步抽象层和 connector skeleton 已预留 | 🔲 骨架已预留 |
|
|
39
|
+
|
|
40
|
+
<a id="quick-start"></a>
|
|
41
|
+
## 🚀 快速开始
|
|
42
|
+
|
|
43
|
+
### 推荐命令
|
|
29
44
|
|
|
30
45
|
```bash
|
|
31
|
-
|
|
32
|
-
npm link
|
|
46
|
+
npm install -g tb-order-sync
|
|
33
47
|
|
|
34
|
-
# 之后直接使用
|
|
35
48
|
tb
|
|
36
|
-
tb menu
|
|
37
49
|
tb setup
|
|
50
|
+
tb check
|
|
51
|
+
tb all --dry-run
|
|
38
52
|
tb all
|
|
39
|
-
tb gp
|
|
40
|
-
tb rm
|
|
41
53
|
tb daemon start
|
|
42
54
|
tb daemon status
|
|
43
55
|
```
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
首次运行说明:
|
|
58
|
+
- 如果本机还没有完整配置,直接执行 `tb` 会自动进入 `setup`
|
|
59
|
+
- `tb check` 会执行启动自检,确认状态目录、A 表、B 表是否可用
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
npm pack
|
|
49
|
-
npm install -g ./tb-order-sync-0.3.0.tgz
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### 方式一:一键启动(推荐)
|
|
61
|
+
### 启动方式
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
| 方式 | 适用场景 | 入口 |
|
|
64
|
+
|------|----------|------|
|
|
65
|
+
| `tb` 命令 | 开发、运维、长期使用 | `tb` / `tb setup` / `tb all` |
|
|
66
|
+
| 双击启动 | 非技术用户 | `启动.bat` / `启动.command` |
|
|
67
|
+
| Python 兼容入口 | 调试或源码环境 | `python main.py` |
|
|
68
|
+
| 打包分发 | 免安装交付 | `dist/sync_service/` |
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|------|------|
|
|
58
|
-
| **Windows** | 双击 `启动.bat` |
|
|
59
|
-
| **macOS** | 双击 `启动.command` |
|
|
60
|
-
|
|
61
|
-
首次运行会自动完成:
|
|
62
|
-
1. 检测 / 下载 Python 环境(如果没有)
|
|
63
|
-
2. 安装依赖
|
|
64
|
-
3. 进入 Rich 控制台
|
|
65
|
-
4. 在控制台中继续配置、执行任务、管理守护进程
|
|
70
|
+
### 常用命令速查
|
|
66
71
|
|
|
67
72
|
```bash
|
|
68
|
-
|
|
69
|
-
# 或直接双击启动脚本
|
|
70
|
-
# 默认进入 Rich 交互式控制台
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 方式二:命令行
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
# 推荐入口
|
|
73
|
+
# 控制台
|
|
77
74
|
tb
|
|
78
75
|
tb menu
|
|
79
76
|
|
|
80
|
-
#
|
|
77
|
+
# 配置
|
|
81
78
|
tb setup
|
|
82
|
-
tb config
|
|
83
|
-
|
|
84
|
-
# 验证配置
|
|
85
79
|
tb check
|
|
86
|
-
tb setup --check
|
|
87
80
|
|
|
88
|
-
#
|
|
81
|
+
# 执行
|
|
89
82
|
tb all
|
|
90
83
|
tb gp
|
|
91
84
|
tb rm
|
|
92
85
|
tb all --dry-run
|
|
93
86
|
tb all --mode full
|
|
94
|
-
tb gp --mode full
|
|
95
87
|
|
|
96
|
-
#
|
|
88
|
+
# 调度 / 守护
|
|
97
89
|
tb start
|
|
98
|
-
tb schedule
|
|
99
|
-
|
|
100
|
-
# 后台守护
|
|
101
90
|
tb daemon start
|
|
102
91
|
tb daemon status
|
|
103
92
|
tb daemon logs --lines 80
|
|
104
93
|
tb daemon stop
|
|
105
|
-
tb daemon
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
python main.py
|
|
109
|
-
python main.py run all
|
|
110
|
-
python main.py run gross-profit
|
|
111
|
-
python main.py run refund-match
|
|
94
|
+
tb daemon autostart-enable
|
|
95
|
+
tb daemon autostart-status
|
|
96
|
+
tb daemon autostart-disable
|
|
112
97
|
```
|
|
113
98
|
|
|
114
|
-
###
|
|
99
|
+
### 双击启动
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
101
|
+
| 平台 | 文件 |
|
|
102
|
+
|------|------|
|
|
103
|
+
| Windows | `启动.bat` |
|
|
104
|
+
| macOS | `启动.command` |
|
|
120
105
|
|
|
121
|
-
|
|
122
|
-
# 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
|
|
123
|
-
# 用户无需安装 Python
|
|
124
|
-
```
|
|
106
|
+
首次运行会自动补齐 Python 运行环境;如果本机尚未配置,会直接进入配置向导。
|
|
125
107
|
|
|
126
|
-
|
|
108
|
+
<a id="api-guide"></a>
|
|
109
|
+
## 🔐 API 获取指引
|
|
127
110
|
|
|
128
111
|
### 腾讯文档 API
|
|
129
112
|
|
|
@@ -136,8 +119,9 @@ python build.py --clean
|
|
|
136
119
|
4. 再回到本项目执行 `tb setup`
|
|
137
120
|
- 当前说明:
|
|
138
121
|
- 本项目 MVP 目前依赖你手工提供有效 `Access Token`
|
|
139
|
-
- `Open ID`
|
|
140
|
-
-
|
|
122
|
+
- 当前运行链路要求 `Client ID + Open ID + Access Token`
|
|
123
|
+
- `Client Secret` 目前保留为可选项,后续接自动刷新 token 时再使用
|
|
124
|
+
- 在线表格 v3 读写链路已经完成真实联调验证
|
|
141
125
|
|
|
142
126
|
### 飞书 API
|
|
143
127
|
|
|
@@ -153,7 +137,8 @@ python build.py --clean
|
|
|
153
137
|
- 本项目里的飞书 connector 目前还是 skeleton
|
|
154
138
|
- 现阶段向导里先采集配置,为后续 C 表接入做准备
|
|
155
139
|
|
|
156
|
-
|
|
140
|
+
<a id="business-rules"></a>
|
|
141
|
+
## 🧠 业务规则
|
|
157
142
|
|
|
158
143
|
### A 表结构(订单表)
|
|
159
144
|
|
|
@@ -187,11 +172,11 @@ python build.py --clean
|
|
|
187
172
|
|
|
188
173
|
### 退款匹配
|
|
189
174
|
|
|
190
|
-
- B 表 A 列单号存在于 A 表 H 列 → I 列写入
|
|
175
|
+
- B 表 A 列单号存在于 A 表 H 列 → I 列写入 `已退款`
|
|
191
176
|
- 不存在 → 清空 I 列(同步取消)
|
|
192
|
-
-
|
|
177
|
+
- `ENABLE_STYLE_UPDATE=true` 时会把匹配行整行改成红色文字
|
|
193
178
|
|
|
194
|
-
## 项目结构
|
|
179
|
+
## 🗂️ 项目结构
|
|
195
180
|
|
|
196
181
|
```
|
|
197
182
|
tb-order-sync/
|
|
@@ -251,7 +236,8 @@ tb-order-sync/
|
|
|
251
236
|
└── test_refund_match_service.py # 6 tests
|
|
252
237
|
```
|
|
253
238
|
|
|
254
|
-
|
|
239
|
+
<a id="architecture"></a>
|
|
240
|
+
## 🧩 架构设计
|
|
255
241
|
|
|
256
242
|
```
|
|
257
243
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
@@ -276,14 +262,15 @@ tb-order-sync/
|
|
|
276
262
|
- **配置化** — 列映射、业务文案、运行模式全部可通过 `.env` 配置
|
|
277
263
|
- **可扩展** — 新增 D 表、E 表时无需重构主程序
|
|
278
264
|
|
|
279
|
-
## 增量策略
|
|
265
|
+
## 🔄 增量策略
|
|
280
266
|
|
|
281
267
|
| 目标 | 策略 | 存储 |
|
|
282
268
|
|------|------|------|
|
|
283
269
|
| A 表毛利 | 对每行关键字段 (C/D/E/F/H) 生成 MD5 指纹,仅处理指纹变化行 | `state/sync_state.json` |
|
|
284
|
-
| B 表退款 |
|
|
270
|
+
| B 表退款 | 同时比较退款集合 hash 和 A 表单号/退款状态扫描 hash,避免漏处理 A 表变化 | `state/sync_state.json` |
|
|
285
271
|
|
|
286
|
-
|
|
272
|
+
<a id="daemon"></a>
|
|
273
|
+
## 🛡️ 守护进程
|
|
287
274
|
|
|
288
275
|
- `tb daemon start`
|
|
289
276
|
- 在后台启动定时调度进程
|
|
@@ -296,18 +283,32 @@ tb-order-sync/
|
|
|
296
283
|
- 停止后台调度
|
|
297
284
|
- `tb daemon restart`
|
|
298
285
|
- 重启后台调度
|
|
286
|
+
- `tb daemon autostart-enable`
|
|
287
|
+
- 启用当前用户登录自启
|
|
288
|
+
- `tb daemon autostart-status`
|
|
289
|
+
- 查看登录自启状态
|
|
290
|
+
- `tb daemon autostart-disable`
|
|
291
|
+
- 关闭登录自启
|
|
299
292
|
|
|
300
293
|
后台控制台输出默认写入:
|
|
301
294
|
- `state/scheduler.console.log`
|
|
295
|
+
- `state/last_run.json`
|
|
296
|
+
|
|
297
|
+
启动建议:
|
|
298
|
+
- 先执行 `tb check`,确认配置和连接正常
|
|
299
|
+
- 再执行 `tb daemon start`
|
|
300
|
+
- 如需电脑登录后自动运行,再执行 `tb daemon autostart-enable`
|
|
302
301
|
|
|
303
|
-
|
|
302
|
+
<a id="configuration"></a>
|
|
303
|
+
## ⚙️ 配置说明
|
|
304
304
|
|
|
305
305
|
运行 `tb setup` 可交互式完成所有配置。也可手动编辑 `.env`:
|
|
306
306
|
|
|
307
307
|
```ini
|
|
308
308
|
# 腾讯文档凭证
|
|
309
309
|
TENCENT_CLIENT_ID=your_client_id
|
|
310
|
-
TENCENT_CLIENT_SECRET=
|
|
310
|
+
TENCENT_CLIENT_SECRET=
|
|
311
|
+
TENCENT_OPEN_ID=your_open_id
|
|
311
312
|
TENCENT_ACCESS_TOKEN=your_access_token
|
|
312
313
|
TENCENT_A_FILE_ID=a_table_file_id
|
|
313
314
|
TENCENT_A_SHEET_ID=a_table_sheet_id
|
|
@@ -319,7 +320,7 @@ GROSS_PROFIT_MODE=incremental # incremental | full
|
|
|
319
320
|
REFUND_MATCH_MODE=incremental # incremental | full
|
|
320
321
|
TASK_INTERVAL_MINUTES=10 # 定时调度间隔
|
|
321
322
|
DRY_RUN=false # true = 模拟执行不写入
|
|
322
|
-
ENABLE_STYLE_UPDATE=
|
|
323
|
+
ENABLE_STYLE_UPDATE=true # true = 退款行标红
|
|
323
324
|
|
|
324
325
|
# 列映射(可自定义)
|
|
325
326
|
A_COL_PRODUCT_PRICE=C
|
|
@@ -332,12 +333,18 @@ A_COL_REFUND_STATUS=I
|
|
|
332
333
|
B_COL_ORDER_NO=A
|
|
333
334
|
```
|
|
334
335
|
|
|
336
|
+
补充说明:
|
|
337
|
+
- `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
|
|
338
|
+
- `tb check` 会做启动自检,不只是看 `.env` 是否存在
|
|
339
|
+
- 当前退款高亮效果是“整行红色文字”,不是背景填充
|
|
340
|
+
|
|
335
341
|
完整配置项见 [.env.example](.env.example)。
|
|
336
342
|
|
|
337
|
-
|
|
343
|
+
<a id="testing"></a>
|
|
344
|
+
## 🧪 测试
|
|
338
345
|
|
|
339
346
|
```bash
|
|
340
|
-
# 运行全部测试(
|
|
347
|
+
# 运行全部测试(49 tests)
|
|
341
348
|
pytest tests/ -v
|
|
342
349
|
|
|
343
350
|
# 单独运行
|
|
@@ -346,7 +353,7 @@ pytest tests/test_gross_profit_service.py -v
|
|
|
346
353
|
pytest tests/test_refund_match_service.py -v
|
|
347
354
|
```
|
|
348
355
|
|
|
349
|
-
## 技术栈
|
|
356
|
+
## 🧱 技术栈
|
|
350
357
|
|
|
351
358
|
| 组件 | 用途 |
|
|
352
359
|
|------|------|
|
|
@@ -360,15 +367,15 @@ pytest tests/test_refund_match_service.py -v
|
|
|
360
367
|
| PyInstaller | 打包为可执行文件 |
|
|
361
368
|
| pytest | 单元测试 |
|
|
362
369
|
|
|
363
|
-
## 部署方式对比
|
|
370
|
+
## 📦 部署方式对比
|
|
364
371
|
|
|
365
372
|
| 方式 | 需要 Python | 适用场景 |
|
|
366
373
|
|------|------------|---------|
|
|
367
|
-
| 双击启动脚本(源码) | 自动下载 | 开发/内部使用 |
|
|
368
|
-
|
|
|
374
|
+
| 双击启动脚本(源码) | 自动下载 / 初始化 | 开发/内部使用 |
|
|
375
|
+
| 打包分发(Windows / macOS) | 不需要 | 给非技术用户 |
|
|
369
376
|
| 命令行直接运行 | 需要 | 开发者 |
|
|
370
377
|
|
|
371
|
-
## Roadmap
|
|
378
|
+
## 🗺️ Roadmap
|
|
372
379
|
|
|
373
380
|
- [x] 毛利自动计算
|
|
374
381
|
- [x] 退款自动匹配
|
|
@@ -381,12 +388,12 @@ pytest tests/test_refund_match_service.py -v
|
|
|
381
388
|
- [ ] 腾讯文档 OAuth2 token 自动刷新
|
|
382
389
|
- [ ] 腾讯文档行样式 API 验证
|
|
383
390
|
|
|
384
|
-
## 已知待确认项
|
|
391
|
+
## ⚠️ 已知待确认项
|
|
385
392
|
|
|
386
393
|
- 腾讯文档 Open API 的实际 endpoint 和 request/response 格式(代码中标注 `TODO / NEED_VERIFY`)
|
|
387
394
|
- 腾讯文档是否支持通过 API 设置单元格样式
|
|
388
395
|
- OAuth2 token 自动刷新流程
|
|
389
396
|
|
|
390
|
-
## License
|
|
397
|
+
## 📄 License
|
|
391
398
|
|
|
392
399
|
MIT
|
package/build.py
CHANGED
|
@@ -17,6 +17,12 @@ ROOT = Path(__file__).resolve().parent
|
|
|
17
17
|
SPEC_FILE = ROOT / "sync_service.spec"
|
|
18
18
|
DIST_DIR = ROOT / "dist"
|
|
19
19
|
BUILD_DIR = ROOT / "build"
|
|
20
|
+
DIST_RUNTIME_FILES = [
|
|
21
|
+
".env.example",
|
|
22
|
+
"启动.bat",
|
|
23
|
+
"启动.command",
|
|
24
|
+
"快速开始.txt",
|
|
25
|
+
]
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def clean() -> None:
|
|
@@ -49,10 +55,10 @@ def build() -> None:
|
|
|
49
55
|
|
|
50
56
|
output = DIST_DIR / "sync_service"
|
|
51
57
|
if output.exists():
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
for name in DIST_RUNTIME_FILES:
|
|
59
|
+
source = ROOT / name
|
|
60
|
+
if source.exists():
|
|
61
|
+
shutil.copy2(source, output / name)
|
|
56
62
|
|
|
57
63
|
# Create state directory
|
|
58
64
|
(output / "state").mkdir(exist_ok=True)
|
package/cli/commands.py
CHANGED
|
@@ -4,10 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import sys
|
|
7
|
+
from datetime import datetime
|
|
7
8
|
from typing import TYPE_CHECKING, Optional
|
|
8
9
|
|
|
9
10
|
from config.settings import Settings, SyncMode, get_settings
|
|
10
|
-
from models.task_models import TaskResult
|
|
11
|
+
from models.task_models import RunSummary, TaskResult
|
|
11
12
|
from services.daemon_service import DaemonService
|
|
12
13
|
from services.state_service import StateService
|
|
13
14
|
from utils.logger import get_logger, setup_logging
|
|
@@ -54,7 +55,7 @@ def has_required_runtime_config(settings: Settings) -> bool:
|
|
|
54
55
|
"""Return whether Tencent runtime essentials are configured."""
|
|
55
56
|
required = [
|
|
56
57
|
settings.tencent_client_id,
|
|
57
|
-
settings.
|
|
58
|
+
settings.tencent_open_id,
|
|
58
59
|
settings.tencent_access_token,
|
|
59
60
|
settings.tencent_a_file_id,
|
|
60
61
|
settings.tencent_a_sheet_id,
|
|
@@ -101,6 +102,12 @@ def execute_tasks(
|
|
|
101
102
|
_print_result(result)
|
|
102
103
|
results.append(result)
|
|
103
104
|
|
|
105
|
+
if results:
|
|
106
|
+
try:
|
|
107
|
+
state_svc.save_last_run(_build_run_summary(results, trigger="manual"))
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning("Failed to save last run summary: %s", exc)
|
|
110
|
+
|
|
104
111
|
return results
|
|
105
112
|
|
|
106
113
|
|
|
@@ -137,9 +144,11 @@ def cmd_daemon(args: argparse.Namespace, settings: Settings) -> None:
|
|
|
137
144
|
daemon = DaemonService(settings)
|
|
138
145
|
action = args.daemon_action
|
|
139
146
|
|
|
140
|
-
if action
|
|
147
|
+
if action in {"start", "restart", "autostart-enable"}:
|
|
141
148
|
if not _ensure_runtime_config(settings):
|
|
142
149
|
return
|
|
150
|
+
|
|
151
|
+
if action == "start":
|
|
143
152
|
status = daemon.start(force=getattr(args, "force", False))
|
|
144
153
|
logger.info(status.message)
|
|
145
154
|
elif action == "stop":
|
|
@@ -153,12 +162,58 @@ def cmd_daemon(args: argparse.Namespace, settings: Settings) -> None:
|
|
|
153
162
|
logger.info(status.message)
|
|
154
163
|
if status.running:
|
|
155
164
|
logger.info("daemon pid=%s log=%s", status.pid, status.log_file)
|
|
165
|
+
autostart = daemon.autostart_status()
|
|
166
|
+
logger.info("%s (%s)", autostart.message, autostart.target)
|
|
167
|
+
_print_last_run_summary(settings)
|
|
156
168
|
elif action == "logs":
|
|
157
169
|
content = daemon.read_log_tail(lines=args.lines)
|
|
158
170
|
if content:
|
|
159
171
|
print(content, end="")
|
|
160
172
|
else:
|
|
161
173
|
logger.info("No daemon log output yet: %s", daemon.log_file)
|
|
174
|
+
elif action == "autostart-enable":
|
|
175
|
+
status = daemon.enable_autostart()
|
|
176
|
+
logger.info(status.message)
|
|
177
|
+
elif action == "autostart-disable":
|
|
178
|
+
status = daemon.disable_autostart()
|
|
179
|
+
logger.info(status.message)
|
|
180
|
+
elif action == "autostart-status":
|
|
181
|
+
status = daemon.autostart_status()
|
|
182
|
+
logger.info("%s (%s)", status.message, status.target)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _build_run_summary(results: list[TaskResult], *, trigger: str) -> RunSummary:
|
|
186
|
+
finished_values = [item.finished_at for item in results if item.finished_at is not None]
|
|
187
|
+
success = all(item.success for item in results)
|
|
188
|
+
message = None
|
|
189
|
+
if not success:
|
|
190
|
+
errors = [f"{item.task_name.value}: {item.error_message}" for item in results if item.error_message]
|
|
191
|
+
message = " | ".join(errors) if errors else "存在任务失败,请检查日志。"
|
|
192
|
+
return RunSummary(
|
|
193
|
+
trigger=trigger,
|
|
194
|
+
success=success,
|
|
195
|
+
started_at=min((item.started_at for item in results), default=datetime.now()),
|
|
196
|
+
finished_at=max(finished_values) if finished_values else None,
|
|
197
|
+
task_count=len(results),
|
|
198
|
+
rows_read=sum(item.rows_read for item in results),
|
|
199
|
+
rows_changed=sum(item.rows_changed for item in results),
|
|
200
|
+
rows_error=sum(item.rows_error for item in results),
|
|
201
|
+
tasks=results,
|
|
202
|
+
message=message,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _print_last_run_summary(settings: Settings) -> None:
|
|
207
|
+
summary = _build_state_service(settings).load_last_run(quiet=True)
|
|
208
|
+
if summary is None:
|
|
209
|
+
return
|
|
210
|
+
status = "成功" if summary.success else "失败"
|
|
211
|
+
logger.info(
|
|
212
|
+
"最近一次执行:%s trigger=%s read=%d changed=%d errors=%d",
|
|
213
|
+
status, summary.trigger, summary.rows_read, summary.rows_changed, summary.rows_error,
|
|
214
|
+
)
|
|
215
|
+
if summary.message:
|
|
216
|
+
logger.info("最近失败信息:%s", summary.message)
|
|
162
217
|
|
|
163
218
|
|
|
164
219
|
def _print_result(result: TaskResult) -> None:
|
|
@@ -168,6 +223,8 @@ def _print_result(result: TaskResult) -> None:
|
|
|
168
223
|
status, result.task_name.value, result.rows_read,
|
|
169
224
|
result.rows_changed, result.rows_error, result.dry_run,
|
|
170
225
|
)
|
|
226
|
+
if result.error_message:
|
|
227
|
+
logger.error("%s failed: %s", result.task_name.value, result.error_message)
|
|
171
228
|
|
|
172
229
|
|
|
173
230
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -206,6 +263,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
206
263
|
daemon_sub.add_parser("status", help="查看后台守护状态")
|
|
207
264
|
daemon_logs = daemon_sub.add_parser("logs", help="查看后台日志")
|
|
208
265
|
daemon_logs.add_argument("--lines", type=int, default=40, help="显示最后 N 行日志")
|
|
266
|
+
daemon_sub.add_parser("autostart-enable", help="启用登录自启")
|
|
267
|
+
daemon_sub.add_parser("autostart-disable", help="停用登录自启")
|
|
268
|
+
daemon_sub.add_parser("autostart-status", help="查看登录自启状态")
|
|
209
269
|
|
|
210
270
|
setup_parser = sub.add_parser("setup", aliases=["config"], help="交互式配置向导")
|
|
211
271
|
setup_parser.add_argument("--check", action="store_true", help="验证当前配置状态")
|
|
@@ -219,12 +279,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
219
279
|
def main(argv: list[str] | None = None) -> None:
|
|
220
280
|
"""CLI main entry point."""
|
|
221
281
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
222
|
-
if not argv:
|
|
223
|
-
argv = ["menu"]
|
|
224
|
-
|
|
225
282
|
settings = get_settings()
|
|
226
283
|
setup_logging(level=settings.log_level, log_dir=settings.state_dir)
|
|
227
284
|
|
|
285
|
+
if not argv:
|
|
286
|
+
argv = ["menu"] if has_required_runtime_config(settings) else ["setup"]
|
|
287
|
+
|
|
228
288
|
parser = build_parser()
|
|
229
289
|
args = parser.parse_args(argv)
|
|
230
290
|
|