-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design
- 客户端不再独立承担全部语音理解和提醒编排。
- 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
- 音频文件通过 HTTP 上传到服务端。
- 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
- 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
- 日程最终落到单一
schedules表中,状态和关联信息一并保存。 - 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。
MVP 要验证三件事:
- 用户能不能快速创建日程。
- 系统能不能在不同场景下使用不同提醒方式。
- 语音创建能不能稳定转成可确认、可执行的日程。
触发方式:
- 应用内弹窗。
- 页面内高亮提示。
- 伴随声音或震动。
适用场景:
- 用户正在使用 App。
- 可以给出最完整的上下文。
- 不需要系统级强打断。
触发方式:
- 系统级全局弹窗。
- 系统通知栏提醒。
适用场景:
- App 仍保持在线。
- 需要由系统层接管提醒触达。
- 可以通过 WS 收到实时控制消息。
客户端根据自身能力自行决定是否降级为普通通知。
日程支持两种入口:
- 手动创建。
- 语音创建。
二者共用一个表单。
语音创建流程:
- 客户端录音。
- 客户端通过 HTTP 上传音频文件。
- 服务端返回上传受理结果。
- 服务端完成 ASR。
- 服务端完成 LLM 结构化提取。
- 服务端通过 WS 推送结构化日程草稿。
- 客户端弹出统一表单。
- 用户补全信息并确认。
- 客户端提交日程最终结果。
- 如果日程包含时间,服务端做时间冲突检测并返回提示。
- 创建成功后,服务端和客户端分别进入自己的提醒准备状态。
日程允许两种主类型:
| 形态 | schedule_type |
必要信息 | 触发条件 |
|---|---|---|---|
| 时间类 | time |
start_time |
进入时间提醒窗口 |
| 地点类 | location |
latitude + longitude
|
用户进入地理围栏 |
语音创建时,schedule_type 由 LLM 根据用户语义做意图识别后输出。手动创建时,由前端根据用户选择或填写内容确定。
schedule_type 只表示主意图类型,时间和地点同时存在时仍按 time 落库,是否触发地点提醒由字段本身判断。
表单内部支持:
- 地理位置选择。
- 地理围栏设置。
- 默认提醒参数补全。
默认值建议:
- 时间提前提醒:
15min - 地理围栏半径:
100m
创建日程时如果填写了时间,必须做时间冲突检测。
如果目标时间段已有日程,服务端返回:
- 冲突提示。
- 冲突项列表。
- 是否允许继续创建的建议。
冲突检测是提示,不一定是硬拦截。
客户端负责:
- 界面展示。
- WebSocket 长连接。
- 音频录制。
- 音频上传。
- 表单编辑和二次确认。
- 本地日程缓存。
- 系统日程和系统闹钟创建、查询与删除。
- 前台弹窗提醒。
- 后台系统弹窗/通知提醒。
- 位置信息上报。
- 根据前后台状态和系统可用性选择具体提醒通道。
服务端负责:
- 音频文件接收。
- ASR 调用。
- LLM 结构化提取。
- WebSocket 消息分发。
- 日程冲突检测。
- 日程状态监控。
- 地点与时间窗口判断。
- 提醒控制消息下发。
- 提醒通道选择由客户端自行完成。
- 第三方 ASR 服务。
- 大模型服务。
- 第三方地图 SDK。
- Android 系统提醒能力。
职责:
- 显示日程列表。
- 显示创建/编辑表单。
- 显示语音解析结果。
- 显示冲突提示。
职责:
- 录音。
- 音频格式转换。
- 发起上传。
职责:
- 建立和维护连接。
- 接收结构化草稿。
- 接收提醒控制消息。
- 上传位置信息。
- 上传日程确认结果。
职责:
- 缓存日程。
- 缓存解析结果。
- 缓存提醒状态。
- 缓存提醒通道状态。
职责:
- 前台弹窗。
- 后台浮窗。
- 系统通知。
- 根据当前应用状态决定提醒展示方式。
职责:
- 地点搜索。
- 地点确认。
- 地理围栏设置。
- 实时位置上报。
职责:
- 接收音频文件。
- 校验格式和大小。
- 生成处理任务 ID。
职责:
- 调用 ASR。
- 调用 LLM。
- 识别
schedule_type。 - 生成结构化草稿。
职责:
- 建立客户端会话。
- 维护设备在线状态。
- 分发解析结果和提醒控制消息。
- 接收位置上报。
职责:
- 创建日程。
- 编辑日程。
- 查询日程。
- 冲突检测。
职责:
- 监听日程时间。
- 监听地理位置。
- 判断时间、空间以及组合触发条件。
- 进入提醒窗口后先触发系统引用检查。
- 根据检查结果决定是否触发软件提醒。
- 触发系统日程和系统闹钟删除指令。
职责:
- 根据上报位置和日程坐标计算距离。
- 判断是否进入地理围栏。
MVP 阶段只保留一张核心业务表。
用途:
- 存储日程本体。
- 存储日程状态。
- 存储地点与提醒关联信息。
- 存储地理围栏布防状态。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
text | 主键,日程 ID |
user_id |
text | 默认用户 ID |
source_mode |
text |
manual / voice
|
schedule_type |
text |
time / location
|
status |
text |
scheduled / done / deleted
|
title |
text | 标题 |
notes |
text | 备注 |
start_time |
text | 开始时间,ISO-8601,可以为空 |
end_time |
text | 结束时间,可为空 |
timezone |
text | 时区,可为空 |
location_name |
text | 地点名称 |
location_address |
text | 地点地址 |
latitude |
real | 纬度 |
longitude |
real | 经度 |
geofence_radius_meters |
integer | 地理围栏半径,默认 100m |
geofence_armed |
integer | 地理围栏是否已布防,默认由服务端根据创建时用户位置计算 |
time_remind_offset_minutes |
integer | 时间提前提醒,默认 15min |
time_triggered_at |
text | 时间提醒触发时间,可为空 |
geo_triggered_at |
text | 地点提醒触发时间,可为空 |
created_at |
text | 创建时间 |
updated_at |
text | 更新时间 |
- 核心字段尽量扁平化。
- 语音解析草稿只通过 WS 传递,不落库。
- 不保存系统侧兜底对象。
- 时间提醒和地理提醒可以同时存在。
- 只保留一张主表,MVP 不拆分额外业务表。
-
status只表达日程本体是否还有效,不表达监听中、已触发、已过期等过程状态。 - MVP 不保存用户自定义的重要程度,提醒方式由应用前后台状态、网络状态和时间/空间窗口决定。
- 冲突检测结果只在接口响应中返回,不写入
schedules表。 -
start_time和地点信息都允许为空,但二者不能同时为空。 -
geofence_armed用于避免“用户在目标地点创建日程后立刻触发位置提醒”。 - 最近一次位置只在会话内或内存中计算,不落库。
-
schedule_type是前端表单必填项控制和后端校验的依据,不再使用全天字段区分业务类型。
建议状态流转:
scheduled -> done
scheduled -> deleted
状态说明:
| 状态 | 含义 |
|---|---|
scheduled |
已创建,等待提醒或正在监听 |
done |
用户已确认完成 |
deleted |
用户删除 |
不进入 status 的过程信息:
| 信息 | 处理方式 |
|---|---|
| 草稿待确认 | 语音解析结果通过 WS 推给前端,用户确认前不创建正式日程 |
| 时间监听中 | 根据 start_time、time_remind_offset_minutes 和当前时间动态判断 |
| 地理监听中 | 根据经纬度、围栏半径、最近位置和 geofence_armed 动态判断 |
| 已触发提醒 | 写入 time_triggered_at 或 geo_triggered_at
|
| 已过期 | 根据 start_time 或 end_time 动态判断 |
| 创建冲突 | 只在接口响应中返回 conflicts,不持久化 |
HTTP 接口统一使用 JSON 响应。
成功响应:
{
"accepted": true
}失败响应:
{
"accepted": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "start_time"
}
}
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
accepted |
boolean | 请求是否被服务端接受 |
error.code |
string | 程序可识别的错误码 |
error.message |
string | 面向前端展示或日志记录的错误说明 |
error.details |
object/null | 具体错误上下文,可为空 |
MVP 常用 HTTP 状态码:
| 状态码 | 含义 |
|---|---|
400 |
请求参数错误 |
404 |
资源不存在 |
409 |
幂等请求冲突 |
413 |
上传文件过大 |
415 |
文件格式不支持 |
500 |
服务端内部错误 |
502 |
第三方服务调用失败 |
WS 消息失败时,不依赖 HTTP 状态码,统一通过 type 和 ok=false 或 *.error 表达。
POST /api/v1/audio/upload
- 上传语音文件。
- 生成解析任务。
- 触发后续 ASR + LLM 流程。
multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id |
string | 是 | 设备 ID |
request_id |
string | 是 | 本次上传请求 ID |
audio_file |
file | 是 | 原始音频 |
audio_format |
string | 是 |
wav / mp3 / m4a
|
duration_ms |
integer | 否 | 时长 |
{
"accepted": true,
"job_id": "job_audio_001",
"request_id": "req_audio_001"
}文件格式不支持:
{
"accepted": false,
"error": {
"code": "UNSUPPORTED_AUDIO_FORMAT",
"message": "音频格式不支持",
"details": {
"audio_format": "aac",
"supported_formats": ["wav", "mp3", "m4a"]
}
}
}文件过大:
{
"accepted": false,
"error": {
"code": "AUDIO_FILE_TOO_LARGE",
"message": "音频文件过大",
"details": {
"max_size_mb": 20
}
}
}任务创建失败:
{
"accepted": false,
"error": {
"code": "AUDIO_JOB_CREATE_FAILED",
"message": "语音解析任务创建失败",
"details": null
}
}- 接口只负责受理和入队。
- 真正的结构化结果通过 WS 返回。
POST /api/v1/schedules/upsert
- 手动创建日程。
- 语音表单确认后提交日程。
- 写入最终状态。
- 执行时间冲突检测。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
request_id |
string | 是 | 幂等 ID |
schedule_id |
string | 否 | 编辑时传 |
source_mode |
string | 是 |
manual / voice
|
schedule_type |
string | 是 |
time / location
|
title |
string | 是 | 标题 |
notes |
string/null | 否 | 备注 |
start_time |
string/null | 否 | 开始时间;只有地点的日程可为空 |
end_time |
string/null | 否 | 结束时间 |
timezone |
string/null | 否 | 时区;有时间日程时建议填写 |
location_name |
string/null | 否 | 地点名称 |
location_address |
string/null | 否 | 地点地址 |
latitude |
number/null | 否 | 纬度 |
longitude |
number/null | 否 | 经度 |
geofence_radius_meters |
integer/null | 否 | 地理围栏半径,默认 100
|
geofence_armed |
boolean/null | 否 | 地理围栏是否已布防,不传则服务端计算 |
time_remind_offset_minutes |
integer/null | 否 | 时间提醒提前量,默认 15
|
{
"accepted": true,
"schedule_id": "schedule_001",
"schedule_type": "time",
"status": "scheduled",
"conflicts": [],
"geofence_armed": true
}{
"accepted": true,
"schedule_id": "schedule_001",
"schedule_type": "time",
"status": "scheduled",
"conflicts": [
{
"schedule_id": "schedule_older",
"title": "已有日程",
"start_time": "2026-07-28T15:00:00+08:00",
"end_time": "2026-07-28T16:00:00+08:00"
}
],
"geofence_armed": true
}请求参数不合法:
{
"accepted": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "schedule_type",
"reason": "schedule_type 为 time 时 start_time 必填;为 location 时 latitude 和 longitude 必填"
}
}
}时间范围不合法:
{
"accepted": false,
"error": {
"code": "INVALID_TIME_RANGE",
"message": "结束时间不能早于开始时间",
"details": {
"start_time": "2026-07-28T16:00:00+08:00",
"end_time": "2026-07-28T15:00:00+08:00"
}
}
}结束时间存在但开始时间为空:
{
"accepted": false,
"error": {
"code": "INVALID_TIME_RANGE",
"message": "只有结束时间但没有开始时间",
"details": {
"start_time": null,
"end_time": "2026-07-28T15:00:00+08:00"
}
}
}编辑的日程不存在:
{
"accepted": false,
"error": {
"code": "SCHEDULE_NOT_FOUND",
"message": "日程不存在",
"details": {
"schedule_id": "schedule_missing"
}
}
}幂等请求冲突:
{
"accepted": false,
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "同一个 request_id 对应的请求内容不一致",
"details": {
"request_id": "req_schedule_001"
}
}
}- 手动创建和语音确认都走同一接口。
- 时间冲突只给提示,不默认阻断。
-
schedule_type=time时,前端表单要求填写时间,地点可选。 -
schedule_type=location时,前端表单要求填写地点,时间可选。 - 如果用户同时填写时间和地点,则
schedule_type仍按time处理,不需要额外的中间枚举。 - 只有
start_time存在时才做时间冲突检测。 - 只有经纬度存在时才做地理围栏监听。
- 如果
geofence_armed不传,服务端根据最近一次位置上报与目标地点距离计算默认值。
GET /api/v1/schedules
- 获取当前用户的所有日程数据。
- 支持前端进入页面后初始化列表。
- 支持前端恢复本地状态和服务端状态对齐。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 否 | 默认用户 ID;MVP 无账号体系时可不传 |
status |
string | 否 | 可选:scheduled / done / deleted
|
include_deleted |
boolean | 否 | 是否包含已删除数据,默认 false
|
{
"accepted": true,
"schedules": [
{
"id": "schedule_001",
"user_id": "default_user",
"source_mode": "voice",
"schedule_type": "time",
"status": "scheduled",
"title": "开会",
"notes": null,
"start_time": "2026-07-29T15:00:00+08:00",
"end_time": null,
"timezone": "Asia/Shanghai",
"location_name": "陆家嘴",
"location_address": null,
"latitude": 31.2451,
"longitude": 121.5067,
"geofence_radius_meters": 100,
"geofence_armed": true,
"time_remind_offset_minutes": 15,
"time_triggered_at": null,
"geo_triggered_at": null,
"created_at": "2026-07-28T12:00:00+08:00",
"updated_at": "2026-07-28T12:00:00+08:00"
}
]
}参数不合法:
{
"accepted": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "status"
}
}
}- MVP 阶段默认查询
default_user的数据。 - 默认只返回
scheduled和done。 -
include_deleted=true时才返回deleted数据。 - 返回结果按
start_time asc nulls last, created_at desc排序。
ws://<host>/ws?device_id=xxx
- 保持设备在线。
- 回传语音结构化结果。
- 下发提醒控制消息。
- 下发系统日程删除指令。
- 下发系统闹钟删除指令。
- 接收位置信息和确认事件。
客户端进入界面后先发:
{
"type": "session.hello",
"device_id": "android_abc123",
"app_version": "1.0.0"
}服务端返回:
{
"type": "session.ready",
"device_id": "android_abc123",
"server_time": "2026-07-28T12:00:00Z"
}连接失败或鉴权失败时,服务端返回错误消息后关闭连接:
{
"type": "session.error",
"ok": false,
"error": {
"code": "INVALID_DEVICE_ID",
"message": "设备 ID 不合法",
"details": null
}
}{
"type": "voice.parse.result",
"request_id": "req_audio_001",
"job_id": "job_audio_001",
"status": "ready_for_confirmation",
"draft": {
"schedule_type": "time",
"title": "开会",
"start_time": "2026-07-29T15:00:00+08:00",
"end_time": null,
"timezone": "Asia/Shanghai",
"location_name": "陆家嘴",
"geofence_radius_meters": 100,
"time_remind_offset_minutes": 15
},
"missing_fields": ["location_address"],
"ambiguous_fields": [],
"needs_confirmation": true
}ASR 或 LLM 处理失败时,服务端通过 WS 推送失败结果:
{
"type": "voice.parse.result",
"request_id": "req_audio_001",
"job_id": "job_audio_001",
"status": "failed",
"error": {
"code": "VOICE_PARSE_FAILED",
"message": "语音解析失败,请重新录音或手动创建",
"details": {
"stage": "asr"
}
}
}- 弹出统一表单。
- 默认填充结构化字段。
- 允许用户补全地点、时间和提醒参数。
{
"type": "location.report",
"schedule_scope": "current",
"latitude": 31.2451,
"longitude": 121.5067,
"accuracy": 18,
"timestamp": "2026-07-28T12:01:00+08:00"
}{
"type": "location.report.ack",
"ok": true
}失败响应:
{
"type": "location.report.ack",
"ok": false,
"error": {
"code": "INVALID_LOCATION",
"message": "位置信息不合法",
"details": {
"field": "latitude"
}
}
}- 服务端根据当前位置计算日程距离。
- 如果日程包含地点且命中围栏,先检查系统引用是否仍存在。
- 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。
服务端只下发“该提醒了”,具体走哪种展示通道由客户端根据前后台状态自行决定。
{
"type": "reminder.control",
"schedule_id": "schedule_001",
"reason": "time_window_reached",
"action": "show"
}客户端执行成功时回传:
{
"type": "reminder.control.ack",
"schedule_id": "schedule_001",
"ok": true
}客户端执行失败时回传:
{
"type": "reminder.control.ack",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "REMINDER_DISPLAY_FAILED",
"message": "提醒展示失败"
}
}在服务端准备下发软件提醒或删除系统兜底提醒之前,先确认用户是否已经在系统日程或系统闹钟中手动删除了对应事项。
如果用户已经删除系统侧提醒,说明用户可能已经表达“不再需要提醒”的意图,服务端不应继续下发应用内弹窗、系统级全局弹窗或删除指令。
服务端不能直接查询 Android 系统日程或系统闹钟,这个检查必须由客户端完成,再通过 WS 回传结果。
该指令作为独立保留的系统侧存在性检查流程,不因提醒策略调整而删除。
{
"type": "system.refs.check",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "before_reminder_control"
}- 根据
system_schedule_ref_id查询系统日程是否仍存在。 - 根据
system_alarm_ref_id查询系统闹钟是否仍存在。 - 将查询结果通过 WS 回传服务端。
- 只做存在性检查,不重新创建系统日程或系统闹钟。
{
"type": "system.refs.check.result",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001",
"system_schedule_exists": false,
"system_alarm_ref_id": "system_alarm_001",
"system_alarm_exists": false,
"checked_at": "2026-07-28T14:45:00+08:00"
}查询失败时,客户端仍需回传失败原因:
{
"type": "system.refs.check.result",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SYSTEM_REF_CHECK_FAILED",
"message": "系统引用检查失败",
"details": {
"reason": "permission_denied"
}
}
}- 如果
system_schedule_exists=true或system_alarm_exists=true,说明兜底提醒仍存在,继续执行软件提醒和重复提醒删除流程。 - 如果
system_schedule_exists=false且system_alarm_exists=false,说明用户可能已经手动删除系统侧提醒,服务端取消本次软件提醒。 - MVP 阶段不新增提醒关闭字段,服务端可将该日程状态更新为
deleted,并停止后续监听。 - 如果客户端超时未响应,服务端按当前在线状态继续提醒,避免漏提醒。
{
"type": "system.schedule.delete",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"reason": "ws_connected_and_time_near"
}- 删除系统日程。
- 取消重复通知。
- 继续保留业务日程本体。
{
"type": "system.schedule.delete.ack",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001"
}删除失败时:
{
"type": "system.schedule.delete.ack",
"schedule_id": "schedule_001",
"ok": false,
"system_schedule_ref_id": "system_schedule_001",
"error": {
"code": "SYSTEM_SCHEDULE_DELETE_FAILED",
"message": "系统日程删除失败",
"details": {
"reason": "not_found"
}
}
}{
"type": "system.alarm.delete",
"schedule_id": "schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "ws_connected_and_time_near"
}- 删除系统闹钟。
- 取消重复闹钟提醒。
- 继续保留业务日程本体。
{
"type": "system.alarm.delete.ack",
"schedule_id": "schedule_001",
"ok": true,
"system_alarm_ref_id": "system_alarm_001"
}删除失败时:
{
"type": "system.alarm.delete.ack",
"schedule_id": "schedule_001",
"ok": false,
"system_alarm_ref_id": "system_alarm_001",
"error": {
"code": "SYSTEM_ALARM_DELETE_FAILED",
"message": "系统闹钟删除失败",
"details": {
"reason": "not_found"
}
}
}作用:
- 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
- 关闭后续监听和所有提醒。
- 与
system.refs.check不同,这不是系统侧存在性检查,而是用户显式完成动作。
{
"type": "schedule.confirmed",
"schedule_id": "schedule_001",
"confirmed": true,
"timestamp": "2026-07-28T12:05:00+08:00"
}- 取消监听。
- 终止后续提醒。
- 如系统日程或系统闹钟仍存在,按需删除。
{
"type": "schedule.confirmed.ack",
"schedule_id": "schedule_001",
"ok": true
}失败响应:
{
"type": "schedule.confirmed.ack",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SCHEDULE_CONFIRM_FAILED",
"message": "日程确认失败",
"details": {
"reason": "schedule_not_found"
}
}
}规则:
- 只有
start_time存在的日程才参与时间监听。 - 时间到达前进入监测窗口。
- 默认提前
15min。 - 进入窗口后优先判断 WS 在线状态和前后台状态。
- 如果 WS 在线,服务端先下发
system.refs.check。 - 如果客户端确认系统日程和系统闹钟都已不存在,服务端取消本次软件提醒并停止监听。
- 如果系统侧兜底提醒仍存在,服务端继续下发
reminder.control,并按需下发系统日程和系统闹钟删除指令。
规则:
- 只有存在经纬度的日程才参与空间监听。
- 默认围栏半径
100m。 - 客户端持续上传位置,服务端判定是否进入围栏。
- 创建日程时,如果用户当前位置已经在目标围栏内,服务端将
geofence_armed=false。 - 当用户离开目标围栏后,服务端将
geofence_armed=true。 - 只有
geofence_armed=true且用户再次进入围栏时,才允许触发地点提醒。 - 如果创建时无法取得用户当前位置,服务端默认
geofence_armed=true,避免错过后续进入提醒。
schedule_type |
日程形态 | 触发规则 |
|---|---|---|
time |
只有时间 | 时间窗口到达后触发 |
location |
只有地点 |
geofence_armed=true 且用户进入地理围栏后触发 |
如果日程同时包含时间和地点,则 schedule_type 仍为 time,但提醒执行时同时参考时间窗口和地理围栏条件。
满足以下任一条件时取消监听:
- 用户通过确认接口标记已完成。
- 有时间的日程时间已过。
- 日程被删除。
- 客户端确认系统日程和系统闹钟均已被用户手动删除。
- 只有
start_time存在时才检查时间区间重叠。 - 如果已有日程落在同一时间段,返回冲突提示。
- 冲突结果包含已有日程的标题、时间和 ID。
- 只有地点、没有时间的日程不做时间冲突检测。
- 提示用户“当前时段已有日程”。
- 用户可继续保存,也可修改时间。
- 冲突不是强制失败,但必须明确提示。