@lichidental/saas-cli 0.0.0-stage → 1.0.1

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,146 @@
1
+ // commands/reservation/create.mjs — 新建预约(主命令)
2
+ import { addCommonOptions, assertValueNotOptionLike } from "../../lib/options.mjs"
3
+ import { buildContext } from "../../lib/context.mjs"
4
+ import { CliError, EXIT_CODES } from "../../lib/errors.mjs"
5
+ import { parseUserTime, formatDateTime, formatMinute } from "../../lib/time.mjs"
6
+ import { addCustomMark, getVisitTypeByCustom, sendCustomMarkSms } from "../../api/custom-mark.mjs"
7
+ import { resolvePatient, resolveDoctorId, resolveTreatItems, findCreatedMark } from "./_shared.mjs"
8
+
9
+ /** @param {import("commander").Command} topic */
10
+ export function register(topic) {
11
+ addCommonOptions(
12
+ topic
13
+ .command("create")
14
+ .description("新建预约(传姓名自动查患者/医生/项目;重名会列候选由使用者指定)")
15
+ .option("--patient <姓名/手机号/病历号>", "患者姓名、手机号或病历号,自动搜索")
16
+ .option("--patient-code <编码或病历号>", "直接指定患者编码或病历号(跳过搜索)")
17
+ .option("--doctor <姓名>", "预约医生姓名,自动匹配")
18
+ .option("--doctor-id <userUid>", "直接指定医生 userUid(跳过搜索)")
19
+ .option("--time <时间>", '预约开始时间,如 "2026-10-15 10:00"(省略=当前时间)')
20
+ .option("--duration <分钟>", "持续时长(分钟,15~480),默认 30", "30")
21
+ .option("--items <名称列表>", '预约项目名称,逗号分隔,如 "洗牙,补牙"')
22
+ .option("--items-json <json>", "直接指定预约项目 JSON 数组(跳过名称匹配)")
23
+ .option("--first <1|0>", "就诊类型:1=初诊,0=复诊(省略则按患者历史自动识别)")
24
+ .option("--remark <备注>", "预约备注")
25
+ .option("--dry-run", "只打印将要提交的请求体,不实际创建")
26
+ .option("--sms", "创建成功后给患者发送预约短信(默认不发送)")
27
+ .option("--yes", "prod 环境为写操作,必须显式确认")
28
+ ).action(async function () {
29
+ const opts = this.opts()
30
+ const { env, out, http } = buildContext(this)
31
+
32
+ // prod 写操作门槛:在任何请求发出之前拦截(--dry-run 是只读预览,不拦截)
33
+ if (!opts.dryRun && env === "prod" && !opts.yes) {
34
+ throw new CliError("prod 环境为写操作,需要显式确认", {
35
+ exitCode: EXIT_CODES.USAGE,
36
+ hint: "确认环境与内容无误后在命令末尾加 --yes 重试(建议先用 --dry-run 预览请求体)",
37
+ })
38
+ }
39
+
40
+ const start = parseUserTime(opts.time)
41
+
42
+ // 时长校验(前端选项范围:15 分钟 ~ 8 小时)
43
+ const duration = Number(opts.duration)
44
+ if (!Number.isInteger(duration) || duration < 15 || duration > 480) {
45
+ throw new CliError(`持续时长必须是 15~480 之间的整数分钟(当前:${opts.duration})`, {
46
+ exitCode: EXIT_CODES.USAGE,
47
+ hint: "例:--duration 30",
48
+ })
49
+ }
50
+
51
+ // 备注是任意文本、会直接进请求体,值像选项时拦下(如 --remark --dry-run 会把 --dry-run 吞成备注且 dry-run 失效)
52
+ assertValueNotOptionLike(opts.remark, {
53
+ option: "--remark",
54
+ hint: '例:--remark "复诊患者";不需要备注时直接省略 --remark',
55
+ })
56
+
57
+ const patient = await resolvePatient(http, { patient: opts.patient, patientCode: opts.patientCode })
58
+ const doctor = await resolveDoctorId(http, { doctor: opts.doctor, doctorId: opts.doctorId })
59
+ const treatItems = await resolveTreatItems(http, { items: opts.items, itemsJson: opts.itemsJson })
60
+
61
+ // 就诊类型:显式 --first 优先,否则按患者历史记录自动识别
62
+ let boolFirst
63
+ if (opts.first !== undefined) {
64
+ if (!["1", "0"].includes(String(opts.first))) {
65
+ throw new CliError(`--first 只能是 1(初诊)或 0(复诊),收到:${opts.first}`, {
66
+ exitCode: EXIT_CODES.USAGE,
67
+ })
68
+ }
69
+ boolFirst = String(opts.first)
70
+ } else {
71
+ try {
72
+ const res = await getVisitTypeByCustom(http, { customCode: patient.customCode })
73
+ boolFirst = String(res.data?.visitType ?? "0")
74
+ } catch (err) {
75
+ throw new CliError(`自动识别就诊类型失败:${err.message}`, {
76
+ exitCode: EXIT_CODES.USAGE,
77
+ hint: "请用 --first 1(初诊)或 --first 0(复诊)显式指定",
78
+ })
79
+ }
80
+ }
81
+
82
+ const body = {
83
+ customCode: patient.customCode,
84
+ doctorId: doctor.doctorId,
85
+ markStartTime: formatDateTime(start),
86
+ markTimeLength: duration,
87
+ treatItemJson: JSON.stringify(treatItems),
88
+ definitionCreateTime: formatDateTime(new Date()),
89
+ boolFirst,
90
+ remark: opts.remark ?? "",
91
+ }
92
+
93
+ if (opts.dryRun) {
94
+ out.success(
95
+ "reservation create",
96
+ env,
97
+ { dryRun: true, requestBody: body },
98
+ ["[dry-run] 未提交,以下是将要发送的请求体(去掉 --dry-run 即真实创建):", JSON.stringify(body, null, 2)].join("\n")
99
+ )
100
+ return
101
+ }
102
+
103
+ const res = await addCustomMark(http, body)
104
+
105
+ // 预约短信(默认不发;发送失败不影响创建结果)
106
+ let smsSent = false
107
+ let smsError = null
108
+ if (opts.sms) {
109
+ try {
110
+ await sendCustomMarkSms(http, { customCode: patient.customCode, markStartTime: formatMinute(start) })
111
+ smsSent = true
112
+ } catch (err) {
113
+ smsError = err.message
114
+ out.log(`⚠ 预约短信发送失败:${err.message}(预约已创建成功)`)
115
+ }
116
+ }
117
+
118
+ // 预约 id:优先取响应里的 data.id;回查用于拿完整预约信息(失败不影响创建结果)
119
+ const reservation = await findCreatedMark(http, {
120
+ customCode: patient.customCode,
121
+ markStartTime: body.markStartTime,
122
+ })
123
+ const newId = res.data?.id ?? reservation?.id
124
+
125
+ const lines = [
126
+ `✓ 预约创建成功(环境:${env})`,
127
+ ` 患者 ${patient.customName ?? patient.customCode}(${patient.customCode})`,
128
+ ` 医生 ${doctor.doctorName}(userUid:${doctor.doctorId})`,
129
+ ` 时间 ${body.markStartTime} 起 ${duration} 分钟`,
130
+ ` 就诊类型 ${boolFirst === "1" ? "初诊" : "复诊"}`,
131
+ ` 项目 ${treatItems.map((i) => i.title).join("、")}`,
132
+ opts.remark ? ` 备注 ${opts.remark}` : null,
133
+ ` 短信 ${opts.sms ? (smsSent ? "已发送" : `发送失败:${smsError}`) : "未发送(如需发送加 --sms)"}`,
134
+ newId !== undefined
135
+ ? ` 预约ID ${newId}`
136
+ : ` 预约ID 未取到,可运行 saas-cli reservation list --patient ${patient.customCode} --env ${env} 查询`,
137
+ ].filter(Boolean)
138
+
139
+ out.success(
140
+ "reservation create",
141
+ env,
142
+ { requestBody: body, response: res, createdId: newId ?? null, smsSent, smsError, reservation },
143
+ lines.join("\n")
144
+ )
145
+ })
146
+ }
@@ -0,0 +1,21 @@
1
+ // commands/reservation/doctors.mjs — 医生列表(用于获取 doctorId)
2
+ import { addCommonOptions } from "../../lib/options.mjs"
3
+ import { buildContext } from "../../lib/context.mjs"
4
+ import { listDoctors } from "../../api/employee.mjs"
5
+
6
+ /** @param {import("commander").Command} topic */
7
+ export function register(topic) {
8
+ addCommonOptions(
9
+ topic.command("doctors").description("医生列表(在职 + 试用,岗位=医生),用于获取 doctorId")
10
+ ).action(async function () {
11
+ const { env, out, http } = buildContext(this)
12
+ const res = await listDoctors(http)
13
+ const list = res.data ?? []
14
+ const lines = [
15
+ `共 ${list.length} 位医生:`,
16
+ ...list.map((d, i) => ` ${i + 1}. ${d.storeUserName ?? d.userName ?? "-"} userUid:${d.userUid}`),
17
+ ]
18
+ if (list.length === 0) lines.push(" (无医生数据;请确认 token 对应的诊所是否正确)")
19
+ out.success("reservation doctors", env, { doctors: list }, lines.join("\n"))
20
+ })
21
+ }
@@ -0,0 +1,41 @@
1
+ // commands/reservation/items.mjs — 预约项目列表(一级分类 → 二级项目)
2
+ import { addCommonOptions, assertValueNotOptionLike } from "../../lib/options.mjs"
3
+ import { buildContext } from "../../lib/context.mjs"
4
+ import { listTreatItems } from "../../api/treat-item.mjs"
5
+
6
+ /** @param {import("commander").Command} topic */
7
+ export function register(topic) {
8
+ addCommonOptions(
9
+ topic
10
+ .command("items")
11
+ .description("预约项目列表(平铺数据按 一级分类 → 二级项目 展示),用于 --items 名称参考")
12
+ .option("--keyword <关键词>", "只显示名称包含该关键词的项目(可选)")
13
+ ).action(async function () {
14
+ const opts = this.opts()
15
+ const { env, out, http } = buildContext(this)
16
+ assertValueNotOptionLike(opts.keyword, {
17
+ option: "--keyword",
18
+ hint: "例:--keyword 补牙;省略则不筛选,列出全部项目",
19
+ })
20
+ const res = await listTreatItems(http)
21
+ const flat = res.data ?? []
22
+ const level1 = flat.filter((i) => i.level === 1)
23
+ const level2 = flat.filter((i) => i.level === 2)
24
+ const kw = opts.keyword ? String(opts.keyword) : null
25
+
26
+ const lines = []
27
+ let shown = 0
28
+ for (const parent of level1) {
29
+ const children = level2.filter((c) => c.upTreatItemId === parent.id && (!kw || String(c.treatItemName ?? "").includes(kw)))
30
+ if (children.length === 0) continue
31
+ lines.push(` ${parent.treatItemName}:`)
32
+ for (const c of children) {
33
+ lines.push(` - ${c.treatItemName} id:${c.id}`)
34
+ shown++
35
+ }
36
+ }
37
+ const head = kw ? `名称包含「${kw}」的项目(共 ${shown} 个):` : `预约项目(共 ${shown} 个):`
38
+ if (shown === 0) lines.push(" (无匹配项目)")
39
+ out.success("reservation items", env, { keyword: kw ?? null, items: flat }, [head, ...lines].join("\n"))
40
+ })
41
+ }
@@ -0,0 +1,78 @@
1
+ // commands/reservation/list.mjs — 查询预约(按患者和日期)
2
+ import { addCommonOptions } from "../../lib/options.mjs"
3
+ import { buildContext } from "../../lib/context.mjs"
4
+ import { CliError, EXIT_CODES } from "../../lib/errors.mjs"
5
+ import { formatDate } from "../../lib/time.mjs"
6
+ import { listMarksByRange } from "../../api/custom-mark.mjs"
7
+ import { listDoctors } from "../../api/employee.mjs"
8
+ import { resolvePatient, normTime, markStatusName, parseItemTitles } from "./_shared.mjs"
9
+
10
+ /** @param {import("commander").Command} topic */
11
+ export function register(topic) {
12
+ addCommonOptions(
13
+ topic
14
+ .command("list")
15
+ .description("查询预约(按患者 + 日期;默认查今天)")
16
+ .option("--patient <姓名/手机号/病历号>", "患者姓名、手机号或病历号,自动搜索")
17
+ .option("--patient-code <编码或病历号>", "直接指定患者编码或病历号(跳过搜索)")
18
+ .option("--date <YYYY-MM-DD>", "查询日期,默认今天")
19
+ ).action(async function () {
20
+ const opts = this.opts()
21
+ const { env, out, http } = buildContext(this)
22
+
23
+ const patient = await resolvePatient(http, { patient: opts.patient, patientCode: opts.patientCode })
24
+
25
+ let date = opts.date
26
+ if (date && !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
27
+ throw new CliError(`--date 格式不正确:「${date}」`, {
28
+ exitCode: EXIT_CODES.USAGE,
29
+ hint: "正确格式例:--date 2026-10-15;省略则查今天",
30
+ })
31
+ }
32
+ if (!date) date = formatDate(new Date())
33
+
34
+ // 照前端患者详情"就诊记录"的做法:customMarkListByCustom + 当天时间范围(服务端过滤实测生效),
35
+ // 返回已按 markStartTime 降序;剔除"仅挂号"记录(照前端卡片"下次预约"tab 的过滤)
36
+ const res = await listMarksByRange(http, {
37
+ customCode: patient.customCode,
38
+ startTime: `${date} 00:00:00`,
39
+ endTime: `${date} 23:59:59`,
40
+ })
41
+ const marks = (res.data?.cmList ?? []).filter((m) => !(m.isMark === 0 && m.isRegister === 1))
42
+
43
+ // 医生名映射(doctorId → 姓名);失败时只显示 id,不影响查询结果
44
+ let docMap = new Map()
45
+ try {
46
+ const docRes = await listDoctors(http)
47
+ docMap = new Map((docRes.data ?? []).map((d) => [String(d.userUid), d.storeUserName ?? d.userName ?? String(d.userUid)]))
48
+ } catch {
49
+ // 忽略
50
+ }
51
+
52
+ const lines = [
53
+ `${patient.customName ?? patient.customCode}(${patient.customCode})在 ${date} 的预约(共 ${marks.length} 条):`,
54
+ ...marks.map((m, i) => {
55
+ const start = normTime(m.markStartTime).slice(11, 16)
56
+ const end = normTime(m.markEndTime).slice(11, 16)
57
+ const time = end ? `${start}~${end}` : start
58
+ const itemText = parseItemTitles(m.treatItemJson).join("、")
59
+ const parts = [
60
+ `${time}(${m.markTimeLength ?? "?"}分钟)`,
61
+ `医生:${docMap.get(String(m.doctorId)) ?? m.doctorId}`,
62
+ `状态:${markStatusName(m)}`,
63
+ itemText ? `项目:${itemText}` : null,
64
+ `id:${m.id}`,
65
+ ].filter(Boolean)
66
+ return ` ${i + 1}. ${parts.join(" ")}`
67
+ }),
68
+ marks.length === 0 ? " (无预约记录)" : null,
69
+ ].filter(Boolean)
70
+
71
+ out.success(
72
+ "reservation list",
73
+ env,
74
+ { date, patient: { customCode: patient.customCode, customName: patient.customName }, reservations: marks },
75
+ lines.join("\n")
76
+ )
77
+ })
78
+ }
@@ -0,0 +1,30 @@
1
+ // commands/reservation/patients.mjs — 患者搜索(用于获取 customCode)
2
+ import { addCommonOptions, assertValueNotOptionLike } from "../../lib/options.mjs"
3
+ import { buildContext } from "../../lib/context.mjs"
4
+ import { searchPatients } from "../../api/custom.mjs"
5
+
6
+ /** @param {import("commander").Command} topic */
7
+ export function register(topic) {
8
+ addCommonOptions(
9
+ topic
10
+ .command("patients")
11
+ .description("搜索患者(姓名/手机号/病历号),用于获取 customCode")
12
+ .requiredOption("--keyword <关键词>", "搜索关键词:姓名 / 手机号 / 病历号")
13
+ ).action(async function () {
14
+ const opts = this.opts()
15
+ const { env, out, http } = buildContext(this)
16
+ assertValueNotOptionLike(opts.keyword, {
17
+ option: "--keyword",
18
+ hint: '例:--keyword 张三(姓名/手机号/病历号);传空字符串 "" 可列出全部患者',
19
+ })
20
+ const res = await searchPatients(http, { keyword: opts.keyword })
21
+ const list = res.data?.list ?? []
22
+ const total = res.data?.total ?? list.length
23
+ const lines = [
24
+ `匹配「${opts.keyword}」共 ${total} 条${total > list.length ? `(显示前 ${list.length} 条)` : ""}:`,
25
+ ...list.map((p, i) => ` ${i + 1}. ${p.customName ?? "-"} 手机:${p.customPhone ?? "-"} 病历号:${p.codeNum ?? "-"} 编码:${p.customCode}`),
26
+ ]
27
+ if (list.length === 0) lines.push(" (无匹配患者)")
28
+ out.success("reservation patients", env, { keyword: opts.keyword, total, patients: list }, lines.join("\n"))
29
+ })
30
+ }
@@ -0,0 +1,198 @@
1
+ # saas-cli 使用手册
2
+
3
+ > **目标读者:会用命令行的使用者** — 无需编程经验,按示例照抄即可完成查询和创建预约。
4
+ > 遇到报错请先看第五节的"常见问题";纯技术同学(写代码对接接口)请看 `api/接口说明.md`。
5
+ > 文中不认识的词(命令行、token、参数……)查 `README.md` 末尾的"名词表"。
6
+
7
+ 一个命令行小工具:不用打开网页界面,就能查医生 / 患者 / 预约项目,并创建预约。适合后端同学造测试数据、联调排查。
8
+
9
+ ---
10
+
11
+ ## 一、安装(1 分钟)
12
+
13
+ 1. 安装 [Node.js](https://nodejs.org) 22.12 或更高版本(命令行执行 `node -v` 能输出版本号即可)
14
+ 2. 全局安装本工具:
15
+
16
+ ```bash
17
+ npm install -g @lichidental/saas-cli
18
+ ```
19
+
20
+ 装完在任何目录都能直接用 `saas-cli` 命令;也可以不安装,用 `npx @lichidental/saas-cli <命令>` 临时跑。
21
+
22
+ (本仓库开发者:在项目根目录 `npm install` 后,下文所有 `saas-cli` 可用 `node cli.mjs` 替代。)
23
+
24
+ ## 二、登录(配置一次即可)
25
+
26
+ 工具需要一个登录凭据(token)才能调接口。**推荐用登录命令,两步完成**:
27
+
28
+ ```bash
29
+ # 第 1 步:打开授权页(需先在浏览器登录 SaaS 系统)
30
+ saas-cli auth login
31
+
32
+ # 在授权页上点「复制 token」,回到命令行执行第 2 步(把 <token> 换成复制的内容):
33
+ saas-cli auth login --token <token>
34
+ ```
35
+
36
+ 第 2 步会自动**验证** token 是否有效,通过后保存到本机;以后所有命令自动使用它。
37
+
38
+ 常用管理命令:
39
+
40
+ ```bash
41
+ saas-cli auth status # 查看登录状态(加 --verify 可联网验证 token 有效性)
42
+ saas-cli auth logout # 退出登录(删除本机凭证)
43
+ ```
44
+
45
+ **其他登录方式**(任选;token 优先级:命令行 `--token` > 环境变量 `SAAS_TOKEN` > 配置文件):
46
+
47
+ ```bash
48
+ # 从剪贴板直接管道输入(PowerShell;先在授权页复制 token)
49
+ powershell -c Get-Clipboard | saas-cli auth login --token -
50
+
51
+ # 临时使用不落盘:每次命令临时传 --token,或设置环境变量 SAAS_TOKEN
52
+ set SAAS_TOKEN=你的token # CMD(仅当前窗口有效)
53
+ $env:SAAS_TOKEN="你的token" # PowerShell(仅当前窗口有效)
54
+ export SAAS_TOKEN=你的token # Git Bash(仅当前窗口有效)
55
+ ```
56
+
57
+ > 授权页还没上线前,可手工获取 token:浏览器登录 SaaS 系统 → 按 F12 → Application(应用)→ Local Storage → 找到 `userInfo` → 复制 `token` 字段的值;或向后端同学要一个。拿到后同样用 `saas-cli auth login --token <token>` 保存。
58
+
59
+ 查看当前配置(兼容命令):
60
+
61
+ ```bash
62
+ saas-cli config show
63
+ ```
64
+
65
+ ## 三、5 分钟上手
66
+
67
+ ```bash
68
+ # 1. 看看有哪些医生(顺便验证 token 是否可用)
69
+ saas-cli reservation doctors
70
+
71
+ # 2. 搜一个患者(关键词可以是姓名/手机号/病历号)
72
+ saas-cli reservation patients --keyword 张
73
+
74
+ # 3. 不实际创建,先预览一下将提交的内容
75
+ saas-cli reservation create --patient 张三 --doctor 李医生 --time "2026-11-05 10:00" --items 补牙 --dry-run
76
+
77
+ # 4. 确认没问题,去掉 --dry-run 真正创建
78
+ saas-cli reservation create --patient 张三 --doctor 李医生 --time "2026-11-05 10:00" --items 补牙
79
+
80
+ # 5. 回查确认
81
+ saas-cli reservation list --patient 张三 --date 2026-11-05
82
+ ```
83
+
84
+ ## 四、命令详解
85
+
86
+ ### 通用参数(所有命令可用)
87
+
88
+ | 参数 | 说明 |
89
+ | ----------------------- | ------------------------------------------------ |
90
+ | `--env test\|pre\|prod` | 目标环境,默认 `test` |
91
+ | `--token <token>` | 临时指定 token(覆盖配置) |
92
+ | `--json` | 输出结构化 JSON(给程序/AI 解析用;默认是人读摘要) |
93
+ | `--verbose` | 打印请求/响应详情(token 打码),排查问题用 |
94
+
95
+ `saas-cli --help` 看全部命令;`saas-cli reservation create --help` 看单个命令帮助。
96
+
97
+ ### reservation create — 新建预约
98
+
99
+ ```bash
100
+ saas-cli reservation create --patient 张三 --doctor 李医生 --time "2026-11-05 10:00" --duration 30 --items "补牙,口腔检查" --remark "备注内容"
101
+ ```
102
+
103
+ | 参数 | 说明 |
104
+ | -------------------------- | -------------------------------------------------------------- |
105
+ | `--patient <姓名/手机号/病历号>` | 自动搜索患者;**重名时会列出候选并报错**,此时用下面两种方式指定 |
106
+ | `--patient-code <编码或病历号>` | 直接指定患者编码(如 YONGCE26073100001)或病历号(如 YONGCE26073100142),跳过搜索 |
107
+ | `--doctor <姓名>` | 自动匹配医生(同理,重名会列候选) |
108
+ | `--doctor-id <userUid>` | 直接指定医生,跳过搜索 |
109
+ | `--time <时间>` | 预约开始时间,如 `"2026-11-05 10:00"`;省略 = 当前时间 |
110
+ | `--duration <分钟>` | 时长,15~480 之间,默认 30 |
111
+ | `--items <名称列表>` | 项目名称,逗号分隔(中英文逗号都行),如 `"洗牙,补牙"` |
112
+ | `--items-json <json>` | 直接给项目 JSON(跳过名称匹配),格式见 `--help` |
113
+ | `--first <1\|0>` | 1=初诊,0=复诊;不传则按患者历史自动判断 |
114
+ | `--dry-run` | 只打印请求体,不创建(强烈建议先用它预览) |
115
+ | `--sms` | 创建后给患者发预约短信(**默认不发**,会给真实手机发短信) |
116
+ | `--yes` | 生产环境(prod)写操作必须加,防止误操作 |
117
+
118
+ 创建成功会输出:
119
+
120
+ ```
121
+ ✓ 预约创建成功(环境:test)
122
+ 患者 张三(YONGCE26073100001)
123
+ 医生 李医生(userUid:26051131994)
124
+ 时间 2026-11-02 09:00:00 起 15 分钟
125
+ 就诊类型 复诊
126
+ 项目 口腔检查
127
+ 短信 未发送(如需发送加 --sms)
128
+ 预约ID 1260618
129
+ ```
130
+
131
+ ### reservation list — 查询预约
132
+
133
+ ```bash
134
+ saas-cli reservation list --patient 张三 # 查今天
135
+ saas-cli reservation list --patient 张三 --date 2026-10-15 # 查指定日期
136
+ saas-cli reservation list --patient-code YONGCE26073100001 --date 2026-10-15
137
+ saas-cli reservation list --patient YONGCE26073100142 --date 2026-10-15 # 病历号也可以
138
+ ```
139
+
140
+ ### reservation doctors / patients / items — 查询辅助
141
+
142
+ ```bash
143
+ saas-cli reservation doctors # 医生列表(拿 userUid)
144
+ saas-cli reservation patients --keyword 张 # 患者列表(拿 customCode)
145
+ saas-cli reservation items --keyword 洁 # 项目列表(名称参考),不加参数列全部
146
+ ```
147
+
148
+ ### auth — 登录管理(推荐)
149
+
150
+ ```bash
151
+ saas-cli auth login # 打开授权页(按提示获取 token)
152
+ saas-cli auth login --token <token> # 验证并保存 token
153
+ saas-cli auth status # 查看登录状态(--verify 联网验证)
154
+ saas-cli auth logout # 退出登录
155
+ ```
156
+
157
+ ### config — 配置管理(兼容旧用法)
158
+
159
+ ```bash
160
+ saas-cli config set-token --env test --token xxx # 保存 token(不做验证)
161
+ saas-cli config show # 查看配置(token 打码)
162
+ ```
163
+
164
+ ## 五、常见问题
165
+
166
+ **Q:报 `✗ 认证失败:登录失效,请重新登录!`(退出码 3)**
167
+ token 过期了。重新走一遍第二节的登录流程(`saas-cli auth login` → 复制 → `saas-cli auth login --token <新token>`)即可。
168
+
169
+ **Q:报 `✗ 环境 test 未配置 token`**
170
+ 还没登录。按第二节操作(`saas-cli auth login`);也可用命令加 `--token` 或环境变量 `SAAS_TOKEN` 临时传入。
171
+
172
+ **Q:报 `患者「张三」匹配到 N 条`**
173
+ 重名。用 `--patient-code <编码>` 指定具体哪一个(先用 `reservation patients --keyword 张三` 查看编码);医生重名同理,用 `--doctor-id`。
174
+
175
+ **Q:报 `✗ 未找到患者「XXX」`**
176
+ 关键词没匹配到。换个关键词(试试手机号或病历号),或先用 `reservation patients --keyword` 确认拼写。
177
+
178
+ **Q:报 `✗ 版本校验失败`(status 999)**
179
+ 后端发现客户端版本号过旧。工具会自动读取目标环境当前版本号规避;若持续出现,检查本机能否访问 `https://test.saas.lichidental.com/index.html`(公司网络是否有限制),然后再试。
180
+
181
+ **Q:报 `✗ 网络请求失败`**
182
+ 网络不通或超时(15 秒)。检查网络/代理后重试;查询类命令可直接重试,创建命令重试前先用 `reservation list` 回查确认上次是否已成功(创建命令每执行一次都会新建一条,确认上次没成功再重试,避免建重)。
183
+
184
+ **Q:命令没反应/时间不对**
185
+ `--time` 用的是本机时区的时间。格式必须是 `"2026-11-05 10:00"` 或带秒 `"2026-11-05 10:00:00"`。
186
+
187
+ **Q:想看接口实际发出去的原始请求**
188
+ 加 `--verbose`,会打印请求地址、请求体、原始响应(token 自动打码)。
189
+
190
+ **Q:输出的 JSON 想直接给程序用**
191
+ 加 `--json`,stdout 只有一段 JSON;错误也是 JSON 格式(方便脚本判断 `ok` 字段和退出码)。
192
+
193
+ ## 六、安全须知
194
+
195
+ - `reservation create` 是**真实写操作**;生产环境(prod)必须加 `--yes`,建议先用 `--dry-run` 预览
196
+ - `--sms` 会**真实给患者手机发短信**,默认关闭,确认需要时再加
197
+ - token 保存在本机配置文件中(Windows: `%APPDATA%\saas-cli\config.json`,权限设为仅本人可读;旧版 `create-saas-cli` 目录会自动兼容迁移);如不希望落盘,就用环境变量或每次 `--token` 传入
198
+ - 不要把 token 提交到代码仓库或发给无关人员