bocai-cli-core 0.2.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.
Files changed (42) hide show
  1. bocai_cli/__init__.py +3 -0
  2. bocai_cli/api/__init__.py +29 -0
  3. bocai_cli/api/auth_client.py +57 -0
  4. bocai_cli/api/card_client.py +222 -0
  5. bocai_cli/api/center_client.py +55 -0
  6. bocai_cli/api/checkin_client.py +29 -0
  7. bocai_cli/api/course_client.py +85 -0
  8. bocai_cli/api/facade.py +40 -0
  9. bocai_cli/api/guanyuan_client.py +50 -0
  10. bocai_cli/api/member_client.py +97 -0
  11. bocai_cli/api/month_dashboard_client.py +130 -0
  12. bocai_cli/api/qywx_client.py +28 -0
  13. bocai_cli/api/report_client.py +41 -0
  14. bocai_cli/api/reservation_client.py +29 -0
  15. bocai_cli/api/transport.py +166 -0
  16. bocai_cli/app.py +12 -0
  17. bocai_cli/client.py +69 -0
  18. bocai_cli/command_utils.py +28 -0
  19. bocai_cli/commands/__init__.py +8 -0
  20. bocai_cli/commands/auth.py +211 -0
  21. bocai_cli/commands/card.py +653 -0
  22. bocai_cli/commands/center.py +138 -0
  23. bocai_cli/commands/checkin.py +71 -0
  24. bocai_cli/commands/course.py +260 -0
  25. bocai_cli/commands/customreport.py +77 -0
  26. bocai_cli/commands/guanyuan.py +94 -0
  27. bocai_cli/commands/member.py +288 -0
  28. bocai_cli/commands/month_dashboard.py +235 -0
  29. bocai_cli/commands/qywx.py +112 -0
  30. bocai_cli/commands/registry.py +70 -0
  31. bocai_cli/commands/report.py +93 -0
  32. bocai_cli/commands/reservation.py +71 -0
  33. bocai_cli/config.py +121 -0
  34. bocai_cli/date_range_validation.py +65 -0
  35. bocai_cli/errors.py +22 -0
  36. bocai_cli/main.py +8 -0
  37. bocai_cli/output.py +28 -0
  38. bocai_cli/product.py +10 -0
  39. bocai_cli_core-0.2.0.dist-info/METADATA +138 -0
  40. bocai_cli_core-0.2.0.dist-info/RECORD +42 -0
  41. bocai_cli_core-0.2.0.dist-info/WHEEL +4 -0
  42. bocai_cli_core-0.2.0.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,288 @@
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_optional_date_order
9
+ from bocai_cli.errors import ConfigValidationError
10
+ from bocai_cli.output import print_json
11
+
12
+ app = typer.Typer(help="会员相关命令。", invoke_without_command=True)
13
+
14
+
15
+ @app.callback()
16
+ def member_root(ctx: typer.Context) -> None:
17
+ if ctx.invoked_subcommand is None:
18
+ typer.echo(ctx.get_help())
19
+ raise typer.Exit(code=0)
20
+
21
+
22
+ def _build_api() -> ApiFacade:
23
+ config = load_config()
24
+ return ApiFacade(
25
+ base_url=config.base_url,
26
+ token=config.token,
27
+ timeout_seconds=config.timeout_seconds,
28
+ )
29
+
30
+
31
+ @command_handler
32
+ def studio_proposed_list(
33
+ center_id: str = typer.Option(
34
+ ...,
35
+ "--center-id",
36
+ help="门店ID。",
37
+ ),
38
+ follower_id: int = typer.Option(
39
+ -1,
40
+ "--follower-id",
41
+ help="跟进人ID,-1表示全部。",
42
+ ),
43
+ source_id: int = typer.Option(
44
+ -1,
45
+ "--source-id",
46
+ help="来源ID,-1表示全部。",
47
+ ),
48
+ tag_id: int = typer.Option(
49
+ -1,
50
+ "--tag-id",
51
+ help="标签ID,-1表示全部。",
52
+ ),
53
+ attend_start_date: str = typer.Option(
54
+ "",
55
+ "--attend-start-date",
56
+ help="加入开始时间,格式 YYYY-MM-DD。",
57
+ ),
58
+ attend_end_date: str = typer.Option(
59
+ "",
60
+ "--attend-end-date",
61
+ help="加入结束时间,格式 YYYY-MM-DD。",
62
+ ),
63
+ follow_status: int = typer.Option(
64
+ -1,
65
+ "--follow-status",
66
+ min=-1,
67
+ max=5,
68
+ help="跟进状态:-1全部,1未体验,2已体验,3跟进中,4完成,5中止。",
69
+ ),
70
+ page_num: int = typer.Option(
71
+ ...,
72
+ "--page-num",
73
+ min=0,
74
+ help="页码,从 0 开始。",
75
+ ),
76
+ page_size: int = typer.Option(
77
+ ...,
78
+ "--page-size",
79
+ min=1,
80
+ help="每页数量,必须大于等于 1。",
81
+ ),
82
+ ) -> None:
83
+ validate_optional_date_order(
84
+ attend_start_date,
85
+ attend_end_date,
86
+ start_option_name="attend-start-date",
87
+ end_option_name="attend-end-date",
88
+ )
89
+
90
+ api = _build_api()
91
+ data = api.member.studio_proposed_list(
92
+ center_id=center_id,
93
+ page_num=page_num,
94
+ page_size=page_size,
95
+ consultant_id=follower_id,
96
+ source=source_id,
97
+ tag_id=tag_id,
98
+ follow_status=follow_status,
99
+ attend_start_date=attend_start_date,
100
+ attend_end_date=attend_end_date,
101
+ )
102
+ print_json({"status": "ok", "data": data})
103
+
104
+
105
+ def _parse_checkin_status(raw_value: str) -> bool:
106
+ normalized = raw_value.strip().lower()
107
+ if normalized == "true":
108
+ return True
109
+ if normalized == "false":
110
+ return False
111
+ raise ConfigValidationError("--checkin-status 只支持 true 或 false。")
112
+
113
+
114
+ @command_handler
115
+ def studio_member_list(
116
+ center_id: str = typer.Option(
117
+ ...,
118
+ "--center-id",
119
+ help="门店ID。",
120
+ ),
121
+ follower_id: int = typer.Option(
122
+ -1,
123
+ "--follower-id",
124
+ help="跟进人ID,-1表示全部。",
125
+ ),
126
+ source_id: int = typer.Option(
127
+ -1,
128
+ "--source-id",
129
+ help="来源ID,-1表示全部。",
130
+ ),
131
+ tag_id: int = typer.Option(
132
+ -1,
133
+ "--tag-id",
134
+ help="标签ID,-1表示全部。",
135
+ ),
136
+ member_type: int = typer.Option(
137
+ -1,
138
+ "--member-type",
139
+ min=-1,
140
+ max=1,
141
+ help="会员类型:-1全部,0普通会员,1课程会员。",
142
+ ),
143
+ member_status: int = typer.Option(
144
+ -1,
145
+ "--member-status",
146
+ min=-1,
147
+ max=1,
148
+ help="会员状态:-1全部,0有效会员,1已失效。",
149
+ ),
150
+ follow_status: int = typer.Option(
151
+ -1,
152
+ "--follow-status",
153
+ min=-1,
154
+ max=5,
155
+ help="跟进状态:-1全部,1未体验,2已体验,3跟进中,4完成,5中止。",
156
+ ),
157
+ expire_start_date: str = typer.Option(
158
+ "",
159
+ "--expire-start-date",
160
+ help="将过期开始日期,格式 YYYY-MM-DD。",
161
+ ),
162
+ expire_end_date: str = typer.Option(
163
+ "",
164
+ "--expire-end-date",
165
+ help="将过期结束日期,格式 YYYY-MM-DD。",
166
+ ),
167
+ birthday_start_date: str = typer.Option(
168
+ "",
169
+ "--birthday-start-date",
170
+ help="生日开始日期,格式 YYYY-MM-DD。",
171
+ ),
172
+ birthday_end_date: str = typer.Option(
173
+ "",
174
+ "--birthday-end-date",
175
+ help="生日结束日期,格式 YYYY-MM-DD。",
176
+ ),
177
+ checkin_status: str = typer.Option(
178
+ "true",
179
+ "--checkin-status",
180
+ help="签到状态:true已签到,false未签到。",
181
+ ),
182
+ last_checkin_start_date: str = typer.Option(
183
+ "",
184
+ "--last-checkin-start-date",
185
+ help="最后签到开始日期,格式 YYYY-MM-DD。",
186
+ ),
187
+ last_checkin_end_date: str = typer.Option(
188
+ "",
189
+ "--last-checkin-end-date",
190
+ help="最后签到结束日期,格式 YYYY-MM-DD。",
191
+ ),
192
+ course_start_date: str = typer.Option(
193
+ "",
194
+ "--course-start-date",
195
+ help="最后上课开始日期,格式 YYYY-MM-DD。",
196
+ ),
197
+ course_end_date: str = typer.Option(
198
+ "",
199
+ "--course-end-date",
200
+ help="最后上课结束日期,格式 YYYY-MM-DD。",
201
+ ),
202
+ page_num: int = typer.Option(
203
+ ...,
204
+ "--page-num",
205
+ min=0,
206
+ help="页码,从 0 开始。",
207
+ ),
208
+ page_size: int = typer.Option(
209
+ ...,
210
+ "--page-size",
211
+ min=1,
212
+ help="每页数量,必须大于等于 1。",
213
+ ),
214
+ ) -> None:
215
+ validate_optional_date_order(
216
+ expire_start_date,
217
+ expire_end_date,
218
+ start_option_name="expire-start-date",
219
+ end_option_name="expire-end-date",
220
+ )
221
+ validate_optional_date_order(
222
+ birthday_start_date,
223
+ birthday_end_date,
224
+ start_option_name="birthday-start-date",
225
+ end_option_name="birthday-end-date",
226
+ )
227
+ validate_optional_date_order(
228
+ last_checkin_start_date,
229
+ last_checkin_end_date,
230
+ start_option_name="last-checkin-start-date",
231
+ end_option_name="last-checkin-end-date",
232
+ )
233
+ validate_optional_date_order(
234
+ course_start_date,
235
+ course_end_date,
236
+ start_option_name="course-start-date",
237
+ end_option_name="course-end-date",
238
+ )
239
+ parsed_checkin_status = _parse_checkin_status(checkin_status)
240
+
241
+ api = _build_api()
242
+ data = api.member.studio_member_list(
243
+ center_id=center_id,
244
+ page_num=page_num,
245
+ page_size=page_size,
246
+ consultant_id=follower_id,
247
+ source=source_id,
248
+ tag_id=tag_id,
249
+ member_type=member_type,
250
+ status=member_status,
251
+ follow_status=follow_status,
252
+ expire_start_date=expire_start_date,
253
+ expire_end_date=expire_end_date,
254
+ birthday_start_date=birthday_start_date,
255
+ birthday_end_date=birthday_end_date,
256
+ check_in_status=parsed_checkin_status,
257
+ last_checkin_start_date=last_checkin_start_date,
258
+ last_checkin_end_date=last_checkin_end_date,
259
+ course_start_date=course_start_date,
260
+ course_end_date=course_end_date,
261
+ )
262
+ print_json({"status": "ok", "data": data})
263
+
264
+
265
+ @app.command("detail", help="获取会员信息")
266
+ @command_handler
267
+ def detail(
268
+ center_id: str = typer.Option(
269
+ ...,
270
+ "--center-id",
271
+ help="门店ID。",
272
+ ),
273
+ member_id: str = typer.Option(
274
+ ...,
275
+ "--member-id",
276
+ help="会员ID。",
277
+ ),
278
+ ) -> None:
279
+ api = _build_api()
280
+ data = api.member.detail(center_id=center_id, member_id=member_id)
281
+ print_json({"status": "ok", "data": data})
282
+
283
+
284
+ studio_app = typer.Typer(help="会员相关命令。", invoke_without_command=True)
285
+ studio_app.callback()(member_root)
286
+ studio_app.command("proposed-list", help="获取准会员列表")(studio_proposed_list)
287
+ studio_app.command("member-list", help="获取会员列表")(studio_member_list)
288
+ studio_app.command("detail", help="获取会员信息")(detail)
@@ -0,0 +1,235 @@
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 monthdashboard_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
+ def _run_dashboard_query(
31
+ method_name: str,
32
+ *,
33
+ center_id: str,
34
+ start_date: str,
35
+ end_date: str,
36
+ ) -> None:
37
+ validate_date_range(start_date, end_date)
38
+
39
+ api = _build_api()
40
+ method = getattr(api.monthdashboard, method_name)
41
+ data = method(
42
+ center_id=center_id,
43
+ start_date=start_date,
44
+ end_date=end_date,
45
+ )
46
+ print_json({"status": "ok", "data": data})
47
+
48
+
49
+ @app.command("month-payment", help="获取月付履约信息")
50
+ @command_handler
51
+ def month_payment(
52
+ center_id: str = typer.Option(
53
+ ...,
54
+ "--center-id",
55
+ help="门店ID。",
56
+ ),
57
+ start_date: str = typer.Option(
58
+ ...,
59
+ "--start-date",
60
+ help="开始日期,格式 YYYY-MM-DD。",
61
+ ),
62
+ end_date: str = typer.Option(
63
+ ...,
64
+ "--end-date",
65
+ help="结束日期,格式 YYYY-MM-DD。",
66
+ ),
67
+ ) -> None:
68
+ _run_dashboard_query(
69
+ "statistics",
70
+ center_id=center_id,
71
+ start_date=start_date,
72
+ end_date=end_date,
73
+ )
74
+
75
+
76
+ @app.command("recognize-income", help="获取会员卡确认收入信息")
77
+ @command_handler
78
+ def recognize_income(
79
+ center_id: str = typer.Option(
80
+ ...,
81
+ "--center-id",
82
+ help="门店ID。",
83
+ ),
84
+ start_date: str = typer.Option(
85
+ ...,
86
+ "--start-date",
87
+ help="开始日期,格式 YYYY-MM-DD。",
88
+ ),
89
+ end_date: str = typer.Option(
90
+ ...,
91
+ "--end-date",
92
+ help="结束日期,格式 YYYY-MM-DD。",
93
+ ),
94
+ ) -> None:
95
+ _run_dashboard_query(
96
+ "recognize_income",
97
+ center_id=center_id,
98
+ start_date=start_date,
99
+ end_date=end_date,
100
+ )
101
+
102
+
103
+ @app.command("clue-conversion", help="获取线索转化信息")
104
+ @command_handler
105
+ def clue_conversion(
106
+ center_id: str = typer.Option(
107
+ ...,
108
+ "--center-id",
109
+ help="门店ID。",
110
+ ),
111
+ start_date: str = typer.Option(
112
+ ...,
113
+ "--start-date",
114
+ help="开始日期,格式 YYYY-MM-DD。",
115
+ ),
116
+ end_date: str = typer.Option(
117
+ ...,
118
+ "--end-date",
119
+ help="结束日期,格式 YYYY-MM-DD。",
120
+ ),
121
+ ) -> None:
122
+ _run_dashboard_query(
123
+ "clue_conversion",
124
+ center_id=center_id,
125
+ start_date=start_date,
126
+ end_date=end_date,
127
+ )
128
+
129
+
130
+ @app.command("service-delivery", help="获取服务与交付信息")
131
+ @command_handler
132
+ def service_delivery(
133
+ center_id: str = typer.Option(
134
+ ...,
135
+ "--center-id",
136
+ help="门店ID。",
137
+ ),
138
+ start_date: str = typer.Option(
139
+ ...,
140
+ "--start-date",
141
+ help="开始日期,格式 YYYY-MM-DD。",
142
+ ),
143
+ end_date: str = typer.Option(
144
+ ...,
145
+ "--end-date",
146
+ help="结束日期,格式 YYYY-MM-DD。",
147
+ ),
148
+ ) -> None:
149
+ _run_dashboard_query(
150
+ "service_delivery",
151
+ center_id=center_id,
152
+ start_date=start_date,
153
+ end_date=end_date,
154
+ )
155
+
156
+
157
+ @app.command("member-count", help="获取会员总数信息")
158
+ @command_handler
159
+ def member_count(
160
+ center_id: str = typer.Option(
161
+ ...,
162
+ "--center-id",
163
+ help="门店ID。",
164
+ ),
165
+ start_date: str = typer.Option(
166
+ ...,
167
+ "--start-date",
168
+ help="开始日期,格式 YYYY-MM-DD。",
169
+ ),
170
+ end_date: str = typer.Option(
171
+ ...,
172
+ "--end-date",
173
+ help="结束日期,格式 YYYY-MM-DD。",
174
+ ),
175
+ ) -> None:
176
+ _run_dashboard_query(
177
+ "member_count",
178
+ center_id=center_id,
179
+ start_date=start_date,
180
+ end_date=end_date,
181
+ )
182
+
183
+
184
+ @app.command("attendance-count", help="获取出勤与活跃信息")
185
+ @command_handler
186
+ def attendance_count(
187
+ center_id: str = typer.Option(
188
+ ...,
189
+ "--center-id",
190
+ help="门店ID。",
191
+ ),
192
+ start_date: str = typer.Option(
193
+ ...,
194
+ "--start-date",
195
+ help="开始日期,格式 YYYY-MM-DD。",
196
+ ),
197
+ end_date: str = typer.Option(
198
+ ...,
199
+ "--end-date",
200
+ help="结束日期,格式 YYYY-MM-DD。",
201
+ ),
202
+ ) -> None:
203
+ _run_dashboard_query(
204
+ "attendance_count",
205
+ center_id=center_id,
206
+ start_date=start_date,
207
+ end_date=end_date,
208
+ )
209
+
210
+
211
+ @app.command("revenue-info", help="获取营收状态信息")
212
+ @command_handler
213
+ def revenue_info(
214
+ center_id: str = typer.Option(
215
+ ...,
216
+ "--center-id",
217
+ help="门店ID。",
218
+ ),
219
+ start_date: str = typer.Option(
220
+ ...,
221
+ "--start-date",
222
+ help="开始日期,格式 YYYY-MM-DD。",
223
+ ),
224
+ end_date: str = typer.Option(
225
+ ...,
226
+ "--end-date",
227
+ help="结束日期,格式 YYYY-MM-DD。",
228
+ ),
229
+ ) -> None:
230
+ _run_dashboard_query(
231
+ "revenue_info",
232
+ center_id=center_id,
233
+ start_date=start_date,
234
+ end_date=end_date,
235
+ )
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
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.errors import ConfigValidationError
13
+ from bocai_cli.output import print_json
14
+
15
+ app = typer.Typer(help="企业微信相关命令。", invoke_without_command=True)
16
+
17
+
18
+ @app.callback()
19
+ def qywx_root(ctx: typer.Context) -> None:
20
+ if ctx.invoked_subcommand is None:
21
+ typer.echo(ctx.get_help())
22
+ raise typer.Exit(code=0)
23
+
24
+
25
+ def _build_api() -> ApiFacade:
26
+ config = load_config()
27
+ return ApiFacade(
28
+ base_url=config.base_url,
29
+ token=config.token,
30
+ timeout_seconds=config.timeout_seconds,
31
+ )
32
+
33
+
34
+ def _parse_send_message_payload(raw_payload: str) -> dict[str, Any]:
35
+ try:
36
+ payload = json.loads(raw_payload)
37
+ except json.JSONDecodeError as exc:
38
+ raise ConfigValidationError("--payload 必须是合法 JSON。") from exc
39
+
40
+ if not isinstance(payload, dict):
41
+ raise ConfigValidationError("--payload 必须是对象。")
42
+
43
+ if "phones" not in payload:
44
+ raise ConfigValidationError("--payload 缺少 phones。")
45
+ if not isinstance(payload["phones"], list):
46
+ raise ConfigValidationError("--payload.phones 必须是数组。")
47
+
48
+ if "type" not in payload:
49
+ raise ConfigValidationError("--payload 缺少 type。")
50
+
51
+ message_type = payload["type"]
52
+ if message_type not in ("file", "text"):
53
+ raise ConfigValidationError("--payload.type 只支持 file 或 text。")
54
+
55
+ if message_type == "file":
56
+ if "media_id" not in payload:
57
+ raise ConfigValidationError("--payload 缺少 media_id。")
58
+ if message_type == "text":
59
+ if "content" not in payload:
60
+ raise ConfigValidationError("--payload 缺少 content。")
61
+
62
+ return payload
63
+
64
+
65
+ def _validate_file_path(file_path: Path) -> Path:
66
+ if not file_path.exists():
67
+ raise ConfigValidationError("--file 指定的文件不存在。")
68
+ if not file_path.is_file():
69
+ raise ConfigValidationError("--file 必须是普通文件。")
70
+ return file_path
71
+
72
+
73
+ @app.command("send-message", help="发送企业微信消息")
74
+ @command_handler
75
+ def send_message(
76
+ center_id: str = typer.Option(
77
+ ...,
78
+ "--center-id",
79
+ help="门店ID。",
80
+ ),
81
+ payload: str = typer.Option(
82
+ ...,
83
+ "--payload",
84
+ help='发送消息内容,JSON 对象字符串。{\\\"phones\\\":[],\\\"type\\\":\\\"file\\\",\\\"media_id\\\":\\\"xxx\\\"} 或 {\\\"phones\\\":[],\\\"type\\\":\\\"text\\\",\\\"content\\\":\\\"xxx\\\"}',
85
+ ),
86
+ ) -> None:
87
+ parsed_payload = _parse_send_message_payload(payload)
88
+
89
+ api = _build_api()
90
+ data = api.qywx.send_message(center_id=center_id, payload=parsed_payload)
91
+ print_json({"status": "ok", "data": data})
92
+
93
+
94
+ @app.command("upload", help="上传企业微信媒体文件")
95
+ @command_handler
96
+ def upload(
97
+ center_id: str = typer.Option(
98
+ ...,
99
+ "--center-id",
100
+ help="门店ID。",
101
+ ),
102
+ file_path: Path = typer.Option(
103
+ ...,
104
+ "--file",
105
+ help="待上传文件路径。",
106
+ ),
107
+ ) -> None:
108
+ validated_file_path = _validate_file_path(file_path)
109
+
110
+ api = _build_api()
111
+ data = api.qywx.upload(center_id=center_id, file_path=validated_file_path)
112
+ print_json({"status": "ok", "data": data})
@@ -0,0 +1,70 @@
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.card import studio_app as card_studio_app
10
+ from bocai_cli.commands.center import app as center_app
11
+ from bocai_cli.commands.center import studio_app as center_studio_app
12
+ from bocai_cli.commands.checkin import app as checkin_app
13
+ from bocai_cli.commands.course import app as course_app
14
+ from bocai_cli.commands.customreport import app as customreport_app
15
+ from bocai_cli.commands.guanyuan import app as guanyuan_app
16
+ from bocai_cli.commands.guanyuan import studio_app as guanyuan_studio_app
17
+ from bocai_cli.commands.member import app as member_app
18
+ from bocai_cli.commands.member import studio_app as member_studio_app
19
+ from bocai_cli.commands.month_dashboard import app as monthdashboard_app
20
+ from bocai_cli.commands.qywx import app as qywx_app
21
+ from bocai_cli.commands.reservation import app as reservation_app
22
+ from bocai_cli.commands.report import app as report_app
23
+ from bocai_cli.product import Product
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class CommandRegistration:
28
+ name: str
29
+ app: typer.Typer
30
+ studio_app: typer.Typer | None = None
31
+ gym_enabled: bool = True
32
+ studio_enabled: bool = True
33
+ aliases: tuple[str, ...] = ()
34
+
35
+
36
+ COMMAND_REGISTRY: tuple[CommandRegistration, ...] = (
37
+ CommandRegistration(name="auth", app=auth_app),
38
+ CommandRegistration(name="card", app=card_app, studio_app=card_studio_app),
39
+ CommandRegistration(name="center", app=center_app, studio_app=center_studio_app),
40
+ CommandRegistration(name="checkin", app=checkin_app),
41
+ CommandRegistration(name="course", app=course_app, studio_enabled=False),
42
+ CommandRegistration(name="customreport", app=customreport_app),
43
+ CommandRegistration(name="guanyuan", app=guanyuan_app, studio_app=guanyuan_studio_app),
44
+ CommandRegistration(
45
+ name="member",
46
+ app=member_app,
47
+ studio_app=member_studio_app,
48
+ gym_enabled=False,
49
+ ),
50
+ CommandRegistration(name="month-dashboard", app=monthdashboard_app),
51
+ CommandRegistration(name="qywx", app=qywx_app),
52
+ CommandRegistration(name="reservation", app=reservation_app),
53
+ CommandRegistration(name="report", app=report_app),
54
+ )
55
+
56
+
57
+ def register_commands(root_app: typer.Typer, *, product: Product = "gym") -> None:
58
+ for registration in COMMAND_REGISTRY:
59
+ if product == "gym" and not registration.gym_enabled:
60
+ continue
61
+ if product == "studio" and not registration.studio_enabled:
62
+ continue
63
+ command_app = (
64
+ registration.studio_app
65
+ if product == "studio" and registration.studio_app is not None
66
+ else registration.app
67
+ )
68
+ root_app.add_typer(command_app, name=registration.name)
69
+ for alias in registration.aliases:
70
+ root_app.add_typer(command_app, name=alias, hidden=True)