docs: define service identity ownership semantics

This commit is contained in:
malei 2026-08-01 20:31:53 +08:00
parent eaed2198b4
commit f8fa38b92d
9 changed files with 112 additions and 29 deletions

View File

@ -56,7 +56,8 @@
| **A9** | **线索脱敏**`/report/leads` 默认不返回完整 `wechatOpenid/wechatUnionid` | 回包只有 `wechatBound`、`wechatOpenidMasked`、`wechatUnionidMasked` | Report Media Backend | ✅ 已实现Batch B | 手工 curl + `mvn test` | 对应 P0-2 | | **A9** | **线索脱敏**`/report/leads` 默认不返回完整 `wechatOpenid/wechatUnionid` | 回包只有 `wechatBound`、`wechatOpenidMasked`、`wechatUnionidMasked` | Report Media Backend | ✅ 已实现Batch B | 手工 curl + `mvn test` | 对应 P0-2 |
| **A10** | **配置安全与启动迁移收口**`application.yml` 不保留真实敏感值;生产 profile 禁用 `ddl-auto:update`/`show-sql`;移除启动期 DDL/历史 UPDATE | 仓库不含真实 DB/微信密钥;生产启动只读环境变量 | Backend Ops / System Architect | ✅ 已实现Batch D | `rg` 扫描 + 启动验证 | 对应 P0-8已暴露凭据需轮换 | | **A10** | **配置安全与启动迁移收口**`application.yml` 不保留真实敏感值;生产 profile 禁用 `ddl-auto:update`/`show-sql`;移除启动期 DDL/历史 UPDATE | 仓库不含真实 DB/微信密钥;生产启动只读环境变量 | Backend Ops / System Architect | ✅ 已实现Batch D | `rg` 扫描 + 启动验证 | 对应 P0-8已暴露凭据需轮换 |
| **A11** | **上传路径与类型保护**`FileController` 归一化路径 + 扩展名/MIME 校验 | `/api/upload/image/../...`、伪装扩展名、超大文件均被拒绝 | Backend Core / System Architect | ✅ 已实现Batch D | 手工 curl + `mvn test` | 对应 P0-9 | | **A11** | **上传路径与类型保护**`FileController` 归一化路径 + 扩展名/MIME 校验 | `/api/upload/image/../...`、伪装扩展名、超大文件均被拒绝 | Backend Core / System Architect | ✅ 已实现Batch D | 手工 curl + `mvn test` | 对应 P0-9 |
| **A12** | **最小测试基线**:补 `SessionTokenServiceTest`、`ReportServiceTest`、`ReportControllerTest`、`AppointmentServiceTest` | `mvn test` 不再输出 `No tests to run`,新增用例全部通过 | Core Flow QA / System Architect | ✅ 已实现Batch D | `mvn -f backend/pom.xml test` | 对应 P0-1026 个用例全部通过 | | **A12** | **最小测试基线**:补 `SessionTokenServiceTest`、`ReportServiceTest`、`ReportControllerTest`、`AppointmentServiceTest` | `mvn test` 不再输出 `No tests to run`,新增用例全部通过 | Core Flow QA / System Architect | ✅ 已实现Batch D | `mvn -f backend/pom.xml test` | 对应 P0-10持续回归数量以 Surefire 当次统计为准 |
| **A13** | **预约/报告身份语义拆分**:客户、创建人、服务技师、报告作者不得复用同一语义字段 | customer 自约身份从 session 派生门店代约必须提供真实宠主宠主报告按预约客户归属查询legacy 字段只做兼容 | System Architect / Backend Core / Report Media Backend | ✅ 已实现Phase 0 RC1 后) | `mvn -f backend/pom.xml test` + H5/小程序构建 | 迁移见 `backend/db/migrations/20260801_split_service_identity.sql` |
--- ---

View File

@ -1,7 +1,7 @@
{"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","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: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","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","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:report","type":"Entity","name":"Report","nameZh":"服务报告","domain":"report","fields":["id","appointment_id","remark","user_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","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: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":"action:register_boss","type":"Action","name":"registerBoss","nameZh":"老板入驻","inputs":["storeName","bossName","phone","password"],"outputs":["entity:store","entity:user","session_token"],"entrypoints":["POST /api/user/register-boss","UserService#registerBoss"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/service/UserService.java#registerBoss","evidence":"anchored","domain":"user"} {"id":"action:register_boss","type":"Action","name":"registerBoss","nameZh":"老板入驻","inputs":["storeName","bossName","phone","password"],"outputs":["entity:store","entity:user","session_token"],"entrypoints":["POST /api/user/register-boss","UserService#registerBoss"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/service/UserService.java#registerBoss","evidence":"anchored","domain":"user"}
{"id":"action:update_store","type":"Action","name":"updateStore","nameZh":"更新店铺","inputs":["entity:store"],"outputs":["entity:store"],"entrypoints":["PUT /api/store/update","StoreController#update","StoreService#update"],"appliesRules":["rule:BR-STORE-001"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#update","test":"StoreControllerTest","evidence":"anchored","domain":"store"} {"id":"action:update_store","type":"Action","name":"updateStore","nameZh":"更新店铺","inputs":["entity:store"],"outputs":["entity:store"],"entrypoints":["PUT /api/store/update","StoreController#update","StoreService#update"],"appliesRules":["rule:BR-STORE-001"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#update","test":"StoreControllerTest","evidence":"anchored","domain":"store"}
@ -10,12 +10,12 @@
{"id":"action:login","type":"Action","name":"login","nameZh":"手机号验证码登录","inputs":["phone","code"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/login","UserService#login"],"appliesRules":["rule:BR-AUTH-001","rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login","test":"UserServiceLoginStrategyTest","evidence":"anchored","domain":"auth"} {"id":"action:login","type":"Action","name":"login","nameZh":"手机号验证码登录","inputs":["phone","code"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/login","UserService#login"],"appliesRules":["rule:BR-AUTH-001","rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login","test":"UserServiceLoginStrategyTest","evidence":"anchored","domain":"auth"}
{"id":"action:wx_phone_login","type":"Action","name":"wxPhoneLogin","nameZh":"微信手机号授权登录","inputs":["phoneCode","loginCode"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/wx-phone-login","UserController#wxPhoneLogin","UserService#loginByVerifiedPhone"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#wxPhoneLogin","evidence":"anchored","domain":"auth"} {"id":"action:wx_phone_login","type":"Action","name":"wxPhoneLogin","nameZh":"微信手机号授权登录","inputs":["phoneCode","loginCode"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/wx-phone-login","UserController#wxPhoneLogin","UserService#loginByVerifiedPhone"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#wxPhoneLogin","evidence":"anchored","domain":"auth"}
{"id":"action:bind_wechat","type":"Action","name":"bindWechat","nameZh":"绑定微信标识","inputs":["userId","openid","unionid"],"outputs":["entity:user"],"entrypoints":["UserService#bindWechatMiniIdentity"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored","domain":"auth"} {"id":"action:bind_wechat","type":"Action","name":"bindWechat","nameZh":"绑定微信标识","inputs":["userId","openid","unionid"],"outputs":["entity:user"],"entrypoints":["UserService#bindWechatMiniIdentity"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored","domain":"auth"}
{"id":"action:create_appointment","type":"Action","name":"createAppointment","nameZh":"创建预约","inputs":["petName","petType","serviceType","appointmentTime","storeId","remark","petId"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/create","AppointmentController#create","AppointmentService#createBooking"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create","evidence":"anchored","domain":"appointment"} {"id":"action:create_appointment","type":"Action","name":"createAppointment","nameZh":"创建预约","inputs":["petName","petType","serviceType","appointmentTime","storeId","remark","petId","customerUserId","customerPhone","customerName"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/create","AppointmentController#create","AppointmentService#createBooking"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-003","rule:BR-APPT-004"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create","test":"AppointmentControllerIdentityTest; AppointmentServiceTest","evidence":"anchored","domain":"appointment"}
{"id":"action:start_service","type":"Action","name":"startService","nameZh":"开始服务","inputs":["appointmentId"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/start","AppointmentController#start","AppointmentService#startService"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#start","test":"AppointmentServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:start_service","type":"Action","name":"startService","nameZh":"开始服务","inputs":["appointmentId"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/start","AppointmentController#start","AppointmentService#startService"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#start","test":"AppointmentServiceTest","evidence":"anchored","domain":"appointment"}
{"id":"action:transition_appointment_status","type":"Action","name":"transitionAppointmentStatus","nameZh":"更新预约状态","inputs":["id","status"],"outputs":["entity:appointment"],"entrypoints":["PUT /api/appointment/status","AppointmentController#updateStatus","AppointmentService#transitionStatus"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus","test":"AppointmentServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:transition_appointment_status","type":"Action","name":"transitionAppointmentStatus","nameZh":"更新预约状态","inputs":["id","status"],"outputs":["entity:appointment"],"entrypoints":["PUT /api/appointment/status","AppointmentController#updateStatus","AppointmentService#transitionStatus"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus","test":"AppointmentServiceTest","evidence":"anchored","domain":"appointment"}
{"id":"action:get_appointment_detail","type":"Action","name":"getAppointmentDetail","nameZh":"预约详情","inputs":["id"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/detail","AppointmentController#detail"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail","test":"AppointmentControllerDetailTest","evidence":"anchored","domain":"appointment"} {"id":"action:get_appointment_detail","type":"Action","name":"getAppointmentDetail","nameZh":"预约详情","inputs":["id"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/detail","AppointmentController#detail"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail","test":"AppointmentControllerDetailTest","evidence":"anchored","domain":"appointment"}
{"id":"action:list_appointments","type":"Action","name":"listAppointments","nameZh":"预约列表","inputs":["status","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/list","AppointmentController#list"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#list","evidence":"anchored","domain":"appointment"} {"id":"action:list_appointments","type":"Action","name":"listAppointments","nameZh":"预约列表","inputs":["status","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/list","AppointmentController#list"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#list","evidence":"anchored","domain":"appointment"}
{"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["entity:report"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create","test":"ReportControllerTest; ReportServiceTest","evidence":"anchored","domain":"report"} {"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["appointmentId","remark","images"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create","test":"ReportControllerTest; ReportServiceTest","evidence":"anchored","domain":"report","note":"authorStaffId 从登录上下文派生customerUserId 从预约派生"}
{"id":"action:get_report_by_token","type":"Action","name":"getReportByToken","nameZh":"公开报告查询","inputs":["token"],"outputs":["entity:report"],"entrypoints":["GET /api/report/get","ReportController#getByAppointmentId"],"appliesRules":["rule:BR-RPT-006"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","evidence":"anchored","domain":"report"} {"id":"action:get_report_by_token","type":"Action","name":"getReportByToken","nameZh":"公开报告查询","inputs":["token"],"outputs":["entity:report"],"entrypoints":["GET /api/report/get","ReportController#getByAppointmentId"],"appliesRules":["rule:BR-RPT-006"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","evidence":"anchored","domain":"report"}
{"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":[],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","evidence":"anchored","domain":"report"} {"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":[],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","evidence":"anchored","domain":"report"}
{"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"} {"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"}
@ -25,7 +25,7 @@
{"id":"event:store_registered","type":"Event","name":"StoreRegistered","nameZh":"门店入驻","domain":"store","payload":["storeId","bossUserId","inviteCode"],"emittedBy":["action:register_boss"],"evidence":"anchored"} {"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: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: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":["appointmentId","storeId","userId","appointmentTime"],"emittedBy":["action:create_appointment"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","evidence":"anchored"} {"id":"event:appointment_created","type":"Event","name":"AppointmentCreated","nameZh":"预约创建","domain":"appointment","payload":["appointmentId","storeId","customerUserId","createdByUserId","appointmentTime"],"emittedBy":["action:create_appointment"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","evidence":"anchored"}
{"id":"event:appointment_status_changed","type":"Event","name":"AppointmentStatusChanged","nameZh":"预约状态迁移","domain":"appointment","payload":["appointmentId","fromStatus","toStatus","assignedUserId"],"emittedBy":["action:start_service","action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus; ReportService#create","test":"AppointmentServiceTest; ReportServiceTest#createDoingAppointmentMarksDone","evidence":"anchored"} {"id":"event:appointment_status_changed","type":"Event","name":"AppointmentStatusChanged","nameZh":"预约状态迁移","domain":"appointment","payload":["appointmentId","fromStatus","toStatus","assignedUserId"],"emittedBy":["action:start_service","action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus; ReportService#create","test":"AppointmentServiceTest; ReportServiceTest#createDoingAppointmentMarksDone","evidence":"anchored"}
{"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["reportId","appointmentId","storeId","reportToken"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create","test":"ReportServiceTest","evidence":"anchored"} {"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["reportId","appointmentId","storeId","reportToken"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create","test":"ReportServiceTest","evidence":"anchored"}
{"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["reportTokenHash","isFirstOpen","source","ts"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","evidence":"anchored"} {"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["reportTokenHash","isFirstOpen","source","ts"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","evidence":"anchored"}
@ -37,7 +37,7 @@
{"id":"rule:BR-STORE-002","type":"Rule","name":"邀请码查询仅 boss/staff + 同店","domain":"store","appliesTo":["action:get_store_by_invite_code"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode","test":"StoreControllerTest#inviteCodeCrossStoreForbidden","evidence":"anchored"} {"id":"rule:BR-STORE-002","type":"Rule","name":"邀请码查询仅 boss/staff + 同店","domain":"store","appliesTo":["action:get_store_by_invite_code"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode","test":"StoreControllerTest#inviteCodeCrossStoreForbidden","evidence":"anchored"}
{"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-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-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:get_appointment_detail","action:transition_appointment_status"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail; #updateStatus","test":"AppointmentControllerDetailTest#customerCannotViewOthersAppointment","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"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","doc":"docs/产品设计文档.md §5.2","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-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-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"}
@ -52,11 +52,14 @@
{"id":"rel:store_has_users","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:user","cardinality":"1—N","evidence":"anchored","doc":"User.store_id → Store.id","domain":"store"} {"id":"rel:store_has_users","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:user","cardinality":"1—N","evidence":"anchored","doc":"User.store_id → Store.id","domain":"store"}
{"id":"rel:user_belongs_to_store","type":"Relation","subject":"entity:user","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"boss/staff 归属一个门店customer 可无门店","domain":"user"} {"id":"rel:user_belongs_to_store","type":"Relation","subject":"entity:user","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"boss/staff 归属一个门店customer 可无门店","domain":"user"}
{"id":"rel:store_has_appointments","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.store_id","domain":"store"} {"id":"rel:store_has_appointments","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.store_id","domain":"store"}
{"id":"rel:user_creates_appointments","type":"Relation","subject":"entity:user","predicate":"CREATES","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.user_id","domain":"user"} {"id":"rel:user_creates_appointments","type":"Relation","subject":"entity:user","predicate":"CREATES","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.created_by_user_id","domain":"user"}
{"id":"rel:user_owns_appointments","type":"Relation","subject":"entity:user","predicate":"RECEIVES_SERVICE_IN","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.customer_user_id客户归属","domain":"user"}
{"id":"rel:user_serves_appointments","type":"Relation","subject":"entity:user","predicate":"SERVES","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.assigned_user_id谁点谁服务","domain":"user"} {"id":"rel:user_serves_appointments","type":"Relation","subject":"entity:user","predicate":"SERVES","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.assigned_user_id谁点谁服务","domain":"user"}
{"id":"rel:appointment_has_report","type":"Relation","subject":"entity:appointment","predicate":"HAS","object":"entity:report","cardinality":"1—1","evidence":"anchored","doc":"Report.appointment_iduk_report_appointment 唯一约束)","domain":"appointment"} {"id":"rel:appointment_has_report","type":"Relation","subject":"entity:appointment","predicate":"HAS","object":"entity:report","cardinality":"1—1","evidence":"anchored","doc":"Report.appointment_iduk_report_appointment 唯一约束)","domain":"appointment"}
{"id":"rel:report_belongs_to_store","type":"Relation","subject":"entity:report","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"Report.store_id","domain":"report"} {"id":"rel:report_belongs_to_store","type":"Relation","subject":"entity:report","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"Report.store_id","domain":"report"}
{"id":"rel:report_served_by_user","type":"Relation","subject":"entity:report","predicate":"SERVED_BY","object":"entity:user","cardinality":"N—1","evidence":"anchored","doc":"Report.user_id技师","domain":"report"} {"id":"rel:report_served_by_user","type":"Relation","subject":"entity:report","predicate":"SERVED_BY","object":"entity:user","cardinality":"N—1","evidence":"anchored","doc":"Report.appointment_id -> Appointment.assigned_user_id服务技师","domain":"report"}
{"id":"rel:report_authored_by_user","type":"Relation","subject":"entity:report","predicate":"AUTHORED_BY","object":"entity:user","cardinality":"N—1","evidence":"anchored","doc":"Report.author_staff_id报告作者","domain":"report"}
{"id":"rel:report_belongs_to_customer","type":"Relation","subject":"entity:report","predicate":"BELONGS_TO_CUSTOMER","object":"entity:user","cardinality":"N—1","evidence":"anchored","doc":"Report.customer_user_id报告归属客户","domain":"report"}
{"id":"rel:report_has_leads","type":"Relation","subject":"entity:report","predicate":"HAS","object":"entity:report_lead","cardinality":"1—N","evidence":"anchored","doc":"ReportLead.report_id同手机号去重","domain":"report"} {"id":"rel:report_has_leads","type":"Relation","subject":"entity:report","predicate":"HAS","object":"entity:report_lead","cardinality":"1—N","evidence":"anchored","doc":"ReportLead.report_id同手机号去重","domain":"report"}
{"id":"rel:lead_belongs_to_store","type":"Relation","subject":"entity:report_lead","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"ReportLead.store_id回访池范围","domain":"lead"} {"id":"rel:lead_belongs_to_store","type":"Relation","subject":"entity:report_lead","predicate":"BELONGS_TO","object":"entity:store","cardinality":"N—1","evidence":"anchored","doc":"ReportLead.store_id回访池范围","domain":"lead"}
{"id":"rel:register_boss_emits_store_registered","type":"Relation","subject":"action:register_boss","predicate":"EMITS","object":"event:store_registered","evidence":"anchored","domain":"user"} {"id":"rel:register_boss_emits_store_registered","type":"Relation","subject":"action:register_boss","predicate":"EMITS","object":"event:store_registered","evidence":"anchored","domain":"user"}

View File

@ -84,12 +84,13 @@
### action:create_appointment ### action:create_appointment
创建预约:半小时占号,取消不占号;`userId` 从当前登录用户派生 创建预约:半小时占号,取消不占号;客户与创建人分开记录
- **入口**`POST /api/appointment/create` → `AppointmentController#create``AppointmentService#createBooking` - **入口**`POST /api/appointment/create` → `AppointmentController#create``AppointmentService#createBooking`
- **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?` - **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId``customerPhone`
- **输出**`entity:appointment` - **输出**`entity:appointment`
- **适用规则**`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-004`(半小时档 + 营业时间 + 占用校验) - **身份口径**customer 自约时客户=创建人=current.userIdboss/staff 代约时创建人=current.userId客户按请求解析legacy userId 镜像客户
- **适用规则**`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(半小时档 + 营业时间 + 占用校验)
- **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#create` - **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#create`
- **证据**`anchored` - **证据**`anchored`
@ -136,10 +137,10 @@
### action:submit_report ### action:submit_report
提交报告:仅 boss/staff`userId` 从上下文派生;报告所属门店必须等于当前用户门店。 提交报告:仅 boss/staff`authorStaffId` 从上下文派生,`customerUserId` 从预约派生;报告所属门店必须等于当前用户门店。
- **入口**`POST /api/report/create` → `ReportController#create``ReportService#create` - **入口**`POST /api/report/create` → `ReportController#create``ReportService#create`
- **输入**`entity:report`(含 `appointmentId`、`images` - **输入**`appointmentId`、`remark?`、`images`;身份字段不从请求体采信
- **输出**`reportToken`、`reportId` - **输出**`reportToken`、`reportId`
- **适用规则**`rule:BR-RPT-001`(必须绑定预约)、`rule:BR-RPT-002`(一约一份)、`rule:BR-RPT-003`(前后照片必填)、`rule:BR-RPT-004`(仅 doing 预约)、`rule:BR-RPT-005`(跨店 403 - **适用规则**`rule:BR-RPT-001`(必须绑定预约)、`rule:BR-RPT-002`(一约一份)、`rule:BR-RPT-003`(前后照片必填)、`rule:BR-RPT-004`(仅 doing 预约)、`rule:BR-RPT-005`(跨店 403
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#create` - **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#create`

View File

@ -40,7 +40,7 @@
预约创建成功(`status=new`,下单即占号生效)。 预约创建成功(`status=new`,下单即占号生效)。
- **触发动作**`action:create_appointment` - **触发动作**`action:create_appointment`
- **payload**`appointmentId`、`storeId`、`userId`、`appointmentTime` - **payload**`appointmentId`、`storeId`、`customerUserId`、`createdByUserId`、`appointmentTime`
- **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking` - **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking`
- **证据**`anchored` - **证据**`anchored`
@ -49,7 +49,7 @@
预约状态迁移(`new→doing`、`new→cancel`、`doing→done`、`doing→cancel`)。 预约状态迁移(`new→doing`、`new→cancel`、`doing→done`、`doing→cancel`)。
- **触发动作**`action:start_service``new→doing`)、`action:transition_appointment_status`、`action:submit_report``doing→done` - **触发动作**`action:start_service``new→doing`)、`action:transition_appointment_status`、`action:submit_report``doing→done`
- **payload**`appointmentId`、`fromStatus`、`toStatus`、`assignedUserId?` - **payload**`appointmentId`、`fromStatus`、`toStatus`、`assignedStaffId?`
- **适用规则**`rule:BR-APPT-001` - **适用规则**`rule:BR-APPT-001`
- **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#startService`/`#transitionStatus`、`backend/src/main/java/com/petstore/service/ReportService.java#create`(置 done - **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#startService`/`#transitionStatus`、`backend/src/main/java/com/petstore/service/ReportService.java#create`(置 done
- **测试**`AppointmentServiceTest`、`ReportServiceTest#createDoingAppointmentMarksDone` - **测试**`AppointmentServiceTest`、`ReportServiceTest#createDoingAppointmentMarksDone`

View File

@ -67,8 +67,10 @@
| appointment_time | DATETIME | 预约时间(半小时档,按营业时间/占用/已过校验) | | appointment_time | DATETIME | 预约时间(半小时档,按营业时间/占用/已过校验) |
| status | VARCHAR | new / doing / done / cancel | | status | VARCHAR | new / doing / done / cancel |
| store_id | BIGINT | 所属门店 | | store_id | BIGINT | 所属门店 |
| user_id | BIGINT | 创建人 ID谁建的 | | customer_user_id | BIGINT | 被服务客户账号;新业务归属主字段 |
| assigned_user_id | BIGINT | 技师 ID开始服务时赋值谁点谁服务 | | created_by_user_id | BIGINT | 实际创建预约的登录账号customer/boss/staff |
| user_id | BIGINT | legacy 兼容;新数据镜像 customer_user_id |
| assigned_user_id | BIGINT | 服务技师 ID开始服务时赋值API 别名 assignedStaffId |
| pet_id | BIGINT | 宠物档案 ID | | pet_id | BIGINT | 宠物档案 ID |
| remark | TEXT | 备注 | | remark | TEXT | 备注 |
| create_time / update_time | DATETIME | 时间戳 | | create_time / update_time | DATETIME | 时间戳 |
@ -89,13 +91,15 @@
| id | BIGINT | 主键 | | id | BIGINT | 主键 |
| appointment_id | BIGINT | 关联预约 ID唯一约束 `uk_report_appointment` | | appointment_id | BIGINT | 关联预约 ID唯一约束 `uk_report_appointment` |
| remark | TEXT | 备注 | | remark | TEXT | 备注 |
| user_id | BIGINT | 技师 ID | | customer_user_id | BIGINT | 报告归属客户,从预约派生 |
| author_staff_id | BIGINT | 实际提交报告的门店账号 |
| user_id | BIGINT | legacy 兼容;新数据镜像 author_staff_id |
| store_id | BIGINT | 所属门店 | | store_id | BIGINT | 所属门店 |
| report_token | VARCHAR | 公开访问令牌UUID不带回包给公开页 | | report_token | VARCHAR | 公开访问令牌UUID不带回包给公开页 |
| pet_name | VARCHAR | 宠物名(冗余) | | pet_name | VARCHAR | 宠物名(冗余) |
| service_type | VARCHAR | 服务类型(冗余) | | service_type | VARCHAR | 服务类型(冗余) |
| appointment_time | DATETIME | 服务时间(冗余) | | appointment_time | DATETIME | 服务时间(冗余) |
| staff_name | VARCHAR | 技师名(冗余 | | staff_name | VARCHAR | 服务技师名快照(优先取预约 assigned_user_id |
| highlight_video_url | VARCHAR(500) | 成片 MP4 相对 URL | | highlight_video_url | VARCHAR(500) | 成片 MP4 相对 URL |
| highlight_video_status | VARCHAR(32) | processing / done / failed | | highlight_video_status | VARCHAR(32) | processing / done / failed |
| highlight_video_error | VARCHAR(500) | 错误信息 | | highlight_video_error | VARCHAR(500) | 错误信息 |

View File

@ -9,10 +9,10 @@
| `Store` 1—N `User` | 一个门店有多个用户boss 1 + staff N + customer 不挂门店) | `User.store_id``Store.id` | | `Store` 1—N `User` | 一个门店有多个用户boss 1 + staff N + customer 不挂门店) | `User.store_id``Store.id` |
| `User` N—1 `Store` | boss/staff 归属一个门店customer 可无门店 | — | | `User` N—1 `Store` | boss/staff 归属一个门店customer 可无门店 | — |
| `Store` 1—N `Appointment` | 一个门店有多个预约 | `Appointment.store_id``Store.id` | | `Store` 1—N `Appointment` | 一个门店有多个预约 | `Appointment.store_id``Store.id` |
| `User` 1—N `Appointment` | 一个用户创建多个预约(`user_id`);技师服务多个预约(`assigned_user_id` | `Appointment.user_id` / `assigned_user_id``User.id` | | `User` 1—N `Appointment` | 一个客户拥有多次预约;一个账号可创建多次预约;一个技师服务多次预约 | `Appointment.customer_user_id` / `created_by_user_id` / `assigned_user_id``User.id` |
| `Appointment` 1—1 `Report` | 一约一份报告(`uk_report_appointment` 唯一约束) | `Report.appointment_id``Appointment.id` | | `Appointment` 1—1 `Report` | 一约一份报告(`uk_report_appointment` 唯一约束) | `Report.appointment_id``Appointment.id` |
| `Report` N—1 `Store` | 报告归属门店 | `Report.store_id``Store.id` | | `Report` N—1 `Store` | 报告归属门店 | `Report.store_id``Store.id` |
| `Report` N—1 `User` | 报告技师 | `Report.user_id` → `User.id` | | `Report` N—1 `User` | 报告分别归属客户与提交作者;服务技师由预约承载 | `Report.customer_user_id` / `author_staff_id` → `User.id` |
| `Report` 1—N `ReportImage` | 报告含多张照片before/after/during | `ReportImage.report_id``Report.id` | | `Report` 1—N `ReportImage` | 报告含多张照片before/after/during | `ReportImage.report_id``Report.id` |
| `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id``Report.id` | | `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id``Report.id` |
| `ReportLead` N—1 `Store` | 留资归属门店(回访池范围) | `ReportLead.store_id``Store.id` | | `ReportLead` N—1 `Store` | 留资归属门店(回访池范围) | `ReportLead.store_id``Store.id` |

View File

@ -73,11 +73,11 @@
### rule:BR-APPT-003 ### rule:BR-APPT-003
**预约跨用户 403**customer 仅可查看/取消自己的预约(`userId == current.userId`),不可操作他人预约。 **预约客户归属与跨用户 403**customer 仅可查看/取消自己的预约(`customerUserId == current.userId`;未回填旧数据可回退 legacy `userId`),不可操作他人预约。customer 自约时客户与创建人均从上下文派生boss/staff 代客预约必须解析真实 customer禁止把操作员工写成客户。`petId` 存在时必须属于该客户。
- **适用动作**`action:get_appointment_detail`、`action:transition_appointment_status`(仅 cancel - **适用动作**`action:create_appointment`、`action:get_appointment_detail`、`action:transition_appointment_status`(仅 cancel
- **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#detail`/`#updateStatus` - **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#detail`/`#updateStatus`
- **测试**`AppointmentControllerDetailTest#customerCannotViewOthersAppointment` - **测试**`AppointmentControllerIdentityTest`、`AppointmentControllerDetailTest#customerCannotViewOthersAppointment`
- **证据**`anchored` - **证据**`anchored`
### rule:BR-APPT-004 ### rule:BR-APPT-004

View File

@ -98,8 +98,10 @@
| appointment_time | DATETIME | 预约时间(以门店营业时间、半小时档、已占用档和已过时间为准;同一门店同一时间档最多一单) | | appointment_time | DATETIME | 预约时间(以门店营业时间、半小时档、已占用档和已过时间为准;同一门店同一时间档最多一单) |
| status | VARCHAR | new待开始/ doing服务中/ done已完成/ cancel已取消 | | status | VARCHAR | new待开始/ doing服务中/ done已完成/ cancel已取消 |
| store_id | BIGINT | 店铺ID | | store_id | BIGINT | 店铺ID |
| user_id | BIGINT | 创建人ID谁建的 | | customer_user_id | BIGINT | 被服务客户账号 |
| assigned_user_id | BIGINT | 技师ID开始服务时赋值 | | created_by_user_id | BIGINT | 实际创建预约的账号 |
| user_id | BIGINT | legacy 兼容,新数据镜像 customer_user_id |
| assigned_user_id | BIGINT | 服务技师ID开始服务时赋值API 别名 assignedStaffId |
| remark | TEXT | 备注 | | remark | TEXT | 备注 |
| create_time | DATETIME | 创建时间 | | create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 | | update_time | DATETIME | 更新时间 |
@ -110,13 +112,15 @@
| id | BIGINT | 主键 | | id | BIGINT | 主键 |
| appointment_id | BIGINT | 预约ID | | appointment_id | BIGINT | 预约ID |
| remark | TEXT | 备注 | | remark | TEXT | 备注 |
| user_id | BIGINT | 技师ID | | customer_user_id | BIGINT | 报告归属客户,从预约派生 |
| author_staff_id | BIGINT | 实际提交报告的门店账号 |
| user_id | BIGINT | legacy 兼容,新数据镜像 author_staff_id |
| store_id | BIGINT | 店铺ID | | store_id | BIGINT | 店铺ID |
| report_token | VARCHAR | 报告访问令牌UUID永久有效 | | report_token | VARCHAR | 报告访问令牌UUID永久有效 |
| pet_name | VARCHAR | 宠物名(冗余) | | pet_name | VARCHAR | 宠物名(冗余) |
| service_type | VARCHAR | 服务类型(冗余) | | service_type | VARCHAR | 服务类型(冗余) |
| appointment_time | DATETIME | 服务时间(冗余) | | appointment_time | DATETIME | 服务时间(冗余) |
| staff_name | VARCHAR | 技师名(冗余) | | staff_name | VARCHAR | 服务技师名快照(冗余) |
| create_time | DATETIME | 创建时间 | | create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 | | update_time | DATETIME | 更新时间 |

View File

@ -0,0 +1,70 @@
# 架构决策:预约与报告身份字段拆分
> 日期2026-08-01
>
> 状态:已采纳,兼容迁移期
>
> 范围Backend Core、Report Media Backend、Store Miniapp、Store Admin
## 1. 决策背景
RC1 之前,`Appointment.user_id` 同时被解释为客户或创建人;门店代客预约会把操作员工写入该字段。`Report.user_id` 实际保存技师/报告提交人,但宠主报告列表又曾把它当客户归属。两处语义冲突会污染客户档案、报告可见性、回访归因和后续经营指标。
## 2. 冻结语义
| 聚合 | 字段 | 语义 |
|---|---|---|
| Appointment | `customer_user_id` | 被服务的客户账号 |
| Appointment | `created_by_user_id` | 实际创建预约的登录账号,可为 customer/boss/staff |
| Appointment | `assigned_user_id` | 服务技师API 新增语义别名 `assignedStaffId` |
| Appointment | `user_id` | legacy 兼容字段;新数据镜像 `customer_user_id` |
| Report | `customer_user_id` | 报告归属客户,从 Appointment 派生 |
| Report | `author_staff_id` | 实际提交报告的门店账号 |
| Report | `user_id` | legacy 兼容字段;新数据镜像 `author_staff_id` |
| Report | `staff_name` | 服务技师姓名快照,优先取 Appointment.assigned_user_id |
“服务技师”和“报告作者”允许不同:老板可代员工提交报告,但报告仍展示实际服务技师。
## 3. 写入规则
### 宠主自行预约
- `customerUserId = current.userId`
- `createdByUserId = current.userId`
- 忽略请求体中的身份字段
### 门店代客预约
- `createdByUserId = current.userId`
- 必须提交 `customerUserId``customerPhone`
- 手机号不存在时创建 customer 账号;若手机号已绑定 boss/staff则拒绝静默复用
- `petId` 存在时必须属于解析出的客户
### 提交报告
- `authorStaffId = current.userId`
- `customerUserId = appointment.customerUserId`
- 服务技师继续来自 `appointment.assignedUserId`
## 4. 历史回填
迁移脚本:`backend/db/migrations/20260801_split_service_identity.sql`。
预约客户按以下证据顺序回填:
1. `pet.owner_user_id`
2. legacy `user_id` 且对应账号角色为 customer
3. 无可靠证据则保持空值,进入人工修正清单
报告客户只从关联预约回填;`report.user_id` 仅用于回填 `author_staff_id`,禁止当作客户。
## 5. 兼容与退出条件
- 兼容期继续读写 legacy `userId` / `assignedUserId`,同时输出 canonical 字段。
- 客户预约查询优先 `customer_user_id`,字段为空时回退 legacy `user_id`
- 客户报告查询通过 `report.customer_user_id` 或关联预约归属,不再按 `report.user_id` 查询。
- 只有在生产回填完成、旧客户端退出、关键指标稳定后,才可另立迁移删除 legacy 字段。
## 6. 非目标
本决策不引入多角色账号、跨店客户共享、StoreCustomer 主档、订单/支付或多门店组织;这些保持在后续独立阶段。