diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index 0735a84..020e0c4 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -21,7 +21,7 @@ - 会员、储值、套餐、复杂 CRM。 - 宠主账号内历史报告汇总。 - 完整经营分析看板。 -- 「到店不排队」等无容量模型承诺,统一文案为「在线选时段,到店更省心」。 +- 即使已有预约容量模型,也没有排队/叫号与准点履约保证;对外仍不承诺「到店不排队」,统一文案为「在线选时段,到店更省心」。 --- @@ -121,7 +121,7 @@ | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | |----|------|----------|-------|------|----------|------| -| **G1** | **无容量模型不写「排队」** | 全局检索「排队」「不排队」;宠主副标题等改为「在线选时段,到店更省心」 | Product Design / Customer Experience FE | ✅ 已实现(Batch A 文档冻结) | `rg "到店不排队|不排队" docs frontend/src` | 与《产品全方位优化说明》D2 一致 | +| **G1** | **无排队/叫号能力不承诺「不排队」** | 全局检索「排队」「不排队」;宠主副标题等改为「在线选时段,到店更省心」 | Product Design / Customer Experience FE | ✅ 已实现(Batch A 文档冻结,容量模型上线后仍成立) | `rg "到店不排队|不排队" docs frontend/src` | 与《产品全方位优化说明》D2 一致 | | **G2** | **对内录单表述(可选)** | 门店端新建预约入口文案是否改为「代客预约」由产品拍板;若改则全局统一 | Product Design | 🟡 待产品拍板 | — | 降低「预约」歧义 | --- diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 4b44d4b..264dcb5 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -1,6 +1,6 @@ -{"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store","test":"StoreControllerTest","evidence":"anchored"} +{"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","booking_capacity","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store","test":"StoreControllerTest","evidence":"anchored"} {"id":"entity:user","type":"Entity","name":"User","nameZh":"用户","domain":"user","fields":["id","username","password","name","phone","avatar","store_id","role","wechat_openid","wechat_unionid","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/产品设计文档.md §三 t_user; docs/.agents/common-context.md 术语规则","test":"UserServiceLoginStrategyTest; UserJsonIgnoreTest","evidence":"anchored"} -{"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","status","store_id","user_id","customer_user_id","created_by_user_id","assigned_user_id","pet_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Appointment.java; backend/src/main/java/com/petstore/service/AppointmentService.java","doc":"docs/产品设计文档.md §三 t_appointment §5.2","test":"AppointmentServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest","evidence":"anchored"} +{"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","duration_minutes","status","store_id","user_id","customer_user_id","created_by_user_id","assigned_user_id","pet_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Appointment.java; backend/src/main/java/com/petstore/service/AppointmentService.java","doc":"docs/产品设计文档.md §三 t_appointment §5.2; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"AppointmentServiceTest; BookingCapacityServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest; BookingCapacityMigrationTest","evidence":"anchored"} {"id":"entity:report","type":"Entity","name":"Report","nameZh":"服务报告","domain":"report","fields":["id","appointment_id","remark","user_id","customer_user_id","author_staff_id","store_id","report_token","pet_name","service_type","appointment_time","staff_name","highlight_video_url","highlight_video_status","highlight_video_error","highlight_duration_sec","highlight_compose_mode","highlight_fail_reason","highlight_share_cover_url","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/Report.java; backend/src/main/java/com/petstore/service/ReportService.java","doc":"docs/产品设计文档.md §三 t_report §5.3; docs/API-报告成片字段契约.md","test":"ReportServiceTest; ReportControllerTest","evidence":"anchored"} {"id":"entity:report_lead","type":"Entity","name":"ReportLead","nameZh":"留资线索","domain":"lead","fields":["id","report_id","store_id","phone","pet_name","service_type","wechat_openid","wechat_unionid","reminder_type","remind_date","remind_status","unsubscribe_token","client_ip","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java; backend/src/main/java/com/petstore/entity/ReportLead.java","doc":"docs/本期主线-下一步产品优化点.md §C 留资","test":"ReportLeadControllerTest","evidence":"anchored"} {"id":"entity:business_event","type":"Entity","name":"BusinessEvent","nameZh":"不可变业务事件","domain":"platform","jpa":true,"fields":["id","event_id","event_type","event_version","store_id","store_customer_id","aggregate_type","aggregate_id","actor_user_id","actor_role","source","occurred_at","metadata_json","idempotency_key","create_time"],"systems":["Backend Core","Report Media Backend","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/BusinessEvent.java; backend/src/main/java/com/petstore/service/BusinessEventService.java","doc":"docs/ontology/objects.md#entity:business_event; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest","evidence":"anchored"} @@ -26,7 +26,7 @@ {"id":"event:store_registered","type":"Event","name":"StoreRegistered","nameZh":"门店入驻","domain":"store","payload":["storeId","bossUserId","inviteCode"],"emittedBy":["action:register_boss"],"evidence":"anchored"} {"id":"event:session_issued","type":"Event","name":"SessionIssued","nameZh":"签发会话令牌","domain":"auth","payload":["userId","storeId","role","exp"],"emittedBy":["action:register_boss","action:login","action:wx_phone_login"],"code":"backend/src/main/java/com/petstore/auth/SessionTokenService.java#issue","test":"SessionTokenServiceTest","evidence":"anchored"} {"id":"event:wx_bound","type":"Event","name":"WxBound","nameZh":"微信标识绑定","domain":"auth","payload":["userId","openid","unionid"],"emittedBy":["action:bind_wechat"],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored"} -{"id":"event:appointment_created","type":"Event","name":"AppointmentCreated","nameZh":"预约创建","domain":"appointment","payload":["storeId","storeCustomerId","aggregateId","actorUserId","actorRole","bookingOrigin","occurredAt"],"emittedBy":["action:create_appointment"],"appliesRules":["rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentCreated","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:appointment_created","type":"Event","name":"AppointmentCreated","nameZh":"预约创建","domain":"appointment","payload":["storeId","storeCustomerId","aggregateId","actorUserId","actorRole","bookingOrigin","durationMinutes","occurredAt"],"emittedBy":["action:create_appointment"],"appliesRules":["rule:BR-BE-001","rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentCreated","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored"} {"id":"event:appointment_status_changed","type":"Event","name":"AppointmentStatusChanged","nameZh":"预约状态迁移","domain":"appointment","payload":["storeId","storeCustomerId","aggregateId","fromStatus","toStatus","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service","action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus; backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; ReportServiceTest#createDoingAppointmentMarksDone; BusinessEventServiceTest","evidence":"anchored"} {"id":"event:service_started","type":"Event","name":"ServiceStarted","nameZh":"服务开始","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"BusinessEventServiceTest#statusToDoingRecordsGenericAndServiceStartedFacts","evidence":"anchored"} {"id":"event:service_completed","type":"Event","name":"ServiceCompleted","nameZh":"服务完成","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged; backend/src/main/java/com/petstore/service/ReportService.java#create","test":"AppointmentServiceTest; ReportServiceTest","evidence":"anchored"} @@ -41,7 +41,7 @@ {"id":"rule:BR-APPT-001","type":"Rule","name":"预约状态机","domain":"appointment","appliesTo":["action:start_service","action:transition_appointment_status","action:submit_report"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus","test":"AppointmentServiceTest","evidence":"anchored"} {"id":"rule:BR-APPT-002","type":"Rule","name":"预约跨店 403","domain":"appointment","appliesTo":["action:list_appointments","action:start_service","action:transition_appointment_status","action:get_appointment_detail"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java","test":"AppointmentControllerDetailTest","evidence":"anchored"} {"id":"rule:BR-APPT-003","type":"Rule","name":"预约客户归属与跨用户 403","domain":"appointment","appliesTo":["action:create_appointment","action:get_appointment_detail","action:transition_appointment_status"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; #detail; #updateStatus; backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","test":"AppointmentControllerIdentityTest; AppointmentControllerDetailTest#customerCannotViewOthersAppointment","evidence":"anchored"} -{"id":"rule:BR-APPT-004","type":"Rule","name":"预约时段校验:半小时档 + 营业时间 + 占用 + 已过","domain":"appointment","appliesTo":["action:create_appointment"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","doc":"docs/产品设计文档.md §5.2","evidence":"anchored"} +{"id":"rule:BR-APPT-004","type":"Rule","name":"真实预约容量:服务时长快照 + 跨桶容量 + 同店并发锁","domain":"appointment","appliesTo":["action:create_appointment","action:get_available_slots","action:create_schedule_block"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/BookingCapacityService.java; backend/src/main/java/com/petstore/service/ScheduleService.java#createBlock","doc":"docs/产品设计文档.md §5.2; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"BookingCapacityServiceTest; AppointmentServiceTest; BookingCapacityMigrationTest","evidence":"anchored"} {"id":"rule:BR-RPT-001","type":"Rule","name":"报告必须绑定预约","domain":"report","appliesTo":["action:submit_report"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; service/ReportService.java#create","test":"ReportControllerTest#createMissingAppointmentIdReturnsBizCode; ReportServiceTest#createWithNullAppointmentIdThrows","evidence":"anchored"} {"id":"rule:BR-RPT-002","type":"Rule","name":"一约一份报告","domain":"report","appliesTo":["action:submit_report"],"code":"backend/src/main/java/com/petstore/entity/Report.java; mapper/ReportMapper.java; service/ReportService.java#create; controller/ReportController.java#create","test":"ReportServiceTest#createDuplicateReportThrows; ReportControllerTest#createDuplicateReportReturnsAlreadyExists","evidence":"anchored"} {"id":"rule:BR-RPT-003","type":"Rule","name":"报告前后照片必填","domain":"report","appliesTo":["action:submit_report"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#hasPhotoType; frontend/src/pages/report/Report.vue#submitReport","test":"ReportControllerTest#createMissingBeforePhotoReturnsBizCode; createMissingAfterPhotoReturnsBizCode","doc":"docs/产品设计文档.md §5.3 报告提交必填项","evidence":"anchored"} @@ -85,8 +85,8 @@ {"id":"rel:track_report_open_emits_report_reopened","type":"Relation","subject":"action:track_report_open","predicate":"EMITS","object":"event:report_reopened","evidence":"documented","domain":"report"} {"id":"rel:submit_lead_emits_lead_submitted","type":"Relation","subject":"action:submit_lead","predicate":"EMITS","object":"event:lead_submitted","evidence":"documented","domain":"lead"} {"id":"entity:pet","type":"Entity","name":"Pet","nameZh":"宠物档案","domain":"pet","fields":["id","name","pet_type","breed","avatar","owner_user_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Pet.java; controller/PetController.java; service/PetService.java","doc":"docs/产品设计文档.md §三 t_pet","test":"PetControllerTest","evidence":"anchored"} -{"id":"entity:service_type","type":"Entity","name":"ServiceType","nameZh":"服务类型","domain":"service_type","fields":["id","store_id","name","create_time"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/ServiceType.java; controller/ServiceTypeController.java","doc":"docs/产品设计文档.md §三 t_service_type §5.2","test":"ServiceTypeControllerTest","evidence":"anchored"} -{"id":"entity:schedule_block","type":"Entity","name":"ScheduleBlock","nameZh":"门店排班占用","domain":"schedule","fields":["id","store_id","slot_start","block_type","note","created_by_user_id","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/ScheduleBlock.java; controller/ScheduleController.java; service/ScheduleService.java","doc":"docs/产品设计文档.md §5.2 预约时段占用","test":"ScheduleControllerTest","evidence":"anchored"} +{"id":"entity:service_type","type":"Entity","name":"ServiceType","nameZh":"服务类型","domain":"service_type","fields":["id","store_id","name","duration_minutes","create_time"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/ServiceType.java; controller/ServiceTypeController.java","doc":"docs/产品设计文档.md §三 t_service_type §5.2; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"ServiceTypeControllerTest; BookingCapacityMigrationTest","evidence":"anchored"} +{"id":"entity:schedule_block","type":"Entity","name":"ScheduleBlock","nameZh":"门店排班占用","domain":"schedule","fields":["id","store_id","slot_start","duration_minutes","block_type","note","created_by_user_id","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/ScheduleBlock.java; controller/ScheduleController.java; service/ScheduleService.java","doc":"docs/产品设计文档.md §5.2 预约时段占用; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"ScheduleControllerTest; BookingCapacityServiceTest; BookingCapacityMigrationTest","evidence":"anchored"} {"id":"entity:report_image","type":"Entity","name":"ReportImage","nameZh":"报告媒体","domain":"report","fields":["id","report_id","photo_url","photo_type","media_type","sort_order","create_time"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/ReportImage.java; mapper/ReportImageMapper.java","doc":"docs/产品设计文档.md §三 t_report_image","test":"ReportServiceTest#createWithImagesSavesEachImage","evidence":"anchored"} {"id":"entity:highlight_video","type":"Entity","name":"HighlightVideo","nameZh":"服务回顾短片","domain":"report","fields":["report_id","highlight_video_url","highlight_video_status","highlight_video_error","highlight_duration_sec","highlight_compose_mode","highlight_fail_reason","highlight_share_cover_url"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/Report.java; service/ReportHighlightVideoService.java","doc":"docs/API-报告成片字段契约.md; docs/洗美报告短视频成片方案.md","test":"ReportHighlightVideoServiceTest","evidence":"anchored","jpa":false} {"id":"entity:session_token","type":"Entity","name":"SessionToken","nameZh":"会话令牌","domain":"auth","fields":["userId","storeId","role","exp"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/auth/SessionTokenService.java; CurrentUser.java; CurrentUserContext.java; AuthInterceptor.java","doc":"docs/ontology/rules.md#rule:BR-AUTH-001","test":"SessionTokenServiceTest","evidence":"anchored","jpa":false} @@ -96,12 +96,12 @@ {"id":"action:delete_pet","type":"Action","name":"deletePet","nameZh":"删除宠物","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/pet/delete","PetController#delete","PetService#delete"],"appliesRules":["rule:BR-PET-001"],"code":"backend/src/main/java/com/petstore/controller/PetController.java#delete","evidence":"anchored","test":"PetControllerTest","domain":"pet"} {"id":"action:get_pet_history","type":"Action","name":"getPetHistory","nameZh":"宠物服务时间线","inputs":["petId"],"outputs":[],"entrypoints":["GET /api/pet/history","PetController#history","PetService#getServiceHistory"],"appliesRules":["rule:BR-PET-001"],"code":"backend/src/main/java/com/petstore/controller/PetController.java#history","evidence":"anchored","test":"PetControllerTest","domain":"pet"} {"id":"action:list_service_types","type":"Action","name":"listServiceTypes","nameZh":"服务类型列表","inputs":["storeId"],"outputs":["entity:service_type"],"entrypoints":["GET /api/service-type/list","ServiceTypeController#list"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#list","evidence":"anchored","domain":"service_type"} -{"id":"action:create_service_type","type":"Action","name":"createServiceType","nameZh":"新增服务类型","inputs":["name"],"outputs":["entity:service_type"],"entrypoints":["POST /api/service-type/create","ServiceTypeController#create","ServiceTypeService#create"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#create","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} -{"id":"action:update_service_type","type":"Action","name":"updateServiceType","nameZh":"编辑服务类型","inputs":["id","name"],"outputs":["entity:service_type"],"entrypoints":["PUT /api/service-type/update","ServiceTypeController#update"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#update","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} +{"id":"action:create_service_type","type":"Action","name":"createServiceType","nameZh":"新增服务类型","inputs":["name","durationMinutes"],"outputs":["entity:service_type"],"entrypoints":["POST /api/service-type/create","ServiceTypeController#create","ServiceTypeService#create"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#create","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} +{"id":"action:update_service_type","type":"Action","name":"updateServiceType","nameZh":"编辑服务类型","inputs":["id","name","durationMinutes"],"outputs":["entity:service_type"],"entrypoints":["PUT /api/service-type/update","ServiceTypeController#update"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#update","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} {"id":"action:delete_service_type","type":"Action","name":"deleteServiceType","nameZh":"删除服务类型","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/service-type/delete","ServiceTypeController#delete"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#delete","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} {"id":"action:init_service_types","type":"Action","name":"initServiceTypes","nameZh":"初始化系统默认服务类型","inputs":[],"outputs":[],"entrypoints":["POST /api/service-type/init","ServiceTypeController#init"],"appliesRules":["rule:BR-ST-001"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java#init","test":"ServiceTypeControllerTest","evidence":"anchored","domain":"service_type"} -{"id":"action:day_agenda","type":"Action","name":"dayAgenda","nameZh":"门店某日日程","inputs":["date"],"outputs":["entity:appointment","entity:schedule_block"],"entrypoints":["GET /api/schedule/day","ScheduleController#day","ScheduleService#dayAgenda"],"appliesRules":["rule:BR-SCH-001"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java#day","test":"ScheduleControllerTest","evidence":"anchored","domain":"schedule"} -{"id":"action:create_schedule_block","type":"Action","name":"createScheduleBlock","nameZh":"手动占用时段","inputs":["slotStart","blockType","note"],"outputs":["entity:schedule_block"],"entrypoints":["POST /api/schedule/block","ScheduleController#createBlock","ScheduleService#createBlock"],"appliesRules":["rule:BR-SCH-001"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java#createBlock","test":"ScheduleControllerTest","evidence":"anchored","domain":"schedule"} +{"id":"action:day_agenda","type":"Action","name":"dayAgenda","nameZh":"门店某日日程","inputs":["date"],"outputs":["entity:appointment","entity:schedule_block"],"entrypoints":["GET /api/schedule/day","ScheduleController#day","ScheduleService#dayAgenda"],"appliesRules":["rule:BR-SCH-001","rule:BR-SCH-002"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java#day","test":"ScheduleControllerTest; BookingCapacityServiceTest","evidence":"anchored","domain":"schedule"} +{"id":"action:create_schedule_block","type":"Action","name":"createScheduleBlock","nameZh":"手动占用时段","inputs":["slotStart","durationMinutes","blockType","note"],"outputs":["entity:schedule_block"],"entrypoints":["POST /api/schedule/block","ScheduleController#createBlock","ScheduleService#createBlock"],"appliesRules":["rule:BR-SCH-001","rule:BR-SCH-002","rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java#createBlock","test":"ScheduleControllerTest; BookingCapacityServiceTest","evidence":"anchored","domain":"schedule"} {"id":"action:delete_schedule_block","type":"Action","name":"deleteScheduleBlock","nameZh":"取消手动占用","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/schedule/block","ScheduleController#deleteBlock","ScheduleService#deleteBlock"],"appliesRules":["rule:BR-SCH-001"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java#deleteBlock; service/ScheduleService.java#deleteBlock","test":"ScheduleControllerTest","evidence":"anchored","domain":"schedule"} {"id":"action:upload_image","type":"Action","name":"uploadImage","nameZh":"上传图片/视频","inputs":["file"],"outputs":["url"],"entrypoints":["POST /api/upload/image","FileController#uploadImage"],"appliesRules":["rule:BR-UPLOAD-001","rule:BR-UPLOAD-002"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#uploadImage","evidence":"anchored","domain":"upload"} {"id":"action:get_upload_image","type":"Action","name":"getUploadImage","nameZh":"读取上传资源","inputs":["path"],"outputs":[],"entrypoints":["GET /api/upload/image/**","FileController#getImage"],"appliesRules":["rule:BR-UPLOAD-001"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#getImage","evidence":"anchored","domain":"upload"} @@ -128,6 +128,7 @@ {"id":"rule:BR-HL-001","type":"Rule","name":"成片仅 boss/staff 可触发","domain":"report","appliesTo":["action:generate_highlight"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#startHighlight","evidence":"anchored","test":"ReportHighlightVideoServiceTest#requestGenerateRejectsCustomerWithoutAppointmentOwnershipPath"} {"id":"rule:BR-HL-002","type":"Rule","name":"成片三态 + composeMode","domain":"report","appliesTo":["action:generate_highlight"],"code":"backend/src/main/java/com/petstore/entity/Report.java; service/ReportHighlightVideoService.java","doc":"docs/洗美报告短视频成片方案.md; docs/API-报告成片字段契约.md","evidence":"anchored","test":"ReportHighlightVideoServiceTest"} {"id":"rule:BR-SCH-001","type":"Rule","name":"排班仅 boss/staff + 跨店 403","domain":"schedule","appliesTo":["action:day_agenda","action:create_schedule_block","action:delete_schedule_block"],"code":"backend/src/main/java/com/petstore/controller/ScheduleController.java; service/ScheduleService.java#deleteBlock","test":"ScheduleControllerTest","evidence":"anchored"} +{"id":"rule:BR-SCH-002","type":"Rule","name":"手动占用容量语义:walk_in 消耗一份,blocked 关闭全部","domain":"schedule","appliesTo":["action:day_agenda","action:create_schedule_block"],"code":"backend/src/main/java/com/petstore/service/ScheduleService.java; backend/src/main/java/com/petstore/service/BookingCapacityService.java","test":"BookingCapacityServiceTest; ScheduleControllerTest","evidence":"anchored"} {"id":"rule:BR-ST-001","type":"Rule","name":"服务类型仅 boss + 同店 + 系统默认不可改删","domain":"service_type","appliesTo":["action:create_service_type","action:update_service_type","action:delete_service_type","action:init_service_types"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java","test":"ServiceTypeControllerTest","evidence":"anchored"} {"id":"rule:BR-PET-001","type":"Rule","name":"宠物跨用户/跨店","domain":"pet","appliesTo":["action:list_pets","action:create_pet","action:update_pet","action:delete_pet","action:get_pet_history"],"code":"backend/src/main/java/com/petstore/controller/PetController.java","evidence":"anchored","test":"PetControllerTest"} {"id":"rule:BR-USER-001","type":"Rule","name":"员工管理仅 boss + 同店","domain":"user","appliesTo":["action:create_staff","action:list_staff","action:delete_staff"],"code":"backend/src/main/java/com/petstore/controller/UserController.java","evidence":"anchored"} @@ -157,7 +158,7 @@ {"id":"entity:service_interval","type":"Entity","name":"ServiceInterval","nameZh":"服务建议周期配置","domain":"service_type","fields":["id","store_id","service_name","pet_type","interval_days_min","interval_days_max","create_time"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/ServiceInterval.java","doc":"docs/ontology/objects.md#entity:service_interval","test":"","evidence":"anchored"} {"id":"action:list_stores","type":"Action","name":"listStores","nameZh":"门店列表","inputs":[],"outputs":["entity:store"],"entrypoints":["GET /api/store/list","StoreController#list"],"appliesRules":["rule:BR-STORE-003"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#list","evidence":"anchored","domain":"store","test":"StoreControllerTest#publicListHidesOwnerIdAndInviteCode"} {"id":"action:get_store","type":"Action","name":"getStore","nameZh":"门店详情","inputs":["id"],"outputs":["entity:store"],"entrypoints":["GET /api/store/get","StoreController#get"],"appliesRules":["rule:BR-STORE-003"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#get","evidence":"anchored","domain":"store","test":"StoreControllerTest#publicGetHidesOwnerIdAndInviteCode"} -{"id":"action:get_available_slots","type":"Action","name":"getAvailableSlots","nameZh":"可预约时段","inputs":["storeId","date"],"outputs":[],"entrypoints":["GET /api/appointment/available-slots","AppointmentController#availableSlots"],"appliesRules":["rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#availableSlots","evidence":"anchored","domain":"appointment"} +{"id":"action:get_available_slots","type":"Action","name":"getAvailableSlots","nameZh":"可预约时段","inputs":["storeId","date","serviceType"],"outputs":[],"entrypoints":["GET /api/appointment/available-slots","AppointmentController#availableSlots"],"appliesRules":["rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#availableSlots; backend/src/main/java/com/petstore/service/BookingCapacityService.java","test":"BookingCapacityServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:get_report_suggestion","type":"Action","name":"getReportSuggestion","nameZh":"报告下次建议日期","inputs":["token"],"outputs":[],"entrypoints":["GET /api/report/{token}/suggestion","ReportLeadController#suggestion"],"appliesRules":["rule:BR-INT-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#suggestion","evidence":"anchored","domain":"lead"} {"id":"action:submit_testimonial","type":"Action","name":"submitTestimonial","nameZh":"宠主寄语提交","inputs":["token","content","isPublic"],"outputs":["entity:report_testimonial"],"entrypoints":["POST /api/report/{token}/testimonial","ReportController#submitTestimonial"],"appliesRules":["rule:BR-TST-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#submitTestimonial","evidence":"anchored","domain":"report","test":"ReportTestimonialServiceTest"} {"id":"action:get_legacy_upload","type":"Action","name":"getLegacyUpload","nameZh":"兼容旧路径读取上传资源","inputs":["path"],"outputs":[],"entrypoints":["GET /api/upload/legacy/**","FileController#getLegacyImage"],"appliesRules":["rule:BR-UPLOAD-001"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#getLegacyImage","evidence":"anchored","domain":"upload"} @@ -214,7 +215,7 @@ {"id":"action:admin_list_leads","type":"Action","name":"adminListLeads","nameZh":"后台回访线索列表","domain":"admin","inputs":["remindStatus","remindDateTo","page","pageSize"],"outputs":["entity:report_lead"],"entrypoints":["Admin UI /leads (planned)","reuses GET /api/report/leads"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/.../ReportLeadController.java#leads (reuse)","doc":"docs/ontology/actions.md#action:admin_list_leads","evidence":"documented"} {"id":"action:admin_list_service_customers","type":"Action","name":"adminListServiceCustomers","nameZh":"后台服务客户列表","domain":"admin","inputs":["source","hasPendingLead","lastVisitFrom","lastVisitTo","q","page","pageSize"],"outputs":["entity:store_customer"],"entrypoints":["GET /api/admin/service-customers","Admin UI /customers"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-ADMIN-004","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#listServiceCustomers; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/actions.md#action:admin_list_service_customers; docs/门店管理后台-PhaseA-页面清单PRD.md §7.6","test":"AdminServiceCustomerControllerTest; AdminServiceCustomerServiceTest","evidence":"anchored","note":"以 t_store_customer 稳定主档为入口,动态聚合宠物/到店/报告/留资;返回 storeCustomerId 和脱敏手机号。"} {"id":"action:admin_update_settings","type":"Action","name":"adminUpdateSettings","nameZh":"后台门店设置读写","domain":"admin","inputs":["entity:store","entity:service_type","entity:user"],"outputs":["entity:store","entity:service_type","entity:user"],"entrypoints":["GET /api/admin/store","Admin UI /settings","PUT /api/store/update"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-STORE-001","rule:BR-ST-001","rule:BR-USER-001"],"code":"","doc":"docs/ontology/actions.md#action:admin_update_settings","evidence":"anchored","note":"后台读本店含 inviteCode(GET /api/admin/store);写资料复用 PUT /api/store/update(仅 boss);服务类型/员工复用既有 API。","code_refs":["backend/src/main/java/com/petstore/controller/AdminStoreController.java#currentStore","backend/src/main/java/com/petstore/controller/StoreController.java#update"]} -{"id":"action:admin_manage_schedule","type":"Action","name":"adminManageSchedule","nameZh":"后台排班与占用","domain":"admin","inputs":["date","entity:schedule_block"],"outputs":["entity:schedule_block","entity:appointment"],"entrypoints":["Admin UI /schedule","GET /api/schedule/day","POST /api/schedule/block","DELETE /api/schedule/block"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-SCH-001"],"code":"","doc":"docs/ontology/actions.md#action:admin_manage_schedule","evidence":"anchored","note":"Web 排班日视图;增删占用复用 ScheduleController。"} +{"id":"action:admin_manage_schedule","type":"Action","name":"adminManageSchedule","nameZh":"后台排班与占用","domain":"admin","inputs":["date","entity:schedule_block"],"outputs":["entity:schedule_block","entity:appointment"],"entrypoints":["Admin UI /schedule","GET /api/schedule/day","POST /api/schedule/block","DELETE /api/schedule/block"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-SCH-001","rule:BR-SCH-002","rule:BR-APPT-004"],"code":"","doc":"docs/ontology/actions.md#action:admin_manage_schedule","evidence":"anchored","note":"Web 排班日视图展示容量占用;增删带时长占用复用 ScheduleController。"} {"id":"event:workbench_viewed","type":"Event","name":"workbenchViewed","nameZh":"工作台已打开","domain":"admin","emittedBy":["action:view_workbench"],"payload":["storeId","userId","ts"],"code":"","doc":"docs/ontology/events.md#event:workbench_viewed","evidence":"gap","note":"可选埋点;A 期可不实现上报。"} {"id":"rel:admin_console_has_workbench","type":"Relation","subject":"entity:admin_console","predicate":"HAS","object":"entity:workbench","domain":"admin","evidence":"documented","cardinality":"1—1","doc":"登录默认工作台"} {"id":"rel:workbench_has_todo_items","type":"Relation","subject":"entity:workbench","predicate":"HAS","object":"entity:workbench_todo_item","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"今日待办卡片"} diff --git a/ontology/actions.md b/ontology/actions.md index 2b6533c..0827152 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -84,13 +84,13 @@ ### action:create_appointment -创建预约:半小时占号,取消不占号;客户与创建人分开记录。 +创建预约:按服务时长跨半小时容量桶占号,并保存时长快照;取消不占号;客户与创建人分开记录。 - **入口**:`POST /api/appointment/create` → `AppointmentController#create` → `AppointmentService#createBooking` - **输入**:`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId` 或 `customerPhone` - **输出**:`entity:appointment` - **身份口径**:customer 自约时客户=创建人=current.userId;boss/staff 代约时创建人=current.userId,客户按请求解析,legacy userId 镜像客户 -- **适用规则**:`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(半小时档 + 营业时间 + 占用校验)、`rule:BR-SC-001`(门店客户建档) +- **适用规则**:`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(服务时长 + 门店容量 + 并发锁)、`rule:BR-SC-001`(门店客户建档) - **代码**:`backend/src/main/java/com/petstore/controller/AppointmentController.java#create` - **证据**:`anchored` @@ -264,7 +264,7 @@ ### action:list_service_types -服务类型列表:公开接口,不传 storeId 返回系统默认;传 storeId 返回系统默认 + 该店自定义。 +服务类型列表:公开接口,不传 storeId 返回系统默认;传 storeId 返回系统默认 + 该店自定义,同名时本店定义优先;每项返回预计服务时长。 - **入口**:`GET /api/service-type/list` → `ServiceTypeController#list`(公开,在 AuthInterceptor allowlist) - **代码**:`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#list` @@ -272,7 +272,7 @@ ### action:create_service_type -新增服务类型:仅 boss;`storeId=current.storeId`。 +新增服务类型:仅 boss;`storeId=current.storeId`;必须填写 30 分钟粒度的预计时长。 - **入口**:`POST /api/service-type/create` → `ServiceTypeController#create` → `ServiceTypeService#create` - **适用规则**:`rule:BR-ST-001`(仅 boss + 同店) @@ -282,7 +282,7 @@ ### action:update_service_type -编辑服务类型:仅 boss;必须为本店自定义(系统默认 `storeId` 为空不可改、他店不可改)。 +编辑服务类型:仅 boss;必须为本店自定义(系统默认 `storeId` 为空不可改、他店不可改);名称和预计时长一起更新。 - **入口**:`PUT /api/service-type/update` → `ServiceTypeController#update` - **适用规则**:`rule:BR-ST-001` @@ -313,7 +313,7 @@ ### action:day_agenda -门店某日日程:仅 boss/staff;`storeId=current.storeId`。 +门店某日日程:仅 boss/staff;`storeId=current.storeId`;每个半小时桶返回预约/手动占用列表及已用、总计、剩余容量。 - **入口**:`GET /api/schedule/day` → `ScheduleController#day` → `ScheduleService#dayAgenda` - **适用规则**:`rule:BR-SCH-001`(仅 boss/staff + 跨店 403) @@ -323,10 +323,10 @@ ### action:create_schedule_block -手动占用半小时档:仅 boss/staff;`storeId=current.storeId`、`createdByUserId=current.userId`。 +手动占用连续半小时档:仅 boss/staff;`storeId=current.storeId`、`createdByUserId=current.userId`;`walk_in` 消耗一个并发名额,`blocked` 关闭全部名额。 - **入口**:`POST /api/schedule/block` → `ScheduleController#createBlock` → `ScheduleService#createBlock` -- **适用规则**:`rule:BR-SCH-001` +- **适用规则**:`rule:BR-SCH-001`、`rule:BR-SCH-002`、`rule:BR-APPT-004` - **代码**:`backend/src/main/java/com/petstore/controller/ScheduleController.java#createBlock` - **测试**:`ScheduleControllerTest` - **证据**:`anchored` @@ -437,7 +437,7 @@ ### action:get_available_slots -门店某日可预约时段:公开接口,半小时档,含已占用/已过/可约。 +门店某日可预约开始时段:公开接口,接收可选 `serviceType`,按服务时长检查覆盖的所有容量桶,返回开始/结束时间、容量与不可约原因。 - **入口**:`GET /api/appointment/available-slots` → `AppointmentController#availableSlots`(公开) - **适用规则**:`rule:BR-APPT-004`(时段校验) diff --git a/ontology/events.md b/ontology/events.md index db07d89..28e86a4 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -37,7 +37,7 @@ ### event:appointment_created -预约创建成功(`status=new`,下单即占号生效)。 +预约创建成功(`status=new`,按服务时长跨容量桶占号生效);低敏 metadata 保存 `bookingOrigin` 与 `durationMinutes` 快照。 - **触发动作**:`action:create_appointment` - **持久字段**:`storeId`、`storeCustomerId`、`aggregateType=appointment`、`aggregateId`、`actorUserId/role`、`bookingOrigin` diff --git a/ontology/objects.md b/ontology/objects.md index 27d094c..c24ea28 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -20,6 +20,7 @@ | invite_code | VARCHAR(8) | 员工邀请码 | | booking_day_start | LocalTime | 预约首号 | | booking_last_slot_start | LocalTime | 预约末号 | +| booking_capacity | INT | 门店承诺的并发接待数(1~10) | | create_time / update_time | DATETIME | 时间戳 | | deleted | TINYINT(1) | 软删 | @@ -64,7 +65,8 @@ | pet_name | VARCHAR | 宠物名称 | | pet_type | VARCHAR | 宠物类型(猫/狗/其他) | | service_type | VARCHAR | 服务类型 | -| appointment_time | DATETIME | 预约时间(半小时档,按营业时间/占用/已过校验) | +| appointment_time | DATETIME | 预约开始时间(半小时粒度,按服务时长检查跨桶容量) | +| duration_minutes | INT | 下单时的服务时长快照(30~480,30 分钟粒度) | | status | VARCHAR | new / doing / done / cancel | | store_id | BIGINT | 所属门店 | | customer_user_id | BIGINT | 被服务客户账号;新业务归属主字段 | @@ -202,6 +204,7 @@ | id | BIGINT | 主键 | | store_id | BIGINT | 门店 ID(NULL=系统默认) | | name | VARCHAR | 服务名称 | +| duration_minutes | INT | 预计服务时长(30~480,30 分钟粒度) | | create_time | DATETIME | 创建时间 | - **归属系统**:Backend Core(`ServiceTypeController` / `ServiceTypeService` / `ServiceTypeMapper`) @@ -212,13 +215,14 @@ ## entity:schedule_block -**门店日程手动占用**。与线上预约一样占用半小时档,用于到店客(`walk_in`)或暂停预约(`blocked`)。 +**门店日程手动占用**。以半小时为粒度连续占用,用于到店客(`walk_in`,消耗一个容量)或暂停预约(`blocked`,关闭覆盖范围内全部容量)。 | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键 | | store_id | BIGINT | 所属门店(非空) | | slot_start | DATETIME | 半小时档起始时刻 | +| duration_minutes | INT | 连续占用时长(30~480,30 分钟粒度) | | block_type | VARCHAR(32) | walk_in / blocked | | note | VARCHAR(500) | 备注 | | created_by_user_id | BIGINT | 创建人(从上下文派生) | @@ -228,7 +232,7 @@ - **归属系统**:Backend Core(`ScheduleController` / `ScheduleService` / `ScheduleBlockMapper`) - **代码**:`backend/src/main/java/com/petstore/entity/ScheduleBlock.java`、`controller/ScheduleController.java`、`service/ScheduleService.java` - **文档**:`docs/产品设计文档.md §5.2(隐含,预约时段占用)` -- **测试**:`ScheduleControllerTest`(7 用例) +- **测试**:`ScheduleControllerTest`、`BookingCapacityServiceTest` - **证据**:`anchored` ## entity:report_image diff --git a/ontology/rules.md b/ontology/rules.md index 3a47f80..8fa7541 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -82,10 +82,11 @@ ### rule:BR-APPT-004 -**预约时段校验**:半小时档,按门店营业时间(`booking_day_start`~`booking_last_slot_start`)、已占用档、已过时间校验;同一门店同一时间档最多一单。 +**真实预约容量校验**:开始时间按半小时对齐;服务必须在 `booking_day_start` 到末容量桶结束时刻内完成。预约保存服务时长快照,并检查覆盖的每个半小时容量桶;有效预约与 `walk_in` 各消耗一个 `booking_capacity`,`blocked` 关闭覆盖范围内全部容量。创建预约/占用时悲观锁定门店行,避免并发超卖;取消预约不占容量。 -- **适用动作**:`action:create_appointment` -- **代码**:`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking` +- **适用动作**:`action:create_appointment`、`action:get_available_slots`、`action:create_schedule_block` +- **代码**:`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking`、`BookingCapacityService`、`ScheduleService#createBlock` +- **测试**:`BookingCapacityServiceTest`、`AppointmentServiceTest`、`BookingCapacityMigrationTest` - **文档**:`docs/产品设计文档.md §5.2` - **证据**:`anchored` @@ -234,11 +235,20 @@ - **测试**:`ScheduleControllerTest` - **证据**:`anchored` +### rule:BR-SCH-002 + +**手动占用容量语义**:`durationMinutes` 为 30~480 的 30 分钟倍数;`walk_in` 在覆盖的每个容量桶消耗一个并发名额,`blocked` 仅允许创建在没有预约或其他占用的范围,并关闭全部号源。 + +- **适用动作**:`action:day_agenda`、`action:create_schedule_block` +- **代码**:`backend/src/main/java/com/petstore/service/ScheduleService.java`、`BookingCapacityService.java` +- **测试**:`BookingCapacityServiceTest`、`ScheduleControllerTest` +- **证据**:`anchored` + ## ServiceType ### rule:BR-ST-001 -**服务类型仅 boss + 同店 + 系统默认不可改删**:`create`/`update`/`delete` 仅 `isBoss()`;`storeId=current.storeId`;`update`/`delete` 时 `existing.storeId` 必须非空且等于 `current.storeId`(系统默认 `storeId=null` 不可改删,他店不可改删);`init` 仅 boss/staff。 +**服务类型仅 boss + 同店 + 系统默认不可改删**:`create`/`update`/`delete` 仅 `isBoss()`;`storeId=current.storeId`;`update`/`delete` 时 `existing.storeId` 必须非空且等于 `current.storeId`(系统默认 `storeId=null` 不可改删,他店不可改删);预计时长须为 30~480 的 30 分钟倍数;`init` 仅 boss/staff。 - **适用动作**:`action:create_service_type`、`action:update_service_type`、`action:delete_service_type`、`action:init_service_types` - **代码**:`backend/src/main/java/com/petstore/controller/ServiceTypeController.java` diff --git a/产品全方位优化说明-v1.md b/产品全方位优化说明-v1.md index fc292d5..b22a43b 100644 --- a/产品全方位优化说明-v1.md +++ b/产品全方位优化说明-v1.md @@ -23,7 +23,7 @@ | ID | 议题 | 选项 | 建议 | |----|------|------|------| | D1 | 主色唯一源 | A `#2db96d`(`--c-brand`,与 `pages.json`/全局样式一致) / B `#16a34a` | **已采纳 A**:以代码与品牌 VI 为准;见《前端 UI 规范》修订 | -| D2 | 对外是否承诺「排队」 | 有容量模型再写 / 不写 | **已冻结:不写**「排队」「不排队」类绝对话术;统一文案为「在线选时段,到店更省心」 | +| D2 | 对外是否承诺「排队」 | 有排队/叫号和履约保障再评估 / 不写 | **已冻结:不写**「排队」「不排队」类绝对话术;容量模型只防超卖,统一文案仍为「在线选时段,到店更省心」 | | D3 | 报告 token | 仅页内提示 / + 作废重发 MVP / + 有效期 | 短期提示 + 埋点;中期 **作废重发** 优先于复杂有效期 | | D4 | 会员/储值/套餐是否进入 P0 | 进 P0 / 推到 P1/P2 | **已冻结:推到 P1/P2**;本期 P0 只做预约/公开报告/留资/基础埋点/报告分享/门店服务闭环 | | D5 | 宠主历史报告汇总是否进入 P0 | 进 P0 / 推到 P2 | **已冻结:推到 P2**;本期不实现宠主账号内历史报告汇总 | diff --git a/产品设计文档.md b/产品设计文档.md index 112b687..539e0d2 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -71,6 +71,9 @@ | intro | TEXT | 简介 | | owner_id | BIGINT | 老板用户ID | | invite_code | VARCHAR | 员工邀请码(8位) | +| booking_day_start | TIME | 每日首个容量桶开始时间 | +| booking_last_slot_start | TIME | 每日最后一个容量桶开始时间 | +| booking_capacity | INT | 门店并发接待数(1~10) | | create_time | DATETIME | 创建时间 | | update_time | DATETIME | 更新时间 | @@ -121,7 +124,8 @@ | pet_name | VARCHAR | 宠物名称 | | pet_type | VARCHAR | 宠物类型(猫/狗/其他) | | service_type | VARCHAR | 服务类型 | -| appointment_time | DATETIME | 预约时间(以门店营业时间、半小时档、已占用档和已过时间为准;同一门店同一时间档最多一单) | +| appointment_time | DATETIME | 预约开始时间(半小时粒度) | +| duration_minutes | INT | 下单时服务时长快照;修改服务项目不影响历史预约 | | status | VARCHAR | new(待开始)/ doing(服务中)/ done(已完成)/ cancel(已取消) | | store_id | BIGINT | 店铺ID | | customer_user_id | BIGINT | 被服务客户账号 | @@ -168,6 +172,7 @@ | id | BIGINT | 主键 | | store_id | BIGINT | 店铺ID(NULL表示系统默认) | | name | VARCHAR | 服务名称 | +| duration_minutes | INT | 预计服务时长(30~480,30 分钟粒度) | | create_time | DATETIME | 创建时间 | --- @@ -211,11 +216,12 @@ - 老板/员工均可手动创建 - 字段:宠物名字、宠物类型(猫/狗/其他)、服务类型、预约时间、备注 - 建档人 = 当前登录用户 -- 预约时间以门店营业时间、半小时档、已占用档和已过时间为准;同一门店同一时间档最多一单(统一文案:在线选时段,到店更省心) +- 号源以服务项目预计时长、门店营业容量时段、门店并发接待数、线上预约与手动占用共同计算;创建时检查服务覆盖的每个半小时桶,并串行化同店容量判定避免超卖 +- 线上预约当前不预选技师,容量是门店对外承诺的并发接待数;个人技师日历在具备预约前分配技师能力后再引入 **服务类型:** - 系统默认:洗澡、美容、洗澡+美容、剪指甲、驱虫 -- 老板可在默认基础上新增/编辑/删除自定义服务类型 +- 老板可在默认基础上新增/编辑/删除自定义服务类型,并配置 30~480 分钟的预计时长 - 员工只能使用,不能管理 **预约状态语义(与界面文案对齐):** @@ -338,6 +344,7 @@ | P0 | 员工登录(微信+验证码) | ✅ | | P0 | 预约列表 | ✅ | | P0 | 预约创建 | ✅ | +| P0 | 服务时长 + 门店并发预约容量 + 连续占用 | ✅ | | P0 | 开始服务(分配技师) | ✅ | | P0 | 报告填写 | ✅ | | P0 | 报告过程素材上传(照片/短视频) | ✅ 基础能力已接入 | @@ -369,16 +376,22 @@ CREATE TABLE t_service_type ( id BIGINT PRIMARY KEY AUTO_INCREMENT, store_id BIGINT COMMENT '店铺ID,NULL表示系统默认', name VARCHAR(50) NOT NULL, + duration_minutes INT NOT NULL DEFAULT 60, create_time DATETIME ); -- 系统默认服务类型 -INSERT INTO t_service_type (store_id, name, create_time) VALUES -(NULL, '洗澡', NOW()), -(NULL, '美容', NOW()), -(NULL, '洗澡+美容', NOW()), -(NULL, '剪指甲', NOW()), -(NULL, '驱虫', NOW()); +INSERT INTO t_service_type (store_id, name, duration_minutes, create_time) VALUES +(NULL, '洗澡', 60, NOW()), +(NULL, '美容', 120, NOW()), +(NULL, '洗澡+美容', 150, NOW()), +(NULL, '剪指甲', 30, NOW()), +(NULL, '驱虫', 30, NOW()); + +-- 真实预约容量字段(完整回填与验证见 backend/db/migrations/20260801_create_booking_capacity.sql) +ALTER TABLE t_appointment ADD COLUMN duration_minutes INT NOT NULL DEFAULT 60; +ALTER TABLE t_schedule_block ADD COLUMN duration_minutes INT NOT NULL DEFAULT 30; +ALTER TABLE t_store ADD COLUMN booking_capacity INT NOT NULL DEFAULT 1; -- 报告媒体表(前后图 + 过程素材) CREATE TABLE t_report_image ( diff --git a/宠主端预约体验优化-产品说明.md b/宠主端预约体验优化-产品说明.md index 1c2a7f6..29712a6 100644 --- a/宠主端预约体验优化-产品说明.md +++ b/宠主端预约体验优化-产品说明.md @@ -32,7 +32,7 @@ **建议** 1. **分区**:上区 **「我要预约」主按钮**(或保留 FAB 但配一句固定文案「点这里新建预约」首次引导);下区才是「我的预约」与 Tab。 -2. **文案**:副标题从纯说明型改为**价值型**一句,统一为「在线选时段,到店更省心」(不写「到店不排队」等无容量模型承诺;若门店未来上线排队/叫号能力再单独评估)。 +2. **文案**:副标题从纯说明型改为**价值型**一句,统一为「在线选时段,到店更省心」(容量模型只保证系统不超卖,不等同排队/叫号与准点履约,因此仍不写「到店不排队」)。 3. **空列表**:空态时 **主按钮置顶**,避免「空白 + 小加号」的冷启动挫败。 --- @@ -59,7 +59,7 @@ - **冻结策略**:未登录允许浏览首页/极简介绍,**点击「预约」可进入表单填写**;提交时若仍为未登录,则跳转登录页(带 `redirect` 回预约页),登录成功后**回到预约页并保留已填内容**(避免白填)。 - 草稿存储:未登录时写入 guest 草稿 key(`petstore_appt_draft_guest`);登录后优先读 guest 草稿并恢复,恢复成功后删除 guest 草稿;登录态用户可继续使用按 `userId` 维度的草稿 key。 -- 预约时段:以门店营业时间、半小时档、已占用档和已过时间为准;同一门店同一时间档最多一单。对外文案统一「在线选时段,到店更省心」,不承诺排队/叫号能力。 +- 预约时段:按所选服务预计时长检查其覆盖的全部半小时容量桶;线上预约与到店占用共同消耗门店并发接待数,暂停预约关闭全部容量。对外文案统一「在线选时段,到店更省心」,不承诺排队/叫号能力。 - 若业务未来强制「进入预约页即登录」,则需在 README 与本节同步更新冻结策略,并保留草稿恢复逻辑。 --- diff --git a/架构决策-真实预约容量模型-2026-08-01.md b/架构决策-真实预约容量模型-2026-08-01.md new file mode 100644 index 0000000..10ed061 --- /dev/null +++ b/架构决策-真实预约容量模型-2026-08-01.md @@ -0,0 +1,35 @@ +# 架构决策:真实预约容量模型 + +日期:2026-08-01 +状态:Accepted +范围:单店 SaaS 真实试点 + +## 决策 + +1. 服务项目配置 `duration_minutes`,范围 30~480 分钟且按 30 分钟递增。 +2. 预约保存 `duration_minutes` 快照,后续修改服务项目不会改变已承诺号源。 +3. 门店配置 `booking_capacity`(1~10),表示同一半小时容量桶可并行服务的顾客数。 +4. 可约性必须检查服务从开始到结束覆盖的每个半小时桶;有效预约和 `walk_in` 各消耗一个名额,`blocked` 关闭覆盖范围内全部名额。 +5. 创建预约或手动占用时,对门店行使用悲观写锁,在同一事务内重新读取占用并判定,避免两个并发请求同时通过可用性检查。 +6. `booking_last_slot_start` 保留兼容语义:它是最后一个 30 分钟容量桶的开始时间,营业容量结束时刻为其后 30 分钟;服务必须在结束时刻前完成。 + +## 为什么先做门店容量 + +现有预约在开始服务时才绑定 `assigned_user_id`,宠主下单也不选择技师。在这种数据结构下宣称已经具备“技师排班”会制造假精度。试点阶段先由老板配置门店可真实兑现的并发接待数,足以阻止长服务跨档冲突和同店并发超卖。 + +个人技师日历延后到预约前可分配技师、员工可维护可工作时段、服务项目可声明技师技能之后再引入。届时门店容量仍作为总上限,不会推翻本决策。 + +## 兼容与迁移 + +- 旧接口未传 `serviceType` 查询号源时采用 60 分钟安全默认值;创建预约必须解析到本店有效服务项目。 +- 历史系统默认与预约按稳定名称映射回填:洗澡 60、美容 120、洗澡+美容 150、剪指甲/驱虫 30,未知服务 60 分钟。 +- 旧手动占用回填为 30 分钟;旧门店并发接待数回填为 1。 +- 生产不自动执行迁移,发布前按 `backend/db/migrations/README.md` 备份、测试和人工执行。 + +## 验收 + +- 120 分钟服务会占用连续四个半小时桶。 +- 容量为 2 时,同一桶允许两个有效占用,第三个被拒绝。 +- `walk_in` 消耗一个名额;`blocked` 覆盖处全部不可约。 +- 候选服务结束晚于营业容量结束时刻时不可约。 +- 并发创建不会突破门店容量。 diff --git a/门店管理后台-PhaseA-线框说明.md b/门店管理后台-PhaseA-线框说明.md index f89bd11..4c117e0 100644 --- a/门店管理后台-PhaseA-线框说明.md +++ b/门店管理后台-PhaseA-线框说明.md @@ -73,19 +73,19 @@ ## A6 排班占用 `/schedule` -**一句话**:日视图看半小时档谁被约/被占,可加删本店占用。 +**一句话**:日视图看每个半小时容量桶被谁占用、还剩几个名额,可加删连续占用。 ```text [< 昨天] 2026-07-10 [明天 >] -09:00 ████ 预约·豆豆洗澡 -09:30 ░░░░ 空闲 -10:00 ████ 手动占用 [删] +09:00 1/2 预约·豆豆洗澡(至 10:00) +09:30 1/2 预约持续占用 +10:00 暂停 手动占用 60 分钟 [删] … [+ 新增占用] ``` -**主操作**:切日;新增占用;删除本店占用。 +**主操作**:切日;新增带时长的到店占用/暂停预约;删除本店占用。 **不做**:复杂多技师甘特、自动排班算法。 --- diff --git a/门店管理后台-PhaseA-页面清单PRD.md b/门店管理后台-PhaseA-页面清单PRD.md index 11d24fa..ed1d70d 100644 --- a/门店管理后台-PhaseA-页面清单PRD.md +++ b/门店管理后台-PhaseA-页面清单PRD.md @@ -208,8 +208,9 @@ entity:admin_console ### 4.1 视图 -- 日视图时间轴(半小时档) -- 叠加:线上预约占用 + `walk_in` / `blocked` 手动占用 +- 日视图时间轴(半小时容量桶),展示已用/总计/剩余容量 +- 叠加:按服务时长跨桶的线上预约 + 可配置时长的 `walk_in` / `blocked` 手动占用 +- `walk_in` 消耗一个并发名额;`blocked` 关闭覆盖范围内全部名额 - 操作:新增占用、删除本店占用;不可改他店 ### 4.2 API 复用 @@ -221,6 +222,7 @@ entity:admin_console ### 4.3 验收 - [ ] 与小程序号源占用一致 +- [ ] 并发请求不突破门店配置容量;长服务覆盖的后续时段不可超卖 - [ ] staff/boss 同店可操作;跨店 403 --- diff --git a/门店管理后台升级方案-对标宠老板.md b/门店管理后台升级方案-对标宠老板.md index e52f498..dcd841c 100644 --- a/门店管理后台升级方案-对标宠老板.md +++ b/门店管理后台升级方案-对标宠老板.md @@ -527,7 +527,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 |------|--------|------------| | 模型 | 服务目录 + 多规格 SKU | `entity:service_type`:**仅 name + storeId**(系统默认不可改删) | | 价格 | 零售价 / 会员价 | **无**(预约不收银) | -| 时长 | 规格级时长(15 分钟档) | 号源是门店半小时档,**不绑服务时长** | +| 时长 | 规格级时长(15 分钟档) | 服务项目级预计时长(30 分钟粒度),预约保存时长快照并跨容量桶占用 | | 小程序 | 商城上架 + 预约开关分离 | 有名称即可出现在预约选项 | | 复购 | 规格级「复购提醒」 | 有 `service_interval` 建议周期,但是**按服务名匹配**,非规格级 | | 押金/券 | 微信支付押金、付费券 | **无** | @@ -581,7 +581,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 - 可约:关联排班 / 服务时长 / 星期时段+人数库存 - 按手机号自动匹配或创建会员 +「未知」宠物 -与宠小它:我们是 **下单即占号**,完成导向 **出报告/成片**,号源是半小时档+占用块——**A 期按自有模型设计即可,不必再等预约列表截图**。 +与宠小它:我们是 **下单即占号**,完成导向 **出报告/成片**,号源按服务时长跨半小时容量桶并叠加占用块——**A 期按自有模型设计即可,不必再等预约列表截图**。 #### 对方近期迭代方向(从 PDF 汇总)