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.
- bocai_cli/__init__.py +3 -0
- bocai_cli/api/__init__.py +29 -0
- bocai_cli/api/auth_client.py +57 -0
- bocai_cli/api/card_client.py +222 -0
- bocai_cli/api/center_client.py +55 -0
- bocai_cli/api/checkin_client.py +29 -0
- bocai_cli/api/course_client.py +85 -0
- bocai_cli/api/facade.py +40 -0
- bocai_cli/api/guanyuan_client.py +50 -0
- bocai_cli/api/member_client.py +97 -0
- bocai_cli/api/month_dashboard_client.py +130 -0
- bocai_cli/api/qywx_client.py +28 -0
- bocai_cli/api/report_client.py +41 -0
- bocai_cli/api/reservation_client.py +29 -0
- bocai_cli/api/transport.py +166 -0
- bocai_cli/app.py +12 -0
- bocai_cli/client.py +69 -0
- bocai_cli/command_utils.py +28 -0
- bocai_cli/commands/__init__.py +8 -0
- bocai_cli/commands/auth.py +211 -0
- bocai_cli/commands/card.py +653 -0
- bocai_cli/commands/center.py +138 -0
- bocai_cli/commands/checkin.py +71 -0
- bocai_cli/commands/course.py +260 -0
- bocai_cli/commands/customreport.py +77 -0
- bocai_cli/commands/guanyuan.py +94 -0
- bocai_cli/commands/member.py +288 -0
- bocai_cli/commands/month_dashboard.py +235 -0
- bocai_cli/commands/qywx.py +112 -0
- bocai_cli/commands/registry.py +70 -0
- bocai_cli/commands/report.py +93 -0
- bocai_cli/commands/reservation.py +71 -0
- bocai_cli/config.py +121 -0
- bocai_cli/date_range_validation.py +65 -0
- bocai_cli/errors.py +22 -0
- bocai_cli/main.py +8 -0
- bocai_cli/output.py +28 -0
- bocai_cli/product.py +10 -0
- bocai_cli_core-0.2.0.dist-info/METADATA +138 -0
- bocai_cli_core-0.2.0.dist-info/RECORD +42 -0
- bocai_cli_core-0.2.0.dist-info/WHEEL +4 -0
- 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)
|