bocai-cli 0.1.0__py3-none-any.whl

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,260 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from bocai_cli.api import ApiFacade
10
+ from bocai_cli.command_utils import command_handler
11
+ from bocai_cli.config import load_config
12
+ from bocai_cli.date_range_validation import validate_date_range
13
+ from bocai_cli.errors import ConfigValidationError
14
+ from bocai_cli.output import print_json
15
+
16
+ app = typer.Typer(help="卡相关命令。", invoke_without_command=True)
17
+
18
+
19
+ @app.callback()
20
+ def card_root(ctx: typer.Context) -> None:
21
+ if ctx.invoked_subcommand is None:
22
+ typer.echo(ctx.get_help())
23
+ raise typer.Exit(code=0)
24
+
25
+
26
+ def _build_api() -> ApiFacade:
27
+ config = load_config()
28
+ return ApiFacade(
29
+ base_url=config.base_url,
30
+ token=config.token,
31
+ timeout_seconds=config.timeout_seconds,
32
+ )
33
+
34
+
35
+ def _parse_transaction_ids(raw_transaction_ids: str) -> list[str]:
36
+ values = [item.strip() for item in raw_transaction_ids.split(",")]
37
+ non_empty_values = [item for item in values if item]
38
+
39
+ unique_values: list[str] = []
40
+ seen: set[str] = set()
41
+ for value in non_empty_values:
42
+ if value not in seen:
43
+ seen.add(value)
44
+ unique_values.append(value)
45
+
46
+ if not unique_values:
47
+ raise ConfigValidationError("--transaction-ids 至少要包含一个有效ID。")
48
+
49
+ return unique_values
50
+
51
+
52
+ def _parse_performance_update_payload(raw_payload: str) -> list[dict[str, Any]]:
53
+ try:
54
+ payload = json.loads(raw_payload)
55
+ except json.JSONDecodeError as exc:
56
+ raise ConfigValidationError("--payload 必须是合法 JSON。") from exc
57
+
58
+ if not isinstance(payload, list):
59
+ raise ConfigValidationError("--payload 必须是数组。")
60
+
61
+ if not payload:
62
+ raise ConfigValidationError("--payload 至少要包含一条交易记录。")
63
+
64
+ for index, item in enumerate(payload, start=1):
65
+ if not isinstance(item, dict):
66
+ raise ConfigValidationError(f"--payload 第 {index} 项必须是对象。")
67
+ if "transactionId" not in item:
68
+ raise ConfigValidationError(f"--payload 第 {index} 项缺少 transactionId。")
69
+ if "performance" not in item:
70
+ raise ConfigValidationError(f"--payload 第 {index} 项缺少 performance。")
71
+
72
+ return payload
73
+
74
+
75
+ def _parse_performance_new_payload(raw_payload: str) -> dict[str, Any]:
76
+ try:
77
+ payload = json.loads(raw_payload)
78
+ except json.JSONDecodeError as exc:
79
+ raise ConfigValidationError("--payload 必须是合法 JSON。") from exc
80
+
81
+ if not isinstance(payload, dict):
82
+ raise ConfigValidationError("--payload 必须是对象。")
83
+
84
+ if "data" not in payload:
85
+ raise ConfigValidationError("--payload 缺少 data。")
86
+
87
+ data = payload["data"]
88
+ if not isinstance(data, list):
89
+ raise ConfigValidationError("--payload.data 必须是数组。")
90
+
91
+ if not data:
92
+ raise ConfigValidationError("--payload.data 至少要包含一条合同记录。")
93
+
94
+ required_fields = (
95
+ "seq",
96
+ "date",
97
+ "activateNow",
98
+ "phone",
99
+ "name",
100
+ "cardName",
101
+ "contractNum",
102
+ "count",
103
+ "payAmount",
104
+ "payMethod",
105
+ "performanceAmount",
106
+ "performance",
107
+ "saleType",
108
+ )
109
+ for index, item in enumerate(data, start=1):
110
+ if not isinstance(item, dict):
111
+ raise ConfigValidationError(f"--payload.data 第 {index} 项必须是对象。")
112
+ for field in required_fields:
113
+ if field not in item:
114
+ raise ConfigValidationError(f"--payload.data 第 {index} 项缺少 {field}。")
115
+
116
+ return payload
117
+
118
+
119
+ @app.command("sale-performance", help="获取售卡业绩数据")
120
+ @command_handler
121
+ def sale_performance(
122
+ start_date: str = typer.Option(
123
+ ...,
124
+ "--start-date",
125
+ help="开始日期,格式 YYYY-MM-DD。",
126
+ ),
127
+ end_date: str = typer.Option(
128
+ ...,
129
+ "--end-date",
130
+ help="结束日期,格式 YYYY-MM-DD。",
131
+ ),
132
+ page_num: int = typer.Option(
133
+ ...,
134
+ "--page-num",
135
+ min=0,
136
+ help="页码,必须大于等于 0。",
137
+ ),
138
+ page_size: int = typer.Option(
139
+ ...,
140
+ "--page-size",
141
+ min=1,
142
+ help="每页条数,必须大于等于 1。",
143
+ ),
144
+ type_: int = typer.Option(
145
+ ...,
146
+ "--type",
147
+ min=1,
148
+ max=2,
149
+ help="卡类型:1会员卡,2培训卡。",
150
+ ),
151
+ center_id: str | None = typer.Option(
152
+ None,
153
+ "--center-id",
154
+ help="门店ID。",
155
+ ),
156
+ ) -> None:
157
+ validate_date_range(start_date, end_date)
158
+
159
+ api = _build_api()
160
+ data = api.card.sale_performance(
161
+ start_date=start_date,
162
+ end_date=end_date,
163
+ page_num=page_num,
164
+ page_size=page_size,
165
+ type_=type_,
166
+ center_id=center_id,
167
+ )
168
+ print_json({"status": "ok", "data": data})
169
+
170
+
171
+ @app.command("sale-performance-clear", help="清除售卡业绩数据")
172
+ @command_handler
173
+ def sale_performance_clear(
174
+ transaction_ids: str = typer.Option(
175
+ ...,
176
+ "--transaction-ids",
177
+ help="交易ID列表,逗号分隔。",
178
+ ),
179
+ phone: str = typer.Option(
180
+ ...,
181
+ "--phone",
182
+ help="不分业绩员工手机号。",
183
+ ),
184
+ center_id: str | None = typer.Option(
185
+ None,
186
+ "--center-id",
187
+ help="门店ID。",
188
+ ),
189
+ ) -> None:
190
+ parsed_transaction_ids = _parse_transaction_ids(transaction_ids)
191
+
192
+ api = _build_api()
193
+ data = api.card.performance_clear(
194
+ transaction_ids=parsed_transaction_ids,
195
+ phone=phone,
196
+ center_id=center_id,
197
+ )
198
+ print_json({"status": "ok", "data": data})
199
+
200
+
201
+ @app.command("sale-performance-update", help="更新售卡业绩数据")
202
+ @command_handler
203
+ def sale_performance_update(
204
+ payload: str = typer.Option(
205
+ ...,
206
+ "--payload",
207
+ help="业绩更新列表,JSON 数组字符串。[{\\\"transactionId\\\":1, \\\"performance\\\":[{\\\"sellerName\\\":\\\"测试\", \\\"amount\\\":10.0}]}]",
208
+ ),
209
+ phone: str = typer.Option(
210
+ ...,
211
+ "--phone",
212
+ help="不分业绩员工手机号。",
213
+ ),
214
+ center_id: str | None = typer.Option(
215
+ None,
216
+ "--center-id",
217
+ help="门店ID。",
218
+ ),
219
+ ) -> None:
220
+ parsed_payload = _parse_performance_update_payload(payload)
221
+
222
+ api = _build_api()
223
+ data = api.card.performance_update(
224
+ phone=phone,
225
+ transactions=parsed_payload,
226
+ center_id=center_id,
227
+ )
228
+ print_json({"status": "ok", "data": data})
229
+
230
+
231
+ @app.command("sale-performance-new", help="录卡合同业绩数据")
232
+ @command_handler
233
+ def sale_performance_new(
234
+ type_: int = typer.Option(
235
+ ...,
236
+ "--type",
237
+ min=1,
238
+ max=2,
239
+ help="卡类型:1会员卡,2培训卡。",
240
+ ),
241
+ payload: str = typer.Option(
242
+ ...,
243
+ "--payload",
244
+ help="待录单合同列表,JSON 对象字符串。{\\\"data\":[{\\\"seq\\\":1,\\\"date\\\":\"2026-01-01\\\",\\\"activateNow\\\":true,\\\"phone\\\":\\\"xxx\\\",\\\"name\\\":\\\"xxxx\\\",\\\"cardName\\\":\\\"xxx\\\",\\\"contractNum\\\":\\\"xxx\\\",\\\"count\\\":3,\\\"payAmount\\\":1000,\\\"payMethod\\\":\\\"\\\",\\\"performanceAmount\\\":1000,\\\"performance\\\":[{\\\"sellerName\\\":\\\"xxx\\\",\\\"amount\\\":1000}],\\\"saleType\\\":\\\"新卡\\\",\\\"remarks\\\":\\\"\\\"}]} ",
245
+ ),
246
+ center_id: str | None = typer.Option(
247
+ None,
248
+ "--center-id",
249
+ help="门店ID。",
250
+ ),
251
+ ) -> None:
252
+ parsed_payload = _parse_performance_new_payload(payload)
253
+
254
+ api = _build_api()
255
+ data = api.card.performance_new(
256
+ type_=type_,
257
+ payload=parsed_payload,
258
+ center_id=center_id,
259
+ )
260
+ print_json({"status": "ok", "data": data})
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from bocai_cli.api import ApiFacade
6
+ from bocai_cli.command_utils import command_handler
7
+ from bocai_cli.config import load_config
8
+ from bocai_cli.date_range_validation import validate_date_range
9
+ from bocai_cli.output import print_json
10
+
11
+ app = typer.Typer(help="签到相关命令。", invoke_without_command=True)
12
+
13
+
14
+ @app.callback()
15
+ def checkin_root(ctx: typer.Context) -> None:
16
+ if ctx.invoked_subcommand is None:
17
+ typer.echo(ctx.get_help())
18
+ raise typer.Exit(code=0)
19
+
20
+
21
+ def _build_api() -> ApiFacade:
22
+ config = load_config()
23
+ return ApiFacade(
24
+ base_url=config.base_url,
25
+ token=config.token,
26
+ timeout_seconds=config.timeout_seconds,
27
+ )
28
+
29
+
30
+ @app.command("list", help="获取签到记录数据")
31
+ @command_handler
32
+ def list_checkin(
33
+ start_date: str = typer.Option(
34
+ ...,
35
+ "--start-date",
36
+ help="开始日期,格式 YYYY-MM-DD。",
37
+ ),
38
+ end_date: str = typer.Option(
39
+ ...,
40
+ "--end-date",
41
+ help="结束日期,格式 YYYY-MM-DD。",
42
+ ),
43
+ page_num: int = typer.Option(
44
+ ...,
45
+ "--page-num",
46
+ min=0,
47
+ help="页码,必须大于等于 0。",
48
+ ),
49
+ page_size: int = typer.Option(
50
+ ...,
51
+ "--page-size",
52
+ min=1,
53
+ help="每页条数,必须大于等于 1。",
54
+ ),
55
+ center_id: str | None = typer.Option(
56
+ None,
57
+ "--center-id",
58
+ help="门店ID。",
59
+ ),
60
+ ) -> None:
61
+ validate_date_range(start_date, end_date)
62
+
63
+ api = _build_api()
64
+ data = api.checkin.list(
65
+ start_date=start_date,
66
+ end_date=end_date,
67
+ page_num=page_num,
68
+ page_size=page_size,
69
+ center_id=center_id,
70
+ )
71
+ print_json({"status": "ok", "data": data})
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from bocai_cli.api import ApiFacade
10
+ from bocai_cli.command_utils import command_handler
11
+ from bocai_cli.config import load_config
12
+ from bocai_cli.date_range_validation import validate_date_range
13
+ from bocai_cli.errors import ConfigValidationError
14
+ from bocai_cli.output import print_json
15
+
16
+ app = typer.Typer(help="课程相关命令。", invoke_without_command=True)
17
+
18
+
19
+ @app.callback()
20
+ def course_root(ctx: typer.Context) -> None:
21
+ if ctx.invoked_subcommand is None:
22
+ typer.echo(ctx.get_help())
23
+ raise typer.Exit(code=0)
24
+
25
+
26
+ def _build_api() -> ApiFacade:
27
+ config = load_config()
28
+ return ApiFacade(
29
+ base_url=config.base_url,
30
+ token=config.token,
31
+ timeout_seconds=config.timeout_seconds,
32
+ )
33
+
34
+
35
+ def _parse_transaction_ids(raw_transaction_ids: str) -> list[str]:
36
+ values = [item.strip() for item in raw_transaction_ids.split(",")]
37
+ non_empty_values = [item for item in values if item]
38
+
39
+ unique_values: list[str] = []
40
+ seen: set[str] = set()
41
+ for value in non_empty_values:
42
+ if value not in seen:
43
+ seen.add(value)
44
+ unique_values.append(value)
45
+
46
+ if not unique_values:
47
+ raise ConfigValidationError("--transaction-ids 至少要包含一个有效ID。")
48
+
49
+ return unique_values
50
+
51
+
52
+ def _parse_performance_update_payload(raw_payload: str) -> list[dict[str, Any]]:
53
+ try:
54
+ payload = json.loads(raw_payload)
55
+ except json.JSONDecodeError as exc:
56
+ raise ConfigValidationError("--payload 必须是合法 JSON。") from exc
57
+
58
+ if not isinstance(payload, list):
59
+ raise ConfigValidationError("--payload 必须是数组。")
60
+
61
+ if not payload:
62
+ raise ConfigValidationError("--payload 至少要包含一条交易记录。")
63
+
64
+ for index, item in enumerate(payload, start=1):
65
+ if not isinstance(item, dict):
66
+ raise ConfigValidationError(f"--payload 第 {index} 项必须是对象。")
67
+ if "transactionId" not in item:
68
+ raise ConfigValidationError(f"--payload 第 {index} 项缺少 transactionId。")
69
+ if "performance" not in item:
70
+ raise ConfigValidationError(f"--payload 第 {index} 项缺少 performance。")
71
+
72
+ return payload
73
+
74
+
75
+ def _parse_performance_new_payload(raw_payload: str) -> dict[str, Any]:
76
+ try:
77
+ payload = json.loads(raw_payload)
78
+ except json.JSONDecodeError as exc:
79
+ raise ConfigValidationError("--payload 必须是合法 JSON。") from exc
80
+
81
+ if not isinstance(payload, dict):
82
+ raise ConfigValidationError("--payload 必须是对象。")
83
+
84
+ if "data" not in payload:
85
+ raise ConfigValidationError("--payload 缺少 data。")
86
+
87
+ data = payload["data"]
88
+ if not isinstance(data, list):
89
+ raise ConfigValidationError("--payload.data 必须是数组。")
90
+
91
+ if not data:
92
+ raise ConfigValidationError("--payload.data 至少要包含一条合同记录。")
93
+
94
+ required_fields = (
95
+ "seq",
96
+ "date",
97
+ "activateNow",
98
+ "phone",
99
+ "name",
100
+ "courseName",
101
+ "contractNum",
102
+ "count",
103
+ "payAmount",
104
+ "payMethod",
105
+ "performanceAmount",
106
+ "performance",
107
+ "saleType",
108
+ )
109
+ for index, item in enumerate(data, start=1):
110
+ if not isinstance(item, dict):
111
+ raise ConfigValidationError(f"--payload.data 第 {index} 项必须是对象。")
112
+ for field in required_fields:
113
+ if field not in item:
114
+ raise ConfigValidationError(f"--payload.data 第 {index} 项缺少 {field}。")
115
+
116
+ return payload
117
+
118
+
119
+ @app.command("sale-performance", help="获取售课业绩数据")
120
+ @command_handler
121
+ def sale_performance(
122
+ start_date: str = typer.Option(
123
+ ...,
124
+ "--start-date",
125
+ help="开始日期,格式 YYYY-MM-DD。",
126
+ ),
127
+ end_date: str = typer.Option(
128
+ ...,
129
+ "--end-date",
130
+ help="结束日期,格式 YYYY-MM-DD。",
131
+ ),
132
+ page_num: int = typer.Option(
133
+ ...,
134
+ "--page-num",
135
+ min=0,
136
+ help="页码,必须大于等于 0。",
137
+ ),
138
+ page_size: int = typer.Option(
139
+ ...,
140
+ "--page-size",
141
+ min=1,
142
+ help="每页条数,必须大于等于 1。",
143
+ ),
144
+ type_: int = typer.Option(
145
+ ...,
146
+ "--type",
147
+ min=1,
148
+ max=2,
149
+ help="课程类型:1私教课,2培训课。",
150
+ ),
151
+ center_id: str | None = typer.Option(
152
+ None,
153
+ "--center-id",
154
+ help="门店ID。",
155
+ ),
156
+ ) -> None:
157
+ validate_date_range(start_date, end_date)
158
+
159
+ api = _build_api()
160
+ data = api.course.sale_performance(
161
+ start_date=start_date,
162
+ end_date=end_date,
163
+ page_num=page_num,
164
+ page_size=page_size,
165
+ type_=type_,
166
+ center_id=center_id,
167
+ )
168
+ print_json({"status": "ok", "data": data})
169
+
170
+
171
+ @app.command("sale-performance-clear", help="清除售课业绩数据")
172
+ @command_handler
173
+ def sale_performance_clear(
174
+ transaction_ids: str = typer.Option(
175
+ ...,
176
+ "--transaction-ids",
177
+ help="交易ID列表,逗号分隔。",
178
+ ),
179
+ phone: str = typer.Option(
180
+ ...,
181
+ "--phone",
182
+ help="不分业绩员工手机号。",
183
+ ),
184
+ center_id: str | None = typer.Option(
185
+ None,
186
+ "--center-id",
187
+ help="门店ID。",
188
+ ),
189
+ ) -> None:
190
+ parsed_transaction_ids = _parse_transaction_ids(transaction_ids)
191
+
192
+ api = _build_api()
193
+ data = api.course.performance_clear(
194
+ transaction_ids=parsed_transaction_ids,
195
+ phone=phone,
196
+ center_id=center_id,
197
+ )
198
+ print_json({"status": "ok", "data": data})
199
+
200
+
201
+ @app.command("sale-performance-update", help="更新售课业绩数据")
202
+ @command_handler
203
+ def sale_performance_update(
204
+ payload: str = typer.Option(
205
+ ...,
206
+ "--payload",
207
+ help="业绩更新列表,JSON 数组字符串。[{\\\"transactionId\\\":1, \\\"performance\\\":[{\\\"sellerName\\\":\\\"测试\", \\\"amount\\\":10.0}]}]",
208
+ ),
209
+ phone: str = typer.Option(
210
+ ...,
211
+ "--phone",
212
+ help="不分业绩员工手机号。",
213
+ ),
214
+ center_id: str | None = typer.Option(
215
+ None,
216
+ "--center-id",
217
+ help="门店ID。",
218
+ ),
219
+ ) -> None:
220
+ parsed_payload = _parse_performance_update_payload(payload)
221
+
222
+ api = _build_api()
223
+ data = api.course.performance_update(
224
+ phone=phone,
225
+ transactions=parsed_payload,
226
+ center_id=center_id,
227
+ )
228
+ print_json({"status": "ok", "data": data})
229
+
230
+
231
+ @app.command("sale-performance-new", help="录课程合同业绩数据")
232
+ @command_handler
233
+ def sale_performance_new(
234
+ type_: int = typer.Option(
235
+ ...,
236
+ "--type",
237
+ min=1,
238
+ max=2,
239
+ help="课程类型:1私教课,2培训课。",
240
+ ),
241
+ payload: str = typer.Option(
242
+ ...,
243
+ "--payload",
244
+ help="待录单合同列表,JSON 对象字符串。{\\\"data\\\":[{\\\"seq\\\":1,\\\"date\\\":\\\"2026-01-01\\\",\\\"activateNow\\\":true,\\\"phone\\\":\\\"xxx\\\",\\\"name\\\":\\\"xxxx\\\",\\\"courseName\\\":\\\"xxx\\\",\\\"contractNum\\\":\\\"xxx\\\",\\\"count\\\":3,\\\"payAmount\\\":1000,\\\"payMethod\\\":\\\"\\\",\\\"performanceAmount\\\":1000,\\\"performance\\\":[{\\\"sellerName\\\":\\\"xxx\\\",\\\"amount\\\":1000}],\\\"saleType\\\":\\\"新购课\\\", \\\"remarks\\\":\\\"测试备注\\\"}]} ",
245
+ ),
246
+ center_id: str | None = typer.Option(
247
+ None,
248
+ "--center-id",
249
+ help="门店ID。",
250
+ ),
251
+ ) -> None:
252
+ parsed_payload = _parse_performance_new_payload(payload)
253
+
254
+ api = _build_api()
255
+ data = api.course.performance_new(
256
+ type_=type_,
257
+ payload=parsed_payload,
258
+ center_id=center_id,
259
+ )
260
+ print_json({"status": "ok", "data": data})
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import typer
6
+
7
+ from bocai_cli.commands.auth import app as auth_app
8
+ from bocai_cli.commands.card import app as card_app
9
+ from bocai_cli.commands.checkin import app as checkin_app
10
+ from bocai_cli.commands.course import app as course_app
11
+ from bocai_cli.commands.reservation import app as reservation_app
12
+ from bocai_cli.commands.report import app as report_app
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CommandRegistration:
17
+ name: str
18
+ app: typer.Typer
19
+ aliases: tuple[str, ...] = ()
20
+
21
+
22
+ COMMAND_REGISTRY: tuple[CommandRegistration, ...] = (
23
+ CommandRegistration(name="auth", app=auth_app),
24
+ CommandRegistration(name="card", app=card_app),
25
+ CommandRegistration(name="checkin", app=checkin_app),
26
+ CommandRegistration(name="course", app=course_app),
27
+ CommandRegistration(name="reservation", app=reservation_app),
28
+ CommandRegistration(name="report", app=report_app),
29
+ )
30
+
31
+
32
+ def register_commands(root_app: typer.Typer) -> None:
33
+ for registration in COMMAND_REGISTRY:
34
+ root_app.add_typer(registration.app, name=registration.name)
35
+ for alias in registration.aliases:
36
+ root_app.add_typer(registration.app, name=alias, hidden=True)