docs: define store customer master

This commit is contained in:
malei 2026-08-01 21:56:07 +08:00
parent f8fa38b92d
commit 32b073feaa
14 changed files with 130 additions and 42 deletions

View File

@ -45,7 +45,7 @@
项目领域知识以本体论框架建模为 SSOT位于 `docs/ontology/` 项目领域知识以本体论框架建模为 SSOT位于 `docs/ontology/`
- `objects.md` — 实体(`admin_console`/`workbench`/`service_customer` Phase A 预录入P1/P2 gap 占位见 membership 等) - `objects.md` — 实体(`store_customer` 已落为 JPA `StoreCustomer``admin_console`/`workbench` 为后台读模型P1/P2 gap 占位见 membership 等)
- `actions.md` — 动作controller 接口 + 客户端埋点 + **admin_* 后台动作 documented** - `actions.md` — 动作controller 接口 + 客户端埋点 + **admin_* 后台动作 documented**
- `events.md` — 事件(含 `workbench_viewed` gap - `events.md` — 事件(含 `workbench_viewed` gap
- `rules.md` — 规则(含 `rule:BR-ADMIN-001`…`004` 后台门禁与范围闸门) - `rules.md` — 规则(含 `rule:BR-ADMIN-001`…`004` 后台门禁与范围闸门)

View File

@ -58,6 +58,7 @@
| **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-10持续回归数量以 Surefire 当次统计为准 | | **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` | | **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` |
| **A14** | **StoreCustomer 门店客户主档**:客户列表不再临时拼成无 ID 投影 | 预约/留资自动建档同店账号与手机号唯一API 返回 storeCustomerId 且手机脱敏;历史数据可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` | 迁移见 `backend/db/migrations/20260801_create_store_customer.sql` |
--- ---

View File

@ -122,7 +122,7 @@ python3 docs/graph/audit_drift.py
|----|------|------| |----|------|------|
| `entity:admin_console` | Entity | Web 管理端壳 | | `entity:admin_console` | Entity | Web 管理端壳 |
| `entity:workbench` / `workbench_todo_item` | Entity | 今日工作台与待办 | | `entity:workbench` / `workbench_todo_item` | Entity | 今日工作台与待办 |
| `entity:service_customer` | Entity | 瘦身客户主档(非会员卡) | | `entity:store_customer` | Entity | 门店客户稳定主档(非会员卡) |
| `action:view_workbench` 等 8 个 admin_* | Action | 工作台/列表/设置/排班 | | `action:view_workbench` 等 8 个 admin_* | Action | 工作台/列表/设置/排班 |
| `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 | | `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 |
| `event:workbench_viewed` | Event | 可选埋点gap | | `event:workbench_viewed` | Event | 可选埋点gap |

View File

@ -10,7 +10,7 @@
{"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","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: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","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchBooking","test":"AppointmentControllerIdentityTest; AppointmentServiceTest; StoreCustomerServiceTest","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"}
@ -19,7 +19,7 @@
{"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"}
{"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder","evidence":"anchored","test":"ReportLeadControllerTest","domain":"lead"} {"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchLead","evidence":"anchored","test":"ReportLeadControllerTest; ReportLeadServiceStoreCustomerTest; StoreCustomerServiceTest","domain":"lead"}
{"id":"action:unsubscribe","type":"Action","name":"unsubscribe","nameZh":"一键退订","inputs":["unsubscribeToken"],"outputs":[],"entrypoints":["POST /api/report/unsubscribe","ReportLeadController#unsubscribe"],"appliesRules":["rule:BR-UNSUB-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe","evidence":"anchored","domain":"lead","test":"ReportLeadControllerTest"} {"id":"action:unsubscribe","type":"Action","name":"unsubscribe","nameZh":"一键退订","inputs":["unsubscribeToken"],"outputs":[],"entrypoints":["POST /api/report/unsubscribe","ReportLeadController#unsubscribe"],"appliesRules":["rule:BR-UNSUB-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe","evidence":"anchored","domain":"lead","test":"ReportLeadControllerTest"}
{"id":"action:list_leads","type":"Action","name":"listLeads","nameZh":"回访池列表","inputs":["status"],"outputs":["entity:report_lead"],"entrypoints":["GET /api/report/leads","ReportLeadController#leads"],"appliesRules":["rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored","test":"ReportLeadControllerTest","domain":"lead"} {"id":"action:list_leads","type":"Action","name":"listLeads","nameZh":"回访池列表","inputs":["status"],"outputs":["entity:report_lead"],"entrypoints":["GET /api/report/leads","ReportLeadController#leads"],"appliesRules":["rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored","test":"ReportLeadControllerTest","domain":"lead"}
{"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"}
@ -47,6 +47,7 @@
{"id":"rule:BR-RPT-006","type":"Rule","name":"公开报告字段裁剪","domain":"report","appliesTo":["action:get_report_by_token"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","frontend":"frontend/src/pages/report-view/reportView.vue","evidence":"anchored"} {"id":"rule:BR-RPT-006","type":"Rule","name":"公开报告字段裁剪","domain":"report","appliesTo":["action:get_report_by_token"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","frontend":"frontend/src/pages/report-view/reportView.vue","evidence":"anchored"}
{"id":"rule:BR-RPT-007","type":"Rule","name":"token hash 日志","domain":"report","appliesTo":["action:track_report_open"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash; #trackReportOpen","evidence":"anchored"} {"id":"rule:BR-RPT-007","type":"Rule","name":"token hash 日志","domain":"report","appliesTo":["action:track_report_open"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash; #trackReportOpen","evidence":"anchored"}
{"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"} {"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"}
{"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest","evidence":"anchored"}
{"id":"rule:BR-LEAD-002","type":"Rule","name":"留资标识脱敏","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#maskIdentifier; #leads","frontend":"frontend/src/pages/mine/Leads.vue","evidence":"anchored"} {"id":"rule:BR-LEAD-002","type":"Rule","name":"留资标识脱敏","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#maskIdentifier; #leads","frontend":"frontend/src/pages/mine/Leads.vue","evidence":"anchored"}
{"id":"rule:BR-LEAD-003","type":"Rule","name":"回访池跨店 403","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored"} {"id":"rule:BR-LEAD-003","type":"Rule","name":"回访池跨店 403","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored"}
{"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"}
@ -193,7 +194,7 @@
{"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"} {"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"}
{"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","pendingLeadCount","highlightFailedCount","todoItems","reportFunnel","anomalyList"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"","doc":"docs/ontology/objects.md#entity:workbench","test":"","evidence":"documented","note":"登录落点;含今日待办弹层与指标卡。聚合 appointment/report/lead/highlight 现有数据,非独立表。","status":"planned"} {"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","pendingLeadCount","highlightFailedCount","todoItems","reportFunnel","anomalyList"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"","doc":"docs/ontology/objects.md#entity:workbench","test":"","evidence":"documented","note":"登录落点;含今日待办弹层与指标卡。聚合 appointment/report/lead/highlight 现有数据,非独立表。","status":"planned"}
{"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台待办项","domain":"admin","jpa":false,"fields":["kind","count","drilldownAction","filter"],"systems":["Store Admin FE"],"code":"","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"","evidence":"documented","note":"kind: today_appointments | pending_report | highlight_anomaly | pending_lead","status":"planned"} {"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台待办项","domain":"admin","jpa":false,"fields":["kind","count","drilldownAction","filter"],"systems":["Store Admin FE"],"code":"","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"","evidence":"documented","note":"kind: today_appointments | pending_report | highlight_anomaly | pending_lead","status":"planned"}
{"id":"entity:service_customer","type":"Entity","name":"ServiceCustomer","nameZh":"服务客户主档(瘦身)","domain":"admin","jpa":false,"fields":["userId","displayName","phoneMasked","pets","lastVisitAt","lastReportId","leadStatus","source","storeId"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"","doc":"docs/ontology/objects.md#entity:service_customer","test":"","evidence":"documented","note":"服务经营视角客户投影。列表经 GET /api/admin/service-customersplanned。不含余额/次卡/积分。","status":"planned"} {"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"}
{"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据customer 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"} {"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据customer 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"}
{"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId忽略跨店请求参数。连锁同步不在 Phase A。"} {"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId忽略跨店请求参数。连锁同步不在 Phase A。"}
{"id":"rule:BR-ADMIN-003","type":"Rule","name":"adminInternalFieldsAllowed","nameZh":"后台可看内部字段(与公开裁剪对称)","domain":"admin","appliesTo":["action:admin_list_reports","action:admin_update_settings","action:admin_list_service_customers"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"经 BR-ADMIN-001 鉴权后,后台可返回 inviteCode、完整报告内部字段等公开 API 仍受 BR-STORE-003/BR-RPT-006 约束。手机号默认仍脱敏,明文需更高权限(预留)。"} {"id":"rule:BR-ADMIN-003","type":"Rule","name":"adminInternalFieldsAllowed","nameZh":"后台可看内部字段(与公开裁剪对称)","domain":"admin","appliesTo":["action:admin_list_reports","action:admin_update_settings","action:admin_list_service_customers"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"经 BR-ADMIN-001 鉴权后,后台可返回 inviteCode、完整报告内部字段等公开 API 仍受 BR-STORE-003/BR-RPT-006 约束。手机号默认仍脱敏,明文需更高权限(预留)。"}
@ -203,7 +204,7 @@
{"id":"action:admin_list_appointments","type":"Action","name":"adminListAppointments","nameZh":"后台预约列表","domain":"admin","inputs":["status","dateFrom","dateTo","assignedUserId","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["Admin UI /appointments (planned)","reuses GET /api/appointment/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-APPT-001","rule:BR-APPT-002"],"code":"backend/.../AppointmentController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_appointments","evidence":"documented","note":"Web 列表增强筛选;业务规则复用现有 Appointment*不另造状态机。Web 允许调用 start_service产品决策 2026-07-10"} {"id":"action:admin_list_appointments","type":"Action","name":"adminListAppointments","nameZh":"后台预约列表","domain":"admin","inputs":["status","dateFrom","dateTo","assignedUserId","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["Admin UI /appointments (planned)","reuses GET /api/appointment/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-APPT-001","rule:BR-APPT-002"],"code":"backend/.../AppointmentController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_appointments","evidence":"documented","note":"Web 列表增强筛选;业务规则复用现有 Appointment*不另造状态机。Web 允许调用 start_service产品决策 2026-07-10"}
{"id":"action:admin_list_reports","type":"Action","name":"adminListReports","nameZh":"后台报告列表","domain":"admin","inputs":["dateFrom","dateTo","highlightStatus","page","pageSize"],"outputs":["entity:report","entity:highlight_video"],"entrypoints":["Admin UI /reports (planned)","reuses GET /api/report/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-RPT-005"],"code":"backend/.../ReportController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_reports","evidence":"documented","note":"含成片三态、复制公开链接、触发重生成(复用 generate_highlight。"} {"id":"action:admin_list_reports","type":"Action","name":"adminListReports","nameZh":"后台报告列表","domain":"admin","inputs":["dateFrom","dateTo","highlightStatus","page","pageSize"],"outputs":["entity:report","entity:highlight_video"],"entrypoints":["Admin UI /reports (planned)","reuses GET /api/report/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-RPT-005"],"code":"backend/.../ReportController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_reports","evidence":"documented","note":"含成片三态、复制公开链接、触发重生成(复用 generate_highlight。"}
{"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_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:service_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"],"code":"","doc":"docs/ontology/actions.md#action:admin_list_service_customers; docs/门店管理后台-PhaseA-页面清单PRD.md §7.6","evidence":"anchored","note":"产品决策:新建聚合 API不以前端拼装为正式方案。Query: source,hasPendingLead,lastVisitFrom,lastVisitTo,q,page,pageSize2026-07-10 已实现聚合 API","code_refs":["backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#listServiceCustomers","backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java"]} {"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":"后台读本店含 inviteCodeGET /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_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":"后台读本店含 inviteCodeGET /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"],"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":"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 期可不实现上报。"}
@ -212,8 +213,9 @@
{"id":"rel:workbench_reads_appointments","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:appointment","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"今日预约指标"} {"id":"rel:workbench_reads_appointments","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:appointment","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"今日预约指标"}
{"id":"rel:workbench_reads_reports","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待出报告/成片异常"} {"id":"rel:workbench_reads_reports","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待出报告/成片异常"}
{"id":"rel:workbench_reads_leads","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report_lead","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待回访"} {"id":"rel:workbench_reads_leads","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report_lead","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待回访"}
{"id":"rel:service_customer_is_user","type":"Relation","subject":"entity:service_customer","predicate":"PROJECTS","object":"entity:user","domain":"admin","evidence":"documented","cardinality":"N—1","doc":"客户投影自 User(customer)"} {"id":"rel:store_has_service_customers","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:store_customer","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"StoreCustomer.store_id"}
{"id":"rel:service_customer_has_pets","type":"Relation","subject":"entity:service_customer","predicate":"HAS","object":"entity:pet","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"一人多宠"} {"id":"rel:service_customer_is_user","type":"Relation","subject":"entity:store_customer","predicate":"LINKS","object":"entity:user","domain":"admin","evidence":"anchored","cardinality":"N—0..1","doc":"StoreCustomer.customer_user_id留资客户可先为空"}
{"id":"rel:service_customer_has_pets","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:pet","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"customer_user_id -> Pet.owner_user_id动态读模型"}
{"id":"rel:view_workbench_emits_viewed","type":"Relation","subject":"action:view_workbench","predicate":"EMITS","object":"event:workbench_viewed","domain":"admin","evidence":"documented"} {"id":"rel:view_workbench_emits_viewed","type":"Relation","subject":"action:view_workbench","predicate":"EMITS","object":"event:workbench_viewed","domain":"admin","evidence":"documented"}
{"id":"rel:br_admin_001_applies_workbench","type":"Relation","subject":"rule:BR-ADMIN-001","predicate":"APPLIES_TO","object":"action:view_workbench","domain":"admin","evidence":"documented"} {"id":"rel:br_admin_001_applies_workbench","type":"Relation","subject":"rule:BR-ADMIN-001","predicate":"APPLIES_TO","object":"action:view_workbench","domain":"admin","evidence":"documented"}
{"id":"rel:br_admin_002_applies_workbench","type":"Relation","subject":"rule:BR-ADMIN-002","predicate":"APPLIES_TO","object":"action:view_workbench","domain":"admin","evidence":"documented"} {"id":"rel:br_admin_002_applies_workbench","type":"Relation","subject":"rule:BR-ADMIN-002","predicate":"APPLIES_TO","object":"action:view_workbench","domain":"admin","evidence":"documented"}
@ -221,5 +223,5 @@
{"id":"rel:admin_list_appts_reads_appointment","type":"Relation","subject":"action:admin_list_appointments","predicate":"READS","object":"entity:appointment","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_appts_reads_appointment","type":"Relation","subject":"action:admin_list_appointments","predicate":"READS","object":"entity:appointment","domain":"admin","evidence":"documented"}
{"id":"rel:admin_list_reports_reads_report","type":"Relation","subject":"action:admin_list_reports","predicate":"READS","object":"entity:report","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_reports_reads_report","type":"Relation","subject":"action:admin_list_reports","predicate":"READS","object":"entity:report","domain":"admin","evidence":"documented"}
{"id":"rel:admin_list_leads_reads_lead","type":"Relation","subject":"action:admin_list_leads","predicate":"READS","object":"entity:report_lead","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_leads_reads_lead","type":"Relation","subject":"action:admin_list_leads","predicate":"READS","object":"entity:report_lead","domain":"admin","evidence":"documented"}
{"id":"rel:admin_list_customers_reads_sc","type":"Relation","subject":"action:admin_list_service_customers","predicate":"READS","object":"entity:service_customer","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_customers_reads_sc","type":"Relation","subject":"action:admin_list_service_customers","predicate":"READS","object":"entity:store_customer","domain":"admin","evidence":"anchored"}
{"id":"rel:drilldown_todo_to_list_appts","type":"Relation","subject":"action:drilldown_workbench_todo","predicate":"ROUTES_TO","object":"action:admin_list_appointments","domain":"admin","evidence":"documented","doc":"kind=today_appointments|pending_report 等"} {"id":"rel:drilldown_todo_to_list_appts","type":"Relation","subject":"action:drilldown_workbench_todo","predicate":"ROUTES_TO","object":"action:admin_list_appointments","domain":"admin","evidence":"documented","doc":"kind=today_appointments|pending_report 等"}

View File

@ -61,7 +61,7 @@ rel:subject_predicate_object # rel:report_belongs_to_appointment
已覆盖 P0 主闭环实体与动作,并补录:`Pet`、`ServiceType`、`ScheduleBlock`、`HighlightVideo`、`SessionToken`、`ReportImage`、`ReportTestimonial`、`ServiceInterval`、`HighlightFailReason`、`CurrentUser`,以及成片/上传/鉴权/寄语/退订相关规则。 已覆盖 P0 主闭环实体与动作,并补录:`Pet`、`ServiceType`、`ScheduleBlock`、`HighlightVideo`、`SessionToken`、`ReportImage`、`ReportTestimonial`、`ServiceInterval`、`HighlightFailReason`、`CurrentUser`,以及成片/上传/鉴权/寄语/退订相关规则。
**门店 Web 后台 Phase Adocumented 预录入)**`AdminConsole`、`Workbench`、`WorkbenchTodoItem`、`ServiceCustomer` 与 `BR-ADMIN-001`…`004`、对应 admin actions。设计须引用这些 ID;收银/会员资产仍为 gap。 **门店 Web 后台 Phase A**`AdminConsole`、`Workbench`、`WorkbenchTodoItem` 仍是非 JPA 读模型;`StoreCustomer` 已落为 JPA 稳定主档。`BR-ADMIN-001`…`004` 与 admin actions 继续约束后台;收银/会员资产仍为 gap。
P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。 P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。

View File

@ -90,7 +90,7 @@
- **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId``customerPhone` - **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId``customerPhone`
- **输出**`entity:appointment` - **输出**`entity:appointment`
- **身份口径**customer 自约时客户=创建人=current.userIdboss/staff 代约时创建人=current.userId客户按请求解析legacy userId 镜像客户 - **身份口径**customer 自约时客户=创建人=current.userIdboss/staff 代约时创建人=current.userId客户按请求解析legacy userId 镜像客户
- **适用规则**`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(半小时档 + 营业时间 + 占用校验) - **适用规则**`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` - **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#create`
- **证据**`anchored` - **证据**`anchored`
@ -184,9 +184,10 @@
- **入口**`POST /api/report/{token}/reminder` → `ReportLeadController#submitReminder``ReportLeadService#submit`(公开) - **入口**`POST /api/report/{token}/reminder` → `ReportLeadController#submitReminder``ReportLeadService#submit`(公开)
- **输入**`phone`、`consent`、`loginCode?` - **输入**`phone`、`consent`、`loginCode?`
- **输出**`leadId`、`remindDate`、`unsubscribeToken`、`wechatBound` - **输出**`leadId`、`remindDate`、`unsubscribeToken`、`wechatBound`
- **适用规则**`rule:BR-LEAD-001`(去重) - **适用规则**`rule:BR-LEAD-001`(去重)、`rule:BR-SC-001`(门店客户建档/绑定)
- **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder` - **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder`
- **证据**`documented`(去重逻辑未补测试用例) - **测试**`ReportLeadControllerTest`、`ReportLeadServiceStoreCustomerTest`
- **证据**`anchored`
### action:unsubscribe ### action:unsubscribe
@ -631,11 +632,11 @@
后台服务客户列表(瘦身主档;姓名/手机/宠物名搜索)。 后台服务客户列表(瘦身主档;姓名/手机/宠物名搜索)。
- **入口**`GET /api/admin/service-customers` → `AdminServiceCustomerController#listServiceCustomers` → Admin UI `/customers` - **入口**`GET /api/admin/service-customers` → `AdminServiceCustomerController#listServiceCustomers` → Admin UI `/customers`
- **输出**`entity:service_customer` - **输出**`entity:store_customer`
- **适用规则**`rule:BR-ADMIN-001` … `BR-ADMIN-004` - **适用规则**`rule:BR-ADMIN-001` … `BR-ADMIN-004`、`rule:BR-SC-001`
- **证据**`anchored` - **证据**`anchored`
- **代码**`backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java`、`AdminServiceCustomerService` - **代码**`backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java`、`AdminServiceCustomerService`
- **产品决策**新建聚合 API不以前端拼装为正式方案;不做余额/次卡列、持卡会员导入 - **产品决策**`t_store_customer` 稳定主档为入口聚合,返回 `storeCustomerId`;不做余额/次卡列、持卡会员导入
- **文档**`docs/门店管理后台-PhaseA-页面清单PRD.md §7.6` - **文档**`docs/门店管理后台-PhaseA-页面清单PRD.md §7.6`
### action:admin_update_settings ### action:admin_update_settings

View File

@ -385,20 +385,24 @@
- **证据**`documented` - **证据**`documented`
## entity:service_customer ## entity:store_customer
**服务客户主档(瘦身)**。服务经营视角的客户投影User + Pet + 预约/报告/留资),**不是** `entity:membership` **StoreCustomer 门店客户主档(瘦身)**。门店与客户的稳定关系实体,**不是** `entity:membership`。后台读模型以它为入口,再聚合 Pet + Appointment + Report + ReportLead
| 字段 | 说明 | | 字段 | 说明 |
|------|------| |------|------|
| userId / displayName / phoneMasked | 身份(手机默认脱敏) | | id | 本店客户稳定 IDAPI: storeCustomerId |
| pets | 关联宠物摘要 | | store_id | 本店范围 |
| lastVisitAt / lastReportId | 最近到店 / 最近报告 | | customer_user_id | 可选全局 customer 账号;留资客户可先为空 |
| leadStatus / source | 留资状态;来源:预约/报告留资 | | phone / display_name | 本店联系手机号与称呼API 手机号默认脱敏 |
| storeId | 本店范围 | | source | 首次来源customer_booking / assisted_booking / report_lead |
| first_contact_at / last_contact_at | 首次/最近接触时间 |
| create_time / update_time / deleted | 审计与软删除 |
API 动态聚合字段:`pets`、`lastVisitAt`、`lastReportId`、`leadStatus`、`source`。`lastVisitAt` 只统计已开始/已完成的历史服务。
- **不做**:余额、押金、积分、次卡、会员等级、批量导入持卡会员 - **不做**:余额、押金、积分、次卡、会员等级、批量导入持卡会员
- **证据**`documented`(可先无独立表,查询聚合) - **证据**`anchored``t_store_customer` + 动态读模型
- **动作**`action:admin_list_service_customers` - **动作**`action:admin_list_service_customers`
--- ---

View File

@ -134,7 +134,7 @@
| `entity:service_interval` | Report Media Backend | | `entity:service_interval` | Report Media Backend |
| `entity:highlight_fail_reason` | Report Media Backend枚举 | | `entity:highlight_fail_reason` | Report Media Backend枚举 |
| `entity:current_user` | Backend Coreauth 包) | | `entity:current_user` | Backend Coreauth 包) |
| `entity:admin_console` / `workbench` / `workbench_todo_item` / `service_customer` | Store Admin FEplanned+ Backend Core | | `entity:admin_console` / `workbench` / `workbench_todo_item` / `store_customer` | Store Admin FE + Backend Core |
| P1/P2 gap 占位 | 未实现membership / stored_value / service_package / report_history / analytics_dashboardPhase A 后台禁止偷渡 | | P1/P2 gap 占位 | 未实现membership / stored_value / service_package / report_history / analytics_dashboardPhase A 后台禁止偷渡 |
## 其他关系Batch 补全) ## 其他关系Batch 补全)
@ -151,8 +151,9 @@
| `admin_console` HAS `workbench` | 登录默认工作台 | | `admin_console` HAS `workbench` | 登录默认工作台 |
| `workbench` HAS `workbench_todo_item` | 今日待办 | | `workbench` HAS `workbench_todo_item` | 今日待办 |
| `workbench` AGGREGATES `appointment` / `report` / `report_lead` | 指标与异常来源 | | `workbench` AGGREGATES `appointment` / `report` / `report_lead` | 指标与异常来源 |
| `service_customer` PROJECTS `user` | 客户投影 | | `store` HAS `store_customer` | 一店多个稳定客户关系 |
| `service_customer` HAS `pet` | 一人多宠 | | `store_customer` LINKS `user` | 可选关联全局 customer 账号;留资客户可先不关联 |
| `store_customer` HAS `pet` | 通过 customer user 关联一人多宠 |
| `view_workbench` EMITS `workbench_viewed` | 可选埋点 | | `view_workbench` EMITS `workbench_viewed` | 可选埋点 |
| `BR-ADMIN-001/002` APPLIES_TO `view_workbench` 等 | 角色与店范围 | | `BR-ADMIN-001/002` APPLIES_TO `view_workbench` 等 | 角色与店范围 |
| `BR-ADMIN-004` CONSTRAINS `admin_console` | Phase A 范围闸门 | | `BR-ADMIN-004` CONSTRAINS `admin_console` | Phase A 范围闸门 |

View File

@ -332,7 +332,7 @@
- **测试**`ReportLeadControllerTest` - **测试**`ReportLeadControllerTest`
- **证据**`anchored` - **证据**`anchored`
## 门店 Web 后台Phase A · documented ## 门店 Web 后台Phase A
### rule:BR-ADMIN-001 ### rule:BR-ADMIN-001
@ -364,3 +364,12 @@
- **适用**`entity:admin_console`、`entity:workbench` 及一切 admin Phase A 动作 - **适用**`entity:admin_console`、`entity:workbench` 及一切 admin Phase A 动作
- **文档**`docs/门店管理后台升级方案-对标宠老板.md` - **文档**`docs/门店管理后台升级方案-对标宠老板.md`
- **证据**`documented` - **证据**`documented`
### rule:BR-SC-001
**门店客户主档唯一性与合并**:同一门店内,`customer_user_id` 和手机号分别只能对应一条有效 StoreCustomer。留资可先按手机号建档后续预约/登录识别到 customer 后绑定或合并。不得将 boss/staff 绑定为客户主档。
- **适用动作**`action:create_appointment`、`action:submit_lead`、`action:admin_list_service_customers`
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerService.java`
- **测试**`StoreCustomerServiceTest`、`ReportLeadServiceStoreCustomerTest`、`IdentityOwnershipQueryTest`
- **证据**`anchored`

View File

@ -84,10 +84,23 @@
| phone | VARCHAR | 手机号 | | phone | VARCHAR | 手机号 |
| avatar | VARCHAR | 头像 | | avatar | VARCHAR | 头像 |
| store_id | BIGINT | 所属店铺 | | store_id | BIGINT | 所属店铺 |
| role | VARCHAR | boss / staff | | role | VARCHAR | boss / staff / customer |
| create_time | DATETIME | 创建时间 | | create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 | | update_time | DATETIME | 更新时间 |
### t_store_customer门店客户主档
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 本店客户稳定主键 |
| store_id | BIGINT | 门店 ID |
| customer_user_id | BIGINT | 全局 customer 账号,留资客户可空 |
| phone | VARCHAR(20) | 本店最后确认手机号API 默认脱敏 |
| display_name | VARCHAR(64) | 门店视角客户称呼 |
| source | VARCHAR(32) | customer_booking / assisted_booking / report_lead |
| first_contact_at / last_contact_at | DATETIME | 首次/最近接触时间 |
| create_time / update_time | DATETIME | 创建/更新时间 |
| deleted | TINYINT | 软删除标记 |
### t_appointment预约表 ### t_appointment预约表
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|

View File

@ -0,0 +1,55 @@
# 架构决策StoreCustomer 门店客户主档
> 日期2026-08-01
>
> 状态:已采纳,待生产数据迁移
>
> 范围Backend Core、Report Media Backend、Store Admin、Data Model
## 1. 决策背景
原「服务客户」列表每次请求时才从 User、Pet、Appointment、Report 和 ReportLead 拼装。同一个客户在不同门店没有稳定关系 ID留资手机号与后续 customer 账号也可能生成两条投影,无法可靠承载客户时间线、回访任务和再次预约。
## 2. 冻结模型
`StoreCustomer` 是「门店与客户的关系主档」,不是全局账号,也不是会员资产。
| 字段 | 语义 |
|---|---|
| `id` | 本店客户稳定 ID后续时间线/回访任务的关联键 |
| `store_id` | 门店数据范围 |
| `customer_user_id` | 全局 customer 账号;可空,便于先承接留资 |
| `phone` | 本店最后确认联系号;后台 API 默认脱敏 |
| `display_name` | 门店视角称呼 |
| `source` | 首次来源:`customer_booking` / `assisted_booking` / `report_lead` |
| `first_contact_at` / `last_contact_at` | 本店关系首次/最近触达时间 |
同店分别对 `(store_id, customer_user_id)``(store_id, phone)` 加唯一约束。`customer_user_id` 为空时可先以手机号建档;后续识别到 customer 账号时绑定或合并,禁止同手机号指向两个 customer。
## 3. 写入入口
- 宠主自助预约:建档来源 `customer_booking`
- boss/staff 代客预约:建档来源 `assisted_booking`
- 报告留资:建档来源 `report_lead`;若手机号已对应 customer直接绑定。
- 后续触达只更新 `last_contact_at`,不覆盖可靠的首次来源。
## 4. 读模型与口径
`GET /api/admin/service-customers``StoreCustomer` 为列表入口,再聚合宠物、预约、报告和留资,返回稳定 `storeCustomerId`
- `lastVisitAt` 只计算已开始/已完成的历史服务;未来预约不算到店。
- `lastReportId`、`leadStatus`、`pets` 仍从事实表实时读取,避免将易变经营指标重复写入主档。
- 返回 `phoneMasked`,不返回明文 `phone`
## 5. 历史迁移
按顺序执行:
1. `backend/db/migrations/20260801_split_service_identity.sql`
2. `backend/db/migrations/20260801_create_store_customer.sql`
第二个脚本先从已确定客户归属的预约建档,再合并报告留资。末尾三个验证计数必须全部为 0 才可发布新后端。
## 6. 非目标
本阶段不引入会员等级、积分、储值、次卡、标签群发、跨店共享或客户合并后台。这些需要独立的权限、资产和审计模型。

View File

@ -62,22 +62,22 @@ cd /Users/apple/_src/petstore/admin && npm run build
| 字段 | 内容 | | 字段 | 内容 |
|------|------| |------|------|
| Task Name | 新增服务客户聚合列表接口 | | Task Name | 建立 StoreCustomer 主档与服务客户聚合列表 |
| Role | `Backend Core` | | Role | `Backend Core` |
| Owner Agent | Backend Core | | Owner Agent | Backend Core |
| Paired QA | Core Flow QA | | Paired QA | Core Flow QA |
| Workstream | Store Admin Console | | Workstream | Store Admin Console |
| Track | Coding | | Track | Coding |
| Status | Ready待授权 | | Status | Done2026-08-01 |
| Repo/Path | `backend/.../controller|service`admin 客户域) | | Repo/Path | `backend/.../entity|mapper|controller|service`admin 客户域) |
| Service Lock | `admin-service-customers` | | Service Lock | `admin-service-customers` |
| Depends On | 用户授权编码PRD §7.6 | | Depends On | 用户授权编码PRD §7.6 |
| ontologyRefs | `[entity:service_customer, action:admin_list_service_customers, rule:BR-ADMIN-001, rule:BR-ADMIN-002, rule:BR-ADMIN-003, rule:BR-ADMIN-004]` | | ontologyRefs | `[entity:store_customer, action:admin_list_service_customers, rule:BR-SC-001, rule:BR-ADMIN-001, rule:BR-ADMIN-002, rule:BR-ADMIN-003, rule:BR-ADMIN-004]` |
| Architecture Review Required | Yes`/api/admin/**` 前缀) | | Architecture Review Required | Yes`/api/admin/**` 前缀) |
| Data Model Review Required | No投影聚合无新表优先 | | Data Model Review Required | Yes`t_store_customer` 唯一约束与历史迁移 |
| Access / Privacy Review Required | Yes手机号脱敏、本店 | | Access / Privacy Review Required | Yes手机号脱敏、本店 |
**验收**boss/staff 本店 200customer 403跨店不可见无余额/次卡字段;搜索含宠物名;本体 entrypoint 升 anchored 且 `audit_drift` 无漂移。 **验收**boss/staff 本店 200customer 403跨店不可见返回稳定 `storeCustomerId` 且手机脱敏;无余额/次卡字段;搜索含宠物名;本体与代码无漂移。
**验证** **验证**
```bash ```bash
@ -146,7 +146,7 @@ python3 /Users/apple/_src/petstore/docs/graph/audit_drift.py
| Repo/Path | `admin/src/**`customers/schedule/settings | | Repo/Path | `admin/src/**`customers/schedule/settings |
| Service Lock | `admin-a3-pages` | | Service Lock | `admin-a3-pages` |
| Depends On | T1、T2 | | Depends On | T1、T2 |
| ontologyRefs | `[entity:service_customer, action:admin_list_service_customers, action:admin_manage_schedule, action:admin_update_settings, rule:BR-USER-001, rule:BR-ST-001]` | | ontologyRefs | `[entity:store_customer, action:admin_list_service_customers, action:admin_manage_schedule, action:admin_update_settings, rule:BR-USER-001, rule:BR-ST-001]` |
| Access / Privacy Review Required | Yes | | Access / Privacy Review Required | Yes |
**验收**:客户页无余额列;排班与小程序一致;设置改时段生效;删员工仅 boss。 **验收**:客户页无余额列;排班与小程序一致;设置改时段生效;删员工仅 boss。

View File

@ -300,7 +300,7 @@ entity:admin_console
**页面 ID**`A5-customers` **页面 ID**`A5-customers`
**路由**`/customers` **路由**`/customers`
**ontologyRefs**`[entity:service_customer, entity:pet, entity:user, action:admin_list_service_customers, rule:BR-ADMIN-001, rule:BR-ADMIN-002, rule:BR-ADMIN-003, rule:BR-ADMIN-004]` **ontologyRefs**`[entity:store_customer, entity:pet, entity:user, action:admin_list_service_customers, rule:BR-SC-001, rule:BR-ADMIN-001, rule:BR-ADMIN-002, rule:BR-ADMIN-003, rule:BR-ADMIN-004]`
### 7.1 定位 ### 7.1 定位
@ -337,14 +337,16 @@ entity:admin_console
GET /api/admin/service-customers GET /api/admin/service-customers
Query: source, hasPendingLead, lastVisitFrom, lastVisitTo, q, page, pageSize Query: source, hasPendingLead, lastVisitFrom, lastVisitTo, q, page, pageSize
Auth: boss | staffstoreId=current.storeId Auth: boss | staffstoreId=current.storeId
Response: { total, summary: { customerCount, pendingLeadCount, monthVisitCount }, list: ServiceCustomer[] } Response: { total, summary: { customerCount, pendingLeadCount, monthVisitCount }, list: StoreCustomerView[] }
``` ```
`StoreCustomerView` 必含稳定 `storeCustomerId`,并包含 `userId?`、`displayName`、`phoneMasked`、`originSource`、`firstContactAt`、`lastContactAt`、`pets`、`lastVisitAt`、`lastReportId`、`leadStatus`、`source`。明文手机号不得返回。
- 对应动作:`action:admin_list_service_customers` - 对应动作:`action:admin_list_service_customers`
- 对应实体:`entity:service_customer` - 对应实体:`entity:store_customer`
- **不做**前端拼装多接口作为正式方案(可临时 mock上线以新 API 为准) - **不做**前端拼装多接口作为正式方案(可临时 mock上线以新 API 为准)
可选后续:`GET /api/admin/service-customers/{userId}` 详情。 可选后续:`GET /api/admin/service-customers/{storeCustomerId}` 详情。
### 7.7 验收 ### 7.7 验收
@ -408,7 +410,7 @@ API`user/staff-list`、`create-staff`、删除接口
| A6 排班 | `admin_manage_schedule` | `schedule_block` | `/api/schedule/*` | | A6 排班 | `admin_manage_schedule` | `schedule_block` | `/api/schedule/*` |
| A3 报告 | `admin_list_reports` | `report` / `highlight_video` | `/api/report/*` | | A3 报告 | `admin_list_reports` | `report` / `highlight_video` | `/api/report/*` |
| A4 线索 | `admin_list_leads` | `report_lead` | `/api/report/leads` | | A4 线索 | `admin_list_leads` | `report_lead` | `/api/report/leads` |
| A5 客户 | `admin_list_service_customers` | `service_customer` | **已实现** `GET /api/admin/service-customers` | | A5 客户 | `admin_list_service_customers` | `store_customer` | **已实现** `GET /api/admin/service-customers` |
| A7 设置 | `admin_update_settings` | `store` / `service_type` / `user` | store/service-type/user | | A7 设置 | `admin_update_settings` | `store` / `service_type` / `user` | store/service-type/user |
--- ---

View File

@ -184,7 +184,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置****允许「
## 9. 推荐下一步 ## 9. 推荐下一步
**本体预录入2026-07-10 已完成)**Phase A 后台概念已写入 ontology`entity:admin_console` / `workbench` / `service_customer``rule:BR-ADMIN-001`…`004``action:view_workbench` 等);设计须引用这些 ID`BR-ADMIN-004` 闸门约束 **本体状态**Phase A 后台概念已写入 ontology`entity:admin_console` / `workbench` / `store_customer``rule:BR-ADMIN-001`…`004``action:view_workbench` 等);其中 `store_customer` 已落为稳定 JPA 主档,其余读模型按实现状态为准
下一文档步骤:~~用上述 ontologyRefs 写 Phase A 页面清单 PRD~~ → **已完成**`docs/门店管理后台-PhaseA-页面清单PRD.md` 下一文档步骤:~~用上述 ontologyRefs 写 Phase A 页面清单 PRD~~ → **已完成**`docs/门店管理后台-PhaseA-页面清单PRD.md`