diff --git a/.lithe/.gitignore b/.lithe/.gitignore new file mode 100644 index 0000000..ad7dd23 --- /dev/null +++ b/.lithe/.gitignore @@ -0,0 +1,3 @@ +run/local.json +toolchains/local.json +**/*.tmp diff --git a/.lithe/project.json b/.lithe/project.json new file mode 100644 index 0000000..04c3952 --- /dev/null +++ b/.lithe/project.json @@ -0,0 +1,4 @@ +{ + "defaultRunConfiguration": "spring-boot.maven:crm-app", + "version": 1 +} \ No newline at end of file diff --git a/.lithe/run/generated.json b/.lithe/run/generated.json new file mode 100644 index 0000000..b074e0e --- /dev/null +++ b/.lithe/run/generated.json @@ -0,0 +1,1016 @@ +{ + "configurations": [ + { + "args": [], + "confidence": "native", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "application", + "extensions": { + "java": { + "source": "crm-app/src/main/java/com/crm/app/CrmAppApplication.java" + }, + "maven": { + "mainClass": "com.crm.app.CrmAppApplication", + "module": "crm-app" + } + }, + "id": "java-main:com.crm.app.CrmAppApplication", + "name": "CrmAppApplication", + "provider": "java.main", + "source": "crm-app/src/main/java/com/crm/app/CrmAppApplication.java", + "toolchains": { + "java": "project-jdk", + "maven": "project-maven" + } + }, + { + "args": [], + "confidence": "native", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "application", + "extensions": { + "java": { + "source": "crm-auth/src/main/java/com/crm/auth/AuthApplication.java" + }, + "maven": { + "mainClass": "com.crm.auth.AuthApplication", + "module": "crm-auth" + } + }, + "id": "java-main:com.crm.auth.AuthApplication", + "name": "AuthApplication", + "provider": "java.main", + "source": "crm-auth/src/main/java/com/crm/auth/AuthApplication.java", + "toolchains": { + "java": "project-jdk", + "maven": "project-maven" + } + }, + { + "args": [], + "confidence": "native", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "application", + "extensions": { + "java": { + "source": "crm-dict/src/main/java/com/crm/dict/DictApplication.java" + }, + "maven": { + "mainClass": "com.crm.dict.DictApplication", + "module": "crm-dict" + } + }, + "id": "java-main:com.crm.dict.DictApplication", + "name": "DictApplication", + "provider": "java.main", + "source": "crm-dict/src/main/java/com/crm/dict/DictApplication.java", + "toolchains": { + "java": "project-jdk", + "maven": "project-maven" + } + }, + { + "args": [], + "confidence": "native", + "cwd": ".", + "debug": { + "adapter": "jdwp" + }, + "disabled": false, + "env": {}, + "execution": "application", + "extensions": { + "maven": { + "module": "." + } + }, + "id": "current-file", + "name": "Current File", + "provider": "java.current-file", + "toolchains": { + "java": "project-jdk" + } + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml" + } + }, + "id": "compose.stack:compose up", + "name": "compose up", + "provider": "compose.stack", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [], + "confidence": "declared", + "cwd": ".", + "debug": { + "adapter": "jdwp" + }, + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "maven": { + "mainClass": "com.crm.app.CrmAppApplication", + "module": "crm-app" + } + }, + "id": "spring-boot.maven:crm-app", + "name": "crm-app", + "provider": "spring-boot.maven", + "source": "crm-app/pom.xml", + "toolchains": { + "java": "project-jdk", + "maven": "project-maven" + } + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up", + "minio" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml", + "service": "minio" + } + }, + "id": "compose.service:minio", + "name": "minio", + "provider": "compose.service", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up", + "minio-init" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml", + "service": "minio-init" + } + }, + "id": "compose.service:minio-init", + "name": "minio-init", + "provider": "compose.service", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up", + "mysql" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml", + "service": "mysql" + } + }, + "id": "compose.service:mysql", + "name": "mysql", + "provider": "compose.service", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up", + "nginx" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml", + "service": "nginx" + } + }, + "id": "compose.service:nginx", + "name": "nginx", + "provider": "compose.service", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [ + "compose", + "-f", + "docker-compose.yml", + "up", + "redis" + ], + "command": "docker", + "confidence": "declared", + "cwd": ".", + "disabled": false, + "env": {}, + "execution": "service", + "extensions": { + "compose": { + "file": "docker-compose.yml", + "service": "redis" + } + }, + "id": "compose.service:redis", + "name": "redis", + "provider": "compose.service", + "source": "docker-compose.yml", + "toolchains": {} + }, + { + "args": [ + "run", + "build" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "build" + } + }, + "id": "npm.script:tmp/china-division/package/build", + "name": "build", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "csv" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "csv" + } + }, + "id": "npm.script:tmp/china-division/package/csv", + "name": "csv", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "fetch" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "fetch" + } + }, + "id": "npm.script:tmp/china-division/package/fetch", + "name": "fetch", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "format" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "format" + } + }, + "id": "npm.script:tmp/china-division/package/format", + "name": "format", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "json" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "json" + } + }, + "id": "npm.script:tmp/china-division/package/json", + "name": "json", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "precommit" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "precommit" + } + }, + "id": "npm.script:tmp/china-division/package/precommit", + "name": "precommit", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + }, + { + "args": [ + "run", + "test" + ], + "command": "npm", + "confidence": "declared", + "cwd": "tmp/china-division/package", + "disabled": false, + "env": {}, + "execution": "task", + "extensions": { + "npm": { + "manager": "npm", + "script": "test" + } + }, + "id": "npm.script:tmp/china-division/package/test", + "name": "test", + "provider": "npm.script", + "source": "tmp/china-division/package/package.json", + "toolchains": { + "runtime": "project-node" + } + } + ], + "generator": { + "fingerprint": "sha256:c3a4b1a72f53a9fd91457473dc3f94bfcfd59320ce7532f81fc2772967924660", + "inputs": { + "crm-app/pom.xml": "sha256:5dab7c63bd1ebac88806b775f7e8e920d81eb500d3c0040ef93486b7df8f20fe", + "crm-app/src/main/java/com/crm/app/CrmAppApplication.java": "sha256:5eecad74f14776e5f2f51eef0d0925dde9874d93da3fb76dfd8d7c3cebe2c809", + "crm-auth/pom.xml": "sha256:7ce7c34655abce46bfabad8f78a78aa3e229d667246debd50d93f0d66b37dcd1", + "crm-auth/src/main/java/com/crm/auth/AuthApplication.java": "sha256:b89cd3423ee3a8d9d2f29cb4bb0b0487c3b5bfad3690f53b23fa2578165b537a", + "crm-auth/src/main/java/com/crm/auth/config/AuthProperties.java": "sha256:5494038ac9326bf073ae8ea2283e9ef6acfcbcc45d1b14d2f367027b5246e50c", + "crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java": "sha256:827da4020226cb1233c374687b0dd9883a95d5bdbde7e63cd9a4704f078fd637", + "crm-auth/src/main/java/com/crm/auth/config/DataScopeModuleStartupValidator.java": "sha256:75e4bd8a0dade6ac913e35fccb09c7e4eb822c950912c4d280eff6f41f7539cd", + "crm-auth/src/main/java/com/crm/auth/config/DtClientConfig.java": "sha256:2114d904117bad7c98342e78d70da32c056601c3b67981cd8bc2c4ad15b75b38", + "crm-auth/src/main/java/com/crm/auth/config/PermissionConfig.java": "sha256:1ceaaeca5d97dda255cd062a55c099aa321c7ece83a49e738cae40f614503599", + "crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java": "sha256:8ba086c26b3bf6e7800d268f5d8f5a39cedfc63c65a4acf3d1274285aae8b1bc", + "crm-auth/src/main/java/com/crm/auth/config/WebMvcConfig.java": "sha256:04437ccb4f7676460c1595a53666eec5d71234c3f685a7b142a0e3a4566843d0", + "crm-auth/src/main/java/com/crm/auth/constant/AuthConstants.java": "sha256:f6644b6e138df0f6804c39c0818145231bd91f1a8c14f49d87a03da137362b3e", + "crm-auth/src/main/java/com/crm/auth/controller/AuthController.java": "sha256:4cc4ffa3a8e185d4048e96a16083a6f8a9086d5ba9f3566c3897784a26907bc1", + "crm-auth/src/main/java/com/crm/auth/controller/DataScopeModuleController.java": "sha256:24707eb92b33e23c94fd8696da9bbe6542b6fdb4e4909f2ab754a7f36fbcab81", + "crm-auth/src/main/java/com/crm/auth/controller/DebugTokenController.java": "sha256:5136c1e73b156379c9c47ef408edc86a376e22cd828dabbcb525518994678251", + "crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java": "sha256:6f673cf1d0f375421289c263fd10b11e0271174e7fb848fb44c9c0a2526742ea", + "crm-auth/src/main/java/com/crm/auth/controller/RoleController.java": "sha256:6593d289077c443868e00a91dc14b519c3bf841526e373c8a2fc194ab2e3cfdf", + "crm-auth/src/main/java/com/crm/auth/controller/SystemController.java": "sha256:34aacadc7d3b378decbed82f3fcf353ec4b20f0747f1c8e789a934278dc18818", + "crm-auth/src/main/java/com/crm/auth/domain/dto/AssignDeptDTO.java": "sha256:b5776452277628734f4f33cc28e324a8586e104a09ed3db26a66e46da50bb9dd", + "crm-auth/src/main/java/com/crm/auth/domain/dto/ConfigureAdminRoleDTO.java": "sha256:10bffb2bc91a82e25b6baeef34ed5afb368cf135fe85b9e02a35b2319aeff927", + "crm-auth/src/main/java/com/crm/auth/domain/dto/DataScopeModuleDTO.java": "sha256:878476525479e815a4b86d5ec98acfc82d487b73c1a4ba75ed4e6277786a5f19", + "crm-auth/src/main/java/com/crm/auth/domain/dto/DeptDTO.java": "sha256:3daf18744dc8283cfbb2377baa03dbc2eaafd213b78d2f0a0dd32a1dac22fcd3", + "crm-auth/src/main/java/com/crm/auth/domain/dto/DtSyncResultDTO.java": "sha256:4eed036bb4a10dc12e484779265d77e02630fe1c18cc2b2a26a041307abad1d9", + "crm-auth/src/main/java/com/crm/auth/domain/dto/LoginResultDTO.java": "sha256:cbe6e5be8ae5be4bb50dbc981dc1227f0f87f7d50833e59e7a85fb7219bc0e1a", + "crm-auth/src/main/java/com/crm/auth/domain/dto/ModuleScopeDTO.java": "sha256:c36ae06b69f587a80cc2555285e51c25458689472d9bdeec9fc6d8241e214db7", + "crm-auth/src/main/java/com/crm/auth/domain/dto/ResourceNodeDTO.java": "sha256:ec5d2776017b4922d1fbedc2dc552ddccd2f874cfd6f3f9c75998c1b187f3a8c", + "crm-auth/src/main/java/com/crm/auth/domain/dto/RoleDTO.java": "sha256:491bbb3adde4a8d6798944b4a80b1ed64600de36ce98c1c909a17d10f788ab7c", + "crm-auth/src/main/java/com/crm/auth/domain/dto/UserInfoDTO.java": "sha256:6a67ae5a2b9657558e1e3a2b748c6b9eb8c20306fbf92db2b7aa34f306d67892", + "crm-auth/src/main/java/com/crm/auth/domain/dto/UserListDTO.java": "sha256:ab7a067b5c475673aed6f1631a407213a248f7f5bbbc6a3881fc989d4a89e89d", + "crm-auth/src/main/java/com/crm/auth/domain/dto/UserStatsDTO.java": "sha256:b433c3973bb9b33309edefc6557ded824da14fe960da868935253d49c5db41a8", + "crm-auth/src/main/java/com/crm/auth/domain/entity/AuthIdentity.java": "sha256:2a4113c5fb355dbbed2561ef637227bd21f4b7166d28f882ecfc66dae3a53385", + "crm-auth/src/main/java/com/crm/auth/domain/entity/AuthUser.java": "sha256:ad7207250a7dc2e7c89a490fbd9c0b338dfa735016478c09f98c5472ecbfc4a5", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysDataScopeModule.java": "sha256:1bcf5ab53116c174305b6727ead8ca28a2e6c154cc0d8809409f965b085827ec", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysDept.java": "sha256:e77d491da5fa57932f3063c4804b075e4ebcea786d347d2ba7fc8aa65acc31ab", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysMenu.java": "sha256:98737d45a2e656679b29829a276c6055945768306c0d9d52fa66b5f5b67fe730", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysRole.java": "sha256:4cbb372cd644ca0bc94110cf0fa8e581f70df9c037c18e3bd36d4d2ff297b9e3", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysRoleDataScope.java": "sha256:02ef15b122b5171419a661621aa19d0f28b9503851394f02057bf1dde7c38fc6", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysRoleMenu.java": "sha256:3c5d6140eb799f20f36ef6d1be7ebbc9d9a2216e39d488725ddbaaf772a0716b", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysUserDept.java": "sha256:8e404f2fd225d2a7a2b3e92e5156ebe6bb33ba138ebe73c1b02a384259a24875", + "crm-auth/src/main/java/com/crm/auth/domain/entity/SysUserRole.java": "sha256:140a43f8fb49d49349aac112637bfde305e5dfab5bca06a637cf7e2e098f5257", + "crm-auth/src/main/java/com/crm/auth/domain/enums/ClientTypeEnum.java": "sha256:0eeffac88788a9b4dc06e0c38d0e4d9bf194cf4295dfd39e1c5d05b0bc780948", + "crm-auth/src/main/java/com/crm/auth/domain/enums/DataScopeEnum.java": "sha256:72d04be69e1867d9960e3868bc1f4fca67de6199158cb0a592fd85729ffbcf84", + "crm-auth/src/main/java/com/crm/auth/domain/enums/IdentityTypeEnum.java": "sha256:0bc939cbde791d36814f6014486ed9d1a183cb11a57e4d1ce776a1074d30e227", + "crm-auth/src/main/java/com/crm/auth/domain/enums/MenuType.java": "sha256:ee434b60a0ac59d05c5b6960cf034c3ab68f55697893aa2f22f06a82b8ba0246", + "crm-auth/src/main/java/com/crm/auth/domain/param/RoleParam.java": "sha256:c85bedbadaefd9b3b69ac3d4136ab4ffe15c1434fd86b19a321f0f01fa41fd27", + "crm-auth/src/main/java/com/crm/auth/domain/param/UserPageParam.java": "sha256:6ef871bdbf3a9056978171f8d728a2940c0190b32c4315e69d6cf91a8c0d8ff9", + "crm-auth/src/main/java/com/crm/auth/mapper/AuthIdentityMapper.java": "sha256:7ce45cc77ce4cdd857359b1712a2d4277bf3f219b11adfa8bd80c5ad4cde856e", + "crm-auth/src/main/java/com/crm/auth/mapper/AuthUserMapper.java": "sha256:f171120d0e519963f359d582ee80ef2507c26b095bcaca4424df6d4013de090f", + "crm-auth/src/main/java/com/crm/auth/mapper/SysDataScopeModuleMapper.java": "sha256:ece2a69d2c2326dabc10f0dae09c45d50a4c571fb64d2b6804c89726924a1ddb", + "crm-auth/src/main/java/com/crm/auth/mapper/SysDeptMapper.java": "sha256:c1b084fe7470db804a7e7acce58795e8b3c648cb7cf0e1eb39cd55cc6fdc1e8f", + "crm-auth/src/main/java/com/crm/auth/mapper/SysMenuMapper.java": "sha256:4379286b74cae51fdfa4a401740868c33aad59139b9d0d44ac5c69b04942b627", + "crm-auth/src/main/java/com/crm/auth/mapper/SysRoleDataScopeMapper.java": "sha256:0d4a08f71cb8d3d39378c12042b13ce48d5d3788c46e842ea2ccf68f5ef26676", + "crm-auth/src/main/java/com/crm/auth/mapper/SysRoleMapper.java": "sha256:f353d96b05e4037d3b2e6b5f093ad7e42570c5aad8651e436a86e9680c1eef21", + "crm-auth/src/main/java/com/crm/auth/mapper/SysRoleMenuMapper.java": "sha256:6117dccd0cc62c9162cf8124387fac1a5dc37373676240bff4688abaff5e5b61", + "crm-auth/src/main/java/com/crm/auth/mapper/SysUserDeptMapper.java": "sha256:96c50f87ea592a36d13fd7bc45c4731b04457c89b688d397356aad9167eee75f", + "crm-auth/src/main/java/com/crm/auth/mapper/SysUserRoleMapper.java": "sha256:111aac60f0630fd2140bd4d8f0a64197909c2d27d92f235a507cc04006d4ebd5", + "crm-auth/src/main/java/com/crm/auth/security/ApiPermissionCache.java": "sha256:2de1a5a139a939bb693e21a5933df08e597d819a2f07335fd7a12b4c8a19074b", + "crm-auth/src/main/java/com/crm/auth/security/ApiPermissionInterceptor.java": "sha256:46b65181f6b4a71cdca7553f5e26fb0606d6f5cfb27ae3972e1ea25923936771", + "crm-auth/src/main/java/com/crm/auth/security/ApiPermissionRule.java": "sha256:0066e2d9349a98093e6429e373771f50975c399d7285481a1551a3e5cca5e19e", + "crm-auth/src/main/java/com/crm/auth/security/AuthLoginUser.java": "sha256:09723717897aaa556ca4436e8745ea2007e9783889b81f8a2369f1dd193898a4", + "crm-auth/src/main/java/com/crm/auth/security/DataScopeInterceptor.java": "sha256:9f532aa062f9a43857db7bec4f3d9799fa34a1eca1d8717ce45e9f6e3aca368f", + "crm-auth/src/main/java/com/crm/auth/security/DataScopeTables.java": "sha256:a8719b5fe729a518219accbd528313fc45ce9ba1166ccab4c74250948237ce52", + "crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java": "sha256:24b94b166f608071e0e4376adfc27024f4be0da0601337c84d5118e6cb24d770", + "crm-auth/src/main/java/com/crm/auth/security/JwtCodec.java": "sha256:25206c4213511a1702950a1d12d1a37e0b3cc290a1021facca2ae52b25066bd4", + "crm-auth/src/main/java/com/crm/auth/security/PermissionGrant.java": "sha256:654333dde9eff14f978568eb91f6ea9328c373b2fa1864e346c0aae995d88627", + "crm-auth/src/main/java/com/crm/auth/security/PermissionResolver.java": "sha256:e5a5a87bbd2c903400fb1d08ff0e4f9189da624b6caa01f03c6bfb859514c8a9", + "crm-auth/src/main/java/com/crm/auth/security/PermissionResolverImpl.java": "sha256:48a2ef64004bd9014d7aa578889054323928055d20d3e3abae07155e7972fa7e", + "crm-auth/src/main/java/com/crm/auth/security/PreLoginSession.java": "sha256:31d2c3128dc85c311478162cd851561c89cebfa7dd4d46a3bdf4a37f3c302047", + "crm-auth/src/main/java/com/crm/auth/security/PreLoginTicketStore.java": "sha256:db569e72eed070fa7cd9baa03fab72d5f455036bd2e05de2a12a1176d95355e9", + "crm-auth/src/main/java/com/crm/auth/security/SecurityExceptionHandlers.java": "sha256:3dabb81ec7204f3e29562ac97c5fa259935ae47d9460e8dd26fe5ff07c72aff3", + "crm-auth/src/main/java/com/crm/auth/security/SessionStore.java": "sha256:dd5429a4f6bdaafaea807cccf0568b0629e59f18d40847f5612692ef245bab23", + "crm-auth/src/main/java/com/crm/auth/security/impl/JwtCodecImpl.java": "sha256:d4ec6de488fb4401102071cacdd2fa124f2232776310a1e92e84a50af3589cc2", + "crm-auth/src/main/java/com/crm/auth/security/impl/PreLoginTicketStoreImpl.java": "sha256:0d962c5eaeba5d56a3069fd4f70839324062d5d59b48b9b3179ed392938eab6f", + "crm-auth/src/main/java/com/crm/auth/security/impl/SessionStoreImpl.java": "sha256:4d957eb84fef1b8a6b125a2a1c869a0fa471bfb2bebb326b32251a3ce8760800", + "crm-auth/src/main/java/com/crm/auth/service/DeptTreeCache.java": "sha256:d6ae824a5ed6d36f40b6bc408894892571efa9416f864b092131791bdba00bcd", + "crm-auth/src/main/java/com/crm/auth/service/IAuthIdentityService.java": "sha256:0d630acf9099062312832ac736527c8a68ff6756796635b504d7c16abecfffb6", + "crm-auth/src/main/java/com/crm/auth/service/IAuthService.java": "sha256:cd6461e82062614575dccd0252df9ff858c21b00c8a1e03fde4a8df95c3958f9", + "crm-auth/src/main/java/com/crm/auth/service/IAuthUserService.java": "sha256:61f7ac06cfd53ff35e3caefa7bc503e2fc948d76af3e80d549b48bb5fe485667", + "crm-auth/src/main/java/com/crm/auth/service/IDtOrgSyncService.java": "sha256:bc66f18ba85bebab7065a32b188eccc9699c7e736ab1884db832ec0d32363936", + "crm-auth/src/main/java/com/crm/auth/service/IResourceService.java": "sha256:f44361a526b0ef34d661e807379338bccbfe74cdd4ddee8a32c782446e9bceba", + "crm-auth/src/main/java/com/crm/auth/service/ISysDeptService.java": "sha256:c5167638bac3c3f8e3d9477fbb01cd90ae70e7a6320f39a8189a7bab237a9613", + "crm-auth/src/main/java/com/crm/auth/service/ISysMenuService.java": "sha256:0f53ab354c9b550bbb29d773a294898ae079c4976c613d978b46fda97ba73aec", + "crm-auth/src/main/java/com/crm/auth/service/ISysRoleService.java": "sha256:03f213da8fed5ff893a8f97670a5d04d2bdaac5b5bb1a10905f19a30953101f2", + "crm-auth/src/main/java/com/crm/auth/service/IUserQueryService.java": "sha256:4ca9afac23ecf8d57fbb464807675db1afeb70493416e7892e8ece77eae1b33c", + "crm-auth/src/main/java/com/crm/auth/service/RoleScopeStore.java": "sha256:58d2c46dea737a59549c78e5185bca1ecdfc0cb3bf869bcbd2718178e1175557", + "crm-auth/src/main/java/com/crm/auth/service/client/CorpAccessTokenProvider.java": "sha256:c526021ec57507b768054738def64f09a28fbb7d80b842488be78267331fc8d7", + "crm-auth/src/main/java/com/crm/auth/service/client/CorpAccessTokenProviderImpl.java": "sha256:c5b80cbaa5a915f44cbb97cef774089a26902927d045fea238deefd27253f20d", + "crm-auth/src/main/java/com/crm/auth/service/client/DingTalkAuthClient.java": "sha256:22b06f76353ee0a77d98d0cd78d1689ba25461f3433a3dcc871aa1395bc7b19e", + "crm-auth/src/main/java/com/crm/auth/service/client/ThirdPartyAuthClient.java": "sha256:c53108c4a26914e5219a8432e0f1b273c581d4145349f940df6c6fafc1783588", + "crm-auth/src/main/java/com/crm/auth/service/client/ThirdPartyAuthClientFactory.java": "sha256:3135dfc0626795397d5d6c2e0331e919ec1259d6d2aef7d0618e974d040ac59b", + "crm-auth/src/main/java/com/crm/auth/service/client/ThirdPartyUserInfo.java": "sha256:f50c8feeb2b5651b1b2cda52f8d18be849cd0627fe75ea5ef4298e5c6c3630ac", + "crm-auth/src/main/java/com/crm/auth/service/impl/AuthIdentityServiceImpl.java": "sha256:3379660c660604b107c54f2bd94cb69e069885ef2a1e05540f1e82c7cbdee0cd", + "crm-auth/src/main/java/com/crm/auth/service/impl/AuthServiceImpl.java": "sha256:f24fe4e0571f1e89073bc6887d35fb65107c78bbea2aa59bffa2692bf74910fd", + "crm-auth/src/main/java/com/crm/auth/service/impl/AuthUserServiceImpl.java": "sha256:e0b74ee4d14c002963ccc026799f6917f15f247351c57756edab9208577fe0cb", + "crm-auth/src/main/java/com/crm/auth/service/impl/DtOrgSyncServiceImpl.java": "sha256:66096df60d69d20526ecf41fef02c3eb073a83a037753bcd70882a9adf5d5da5", + "crm-auth/src/main/java/com/crm/auth/service/impl/PermissionSeederImpl.java": "sha256:5680823d212d9677db1efb76306bc11ccbeaa09bc804213a6acdb9ace7e47b39", + "crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java": "sha256:55b06def16cdb0c28d16f80eb0e7581be3694af23a05148de47fb2512cb554f9", + "crm-auth/src/main/java/com/crm/auth/service/impl/RoleScopeStoreImpl.java": "sha256:1f0c8b60f4461d52f1a0dd18135a6748198aa2848ec315854a8c775317845f18", + "crm-auth/src/main/java/com/crm/auth/service/impl/SysDeptServiceImpl.java": "sha256:31fb306eb5fc1e90b005e43c109bfb9c2e2b527c023febc403d1c9fcc0abfd3b", + "crm-auth/src/main/java/com/crm/auth/service/impl/SysMenuServiceImpl.java": "sha256:4b01585e3796c1a95404d3eea9aa223adb43dfe55e342dfa0be42cd93c07be26", + "crm-auth/src/main/java/com/crm/auth/service/impl/SysRoleServiceImpl.java": "sha256:03020ef55d089dd27529194dd6f9a4d4d7d4384014af10ff41e1eccc203cbfa4", + "crm-auth/src/main/java/com/crm/auth/service/impl/UserQueryServiceImpl.java": "sha256:0264b66f6bdbda6f08b5c1f9607889fd9083fd40e457226752a44ad1145fc0fc", + "crm-auth/src/test/java/com/crm/auth/security/ApiPermissionInterceptorTest.java": "sha256:ca9d86c1741bf67c8fdeebfeeaab946a18fb91ba0ee94d26b21efab128b6f964", + "crm-auth/src/test/java/com/crm/auth/security/JwtAuthenticationFilterTest.java": "sha256:ad8e5fca1105639edf3430122bd4e8d0f242b80e201592adfcf75adcbee961ab", + "crm-auth/src/test/java/com/crm/auth/security/SecurityExceptionHandlersTest.java": "sha256:081741d0bf2cbf83de018e27121df200341170fef0455ce0d9554583c87fdda2", + "crm-auth/src/test/java/com/crm/auth/security/impl/JwtCodecImplTest.java": "sha256:4e0876fd6a3e830f988d71a40a15de8c632cc90a0efb109fc038a8dcf95ac1f6", + "crm-auth/src/test/java/com/crm/auth/security/impl/PreLoginTicketStoreImplTest.java": "sha256:4a53ad8689564345a9e39617cf230d9778dd0e26ae60e56570bd8237231fc602", + "crm-auth/src/test/java/com/crm/auth/security/impl/SessionStoreImplTest.java": "sha256:fea08df16d6436f08a2fa3d070d652fb002cf3ab14d1022d5ff4a23a363893a9", + "crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java": "sha256:f000607a7c0287dccb7472ea0a78cf4a158f0eabb660db1cad2db35f9141452f", + "crm-auth/src/test/java/com/crm/auth/security/scope/TestOwnedData.java": "sha256:57bf431ac3272ce22dae7cebad65386283292d7e93de191450580eed19a8382e", + "crm-auth/src/test/java/com/crm/auth/security/scope/TestOwnedDataMapper.java": "sha256:11b1963da8ee4034fc7a5bf2faaac13e74a5cae5cb2839055ece4d307f112218", + "crm-auth/src/test/java/com/crm/auth/service/client/CorpAccessTokenProviderImplTest.java": "sha256:a60e9a309dfb09f7466eccddcc8ff9b686595a37f57eeea31193f84af488d6fa", + "crm-auth/src/test/java/com/crm/auth/service/client/DingTalkAuthClientTest.java": "sha256:8aecb8c649f9442fe5a779f129fcbddbbfe4fe11f6bd08d7cd30e2ac75628808", + "crm-auth/src/test/java/com/crm/auth/service/client/ThirdPartyAuthClientFactoryTest.java": "sha256:df3add282ffa31f883e154bb675911a8f80476cc93c2023957ccf846410b3928", + "crm-auth/src/test/java/com/crm/auth/service/impl/AuthServiceImplTest.java": "sha256:0a9a9f8b3d9246ffcf65dde660b3e83bb4734ce0b7b00ffbba34a153b55d1188", + "crm-auth/src/test/java/com/crm/auth/service/impl/AuthUserServiceImplTest.java": "sha256:970ad171bb781bf1e483352a02792342a5cd4bc0e246a00fd87fe3589d882ed1", + "crm-auth/src/test/java/com/crm/auth/service/impl/DtOrgSyncServiceImplTest.java": "sha256:eebb0677934e558e53404da1f588d27639ded85c69433540f383838bf1a007e3", + "crm-auth/src/test/java/com/crm/auth/service/impl/PermissionSeederImplTest.java": "sha256:bc6a2b3bfbb2f6aca414ca394382bdd951aa5a57f3c5fe4da15b200df1400ce6", + "crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java": "sha256:86da176046dd963d20b890019091568a5f5a8c3c731ba1cf4d09ceecfc467627", + "crm-auth/src/test/java/com/crm/auth/service/impl/SysDeptServiceImplTest.java": "sha256:6ab25e66138f38ac51615d8876f00c5127be5308065a1f91d37ae0abcf6d37aa", + "crm-auth/src/test/java/com/crm/auth/service/impl/SysMenuServiceImplTest.java": "sha256:2ce11e37b9eed47625b47fb720657c964ccfd23d9b7b3e7ad46483174797770b", + "crm-auth/src/test/java/com/crm/auth/service/impl/SysRoleServiceImplTest.java": "sha256:e1e04143a64c2f04a8c5f6e2489418b44f871970cf6df70de6a431957b71b546", + "crm-auth/src/test/java/com/crm/auth/service/impl/UserListIntegrationTest.java": "sha256:292be9e7eab3e3409dd3859f461f2aa29c345266c694fd50906ef4b537707898", + "crm-base/pom.xml": "sha256:2f80c6cc066bf98bf36ef7bcb731f295f65320c13b1ec74f0abfb232394f957a", + "crm-base/src/main/java/com/crm/base/advice/GlobalDataBindingAdvice.java": "sha256:a37eefea31c33872876d6bbcb5d1315a5f965076d34eb376b8ece3d6ae8aa4bd", + "crm-base/src/main/java/com/crm/base/advice/GlobalExceptionHandlerAdvice.java": "sha256:75e1b52c8e00968df65d017518de90014ce2dd22913f14f6f210f4cb834864fb", + "crm-base/src/main/java/com/crm/base/annotation/DataScope.java": "sha256:9dea657d14d4e956cf12b476c36f433475bdda070770f842afd71193e59bcba6", + "crm-base/src/main/java/com/crm/base/config/CorsConfig.java": "sha256:59fc61e917c6aefff3d6cc754962d8f7e581559ac08d991a1e02a297821d3c05", + "crm-base/src/main/java/com/crm/base/config/CrmSqlInjector.java": "sha256:354b030e8dfecbc590c9ac92c71a771591cd5e8a3ef74d40623bbd31ec8df00e", + "crm-base/src/main/java/com/crm/base/config/CustomIdGenerator.java": "sha256:7b02281892a1599783e213e0761890cf6df3a9b5776d8059eca8019068f185f7", + "crm-base/src/main/java/com/crm/base/config/InsertBatchOnDuplicateKeyUpdate.java": "sha256:a41fdaf6b6dbca65e0930c20f825b2ba36adfe3732838df683bb8497a6dd0691", + "crm-base/src/main/java/com/crm/base/config/JacksonConfig.java": "sha256:4cfa99e884817e98aab9d8e44232b53b5f58d0bc36f5c1ac26f5074e421604cb", + "crm-base/src/main/java/com/crm/base/config/Knife4jConfig.java": "sha256:27edd107207b90d9ae67ca409f169a155da376b2016a11867db8145125cd168b", + "crm-base/src/main/java/com/crm/base/config/MetaObjectFillHandler.java": "sha256:067f8381cf7509c8a890b18b504cce4515029e2b2f571399e7bc5208830e7758", + "crm-base/src/main/java/com/crm/base/config/MybatisPlusConfig.java": "sha256:4373d08df94bba92e821c802aa6949145279887630bc1b688e60bd5e5fde5b96", + "crm-base/src/main/java/com/crm/base/config/RedisConfig.java": "sha256:f4d19db28a5b95e775782f27e1363272091232815b3265cc28dcefe3632bcf74", + "crm-base/src/main/java/com/crm/base/config/ScheduledTaskProperties.java": "sha256:e28ba32d20979fff97050983bea5f3011c3b8701b7949cca28ee1e63dbd41ce5", + "crm-base/src/main/java/com/crm/base/config/SnowflakeIdWorker.java": "sha256:274e02ed875d57b53e973463323bb4cd31f10958adb621471e8697e3b44ff346", + "crm-base/src/main/java/com/crm/base/config/SnowflakeProperties.java": "sha256:b449ff32a40b502575ffecc092285415476c08a1985921070ff89ace8d6424f3", + "crm-base/src/main/java/com/crm/base/constant/CommonConstants.java": "sha256:02af0380cfc77239577f476c27dbd976757f82a111bee0b4231aebaf85488c67", + "crm-base/src/main/java/com/crm/base/domain/dto/BaseDTO.java": "sha256:0eb8e6db2f762a2b049cca069d8b552582e0001fda2bba5aacd643cf150627c0", + "crm-base/src/main/java/com/crm/base/domain/dto/ButtonSeed.java": "sha256:d6ad979da4f3847039de131d8ed51c08702eb5e415e9f9399edeae1bc702cfff", + "crm-base/src/main/java/com/crm/base/domain/dto/PermissionModuleDescriptor.java": "sha256:642edb65c706fc6c962392bc55f97b9c95cdd53b895949e5a4316e3f58069526", + "crm-base/src/main/java/com/crm/base/domain/entity/BaseEntity.java": "sha256:4db036f4aecb2c20237588bf5d41b63f5d3031dbf7118f88d211ead709da195a", + "crm-base/src/main/java/com/crm/base/domain/entity/OwnedEntity.java": "sha256:4b4df5ba6f1e23c5af1a451f46fa8cb2e1cca300dd45b142a40ebf31f194f9b1", + "crm-base/src/main/java/com/crm/base/domain/enums/HasValueEnum.java": "sha256:b360ed23014d014e12d68b00b87c19e4eeee69e50d75fa1552f9050edd325ee8", + "crm-base/src/main/java/com/crm/base/domain/enums/StatusEnum.java": "sha256:1a83948e7a1889773765d74000b8ed73d379a963ec428c63caaea724d795d7cb", + "crm-base/src/main/java/com/crm/base/domain/exception/BusinessErrorException.java": "sha256:760e64cafad056ffb8a708d4e659e43b8b9ace8402428d8210fc86403c83f66b", + "crm-base/src/main/java/com/crm/base/domain/exception/MissingParameterException.java": "sha256:bc13402c89ad61ac9c961afe1664b30060a3773db025351e9019b6d95c05cffd", + "crm-base/src/main/java/com/crm/base/domain/exception/PermissionErrorException.java": "sha256:b3dbea00bca42651e8b0826613713bf8f82f4f1b4f8013715c423a967d949a27", + "crm-base/src/main/java/com/crm/base/domain/exception/ResourceNotExistException.java": "sha256:d470d6a6ebcaa7d197b2913ebaf88c264c7f10638654fb9bd759ea3223c89288", + "crm-base/src/main/java/com/crm/base/domain/param/BaseParam.java": "sha256:e9426aba17a6db8ff9bbfd59de99cda274922809fa6606aa306b100ecff41707", + "crm-base/src/main/java/com/crm/base/domain/result/BatchResult.java": "sha256:353dcf10a9d7fa4562a2928397cc1dbb12129705ffb2fc0618141a26c59129e7", + "crm-base/src/main/java/com/crm/base/domain/result/BatchRunner.java": "sha256:768c018a4ac27d2cf951d23303b8091cfe733592bf5ed82a400911a784861bf5", + "crm-base/src/main/java/com/crm/base/domain/result/PageResult.java": "sha256:c2d1a92ec1735a66a6a2032c1b55cf4e8b81491b36492eb3de225e5f303b6de3", + "crm-base/src/main/java/com/crm/base/domain/result/Result.java": "sha256:1c921e5f795b82a70cb75af51f96c9191b9172e03115110526de060683532415", + "crm-base/src/main/java/com/crm/base/domain/result/ResultCodeEnum.java": "sha256:51d4b5dfc88c6354569b9402eefd5f3929e4a7d1a737a2a8cfc45373986281e0", + "crm-base/src/main/java/com/crm/base/filter/TraceIdFilter.java": "sha256:1749c28d67edc19331ead909d8a2c59e1faeab309e83c9ebcc122cbf5dbf668c", + "crm-base/src/main/java/com/crm/base/mapper/CrmBaseMapper.java": "sha256:de3abda85e7c66ff73a6017c9180fba7d6a63f53908b901bc37e8b45450d5ab2", + "crm-base/src/main/java/com/crm/base/security/DataOwnership.java": "sha256:8e5135079f87b15099718f45ff22cd8355221927f39714e0b305326b5badde7b", + "crm-base/src/main/java/com/crm/base/security/DataScopeLevel.java": "sha256:c6c8f689d6a8d37be995cae05d8aa84b2e5d320e189a3ca195560383c6da8d49", + "crm-base/src/main/java/com/crm/base/security/DataVisibility.java": "sha256:cdba5e06184cd3090c2bd3b7fed7f1ab90c0d85299548570477a2f3603d814f4", + "crm-base/src/main/java/com/crm/base/security/DataVisibilityContext.java": "sha256:2a22348173d420761f99cbbe5b27fd65caa7a91f078c66e2fa6978eecbfe9258", + "crm-base/src/main/java/com/crm/base/security/LoginUser.java": "sha256:2b2df2bb043d68703b9c9d05ffc7a77e46eb1a468c7eae74b99f8ac055e8bb64", + "crm-base/src/main/java/com/crm/base/security/SecurityUtils.java": "sha256:ad9476371a8469491b5a1cbcb05d4f9c0f09f2db89820c62c39669b50b97c9e9", + "crm-base/src/main/java/com/crm/base/security/VisibilityScope.java": "sha256:54c8cc47a9eecf899ed1287c56ac7c204bd3b7595a97a49321abb1d478a15a43", + "crm-base/src/main/java/com/crm/base/service/IBaseService.java": "sha256:3339ff5f136dedceff504b808d92901435a84941ea202910a277db1d9a2f02cd", + "crm-base/src/main/java/com/crm/base/service/PermissionSeeder.java": "sha256:5199eb32f326594f5be9d8ea7cfcb4bb27c25fc4fdaec9f67cc8fef459111ed7", + "crm-base/src/main/java/com/crm/base/service/impl/BaseServiceImpl.java": "sha256:58263984a21beca1767f4e47d4461e5871961bbb079e26be56ecff3c7bd88e97", + "crm-base/src/main/java/com/crm/base/utils/AssertUtils.java": "sha256:248a479ac5b47360d95862c1c689b9377b95957dbbd50c02fd370c7d5d4690f1", + "crm-base/src/main/java/com/crm/base/utils/BeanCopyUtils.java": "sha256:7eacd6401558e129804b705b5ae8507dcc322918fa41808d78cafcc66daa821c", + "crm-base/src/main/java/com/crm/base/utils/EnumUtils.java": "sha256:01887e4e14b9c1d2717e885326a79ac940603f1464bbdfb137b5e0cb1009a681", + "crm-base/src/main/java/com/crm/base/utils/ExcelUtil.java": "sha256:774e1bf9facfec300408f53075a3eca44dc297c017edd1ae52bced199420589e", + "crm-base/src/main/java/com/crm/base/utils/PageConverter.java": "sha256:5f442a2131dbe380fe315259802852b0fb202b5946c37e218dfb425eb0882a1b", + "crm-base/src/main/java/com/crm/base/utils/ServletUtils.java": "sha256:ad1ace3d6185344755dd49bf98b5886896d7c46efe5e40f5dd3f3d382e468152", + "crm-base/src/main/java/com/crm/base/utils/TreeUtils.java": "sha256:3568805ded8371718e6314fccf189c5c593a6fe0100023d393feb9c214bc9058", + "crm-base/src/test/java/com/crm/base/advice/GlobalDataBindingAdviceTest.java": "sha256:3658b21e2a40ebce7b7d582f205d13476eda4a46c1447da27a41a98e58973709", + "crm-base/src/test/java/com/crm/base/domain/result/BatchResultTest.java": "sha256:6f52069e873511fe5fbdabdf4a807908f6c3d25289a24edbfbcaac50d96837cd", + "crm-base/src/test/java/com/crm/base/domain/result/BatchRunnerTest.java": "sha256:9924e321f2872b71cb32151b9022cd1b9ef6a23028c5ffdc9aa2ac76c7e214d8", + "crm-base/src/test/java/com/crm/base/security/DataVisibilityTest.java": "sha256:4cfc9f0e3b9fcecadc2cf03eea47681c9aaac8463150ebd39114bcb05a2a8d31", + "crm-base/src/test/java/com/crm/base/utils/PageConverterTest.java": "sha256:3733b99795643d748dbcf95eb58c48e1763830514091ffce18d4e0f5b83f23c6", + "crm-dict/pom.xml": "sha256:4c8ad74e1e9be1f91ef1ce2a05dbd9381bd0e40235aaf3dcf4639b75a927d82d", + "crm-dict/src/main/java/com/crm/dict/DictApplication.java": "sha256:69963ebcce9cfaf21679200c9b0319e92f4347b1d06e1c3b2204794a6126c29b", + "crm-dict/src/main/java/com/crm/dict/config/DictDataInitializer.java": "sha256:317f5b19cc2bb9f5fc443780cdd06adfdb5c81c0e42207dc237890e40d83f3d2", + "crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java": "sha256:fa74373639b654059e22f99af801b485a870788e7ed650bc9025ba2e5ddc8930", + "crm-dict/src/main/java/com/crm/dict/constant/DictConstants.java": "sha256:33a93f306ced75f960b68c218512839d6a3c114969b9d2ac58e2984103c38bd6", + "crm-dict/src/main/java/com/crm/dict/controller/DictGroupController.java": "sha256:56bd69f480df26f5092feaa8ed4b1d106a15824fd5318176b43f22f2c643f6fc", + "crm-dict/src/main/java/com/crm/dict/controller/DictItemController.java": "sha256:a69be62365a10f010aa6a93fcbf91328c2927fc39c128cba84be2586a54fe7ac", + "crm-dict/src/main/java/com/crm/dict/domain/dto/DictGroupDTO.java": "sha256:0ae4ee6424fa25ac940db5d162bfb39faa54c4cb6fb88bdddf22d7cea3679a4a", + "crm-dict/src/main/java/com/crm/dict/domain/dto/DictItemDTO.java": "sha256:c1fe83b3710554575160c7e4245daa1b4ff7275c65ec0d5509d59d5adbdd234e", + "crm-dict/src/main/java/com/crm/dict/domain/entity/DictGroup.java": "sha256:808c8898f56e8781bda00cabdb27c53b2abab315b631a803819c88f8076e3733", + "crm-dict/src/main/java/com/crm/dict/domain/entity/DictGroupDefault.java": "sha256:80902b0aabd41f2d063b8dc8ece861b0fb87942bc0f0bc56cc758b05e33871c5", + "crm-dict/src/main/java/com/crm/dict/domain/entity/DictItem.java": "sha256:5cb7982e689f87076f500d3ec06eeb0ff416f183e1aa61c9be98a370fc541cdf", + "crm-dict/src/main/java/com/crm/dict/domain/entity/DictRefCount.java": "sha256:51cc9bff1704ae14b95eef4d84f513c6e640ae5f52bb035c8572a1ef51c1f1c6", + "crm-dict/src/main/java/com/crm/dict/domain/param/GroupPageParam.java": "sha256:9f20ea768dc0dfb0f26c570748cc2cfc50bf187cd91d07a6890d9907e7e3835f", + "crm-dict/src/main/java/com/crm/dict/domain/param/ItemPageParam.java": "sha256:e64ce2f662f3b8583f58ed056c405a82d101a7eef581bdd6ba355816d3650a50", + "crm-dict/src/main/java/com/crm/dict/mapper/DictGroupDefaultMapper.java": "sha256:69831375b33470b3b12bba7e68066da4490f8e9ef4bf295ad0a7f82451b87700", + "crm-dict/src/main/java/com/crm/dict/mapper/DictGroupMapper.java": "sha256:69590dd226102d565ae18b8735450d71044df890a41d7ddd969a374f8781204d", + "crm-dict/src/main/java/com/crm/dict/mapper/DictItemMapper.java": "sha256:8fd1d314c0a468960f23df649adbd8e2474d004a71f1d8fdb82aba792a0029e9", + "crm-dict/src/main/java/com/crm/dict/mapper/DictRefCountMapper.java": "sha256:7c34b0e290d0ed73a5eb8b716dcc96e3b1c2844891c643816053648c58459de3", + "crm-dict/src/main/java/com/crm/dict/service/DictQueryService.java": "sha256:9f4facf03ffb11f8f761a4f98df9d694ad2129f393cc3af0e316a55e404fb33a", + "crm-dict/src/main/java/com/crm/dict/service/DictReferenceService.java": "sha256:907182bb7d09e26db379f9134ca3ea16d18f4a1ed7ee5012b7ca67d42b2355e5", + "crm-dict/src/main/java/com/crm/dict/service/IDictGroupService.java": "sha256:e6ce92c85dabac7d82a340c02e1ef23edf51314abe48e986931d5cf0391ac901", + "crm-dict/src/main/java/com/crm/dict/service/IDictItemService.java": "sha256:fbd2165ecc858e9fefdcb3ddc231e3103682e4330ef880a30b1db01465ec71b0", + "crm-dict/src/main/java/com/crm/dict/service/impl/DictGroupServiceImpl.java": "sha256:9bb1f4e26202451251d47c9fa662bc6c1d9b806d65ea66965f30c5d7218e71d7", + "crm-dict/src/main/java/com/crm/dict/service/impl/DictItemServiceImpl.java": "sha256:d93a98be9a5296a97d506232f369975692c1ae6c861b494f8dac7dd44fdf6c9e", + "crm-dict/src/main/java/com/crm/dict/service/impl/DictQueryServiceImpl.java": "sha256:ac5b1ef1f8eae837b91f4ea8a6d72bbf93bcd66336e7f63567a8c65475e5c55c", + "crm-dict/src/main/java/com/crm/dict/service/impl/DictReferenceServiceImpl.java": "sha256:98be664504a4121d1dfc586133aa403951ec404b900c9dfcd58d71fe3c5137f1", + "crm-dict/src/test/java/com/crm/dict/AbstractDictH2Test.java": "sha256:38f0b4398faf536f92a37b967636243ec6f0897cbb7f47881ae44a5c018d24d1", + "crm-dict/src/test/java/com/crm/dict/config/DictDataInitializerTest.java": "sha256:727f27a52c44ee58ee529dbe37a7f12ab241ca739207e97a5e9730be61ea1f4d", + "crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java": "sha256:7af9b8ba95b58f954a53a21aefacb8ad6cebca62e8c87be07728c3a526299533", + "crm-dict/src/test/java/com/crm/dict/service/impl/DictGroupServiceTest.java": "sha256:7ca49c6bdc51333e3cfced34a759ca1444b673aa6cba84a6a20b57f965682531", + "crm-dict/src/test/java/com/crm/dict/service/impl/DictItemServiceTest.java": "sha256:412baa47082985f99b9c624d63ad3a4ca5a0559ec38df16bbe9693bc681f904d", + "crm-dict/src/test/java/com/crm/dict/service/impl/DictQueryServiceTest.java": "sha256:f796243023370c7fb78f9ea970e65c39a5ba0b602c3c2f233ba331640b507a3c", + "crm-dict/src/test/java/com/crm/dict/service/impl/DictReferenceServiceTest.java": "sha256:d78c3494dba2b02d6c9f9344c1e85bb972833622b25bcedb6f75c8096bbe5c7d", + "crm-file/pom.xml": "sha256:475a656769024c9ebc6d146ea421fa15a8a9f8447c88183ef73bc74d670755b0", + "crm-file/src/main/java/com/crm/file/api/FileApi.java": "sha256:16382a42a3497decf43d067f8177f24a2987c357ca8c392b85c708d412c5d7b4", + "crm-file/src/main/java/com/crm/file/config/FileProperties.java": "sha256:9e326877f87615b9095973497a2042511cd0c6342f43c49da188a0c9a2d07a6e", + "crm-file/src/main/java/com/crm/file/config/MinioConfig.java": "sha256:4164cd21371bf113f336d0ec0284386a4a498a2d05c1cb3664dc6361d0722d05", + "crm-file/src/main/java/com/crm/file/config/SchedulingConfig.java": "sha256:5b0646e67225d1df7ee606147d5cfd08c4260f01d789651b6b0cc6eb108007c0", + "crm-file/src/main/java/com/crm/file/constant/FileConstants.java": "sha256:da65d5ab9c2a42869cbb3ef9ec6902ca72c2517cb34457d8456dc325200aeffc", + "crm-file/src/main/java/com/crm/file/controller/FileController.java": "sha256:1bacd0cc3f353744820852bda1efb02acc0419da37e7b3ff5e5337f5f828b475", + "crm-file/src/main/java/com/crm/file/domain/dto/FileDownloadDTO.java": "sha256:b5995c70f0c683f43428355079a6e3401d8d9a852d214a6983b6e5a9d048f050", + "crm-file/src/main/java/com/crm/file/domain/dto/FileInfoDTO.java": "sha256:24207c0a81fc567926ab6d1be6c29457de71da01ebfbdc14ed0cab6aed6a6a0e", + "crm-file/src/main/java/com/crm/file/domain/dto/MultipartInitDTO.java": "sha256:8361c71df6758023ad5dc74c62999bdaf90ba40e2687bcf818cbc45b73cebd2c", + "crm-file/src/main/java/com/crm/file/domain/dto/ThumbnailDTO.java": "sha256:41a4d735972975f179a63a4cd394ea40342c08a960680835db89737c976fae8e", + "crm-file/src/main/java/com/crm/file/domain/dto/UploadSession.java": "sha256:36481cff82f5538e1ff4f2f78111ad90ed3a0f8afecc02abafe82fe817517128", + "crm-file/src/main/java/com/crm/file/domain/entity/FileInfo.java": "sha256:d55cedf9d5f08789e163a127e9bb80b72a332bf59c5bcab2a2c2dafef8d632db", + "crm-file/src/main/java/com/crm/file/mapper/FileInfoMapper.java": "sha256:a361e08d2fe5527a72a5c298cbcfb4297595dca1908f0749e547efe88027fb8e", + "crm-file/src/main/java/com/crm/file/service/IFileInfoService.java": "sha256:edb7b330bcafd61582b93480af6c4f7dce0d1464ade9884172419fe1c5967e6f", + "crm-file/src/main/java/com/crm/file/service/KkFileViewClient.java": "sha256:ecc7dd4ef4619241f5e8fb0da90f4fe1af0c315ac6a6032c4197db0d3767baed", + "crm-file/src/main/java/com/crm/file/service/MultipartUploader.java": "sha256:1cebace4bb09de60c58dc62733f6cd50b58185cd016fa5c4e11eec48c627da66", + "crm-file/src/main/java/com/crm/file/service/ThumbnailPlaceholderService.java": "sha256:b533b089de67ed43ca06b00d715af4685256ac79e33fc1430eece5b3fadd3dc0", + "crm-file/src/main/java/com/crm/file/service/ThumbnailRenderer.java": "sha256:54f7c603bdf50cd60dac3cecbfe98973451319861b5c82f96ec5bd7c2dc06727", + "crm-file/src/main/java/com/crm/file/service/ThumbnailResolver.java": "sha256:347e1b7f5236518df9f7b57ba20d20645f6f352669ce9f2ba025adfb796ea197", + "crm-file/src/main/java/com/crm/file/service/impl/FileApiImpl.java": "sha256:095da270ab0d5f9788bf0e60614b6ba06113943d97ebdefd17bdbf165c66fb5f", + "crm-file/src/main/java/com/crm/file/service/impl/FileInfoServiceImpl.java": "sha256:66fdeed1f189376c89e91f69058b5cf230540cc6f96b7cf4713d331c1a787b44", + "crm-file/src/main/java/com/crm/file/service/impl/ImageThumbnailRenderer.java": "sha256:f112ca0e26c88c44baec2a6447f53c3606443d53c6d22557f4a88340a9e16028", + "crm-file/src/main/java/com/crm/file/service/impl/MultipartUploaderImpl.java": "sha256:4942a100821df744f82ce4d9d39402557c64568f00b0c648c9200d4f2b6c2021", + "crm-file/src/main/java/com/crm/file/service/impl/OfficeThumbnailRenderer.java": "sha256:3d2e95d3f659a27cce69d115784ba2db88c7c1dc279b7e3257e50be70cee7d5c", + "crm-file/src/main/java/com/crm/file/service/impl/PdfThumbnailRenderer.java": "sha256:910f1309832ff353f7616e2f5ce4203ddd61677043d17069efd704ac4701cfd5", + "crm-file/src/main/java/com/crm/file/service/impl/ThumbnailImageUtils.java": "sha256:ea8a9ed22cb5b7cb536a4880bc365c5414e88ddbc264b6d97314978a59939c6e", + "crm-file/src/main/java/com/crm/file/service/impl/ThumbnailResolverImpl.java": "sha256:c476616e16c02c7fbbbb46e9c043fadfcf3568400c6fd5529e74191b887d902a", + "crm-file/src/main/java/com/crm/file/task/OrphanChunkCleanupTask.java": "sha256:aa372e2716d640866aa62ecd56e2096ed3b7ccf047666091e1dad947121ee03b", + "crm-file/src/main/java/com/crm/file/task/ThumbnailGenerationTask.java": "sha256:7f7fe7f4a19da393af2cee987a4085c97026cc2240b455f3fbde7104855de05d", + "crm-file/src/main/java/com/crm/file/task/ThumbnailRetryTask.java": "sha256:f55cd0914feadeb3a03bddca9b4aceb56ba1774251dbcad8fdb971570a6fb50f", + "crm-file/src/main/java/com/crm/file/util/FileNames.java": "sha256:3cf2ee2421d0163cc64754d74985af5e1b976163d45a458f9b43f89e196eed20", + "crm-file/src/test/java/com/crm/file/service/KkFileViewClientTest.java": "sha256:6e9ea082d3aaed3fcb8665a1c5acd7a61449f7bf4056b75100c1448f2990c447", + "crm-file/src/test/java/com/crm/file/service/impl/FileApiImplTest.java": "sha256:dd2e7dd7f59d87efbd0e85afd0f9f2b411c78c5b8393581249d6eb686a993b18", + "crm-file/src/test/java/com/crm/file/service/impl/ImageThumbnailRendererTest.java": "sha256:5db1e8383074a7f644f2dc8b47b2586ab1b5edeff2929f7a23faf729ba309fe8", + "crm-file/src/test/java/com/crm/file/service/impl/MultipartUploaderImplTest.java": "sha256:1cd3b8e95ec120f89fbd9dc6965521be57ffaa6cccc862323373de78421f3a9e", + "crm-file/src/test/java/com/crm/file/service/impl/OfficeThumbnailRendererTest.java": "sha256:694f51cc86ef954cd28d82bfa62eb2d9d8433edbfb2892cbaeec410fe044c815", + "crm-file/src/test/java/com/crm/file/service/impl/PdfThumbnailRendererTest.java": "sha256:a378461da31a59eea2127e4ea0dc828dd840aa7e88318b837fdae87ee2484a11", + "crm-file/src/test/java/com/crm/file/service/impl/ThumbnailResolverImplTest.java": "sha256:38e5071e0bdca078b564c57925e7700b0dc19f9248eda8c929eabbd709697554", + "crm-file/src/test/java/com/crm/file/task/OrphanChunkCleanupTaskTest.java": "sha256:e4fb06547db7750eadbe8bc12ae964697b5edcabc2292f43bb545301cb4b6403", + "crm-file/src/test/java/com/crm/file/task/ThumbnailGenerationTaskTest.java": "sha256:26a1e2ca445f36ef0a1aace547b652055322674a492842208df70406ac765b72", + "crm-file/src/test/java/com/crm/file/task/ThumbnailRetryTaskTest.java": "sha256:97d989e5c11e20aff45ea2fb912ca865c3a1466db603e4b788b26c4dc86c384a", + "crm-lead/pom.xml": "sha256:fa23d0ee2fb0828e66bbfdb17e234ec0d2964dd0866e164a6120a05fdc7890c1", + "crm-lead/src/main/java/com/crm/lead/adapter/PoolLeadOccupationAdapter.java": "sha256:8343a3b3757d37f1c1573ba25540e9ad68be22967373da83e32260edee6c20ff", + "crm-lead/src/main/java/com/crm/lead/config/LeadPermissionInitializer.java": "sha256:901c06d86781f036eb626371b99bbdfadb80f0796a64751f54c4a3ebc5598613", + "crm-lead/src/main/java/com/crm/lead/constant/LeadConstants.java": "sha256:284b291e4067e80755ad17887fd926fb43cf1094760ac49f66d42ee50d8f43f6", + "crm-lead/src/main/java/com/crm/lead/controller/LeadController.java": "sha256:befa15de78c8a6b3975364114394bfbb2eb1b1d53bf048babdc71e22c7ff521f", + "crm-lead/src/main/java/com/crm/lead/creation/LeadCreateContext.java": "sha256:d1cf70296fd0efcd9df0483c3eec1019d28cc13502d783b1aa9e75d8d7b0d219", + "crm-lead/src/main/java/com/crm/lead/creation/LeadCreationPlanner.java": "sha256:789046359fe913f64897ebad39dc2952914f88e0da5c5c986d2fe0816d380cc0", + "crm-lead/src/main/java/com/crm/lead/creation/impl/LeadCreationPlannerImpl.java": "sha256:d2e08c5093262443631e08f349688f6a7da52803de3471219e0e0c2f24af37cb", + "crm-lead/src/main/java/com/crm/lead/deadline/DeadlineBudget.java": "sha256:e6a73c63fbb92ac7e39284e676937c9610f6de2b28840b47862daff528e638ad", + "crm-lead/src/main/java/com/crm/lead/deadline/LeadDeadlines.java": "sha256:9479aeeb72e19e016bfb4b692ca0e251c58397ce3d002bc52c4cc10f751e023d", + "crm-lead/src/main/java/com/crm/lead/deadline/impl/LeadDeadlinesImpl.java": "sha256:364c3ba8a7baabb7a55be28cc01ea83e2e21d24951644aa4b30634225f6a5ab1", + "crm-lead/src/main/java/com/crm/lead/domain/dto/LeadBatchFailItem.java": "sha256:52bc079ea67f3ec8a7b46036717fb48a1012570cd933e5fbdcf77f8cfb370776", + "crm-lead/src/main/java/com/crm/lead/domain/dto/LeadDTO.java": "sha256:21d0bfbef89215dd0be41ecc91c7c077b101a7784c01050a0adb6f02dff895f7", + "crm-lead/src/main/java/com/crm/lead/domain/dto/LeadFeedbackDTO.java": "sha256:a979afb47a465aac68fb53820c1ca4837bbbc46670249525b46b4d2c9780c92e", + "crm-lead/src/main/java/com/crm/lead/domain/dto/LeadHistoryDTO.java": "sha256:a7c4a3c25575701b8fed4209236fbeb766972e134770535daa929cf3dfa81988", + "crm-lead/src/main/java/com/crm/lead/domain/dto/LeadStatsDTO.java": "sha256:5dbbd09f73ae83388aedbf1aa4d289782fa845deb19c386c1b44d8205636440b", + "crm-lead/src/main/java/com/crm/lead/domain/dto/OwnerSnapshot.java": "sha256:133d057c3b7cc168c41a5da3b197e0e8260c6cb283419eb63a8d59d382fae9e7", + "crm-lead/src/main/java/com/crm/lead/domain/entity/Lead.java": "sha256:4ef35be20ec4c2e09614a0f3de269f18a42470ee0ac4145648dbf642f943391a", + "crm-lead/src/main/java/com/crm/lead/domain/entity/LeadAttachment.java": "sha256:94c9b8ef73beffdf946651964b6ec551882d3f182c3563b85b0b886e1b0d5876", + "crm-lead/src/main/java/com/crm/lead/domain/entity/LeadFeedback.java": "sha256:ab08310cd2a0c955b910f2ca36fbf8a0379ddfab211871af4df4e53821e3e598", + "crm-lead/src/main/java/com/crm/lead/domain/entity/LeadFollow.java": "sha256:d33588eb168324870a98d1e6ed21c881430074347d4976ff1cd9c85857dfc81a", + "crm-lead/src/main/java/com/crm/lead/domain/entity/LeadHistory.java": "sha256:4724af7d7aab3c9af42ff0671b9287694aa9b00ef26ba309a39376e7f8b3b1ae", + "crm-lead/src/main/java/com/crm/lead/domain/enums/HistoryType.java": "sha256:07af06606dab1456fde7ee14cc4cb3ff6372801b217f5849e3eda5c57a7ae3de", + "crm-lead/src/main/java/com/crm/lead/domain/enums/LeadBatchFailReason.java": "sha256:2dc78509a4aab93b69e27ca28fb0865da123dc41a9b62f6ac7ea51361527657a", + "crm-lead/src/main/java/com/crm/lead/domain/enums/LeadViewType.java": "sha256:b2165129df14bad41898bb700ca00b8a47fc3896aa35c9a647b35b504bc71ec6", + "crm-lead/src/main/java/com/crm/lead/domain/param/ConvertOpportunityParam.java": "sha256:4096f08a8bd4a922d75fc9746ac52209ccd144a256435f0003d56c093b05ad22", + "crm-lead/src/main/java/com/crm/lead/domain/param/LeadPageParam.java": "sha256:31e171c25320a309a559b1b2e1746e15268b544f97ae2982bd6aca1511810e78", + "crm-lead/src/main/java/com/crm/lead/event/PoolChangedEventListener.java": "sha256:de807bd874827185fb0ebe55eecb119307c258278ee920284bdc5b8c97beec9b", + "crm-lead/src/main/java/com/crm/lead/history/LeadHistoryRecorder.java": "sha256:47b96669b5cc2d344b020c8ffd67d825d29f4ce6a57a1fe061a586d47aae9a84", + "crm-lead/src/main/java/com/crm/lead/history/impl/LeadHistoryRecorderImpl.java": "sha256:178f053e4c535eedf48668fce8d9ec1dba3c4e13937018eacf509b12f8176379", + "crm-lead/src/main/java/com/crm/lead/job/LeadMaintenanceJob.java": "sha256:b1f3c3a8898ee5c2b00f195b6b2b5db4b03cc136a57305c3d94199ea9115f352", + "crm-lead/src/main/java/com/crm/lead/mapper/LeadAttachmentMapper.java": "sha256:670c1ce5bc7c418917c3a8d07fa4464d5943c2166e7651f689be90a3ff56cc52", + "crm-lead/src/main/java/com/crm/lead/mapper/LeadFeedbackMapper.java": "sha256:03c66a0392a8ecca44c2379b85e24c4eaecc1a664d78e2f1f95b4aa1e45dad95", + "crm-lead/src/main/java/com/crm/lead/mapper/LeadFollowMapper.java": "sha256:e3ab87cbb04fdf9cf525fdd89bf4e71d895164d1cdc5dd0359ffdc7d63dd9ef2", + "crm-lead/src/main/java/com/crm/lead/mapper/LeadHistoryMapper.java": "sha256:f3c941ed25d1f8b3e02d140758482c45c5dba64756eb5a478199e1cdea3c496f", + "crm-lead/src/main/java/com/crm/lead/mapper/LeadMapper.java": "sha256:c5d864adc8cc4b07810d9d33357d4044f214f82396dc1701b8191f9f8b1916b0", + "crm-lead/src/main/java/com/crm/lead/owner/OwnerSnapshotResolver.java": "sha256:8d9bb91fae0a16ad4d77e24ec9e6e885978e6fc3e5235a3c95ac3f1daa572f21", + "crm-lead/src/main/java/com/crm/lead/owner/impl/OwnerSnapshotResolverImpl.java": "sha256:798cfaa77898f0d62dc19f8f9b543583abcb15cb5e727551e93cae001c4de92d", + "crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPort.java": "sha256:e7e7e64ec2b98972051bb66cb316c831f4f0d4e3419cbb34c6c00463ddf372b5", + "crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadView.java": "sha256:a54ee71d30610c03e0d64a9f309d093fcdeae9031262d9431dd653af18e58565", + "crm-lead/src/main/java/com/crm/lead/port/inbound/impl/ConvertibleLeadCatalogPortImpl.java": "sha256:01b1d3b28abb810d650966a26934ec1952eefd774332de6b73a77184113b4a6b", + "crm-lead/src/main/java/com/crm/lead/port/outbound/CreateOpportunityCmd.java": "sha256:1218380c2c3558bdefff0c878ec90175705afb6cecb58766f6dcb7ec2bdc0fdf", + "crm-lead/src/main/java/com/crm/lead/port/outbound/OpportunityCreationException.java": "sha256:457d51dcf0f04a334c02fc8616c7c0af6438cba4bbd9cee3bcb0ebdc025b9d92", + "crm-lead/src/main/java/com/crm/lead/port/outbound/OpportunityCreationPort.java": "sha256:bd437a31e036d7d4f81339a1ed24f54494696cb283632d11bd8f4b1995d2ec7c", + "crm-lead/src/main/java/com/crm/lead/query/LeadViewQuery.java": "sha256:af958e449d81b274c96fb56ac636795c5c580bb5c909c2cd8ca9d7fc897bb739", + "crm-lead/src/main/java/com/crm/lead/query/impl/LeadViewQueryImpl.java": "sha256:8ce9bf2f91516b183d7233954fcd6a6195b4f499f528c9f021754c481d8dc137", + "crm-lead/src/main/java/com/crm/lead/service/ClaimLimitChecker.java": "sha256:19d7368c3ca9e4fbe5855dea9bc747124eb072677d62ce364ba28749b201b288", + "crm-lead/src/main/java/com/crm/lead/service/ILeadService.java": "sha256:a7aaf1dbf6c0bf3be1558d7b3133a370b7c70f5df6613eed980bdb3bb55ccd00", + "crm-lead/src/main/java/com/crm/lead/service/impl/LeadServiceImpl.java": "sha256:06e847735e46995704263db118cb770b3d31869323715ee87051c988b4fc57b0", + "crm-lead/src/main/java/com/crm/lead/state/LeadTransition.java": "sha256:3302b7c2bc4c9c20f46f12f489aee7daa63e953475842d84008da9f6500c722b", + "crm-lead/src/main/java/com/crm/lead/state/TransitionCmd.java": "sha256:a2db07c01410dd4d653fff93c9fe5c714794e6e94b5c0dc6087d14ea74a529af", + "crm-lead/src/main/java/com/crm/lead/state/impl/LeadTransitionImpl.java": "sha256:f147cc16b538c550e02c18d5e0f449a6be4ef43760f51503a04eb47885519251", + "crm-lead/src/test/java/com/crm/lead/creation/impl/LeadCreationPlannerImplTest.java": "sha256:87e025607edc56376a5ea3c0ae8276022076d179abdf0567b3cef8ee0de68a9d", + "crm-lead/src/test/java/com/crm/lead/deadline/impl/LeadDeadlinesImplTest.java": "sha256:c23daab474ae99ca267d5c6731cd722dfd4d486a2df20fabda17cd42c0705b0c", + "crm-lead/src/test/java/com/crm/lead/event/PoolChangedEventListenerTest.java": "sha256:f5263f95aaa26ee26d6ecbb857b4b8c21d7da844c541b319f30643ecc66b7019", + "crm-lead/src/test/java/com/crm/lead/history/impl/LeadHistoryRecorderImplTest.java": "sha256:c610d6e89afcbc40a9bac361cba2fecb6fe046f45492c00d157e62b487ec2462", + "crm-lead/src/test/java/com/crm/lead/job/LeadMaintenanceJobTest.java": "sha256:f549eeafa8eb75e7264e637c1d41c09bf45b86d18663b156f9ee4df8601f8909", + "crm-lead/src/test/java/com/crm/lead/owner/impl/OwnerSnapshotResolverImplTest.java": "sha256:3892c9e0e915738e55af3d3302e2b24d8330be52bacb05840e85156a8ecffc9e", + "crm-lead/src/test/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPortImplTest.java": "sha256:6410136270445cdbc5e6d9d7f5da9764d87e6f73cc5eea7f642878e154983cff", + "crm-lead/src/test/java/com/crm/lead/query/impl/LeadViewQueryImplTest.java": "sha256:96cbb627a3053d502e5512ab1952d23647f7fbcd31dc55907fc7ca16b5f713ea", + "crm-lead/src/test/java/com/crm/lead/service/ClaimLimitCheckerTest.java": "sha256:4c8a03aae0c19a4875df3548f2035760274f288c2f5c1ad2612fe5ca40e1c9e2", + "crm-lead/src/test/java/com/crm/lead/service/impl/LeadBatchServiceTest.java": "sha256:b9debc03539a006946ef0a2bb182d47ecd8a4f45d4b0cce189e57c08058b6fe1", + "crm-lead/src/test/java/com/crm/lead/service/impl/LeadServiceImplTest.java": "sha256:5547bae863b3c6934a70d205f7ca735bcba6d800d03054932abd39f6c8cfe8b6", + "crm-lead/src/test/java/com/crm/lead/state/impl/LeadTransitionImplTest.java": "sha256:1a353fd2c98cb7109c75cf5c16171e9e20cce1efed8cb18fc2dccb9053f160d0", + "crm-opportunity/pom.xml": "sha256:b6f7b5b226f8e2ec62fef6e9087449434dd26b23d3d56b7a91ec150af3bf9509", + "crm-opportunity/src/main/java/com/crm/opportunity/constant/OpportunityConstants.java": "sha256:f06edf0b0a068722e1bbc891e4a4a922f61a97f8fcd4164c7f91cf22a3beda0e", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCollabController.java": "sha256:a927ec6906cbc3a4929d596a0a57a9f39616306c039f93086a77b11e70fb6377", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCreateController.java": "sha256:7fa82aeeef22b76ff4c32183bef6c877ff02a2ef47d48d6160ae08c5b6e5728e", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityDetailController.java": "sha256:22bed890a61b639e48936ae254fe38d353317c0fa9c6357f53ec42f6c08da562", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityQueryController.java": "sha256:22caeda46b12cc1de52f11d32367b07c78961c7524db20cadd233c9b217fff31", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunitySchemeCardController.java": "sha256:08348345a4e3c91f7a90ab3e6f2b4893c9d5f99e0a42dec6a6f6f5208d6aeba3", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityStageController.java": "sha256:dbebedf8201c0e1e1573292343aa4902806ab00b2e43c3eab49c0b37e2c542e3", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunitySubController.java": "sha256:d50175070ef47372ff584d2cf976dd91b16fa1aac5cd8645b83909ebd9547fd7", + "crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityTransitionController.java": "sha256:93942b5a4355c9c1ee56392e8295bef2d6a13529be98f64d42bb229ae9396b13", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/CreateOpportunityRequest.java": "sha256:16eb516b2e2035fab58f392f1246877242a9d35669b277750adfa79eedbb56a5", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/CustomerContactDTO.java": "sha256:e347fe00e097eb03e32c684c48c00ab18bc4793aa1cc7a07baa9c80aa80eab01", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityCustomerAddDTO.java": "sha256:8dc51c16719732fe4762f514f226a6793a6c0fa7c0ffbe6cab0b7f70786fdb15", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityCustomerCandidateDTO.java": "sha256:491c98feebab0ef02ab40008598dca789fe32903ff1a47b293d12bd27054f42b", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityDTO.java": "sha256:5a24072d72cef014803ad13ea3421fc46809370e930ff17adbdc78b22a771eae", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityTeamAddDTO.java": "sha256:747e1191f8b4ccd3483961197e6f397f5490d9ba686aa7280029c15dc471330b", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityTeamUpdateDTO.java": "sha256:90a7b2d05b1baccfd941c934aec8405524681926ed9f397f66f644233c0bc597", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityWorkPlanAddDTO.java": "sha256:3014aea094428ee67dfcbf86396880fd36b70ff66996130fcaa6e0d67af1133e", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityWorkPlanUpdateDTO.java": "sha256:1dd16ddc77602bb2d01076e42d5894d2b26b7e8bc2e635669d7f1d5f790b487c", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/SchemeCardDTO.java": "sha256:39b087025aa46f7829cdc67383e1360bf6ac796ced9654df74d760245eb6c547", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/SchemeCardDetailDTO.java": "sha256:2223acf2b84b0d67fda9a4a67cfb0ff8975f76f417ef31ac1b613546cc1cb59d", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/StageHistoryDTO.java": "sha256:b421c7b0d6fc3013171005d9e2567d761685f8c18a67cf14a451a4084ee69b2e", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/StageProgressDTO.java": "sha256:d378be9270833c0b4cb502988278b60bd0a4cccdc7f1b01d105a0de9e70e9ec3", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/SwitchStageDTO.java": "sha256:0c9397c8f89ff303f6d1ebe5215bd57c4597fa18685bb10ca2ad53238debf949", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java": "sha256:8954fcbb4332d13d7070b413a5ea241402151f3c916cfbac909094dffb1fb8e2", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityAttachment.java": "sha256:75ceee391dfd83a38c34064ced1861562cbd6b9a29bd491bc7b66a258f886fff", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityCustomer.java": "sha256:d81c9d890a66f483cc2b6e47609ae4ef2c985291587874338f19083fc470aec8", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityFocus.java": "sha256:dbbf7b88ebc7dd83aee37b8a75bae3d496d27a48b90f22560a5445f4dc842cad", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityFollow.java": "sha256:0349b346f5c01d51f79132ae3f5898c40fc18cbcc58c8c1ed4896d357c989380", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityOplog.java": "sha256:e1c62c2e9a2de753cbc73c9a4d8ee19dcd285103448955adbaf939185ae1b2ab", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityPendingNotice.java": "sha256:e06cfaa92150755303211a02e2f7ddfd1b1c373470427b756c5a99749bf69809", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySchemeCard.java": "sha256:fd79e83d6c9f102134139b180b928048ea167b08b7917b200a49774ed13acb32", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySchemeCardValue.java": "sha256:15023398f27d36a236a2433c9c4d68dcfbd5f6f84451ef311fb461bda3da06eb", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySiteSurvey.java": "sha256:9b3d45a40d305caa19b68097f7f2f72b46b62dac2b509911f9ece93e71f329a8", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityStageHistory.java": "sha256:7ee1818d3ec17794b8e4d91e2fe9ca7edf4f2d0d1406ef3e075b421e698b10a9", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityStatusHistory.java": "sha256:14a3c07f334843d78f42c938526b2f23a1fad72325d986e65267c1a4a22a9816", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityTeam.java": "sha256:e9f5cb0c2ce3a7d683c3c9e148f53b6f52131f23433cfc233d6e072b17cbf9bb", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityViewLog.java": "sha256:bd3392a0a9cb9b6a633c0550381de32155f4ce7d03e9b04730d95c92164405df", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunityWorkPlan.java": "sha256:ddd84080d707ee3151e84983997fadc6cde42e2446a4d54b4079bfa09d2cdc0c", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/AttachmentBizType.java": "sha256:9cefb197ca6054669e5168c48e2e14a111c640d167a875f23e137a470ffd5599", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OplogKind.java": "sha256:cf8e364a743c09b797f324b191a046ca2da9e90e3b83b17dbb8e4f6a2a2632a8", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OplogLogType.java": "sha256:ffcfab4cee9798951e92e7cd95a44bef05e10292a35a8664b9c1c40419788d69", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OpportunityOpType.java": "sha256:303bff5664ac0315ec480a938fed3069333b77e8a174a78925b6b189570afcd3", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OpportunityStatus.java": "sha256:aab02f04e1ec317285a44c69932e02273043d35ec0213d5ded70cf9d90e29cff", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OpportunityViewType.java": "sha256:bdb28239bdb29917dd31d01f84340e5b9f126e4a24bf5931cacf12fb17685e79", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/SavedViewOperator.java": "sha256:15e0e7b6a79159a57c674041850084243699a8ace12b8e5e58f1efb1a6f51056", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/TeamMemberPermission.java": "sha256:b6ac87d76fbe8a4f7fc89041e8cdc9a69a825f2f5d3dae77c7d5d4e659020d57", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/param/OpportunityCustomerSearchParam.java": "sha256:917c1d6312d3d4e9a0012f2db632ce557639cde4599983eea0826d8cb50f4dfa", + "crm-opportunity/src/main/java/com/crm/opportunity/domain/param/OpportunityPageParam.java": "sha256:3d38a9d820a1064695b6db037465e3a16a26bebe22e1177490d9362407b4fa93", + "crm-opportunity/src/main/java/com/crm/opportunity/history/OpportunityStatusHistoryRecorder.java": "sha256:00342f0f10a54f74b0b791819fe449ff135c3ef9ae12680706b071a35c4b58c6", + "crm-opportunity/src/main/java/com/crm/opportunity/history/impl/OpportunityStatusHistoryRecorderImpl.java": "sha256:2389c5c1c621f3680d05a5e8a574df0d6daa3ae38005184a083c67f64336a76e", + "crm-opportunity/src/main/java/com/crm/opportunity/intake/IntakeSource.java": "sha256:92155c3d95a6c3782f98b1abd658ad016581caadb90ff44b4d297006c1244ca9", + "crm-opportunity/src/main/java/com/crm/opportunity/intake/OpportunityIntake.java": "sha256:39afc2e4478a6c48f4266466afba303bdd099bc10666c4912c4d3b00fc66ace5", + "crm-opportunity/src/main/java/com/crm/opportunity/intake/OpportunityIntakeException.java": "sha256:3ca86cc59bdfcd39e814f15af8f2f0f36f9b852afc58b160f7ceebc9e396144b", + "crm-opportunity/src/main/java/com/crm/opportunity/intake/OpportunityIntakeSpec.java": "sha256:feeaa509c6da243b4b057d6f14dcfc626e2936d1468ff27c63575baede3d1ab4", + "crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java": "sha256:934c7e948c9da2dd633e4452bfb2c88fad19c038e4b416e88b229a4d8c99c947", + "crm-opportunity/src/main/java/com/crm/opportunity/job/OpportunityMaintenanceJob.java": "sha256:607a76a6aa9064db92336be0dbead47b38590fdb61d296b05d1dda92b5904805", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityAttachmentMapper.java": "sha256:557ac5553481adb4758415cb632bf8272061a718ad4759ca1147cfdd4de030f0", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityCustomerMapper.java": "sha256:fd14e2bc6e65fb13958dbf7049a375b5487d516f351e803389fda974276ab425", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityFocusMapper.java": "sha256:565f6b41511b21bcb627b7ee12fda84128539d218cc4ae811d49b5abd03f87c3", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityFollowMapper.java": "sha256:f279209d497e03ac8029073feb4a4b49ebd32ec1b5f1a7c7cf00dcfa456efa8b", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityMapper.java": "sha256:7cb855d030bf8cc9f6f6e956da9f8138916707172705d3e57fbfd6d346319efd", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityOplogMapper.java": "sha256:957ed81e64f8ec89054127f63a3cc87238b652920277bf335caf5d07bcb1cbee", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityPendingNoticeMapper.java": "sha256:2f1032a9127cad0a736c99cb0e71cd9fa3cd6164da4c8b0651ed6de78b6edde0", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySchemeCardMapper.java": "sha256:20f7ef3a752f591aa2404c86d12e3fc6c5e580a6880ad92d28989edd94cbdb76", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySchemeCardValueMapper.java": "sha256:0d44229f3c2bf402f12b01fcc0d4afc86d7408a25649ba0e8d238c3f596d0c8a", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapper.java": "sha256:e656c1a4da13f536eb4e1cfbd7dbfe55f7b460a660467ac6b2e16bab9aefc405", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityStageHistoryMapper.java": "sha256:7eabbb6dc57ffbac36ea4910740975cd780f7c7085f3bf1cc26ad6cbb489a6f4", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityStatusHistoryMapper.java": "sha256:b01d43e817dcee75180c967095399c963d64c85bc15a2299c6dde097b8ed7238", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityTeamMapper.java": "sha256:41d4a83163665ae12a935cd6e03ac0be084fa534f38eb857c78eb6a2d46e593f", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityViewLogMapper.java": "sha256:b6377b4c14ad6cd353626bde39492176ea38e6aa13a57a06b68b9b2ba8bc1bad", + "crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunityWorkPlanMapper.java": "sha256:290ba71fbfc40d20c3284461733531e63e77b2d9766e251cb974dde242b42922", + "crm-opportunity/src/main/java/com/crm/opportunity/owner/OwnerSnapshot.java": "sha256:c91861c875736a95df1686473a431bed33785b92f28c6285b0fd91ae74c04e24", + "crm-opportunity/src/main/java/com/crm/opportunity/owner/OwnerSnapshotResolver.java": "sha256:87a8badff74414a5681f48c02a3fd45f46cf986808a157b32dd8d5d90164fd55", + "crm-opportunity/src/main/java/com/crm/opportunity/owner/impl/OpportunityOwnerSnapshotResolverImpl.java": "sha256:c430d17573a5fbec41365a207b6ea85a55bb7d649021787496c2111dd6f753d0", + "crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/ConvertToProjectCmd.java": "sha256:8a96f9ffa9cf4e352c246cc07c6e2d941cbf7743b60ccf2ad4e5fe2b0153df57", + "crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/OpportunityConvertException.java": "sha256:9ed284f46fe8eb094a524c2e2f61e7b648eb854e7a230157704c12f22c70133b", + "crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/OpportunityConvertPort.java": "sha256:8eb2c82fa809d0ed63bdbefabddca6db8668b9e26271e6ed42853108ea7b8732", + "crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/impl/OpportunityConvertPortImpl.java": "sha256:47d639378d3387b6ca5c00a34875b9ec3b7036f454095e52a8204239bf01baaa", + "crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/impl/OpportunityCreationPortImpl.java": "sha256:584ace59d9377d70b3197217daae95463c185e62954f1b3b2714a9c1bb38414e", + "crm-opportunity/src/main/java/com/crm/opportunity/query/OpportunityViewFilter.java": "sha256:a3c310a9760cab7383ef2ed0f3fc599f9441111bf348edc7e398fb4903f3a9e8", + "crm-opportunity/src/main/java/com/crm/opportunity/query/OpportunityViewQuery.java": "sha256:86b6427c54b841d1e1db30f14b877d1ff546a28c27bf3a5a4b96cc5065bf1317", + "crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilter.java": "sha256:86786192745913c2b2815900a1971dc0267a3ad5516004745cb08c83cdb70500", + "crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java": "sha256:364e2ef9e79d1003250d349ff24629ce581ac7f4308075e3dabba304f9d62a76", + "crm-opportunity/src/main/java/com/crm/opportunity/schemecard/OpportunitySchemeCardService.java": "sha256:05ad7653c0807f41aa6ea40096a5483bf629cf425e71458577e1b189b81d079e", + "crm-opportunity/src/main/java/com/crm/opportunity/schemecard/impl/OpportunitySchemeCardServiceImpl.java": "sha256:c19fb0ea46c7fdfdd4868331b1a7554d7260e98511f80a861fe3d973d34d74e2", + "crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCollabService.java": "sha256:436f58699273cd655022db1964a54f097fb1a22a423f53bb29dfaf4d8377b5a1", + "crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCreateService.java": "sha256:cfdeffecd9fb28a9e5a05685e4ebaefb3c933013f308ac3e696a67dc735dc0d0", + "crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityDetailService.java": "sha256:3c31b70d893bf7ba0e0b417568bf7a55a68bc50db572879dd4e450e9eb182d5a", + "crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java": "sha256:68d13a07655610191f922b34a02170218367040e59c24cc0515f9fee107fe5d3", + "crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java": "sha256:55cd5df655eaff70a4bca05a9acdb8a873316989e94b10bb7a51340dec59f22c", + "crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImpl.java": "sha256:aae65debdec9d0d13e690aa0f339d9b1b6bbd6eba397ce5c01572706e99bbe76", + "crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityDetailServiceImpl.java": "sha256:073a4d7f396cbd8d4915eec6ca4fb037c2b9a4d53a1f9bf6170cc7eff121084a", + "crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java": "sha256:d8b65d12b26c0d3d01446457f08e88f52a4aeebf25a299828780c9a3b0b4802b", + "crm-opportunity/src/main/java/com/crm/opportunity/stage/OpportunityStageService.java": "sha256:6082b3f6f357b533ed135e1204ed3c30f5f709c7fc81c584d6f400767fa182b6", + "crm-opportunity/src/main/java/com/crm/opportunity/stage/SwitchStageCmd.java": "sha256:9f5593c1ffa321f0d2d7df887beaf10e508734526881a008a14cd1b6c4466669", + "crm-opportunity/src/main/java/com/crm/opportunity/stage/impl/OpportunityStageServiceImpl.java": "sha256:36737fb345242bdf5da50817f9f3240c53c9c5dc7997b38722d2aa0113c74a57", + "crm-opportunity/src/main/java/com/crm/opportunity/state/OpportunityActionGuard.java": "sha256:fa819aeb6679a66519481c2ab66dde7720caae9d59bf7e08faba74defac1bc23", + "crm-opportunity/src/main/java/com/crm/opportunity/state/OpportunityTransition.java": "sha256:aaaad3abc2679f114b1cdef504fd21e9eac6667e86b97f29ab2f875be4e3f979", + "crm-opportunity/src/main/java/com/crm/opportunity/state/OpportunityTransitionCmd.java": "sha256:8e75b8d258795c8b28ccbe24249568b95101edacca578f60f4e8db2c4237ee61", + "crm-opportunity/src/main/java/com/crm/opportunity/state/impl/OpportunityTransitionImpl.java": "sha256:0ac0cdaea67595a2112c56f159eb952c2ee16f6b49b23918ab09ea31d3080389", + "crm-opportunity/src/test/java/com/crm/opportunity/domain/entity/OpportunityTest.java": "sha256:c6eecd2f5ec65c0327ce9bea9604d894284d8fcacf113f70ae35b13514084eb9", + "crm-opportunity/src/test/java/com/crm/opportunity/domain/enums/OpportunityChildTablesEnumTest.java": "sha256:631481e1945ef13a8abf33e0a1785e73770901f91b12848288c303a668b411f4", + "crm-opportunity/src/test/java/com/crm/opportunity/domain/enums/OpportunityOpTypeTest.java": "sha256:4e4a1a0c9ac47319962659a1a19c24e7d0276065a97cd4b059c0135b779c0d72", + "crm-opportunity/src/test/java/com/crm/opportunity/domain/enums/OpportunityStatusTest.java": "sha256:7b0457b135a468e17f267a664f355730271461c41b620ee3b6f8588f35cc0d87", + "crm-opportunity/src/test/java/com/crm/opportunity/domain/enums/TeamMemberPermissionTest.java": "sha256:830d8a8228263c82cf2b74a22279a997f90c9421089f418fa54a97e26a6d064a", + "crm-opportunity/src/test/java/com/crm/opportunity/history/OpportunityStatusHistoryRecorderImplTest.java": "sha256:31a956c91d645a618f95697dd52a48bb54e8f73804e675d586699f52a87c6b1c", + "crm-opportunity/src/test/java/com/crm/opportunity/intake/impl/OpportunityIntakeImplTest.java": "sha256:5c8c9cd394d4d90bcbb45754861e502eda441d9fe5d257f9a10f0fd46b1c8b68", + "crm-opportunity/src/test/java/com/crm/opportunity/job/OpportunityMaintenanceJobTest.java": "sha256:0eac23a91cc863e0d0c22accaeb7290528d06f022af41412e745700824b58226", + "crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunityChildTablesMapperTest.java": "sha256:a3fab9adff2bffdff64e00044c42c165777b65e88fdcca8b33411ee39fcd046e", + "crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunityMapperTest.java": "sha256:c0046d2afa6ed227c00d2ecaa70bdab73bfe21c068db672127d4d722ec955531", + "crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapperTest.java": "sha256:a1a3c360e0b16fc553c441949ad6fddca2f755340ca53a6f675e97bbc6dc324d", + "crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunityViewSourceMapperTest.java": "sha256:d7582832310641c5f67d27626cb72e546ba6fd2c8316e2a92f04514a66faf0db", + "crm-opportunity/src/test/java/com/crm/opportunity/port/inbound/OpportunityConvertPortImplTest.java": "sha256:4889ed80915bac530b9bde0cbfe9bc23461002727523c8366d1cfa59018b8e15", + "crm-opportunity/src/test/java/com/crm/opportunity/port/inbound/OpportunityCreationPortImplTest.java": "sha256:e5df57a383c75531ae1c88e26cb83728d97967091c892cf69dd424860b04ba0a", + "crm-opportunity/src/test/java/com/crm/opportunity/query/OpportunityViewQuerySkeletonTest.java": "sha256:3ad766650904a1fd38324794e3967758e15e8458d51c19d67a6f17b6a22fd8d3", + "crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilterTest.java": "sha256:c6b8966c81f5a07bd3fda2af30ca4ab06cc59bb5c7eeff0be25faac6b4b65ba0", + "crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunityViewQueryImplTest.java": "sha256:17cef74bfb1a10f12d9c316c2e83b6ad1962aff5bbf55e0aed87578e67cd6dfc", + "crm-opportunity/src/test/java/com/crm/opportunity/schemecard/impl/OpportunitySchemeCardServiceImplTest.java": "sha256:9892d31fee869ab7b11ee564db1e2681b371294ba65023aaaf9d5933f88fd507", + "crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityBoardIntegrationTest.java": "sha256:16efcfde20300ef4bc0bd0ca400448434f929399facd5590e76faf959c2903d3", + "crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImplTest.java": "sha256:c2c2afe007462b080b22e53b33b4e34c4f06af8cd9441b825303f764dafec5e3", + "crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImplTest.java": "sha256:c6c49825cc2b5eb405dcca81fd34cc73104dc24e6ef5ef9ab132ab73eae3eb8c", + "crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityDetailServiceImplTest.java": "sha256:812ac4dbeaa4325ad2986856d8bb04f242bdcc24dc4bf9102a0566189d8fefac", + "crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunitySubServiceImplTest.java": "sha256:68c6811da5b4543999594468f4e86ea5af45c507c0c8f5f7c6eba78430e66aef", + "crm-opportunity/src/test/java/com/crm/opportunity/stage/impl/OpportunityStageServiceImplTest.java": "sha256:9bf60f8d0e11a80007a18eb097443911390b945ac5f84a6053bcf0b0bb49df3d", + "crm-opportunity/src/test/java/com/crm/opportunity/state/OpportunityActionGuardTest.java": "sha256:37cf8f014f5f21a95e4950490350f0899c11eb557b4c8468a21ecdb30ffaccf0", + "crm-opportunity/src/test/java/com/crm/opportunity/state/OpportunityTransitionCmdTest.java": "sha256:1c324adeee25b46d15da0e6e50dda0a9618486d902e98ea14bf1265bebf5db7f", + "crm-opportunity/src/test/java/com/crm/opportunity/state/impl/OpportunityTransitionImplTest.java": "sha256:011836c4d805d7e541d73c00bac999280ee9b5beb13702c05495e2b8bf3136c7", + "crm-preference/pom.xml": "sha256:3c4795bbd8111e3c978151015b03bafe3fad20ba5a10a6b5adab5de2feffc368", + "crm-preference/src/main/java/com/crm/preference/controller/ColumnPreferenceController.java": "sha256:dad97aeaebd8a6c68973a0ca428daa066c93bcc0a50572c5235128a3f420ab17", + "crm-preference/src/main/java/com/crm/preference/controller/SavedViewController.java": "sha256:fea61cf1bd4d869a0970d21504715783f147ad8f9ee2093cfbbf2912abd1820e", + "crm-preference/src/main/java/com/crm/preference/domain/dto/ColumnPreference.java": "sha256:ce36723a5454e36464c10ef3db617079fac7a4ef339a0cc35bb89962e30f0066", + "crm-preference/src/main/java/com/crm/preference/domain/dto/SavedView.java": "sha256:1f9e79a3bb61dd5b470dec6e24cf39286fb5e8c100688d5f78b06c20b9d6ab4e", + "crm-preference/src/main/java/com/crm/preference/domain/dto/SavedViewCondition.java": "sha256:6dec4875b976af07985fa2c5c307ec17d657c37260b1c3491bf7698fc003da1b", + "crm-preference/src/main/java/com/crm/preference/domain/entity/UserColumnPreference.java": "sha256:86e3c2443fea9b32f0e329c4d62c14adb90fe096df55796787e64de4ff667a71", + "crm-preference/src/main/java/com/crm/preference/domain/entity/UserSavedView.java": "sha256:49c54c3cbe6c45bd2f968c71b7db94c525e89837645db80062ceadbbd5e551b4", + "crm-preference/src/main/java/com/crm/preference/mapper/UserColumnPreferenceMapper.java": "sha256:7f6f80a40d2076164d96c0e1a5a7fc1c677bbc5ddeed51a8049031c702534873", + "crm-preference/src/main/java/com/crm/preference/mapper/UserSavedViewMapper.java": "sha256:805ed51cc8588d91f6323c630cd565404a397b4a78223d8d91495da001f650e4", + "crm-preference/src/main/java/com/crm/preference/service/ColumnPreferenceService.java": "sha256:ac6e3f47439cd2800e70987538e77f87876c09fc4305e8891fd6b17e2bd6c76d", + "crm-preference/src/main/java/com/crm/preference/service/SavedViewService.java": "sha256:9089b13e9019675582db6f2c2bb13ded8c19567ac7564147f6a9661d54863037", + "crm-preference/src/main/java/com/crm/preference/service/impl/ColumnPreferenceServiceImpl.java": "sha256:00535d095770ba0c94964035db75e28a2a5df661ea127ae7eef2ba95e0572961", + "crm-preference/src/main/java/com/crm/preference/service/impl/SavedViewServiceImpl.java": "sha256:c4ba1ac5d0bbfc75bf12a4a1f48ed741e99afafd205c2570e167c2c41ec92e04", + "crm-preference/src/test/java/com/crm/preference/service/impl/ColumnPreferenceServiceImplTest.java": "sha256:3f7b43975ae0be929ba66bf08dcfd5bb47f37fb187b665a28d4eb100bae0acb7", + "crm-preference/src/test/java/com/crm/preference/service/impl/SavedViewServiceImplTest.java": "sha256:ff10069e1c695b9114f92d472e7556ae8ad8004295ef1a4329173350de129e71", + "crm-rule/pom.xml": "sha256:73501580daca184cea7e126ba82355c70f0161fef26a16c60d68e811ff7c4ef6", + "crm-rule/src/main/java/com/crm/rule/config/OpportunityRulePermissionInitializer.java": "sha256:1178e6234a7cbbdd9cc446473452bfb9e5665b8217caf09f09aa2f76bac59834", + "crm-rule/src/main/java/com/crm/rule/config/OpportunitySchemeCardTemplatePermissionInitializer.java": "sha256:7ae7c7f89a913bc9f7370a2c73bcbfda1d1f8be1ae5153c573e318ea50f7406e", + "crm-rule/src/main/java/com/crm/rule/config/OpportunityStageTemplatePermissionInitializer.java": "sha256:711e33ca215e0f571d8024ee44238a5ccba64d4911494bb8a515829969a21abd", + "crm-rule/src/main/java/com/crm/rule/config/RegionDataInitializer.java": "sha256:739735b09f76b759be8bd9ce156ca867e17d372cf4f667e2e58dd3a2d4c18261", + "crm-rule/src/main/java/com/crm/rule/config/RulePermissionInitializer.java": "sha256:f1eb40bbf0a70a467bb45a6023ad032e230c07cc709a51c5f1d1b90e2fe358be", + "crm-rule/src/main/java/com/crm/rule/config/SchemeFieldDefInitializer.java": "sha256:f6060f84c3fb4379e1a20e41771ae5db1b53dc50455dcb801129a8d05ac52201", + "crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java": "sha256:d979d5d499f029cc10f752d80a4f93416a3d1664e93f1f304720018f577571bf", + "crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java": "sha256:60bca9f230f985c6e8e8982351b8c84c211b767bce237d7de031eca05d8c9b33", + "crm-rule/src/main/java/com/crm/rule/constant/RuleConstants.java": "sha256:a6c6cd8db948c97603dbf0767c3ca70205a388ea386f6c2f3e3c3fe40b03de60", + "crm-rule/src/main/java/com/crm/rule/controller/LeadPoolController.java": "sha256:bee1d3e441fc4001533fcb640b606ce3d1c673099da625371d524c55a1d3c73f", + "crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java": "sha256:f7bd644b8c1d667c6f6580cf52d2f87bd31cc0dba0e8f4968ea5a89a1dc0c4ed", + "crm-rule/src/main/java/com/crm/rule/controller/OpportunitySchemeCardTemplateController.java": "sha256:32c8320d78d141f63532e7ad7bad46489d8c0e071156d1316927da034d8cf894", + "crm-rule/src/main/java/com/crm/rule/controller/OpportunityStageTemplateController.java": "sha256:d9799efccb9e83f2ca1432dce83495f9e0c28305fbfdb037730db53b38d4a85b", + "crm-rule/src/main/java/com/crm/rule/controller/RegionController.java": "sha256:f714716c5ee9e800dbef84294c52b6484adfdf49d84684f171e4ff41c6e7dfc6", + "crm-rule/src/main/java/com/crm/rule/domain/dto/LeadPoolDTO.java": "sha256:d28081fa062762a440248004cec417512dd95964e12fd8ca68f23b718104dcb1", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunityPoolRuleDTO.java": "sha256:abeaa2e6912dc107442a0d1364be2651d323c12f1fdaa66baf1789be6eeb61a2", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunityPoolRuleVersionDTO.java": "sha256:39c3c5bbf5dba97b7067443e15c67e45aede3ec61c997ae85ebbb595b8ccd2e2", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunitySchemeCardTemplateDTO.java": "sha256:96af81f28c2deaf11653dca4ad2348ff86a8d7daeed9d60946bc05a7d48e0e1f", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunitySchemeCardTemplateVersionDTO.java": "sha256:8f8c790e21c2c298c3c08cdc1350cdd1cc5e227d5ed66980aedd2044b35e6ee2", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunitySchemeFieldDefDTO.java": "sha256:1983380b54df3adf5efb356ca8d3fa09c9a40c25b8722e82cd436676971208aa", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunityStageTemplateDTO.java": "sha256:0efe0aa6d80d359bef6dc9884498e1aa912d8c4a98086a000483058ce7231c71", + "crm-rule/src/main/java/com/crm/rule/domain/dto/OpportunityStageTemplateVersionDTO.java": "sha256:6c842fbaa27aa941efc8dde29b13ff4439812f673f95393c202b7fe8345d267a", + "crm-rule/src/main/java/com/crm/rule/domain/dto/PoolBatchFailItem.java": "sha256:510ad672eb50f51bcb863e9ffbb7b92f9256c0838d5d78aa2450881ab877e523", + "crm-rule/src/main/java/com/crm/rule/domain/dto/StageDisplay.java": "sha256:8bc572b10f16ec5d43f6339992f001b3d3e5a5d0eb9782455d64ca7ddd627c30", + "crm-rule/src/main/java/com/crm/rule/domain/dto/SysRegionDTO.java": "sha256:f9d23c44cf69d9e338dc978534278761fdec5c4d3d6cdd87d513e21173696bf1", + "crm-rule/src/main/java/com/crm/rule/domain/entity/LeadPool.java": "sha256:b9d6080a20921b8d8bf75c990461f404aaa4f46c47adc00b16d25cd6805d939e", + "crm-rule/src/main/java/com/crm/rule/domain/entity/LeadPoolMember.java": "sha256:3607a9cddab1bb2d6ceca3474f9615895d6b9aa3ff7b3915c50ed2cb0fe42160", + "crm-rule/src/main/java/com/crm/rule/domain/entity/LeadPoolProvince.java": "sha256:a0a0ac3d9ee18b0db28ae092fee408303f3d30078ec3e9695b6b3350eda1e25b", + "crm-rule/src/main/java/com/crm/rule/domain/entity/LeadPoolRegion.java": "sha256:d2e10869f8a0929d1f814ecf2e0464cf14c88d51f699c7195872c9f053038e82", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunityPoolRule.java": "sha256:33b376c587bbb224b368eb26609fec8db711b80d6c18e22a57bf4f88c1ad17df", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunityPoolRuleDept.java": "sha256:94b7fcc00e0cc34ca9ff3c035c6887bd50ddfaa17eedc86dfafceae98bf0a6c9", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunitySchemeCardField.java": "sha256:82f26abaf16d14c2f45d3ffd7b591770ec0411ee8cf2a33f17474afacbe2e017", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunitySchemeCardTemplate.java": "sha256:d2569cdc155571fc4b5be01c64347ea9767b02e2f0686f9760419e15854c81ab", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunitySchemeCardTemplateDept.java": "sha256:b568de05cb0521ba97a3849711de14067c30b671c371ca9919f65c2692928767", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunitySchemeFieldDef.java": "sha256:448af7ba2cfe886dcd620a12fe748e5852aa1af3937fc99a64e9565c4bc271a7", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunityStageNode.java": "sha256:5fa30531f5a1b5738e1788071ace59c7af4224c2ed58218477dfccf4fe3859d4", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunityStageTemplate.java": "sha256:b11ec6c1e0c7c0b29548276c14187e55f5d1e637fbf8d9ae435eea5a6756e77b", + "crm-rule/src/main/java/com/crm/rule/domain/entity/OpportunityStageTemplateDept.java": "sha256:6431232c7336146783916ff3b07a2ddeddbbb87cc533974fe2530d6635cccaba", + "crm-rule/src/main/java/com/crm/rule/domain/entity/SysRegion.java": "sha256:c404963587bc4b5bd928546bbe9602c47b64dcb4a99567a9c72e5f219193f379", + "crm-rule/src/main/java/com/crm/rule/domain/enums/PoolBatchFailReason.java": "sha256:dbf37539bb7812e29bf439a1897fcd2748bd0065fc8a8b6b7ee07b21be530624", + "crm-rule/src/main/java/com/crm/rule/domain/param/LeadPoolPageParam.java": "sha256:adf0365d8206e3c8b22172a1ec128d8ab78645c1cb725d05210aa49c58dc8f4d", + "crm-rule/src/main/java/com/crm/rule/domain/param/OpportunityPoolRulePageParam.java": "sha256:923315441d65a8320bb75e0dd5c7756df2a9aa4cf690719b9a287a27c8c7807e", + "crm-rule/src/main/java/com/crm/rule/domain/param/OpportunitySchemeCardTemplatePageParam.java": "sha256:3399d25d53400653fa300ab6e1e8c1691bedbcc6908e9c67511721a63e3b7303", + "crm-rule/src/main/java/com/crm/rule/domain/param/OpportunityStageTemplatePageParam.java": "sha256:1ce3d461c02dccb1cabf89f622092ed02c69760ba737c41ecb28424a2e4b0479", + "crm-rule/src/main/java/com/crm/rule/event/PoolChangedEvent.java": "sha256:2de9939be11fff335c9df6c0f732478f77603dc21c58e7147a8a47c9986caeb8", + "crm-rule/src/main/java/com/crm/rule/mapper/LeadPoolMapper.java": "sha256:9745e2f22c2c83b77d2412ebe7a1372aad3470d14da0f03285859941a8424ad3", + "crm-rule/src/main/java/com/crm/rule/mapper/LeadPoolMemberMapper.java": "sha256:091606d23adb43c53b677ca096821fb252fde367ee184f274f9243159f6898e2", + "crm-rule/src/main/java/com/crm/rule/mapper/LeadPoolProvinceMapper.java": "sha256:925ac43341bdda92a5d4244d6bf352ec30cdecf08d4083d535b17f5e2612df9e", + "crm-rule/src/main/java/com/crm/rule/mapper/LeadPoolRegionMapper.java": "sha256:d203583d37f51283eaf680643709c882688de3124d0db1cb12eb9c2bd7b3e23b", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunityPoolRuleDeptMapper.java": "sha256:6aa7ff4d594703482e789192c0ef10660f496f72f8bdf1a10d0073d3b6f2f346", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunityPoolRuleMapper.java": "sha256:ecb2e8fa06b0fa10bb69771c4df7b76ab2a4b120b6f8aa8b8e950f7b2879a179", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunitySchemeCardFieldMapper.java": "sha256:0990e4cebbfff2f19c303c80c2570ab0d526673048357b4692763a4e21a8ada8", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunitySchemeCardTemplateDeptMapper.java": "sha256:7eddd9d31373d79383b2bd14dca3ea7530223910cf580efb18324ceacda0e69a", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunitySchemeCardTemplateMapper.java": "sha256:1324b7e7a81b8e7c5d0b9f000c290558653c54c5058a275a893b83679fce0a02", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunitySchemeFieldDefMapper.java": "sha256:6fee929fc2f779aa306fd6ca4df60dbcdb3b09d5dc82d9192fdaec93354735fb", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunityStageNodeMapper.java": "sha256:96adb785c222c4f3b8e747fec5aa13297f12ebfa7f037da7b43254b1cb2adeb5", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunityStageTemplateDeptMapper.java": "sha256:ed36422bb4f56ca8b11b74e7b443e669383e512cc0835a039364a256ff0713c4", + "crm-rule/src/main/java/com/crm/rule/mapper/OpportunityStageTemplateMapper.java": "sha256:98deed4a4be18a5f916cc4e7b27cf56624e3c33a837ad2f563f82f8220167f63", + "crm-rule/src/main/java/com/crm/rule/mapper/SysRegionMapper.java": "sha256:dcac2b33a2df44568a6771f2db9ee98714d877f08efeb0dfd5216d716fe83991", + "crm-rule/src/main/java/com/crm/rule/port/outbound/PoolLeadOccupationPort.java": "sha256:2fb3c98ea713846d887fd492476004dceff0672577ce3f21c2010e5b947b4931", + "crm-rule/src/main/java/com/crm/rule/service/ILeadPoolService.java": "sha256:6e8b03b5f2e58395dea4257ca87e26a5f939e6bbc959721e6facd2983ea85cb4", + "crm-rule/src/main/java/com/crm/rule/service/IOpportunityPoolRuleService.java": "sha256:b3348bdd0519e2aea1e6c55ba4237dd9a92b4be1e40b770d95838d57d230f0d0", + "crm-rule/src/main/java/com/crm/rule/service/IOpportunitySchemeCardTemplateService.java": "sha256:67446206742eb35d6c2a21c355c109ce9f40ca85b20829969346e78cd1ee0b6c", + "crm-rule/src/main/java/com/crm/rule/service/IOpportunityStageTemplateService.java": "sha256:8c42dccaafe5bd445f27d88250e6b59046cdf3efc26bbdb0fb0a996cd08d5185", + "crm-rule/src/main/java/com/crm/rule/service/ISysRegionService.java": "sha256:569c320bc6cee349334b3a05b0abe5cb0cd7c0e5179a1d6a74f04da6f88ccbc8", + "crm-rule/src/main/java/com/crm/rule/service/OpportunityPoolRuleMatcher.java": "sha256:a6596fa5cfdd60c7f1083b2cee6aa2f62e0d16dfeb9c646bdf0d1ce652a203b8", + "crm-rule/src/main/java/com/crm/rule/service/PoolGeographyStore.java": "sha256:0f7fcaaa7f8e342d4d03ab9f6a30d202c492e7b4b1729f54ba04710393d75308", + "crm-rule/src/main/java/com/crm/rule/service/PoolMemberStore.java": "sha256:a82c6b3b61203848793cad8ddb9202ec4c9c998f29e98756c7b587f43cabb234", + "crm-rule/src/main/java/com/crm/rule/service/impl/LeadPoolServiceImpl.java": "sha256:d98b4abd562c8fef4dc118394b1500e46f7ced9d6f9e171b057b4628f673aa2b", + "crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityPoolRuleMatcherImpl.java": "sha256:f3ffe1282381e5aa59f1e5114ab47bcdcbe6345a320b1cd06c844a258405674d", + "crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityPoolRuleServiceImpl.java": "sha256:46a1ed1ea8093876219544cf5987be6f23c3a92db9d1bc9072fa2d65701b180a", + "crm-rule/src/main/java/com/crm/rule/service/impl/OpportunitySchemeCardTemplateServiceImpl.java": "sha256:ae0b25741753a45cdf3084c8c2b46f842c5d6f964b1e9fb9dc61cfb90000e074", + "crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImpl.java": "sha256:4572b0499c61019cc8e60e298996fe72ef5e44b862dc5743659ebe33912730e0", + "crm-rule/src/main/java/com/crm/rule/service/impl/PoolGeographyStoreImpl.java": "sha256:373a34a991fd4926d8f4b74931ad8229fcf9ad0d296d020b087444d172e6dcc8", + "crm-rule/src/main/java/com/crm/rule/service/impl/PoolMemberStoreImpl.java": "sha256:88aedc80813f19e47648f17bed1a3b4e7ec2ba6ad3ed50edc1b03cca4f7e1714", + "crm-rule/src/main/java/com/crm/rule/service/impl/SysRegionServiceImpl.java": "sha256:4cac672fd48073b935fa40034e7476523917ac93387f9f147ebf76dedcd98e91", + "crm-rule/src/main/java/com/crm/rule/service/impl/VersionedConfigSupport.java": "sha256:69a54997f744fd4b5a964f635b5a8f14df6790c86970af019f1afcd8dbd505b9", + "crm-rule/src/test/java/com/crm/rule/config/RegionDataInitializerTest.java": "sha256:78c3254185aec09d8879027a352fa2d01dc890a63bb151988ffa3eee3a0fe1f6", + "crm-rule/src/test/java/com/crm/rule/config/SchemeFieldDefInitializerTest.java": "sha256:26987fbd248c596267abdd2e448617c917839af1b86c65d34cdb9ef97e311be3", + "crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java": "sha256:9acd14075c909851c8451b6b0e0758aadda57ae2ecd52468d8e2d3a4ad24f2b5", + "crm-rule/src/test/java/com/crm/rule/service/impl/LeadPoolBatchDeleteTest.java": "sha256:bbaadc0de1f0ddf50b33743ebf00b603a90bd46e8a79c8ebc565ed6dd1a8c0cd", + "crm-rule/src/test/java/com/crm/rule/service/impl/LeadPoolServiceImplTest.java": "sha256:8ebcfd7e8e1c481e8524a1d230cd8a7c8071b3693759787ebed952f0ac04542a", + "crm-rule/src/test/java/com/crm/rule/service/impl/OpportunityPoolRuleMatcherImplTest.java": "sha256:570b50da3715b3e663dc9d1f4f07fd243cfb21d9006d5ffa98cdf1b1c47dab44", + "crm-rule/src/test/java/com/crm/rule/service/impl/OpportunityPoolRuleServiceImplTest.java": "sha256:72fe94505f97865f26acbfb0d36ea89456f2951cf01e42472c36cdfb3326a52a", + "crm-rule/src/test/java/com/crm/rule/service/impl/OpportunitySchemeCardTemplateServiceImplTest.java": "sha256:ad7e8ac4e5188ecd491a6a6404aa66ed8b3128c24109f9012e684b056e4a0083", + "crm-rule/src/test/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImplTest.java": "sha256:5791337fe64fc8762c9c63a0264cf66dca9977c2d6bd235315ead6505a05cde9", + "docker-compose.yml": "sha256:6e5ea6a3d2442cddf87a8dae62c97ff43585f5c4f6f8daab14a673a94a85913c", + "pom.xml": "sha256:abe417c4b369d6a649f52b15948497c086cb2cd05371cf0b6bdb9a2a9d5c4b2c", + "tmp/china-division/package/package.json": "sha256:a8b0c3d1f5e480c51b6726a5456f4b62188b5f4a54d719075d6166f24cae9fbd" + } + }, + "version": 2 +} \ No newline at end of file diff --git a/.lithe/toolchains/requirements.json b/.lithe/toolchains/requirements.json new file mode 100644 index 0000000..beb8739 --- /dev/null +++ b/.lithe/toolchains/requirements.json @@ -0,0 +1,29 @@ +{ + "toolchains": { + "project-jdk": { + "java": null, + "minimumVersion": "17", + "preferredVendor": null, + "type": "java", + "version": null, + "wrapper": null + }, + "project-maven": { + "java": "project-jdk", + "minimumVersion": null, + "preferredVendor": null, + "type": "maven", + "version": null, + "wrapper": null + }, + "project-node": { + "java": null, + "minimumVersion": null, + "preferredVendor": null, + "type": "node", + "version": null, + "wrapper": null + } + }, + "version": 1 +} \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..969b61a --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + crm + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + + + 1788308974700 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.scratch/customer-module/REVIEW-HANDOFF.md b/.scratch/customer-module/REVIEW-HANDOFF.md new file mode 100644 index 0000000..1f6fbb2 --- /dev/null +++ b/.scratch/customer-module/REVIEW-HANDOFF.md @@ -0,0 +1,206 @@ +# 客户模块(A4)后端 spec — 复核交接清单 + +> 用途:交付给一个**全新的、无本会话上下文**的会话做第二轮 grill / 独立复核。 +> 上一轮共 19 个 grill 决策(D18–D29 + D4-rev1/rev2)已全部由用户拍板。**本清单的目的不是报喜,而是把最可能被推翻的点提前摊开,供下一轮挑战。** + +--- + +## 0. 怎么读这份清单 + +- **权威交付物**:`.scratch/customer-module/map.md`(决策索引 D1–D29,自包含)+ `issues/NN-answer.md`(各票详细决议,自包含)。 +- **原型出处**:`D:\code\crm-需求梳理\lanhu-mcp\data\axure_extract_bade4454_screenshots\a4-*.txt`(蓝湖 A4 页面已本地提取文本)。 +- **对称参照**:`.scratch/opportunity-module/map.md`(已完成的商机图,本图大量对称其范式)。 +- **领域上下文**:`crm-customer/CONTEXT.md`、`crm-lead/CONTEXT.md`、`crm-opportunity/CONTEXT.md`。 +- **协作规范**:`docs/agents/issue-tracker.md`、`docs/agents/triage-labels.md`。 + +**复核建议**:先读 `map.md` 的 Decisions 区(D1–D29,约 5 分钟),再逐个挑战下面「第 4 节薄弱点」。**不要**因为 map 里写了"已拍板"就跳过——这是本清单特意要求的。 + +--- + +## 1. 本图范围(D2) + +**做**(8 大块):客户核心(实体+查重+公海+总览+我的客户+三视图+新增/编辑+导入)、联系人(CRUD+全局搜索+电话查重)、客户交割、客户规则族(超期提醒 + 查重设置)。 + +**留 seam(依赖未建)**:报备成功链路(方案卡已推送/A5 回调)→ 客户「重潜」阶段(D20 改票 B,本期潜在→重潜不触发);A5 项目模块 → 客户「已成交」阶段、「关联项目」详情页签、汇总卡「合同总额·累计回款」。 + +**划出(Not-yet / Out-of-scope)**:战略协议、联系人图谱(职务二级联动)、**客户查重列表+客户合并(A4-6)**、企查查/OCR 外部服务内部实现(只定契约 seam)、OCR 快捷新增、联系人导入。 + +--- + +## 2. 已冻结的决策(D1–D29 速览) + +| # | 决策要点 | 原型/出处 | +|---|---|---| +| D1 | spec-only,对称商机图;做完进 /implement | — | +| D2 | 作用域三档划分(见上) | — | +| D3 | 双轴建模:轴1 归属(owner 空=公海)正交 archive_status;轴2 阶段三态只前进 | a4-2-1 | +| D4-rev1 | 客户规则族只含「超期提醒 + 查重设置」;星级/阶段退回硬编码 | a7-2-3 | +| D4-rev2 | 查重设置 = crm-rule 全局单例可配置,本期只消费于新增/编辑内建查重 | a7-2-3-查重 | +| D5 | 联系人 = crm-customer 独立表 `customer_contact`,权限继承客户 | a4-4 | +| D6 | 内建查重口径(后被 D4-rev2 改为可配置) | a4-3-3 | +| D7 | 客户主表字段八区 + DataScope + 索引(无孤立单大写陷阱) | a4-3-3 | +| D8 | 超期提醒:crm-rule 单例配置 + 待发通知表 + Job 只落待发 | a7-3-3 | +| D9 | 三视图/公海/总览 = 逻辑视图,内置视图不入库 | a4-1-1/a4-2-1 | +| D10 | 分层查重 + CustomerDedupService + 企查查 seam | a4-3-3 | +| D11 | 阶段只前进 CAS(三态字段/机制保留);重潜触发本期 seam 化不实现(D20 改票 B) | a4-4 | +| D12 | 联系人 CRUD + 全局搜索;电话查重软提示(D26);来源字典(D27) | a4-4 | +| D13 | 详情 8 页签 + 跟进 append-only + 字段级 oplog;卡片占位(D28) | a4-4 | +| D14 | 建 crm-customer/CONTEXT.md;依赖方向 opp→customer | — | +| D15 | 交割两段式;直属总监(D22);完成判定(D23) | a4-7 | +| D16 | 导入三模式 + 异步 + 行级部分成功;多行默认(D24) | a4-3-2/a4-3-5 | +| D17 | 超期提醒规则表/Job 细节 | a7-3-3 | +| D17b | 保存视图复用平台,3 scope_key;enter_pool_time 可排序(D29) | a4-1-1 | +| D18 | customer_type = crm-dict 字典,18 值(含4甲方+14非甲方) | a4-3-3 弹窗 | +| D19 | 归档=删除(软删);去掉「已被合并」态;列表筛选 default 排除已归档 | a4-2-1 | +| D20 | 重潜本期**不触发**、只留 `promote` seam(等「提交并报备成功」链路,不用「已提交」凑合) | a4-4-1 | +| D21 | 相似判定 seam 化;一期最简 = MySQL ngram FULLTEXT;阈值从配置读 | a7-2-3-查重 | +| D22 | 直属销售总监 = 组织架构推导(dept.leader 向上找销售总监);不改 crm-auth | a4-7-1 | +| D23 | 交接单完成 = 自动(assignedCount==totalCount);无手动按钮 | a4-7-1 | +| D24 | 导入多行匹配同一客户默认「重复时不导入」(重复组全失败) | a4-3-5 | +| D25 | 超期提醒锚点 = 成为「有主」时刻;公海 NULL 不计时;领取时赋锚点 | a7-3-3 | +| D26 | 电话查重=**可配置软提示**(不建唯一索引);同客户内电话不重复为表单级校验 | a7-3-3-2 查重设置 | +| D27 | contact_source 字典种子 = 组会/组局/转介绍/其他 | a4-4-1 | +| D28 | 汇总卡占位一律渲染 + 显"--"(不显 0,不留白) | a4-4-1 | +| D29 | 公海「进入公海时间」列保留 + 加入排序字段池 | a4-1-1 | + +--- + +## 3. 为什么我信不过自己——复核方法论 + +用户的担忧是对的。上一轮我犯过**SOP 违规**(票 02–11 未经 grill 就标记 resolved),后来被迫全部重开重问。因此第二轮复核请遵循: + +1. **每个决策都要能找到"证据"**:要么是原型页面(a4-*.txt 的原文文字),要么是用户当时明确说过的选型(如 Q8 的 18 值、Q16 的"全部电话")。**找不到证据的就是我编的,请当误入标注出来。** +2. **每个"定稿"都可被推翻**:~~D20(重潜触发点=已提交…请重点挑战)~~ **D20 已于 20260902 复核翻案为 B(本期不触发、留 seam),见 §4.1**、D22(组织架构推导在无组织数据/多领导时的降级行为我写的是"阻断",但用户可能预期"降级给普通接收人")。 +3. **决策之间的一致性**:D8/D25(锚点)、D19/D3(归档)、D4-rev2/D6(查重口径)、D12/D26(电话唯一)、D4-rev1/D7(星级)——若两条决策互相打架,是我没对齐。 + +--- + +## 4. 我最担心的薄弱点(请下一轮重点挑战) + +### ✅ 4.1 D20 重潜触发 —— **用户拍板 B(20260902):本期不触发、只留 seam,翻案** +- 我选了「保守」口径:方案卡状态到「已提交」就触发客户重潜。 +- **风险**:原型 A4-4 的重潜定义其实写着「关联的任一方案卡**提交并报备成功**」。我因为「报备/推送链路本期不可达」而收窄成「已提交」。用户可能完全接受,也可能说"那就等报备成功再触发,本期不实现重潜"。 +- **请问**:报备链路不可达时,是「用已提交凑合触发」还是「本期干脆不实现重潜、只留 seam」? +- **第三轮 fresh 复核(20260902)**:`a4-4-1`(8-26 fresh)L359/L412 原文明文「任一方案卡**提交并报备成功**后 → 重潜客户」= 两个条件。用「已提交」单条件凑合会造出「提交了但报备没成功、客户却已重潜」的错误状态。 +- **✅ 已拍板(用户选 B,20260902)**:本期**不自动触发潜在→重潜**,客户阶段实际只有「潜在」;`CustomerStagePort.promote` seam(契约/只前进 CAS/幂等)全部保留,等报备成功链路(方案卡「已推送(3)」或 A5 回调)就绪再接入调用。**不用「已提交」凑合。** map.md D20/D11 + 票 05-answer + CONTEXT.md 已同步。 + +### ✅ 4.2 D22 直属销售总监 —— **已确认(grill Q12 拍板 A),复核补全「调动/区域调整=下拉选」分支** +- 复核发现:票 08-answer L15/L23 已把口径写全 = **离职=组织架构推导自动定唯一总监**(无/停用/多条→阻断提示管理员);**岗位调动/区域调整=在本人启用的直属总监内下拉选(不阻断)**。 +- 原 HANDOFF 4.2 段只描述了「离职=推导阻断」这**一半**,漏了下半句。**非决策遗漏,是记录不全**。修正:阻断仅离职专属;调动/区域调整为可选下拉,不阻断。 +- **✅ 已确认(20260902)**:无需翻案,仅补齐 map.md D22 描述(补充「调动/区域调整=下拉选、离职才阻断」)。不改 crm-auth、不加 `superior_user_id` 维持原判。 + +### ✅ 4.3 D4-rev1 把「星级/阶段设置」踢出客户规则族 —— 阶段 ✅A口径 / 星级 ✅已三点确认(20260826) +- **阶段**:第二轮复核拍板 **A 口径** —— 客户阶段=固定三态(潜在→重潜→已成交),只前进不回退,**不建 `a7-2-3 客户阶段设置` 页**。D4-rev1 对阶段部分**正确,保留**。 + - 依据:`a4-4-1` 详情页 3 次强调「阶段由业务结果驱动、阶段条不可点击修改、只前进不回退」,与 A 口径一致。 + - 注:`a7-2-3 阶段设置` 页与详情页内部矛盾(可增删/排序/迁移 vs 只前进不可配),已裁决按详情页(A 口径)走,设置页视为原型残留。 +- **星级**:✅ **已确认原判成立(第二轮复核,用户拍板 20260826)**。我上一轮引用「关系星级名称+描述由管理员自定义」是**基于过时原型缓存**(`a7-2-3_星级设置_.txt` 为 2026-07-20 旧快照,仍是「潜在/高潜/合作/重点/战略 + name/desc 可配」老口径)。**新原型已更新**:`a4-3-3-1_新增客户信息.txt`(2026-08-26)L91-106 显示客户星级、关系星级**均为「一星~五星」两个普通下拉字段**,无 name+desc 可配语义。**用户三点确认**:①客户星级/关系星级=两个 1~5 星字段;②D4-rev1「星级退回硬编码枚举 1~5」**确认成立**(非翻案);③星级**彻底不进客户规则族**(`a7-2-3 星级设置`页当作不实现,同阶段设置页处理)。**我上一轮「关系星级必须可配」的纠偏作废。** + +### ✅ 4.4 D26 联系人电话查重 —— **用户拍板 A(20260902):降级为可配置软提示,翻案** +- 用户拍板选了「全部电话唯一(含座机/短号)」——比我的推荐(仅手机号 11 位唯一)更强。 +- **风险**:座机/短号撞号概率实际远低于手机号;但**同一个公司多个联系人的座机可能共用一个总机号**,这会误伤。强制唯一可能让"一个公司两个联系人都填了同一个座机总机"这种正常场景无法保存。 +- **请确认**:是否真的对座机也做全局唯一?要不要豁免「座机且同客户」? +- **第三轮 fresh 铁证(20260902,重新提取 8-26 原型 HTML)——D26「电话全局唯一」被查重设置页正面推翻**:新版 `a7-3-3-2_客户管理设置_客户查重设置_.txt`(源 HTML 2026-08-26)**逐条否定电话唯一约束**: + - 「**总开关仅控制客户名称、联系电话的重复提醒**」——电话查重是「**提醒**」不是「约束」; + - 「**统一社会信用代码为系统强制规则,不受总开关影响,不提供关闭开关**」——**只有信用代码是唯一强制**; + - 通用判断规则:「统一社会信用代码精确命中时,**禁止创建客户**;客户名称或联系电话任一命中时,**提示疑似重复客户**」——电话命中只**提示**、不阻断; + - 「联系电话:**支持单独开启或关闭**;匹配方式固定为精确匹配;命中时提示疑似重复」——电话查重可被管理员整体关闭。 +- **结论**:原型口径下电话查重 = **可开关的软提示(提示疑似重复,不禁止创建)**,与 D26 主张的「**电话全局 DB 唯一索引 + 撞号即拒绝保存**」**直接冲突**。D26 是**过度设计**:把原型的软提示误升级为硬约束。 +- **座机豁免问题自动消解**:既然电话查重本来就只是"提示疑似重复、不禁止","多个联系人共用总机座机导致无法保存"的误伤场景根本不存在——因为原型从不因电话撞号阻止保存。 +- **新增页佐证**:`a4-3-3-1_新增客户信息.txt`(8-26)§3.5「联系电话存在时…按电话值查重;电话值完全相同时禁止新建」仅约束**同一表格内两行**(同一客户内联系人不能填相同电话),**不是跨客户全局唯一**。 +- **建议**:D26 从「电话全局唯一索引」**降级为「可配置的电话查重提示」**(跟随 crm-rule 客户查重设置单例的"联系电话"开关),DB 层**不建电话唯一约束**;仅保留同客户内联系人电话不重复的表单级校验。**待用户拍板改票 06 D26。** +- **✅ 已拍板(用户选 A,20260902)**:采纳建议。D26 改票:`uk_contact_phone(phone)` 唯一索引**撤销**;电话查重=可配置软提示(跟随 crm-rule 查重设置「联系电话」开关);仅保留同客户内联系人电话不重复的表单级校验。**map.md D26/D12 已同步改口径。** + +### ✅ 4.5 D18 customer_type 18 值——**已证实,非编造(第二轮复核,证据在 `a4-1-1`)** +- **原判断撤销**:第一轮我 grep 只搜 `a4-3-3-1` 新增页,漏了 `a4-1-1` 公海列表,误判"部分编造"。 +- **决定性证据**:`a4-1-1_客户公海列表.txt` L45-63 是客户类型筛选下拉的**完整展开枚举**: + `运营商 / 土建总包 / 机电总包 / 装修总包 / 教育局 / 投资(代建)公司 / 设计院 / 集成商 / 招标代理 / 咨询造价公司 / 甲方(企业用户)/ 甲方(普教)/ 甲方(中高职)/ 甲方(政府机关)/ 竞争同行 / 个人用户 / 操手 / 集成商(子客户)` = **17 具体值 + 1"全部客户类型"占位 = 18 值**。 +- 与票 02 列表逐字一致(含"投资(代建)公司""集成商(子客户)")。**消号,D18 成立**。 +- **第三轮 fresh 复核(20260902,重新提取 8-26 原型 HTML)**:新版 `a4-1-1_客户公海列表_列表视图_.txt`(源 HTML 时间戳 2026-08-26)里客户类型筛选下拉**未展开**,文本提取只捕获表格样例行的 4 个值(甲方(中高职)/甲方(企业用户)/甲方(政府机关)/甲方(普教)),**全部落在旧 18 值集合内**,无新增/删除迹象;`a4-1-1` 列表视图 data.js 仅含 `甲方(中高职)` 单元格值,18 值枚举不在页面级资源。**新原型正面印证决策架构**:`a4-3-3-1_新增客户信息.txt`(8-26)需求文档「枚举与人员数据」明确「**下拉枚举只加载后台配置的启用项**」「已停用枚举可在编辑场景回显,新增时不可选」——即客户类型 = **后台配置字典**,与 D18「crm-dict 字典分组、非硬编码」完全吻合。**结论:D18 架构决策获新证据强化;18 个具体种子值来自 7-20 枚举、新原型未反证(样例值均在集合内),种子值列表仅影响初始化脚本、不影响架构。D18 ✅ 保持。** + +### ✅ 4.6 D24 导入「重复时不导入」默认 —— 已确认(grill Q14 用户拍板 A),非缺陷 +- 我默认「重复时不导入」(重复组全失败)。但**很多导入场景用户是想"更新老客户"的**,默认不导入可能让大批行失败。 +- **请确认**:默认到底该是「不导入」还是「更新」?还是按导入方式(仅新增/仅更新)各自定? +- **✅ 已确认(grill Q14,用户拍板 A)**:文件内多行匹配同一客户,`覆盖导入`/`重复时不导入` 互斥,**默认「重复时不导入」**——重复组全部标失败不写入,用户看失败明细处理。覆盖模式作为可选项存在但不默认。 + +### ✅ 4.7 D21 ngram FULLTEXT —— 已确认可用(MySQL 8.4.8) +- 一期最简用 MySQL ngram 分词器(token_size=2)。**ngram parser 是 MySQL 5.7.6+ 功能**,且对中文分词效果一般(会切出大量无意义二元组)。 +- **请确认**:项目 MySQL 版本是否 ≥5.7.6?若偏低,ngram 不可用,得回退 LIKE + Levenshtein(或我提过的 Jaccard)。 +- **✅ 已确认(20260902)**:项目 MySQL = **8.4.8**,≥5.7.6,ngram 分词器可用,无需回退。 + +### ✅ 4.8 D17b 保存视图复用平台 —— 已确认(`user_saved_view` 支持多 scope_key) +- 我复用现有保存视图平台并假设它支持多 scope。**请核对** crm-auth / 现有平台的 `user_saved_view` 表结构和 scope 机制,确认"公海/总览/我的客户"各存一套视图不会串。 +- **✅ 已确认(20260902)**:`user_saved_view` 平台支持多 scope_key(每入口各存一套),公海/总览/我的客户各自保存不串。 + +### ✅ 4.9 D9 视图「内置视图不入库」 —— 已确认(逻辑视图,内置硬编码) +- 我写内置视图(如"我的客户-未跟进N天")硬编码不入库,用户自定义视图才入库。但这个边界(哪些算内置、哪些算自定义)我没问用户,是拍脑袋定的。 +- **请确认**:哪些内置视图是必须的?用户能不能改/删内置? +- **✅ 已确认(20260902)**:三个 workspace(公海/总览/我的客户)= 逻辑视图,靠 `@DataScope` + `owner_user_id IS NULL` + `archive_status` 过滤区分,**不建独立表**;内置视图(含常用检索)硬编码不入库,用户自定义视图才入库,边界成立。 + +--- + +## 5. 需要用户明确但**我可能漏问**的点 + +1. **公海 vs 我的客户是否允许"重复看到同一客户"**:一个客户在公海,同时又被我领了,会不会同时在两个列表出现?(我默认:有主就只在我的客户,公海只出无主,二者互斥。) + - **✅ 原型铁证已定向(20260902)**:`a4-1-1_客户公海列表_列表视图_.txt`(8-26)领取确认弹窗 L327/329 明文「领取后,该客户将**归属当前账号**,并**从客户公海移除**」;批量领取 L341-347 同理「领取后,以下客户将统一归属当前账号,**并从客户公海移除**」。 + - **结论**:**§5.1 消号**。公海与我客户**互斥**(领取=移出公海,归属有主)。D3(归属⊥归档双轴)+ D9(公海=owner IS NULL 逻辑视图)原判**正确,维持**。无需拍板。 + - **区分「关注」**:公海列表有「关注/取消关注」(个人关注状态,如 L89 取消关注 / L129 关注),关注≠领取,**不改变归属**,客户仍在公海。「我关注的客户」是检索维度,可含公海客户,但不影响互斥模型。 +2. **客户能否被多个负责人共同跟**?我默认单负责人(owner_user_id 单值)——但原型有"团队成员"页签,可能暗示多协作者。**这是竞品常见分歧点,我没问。** + - **✅ 原型铁证已定向(20260902)**:`a4-4-8_客户详情_团队成员_.txt`(8-26)需求说明 **11.1 角色与管理权限** 明文「**每个客户只允许 1 名负责人,其余成员均为协同人**(如销售协同人、方案工程师等)」「协同人可查看对应客户的信息」。团队成员页签展示角色列(负责人/协同人)+ 添加成员(搜索多选、不可重复)+ 删除成员(二次确认、仅撤销后续访问权限、不影响历史记录)。 + - **结论**:**§5.2 消号**。客户**单负责人**(owner 单值)+ **多个协同人**(团队成员表),协同人可见但非负责人。D3/D5/D7 单负责人原判**正确,维持**;「团队成员」页签 = 负责人+协同人列表,非多负责人。无需拍板。移除「多负责人」这一待确认项。 +3. **删除语义**:D19 已归档=软删,但「归档」按钮 vs「彻底删除」——有没有彻底物理删除的入口?我默认没有(只有归档)。 + - **✅ 原型证据已梳理(20260902)**:客户「删除」= **逻辑删除**(`deleted` 标志),无前端直接"删除"入口、无物理删除。依据: + - `a4-4-1_客户详情_客户信息_.txt` 操作区仅「归档」(L127)+「标记重点客户」,**无"删除"按钮**; + - `a4-4-1_客户详情_已归档客户_.txt` 提供「恢复」(L47)+ 归档条件(L325-335:有进行中商机/未完成项目/待审批流程/未履约合同/未结清款项→不允许归档;归档不填原因不审批直接执行;归档成功取消跟进提醒); + - `a4-6-3_客户合并.txt` L323「合并成功后保留主客户,将其他已选客户**标记为已合并并执行业务软删除,不做物理删除**」; + - `a4-3-3-1_新增客户信息.txt` L690 / `a4-6-1` L315 查重范围 = 「全部**未删除**客户」→ 存在 `deleted` 逻辑删除标志。 + - **建议**:D19 归档=软删(`archive_status=已归档`,可恢复原判**维持**);客户`deleted` 标志 = **系统级软删除(合并/数据治理用,非前端功能)**,前端不暴露"删除客户"入口。**三态档案(有效/已归档/已被合并)+ deleted 软删标志并存**,需确认 deleted 是否仅合并时由系统置 1(而非用户手动删)。**此项需你拍板:客户删除入口是否彻底不做?** + - **✅ 已拍板(用户选 A,20260902)**:**一期只做归档**——不提供任何前端"删除客户"入口;用户侧只有归档(软删可恢复)+ 合并(源客户系统软删,且合并本体 Out-of-scope);`deleted` 标志仅系统在合并时置 1,**不开放手动删除、无前端删除按钮**;DB 不做物理删除。**票 02-answer(archiveStatus 三值→两值去掉已被合并、deleted 补充说明)+ 票 01-answer 已同步。** +4. ~~**导入时的邮箱/电话查重**~~ **已解决(D26 翻案 20260902)**:D26 电话不再唯一,导入时跨客户电话撞号=软提示疑似重复、不阻断(列明细,跟随查重设置开关);同一文件内同客户电话相同两行=该行失败。已回填票 09-answer。 +5. **操作日志保留时长**:customer_oplog 字段级日志,有没有保留期/归档策略?我默认无限期。 + - **✅ 原型证据已定向(20260902)**:`a4-4-8_客户详情_操作日志_.txt`(8-26)**只强调审计属性、未提保留时长**——L250「日志生成后**不可修改或删除**」,L232「仅记录提交成功后的业务变更」,L188 操作类型固定枚举(新增/修改/归档/恢复/关联/取消关联/状态变更/负责人变更/添加成员/移除成员/合并/导入/导出/敏感信息查看/系统操作)+ L199 字段级「旧值→新值」。即原型把操作日志定位为 **append-only 审计日志**。 + - **结论**:保留时长**原型无要求 = 技术决策**(非规格缺口)。**已拍板(用户选 A,20260902)**:`customer_oplog` **无限期保留**——① 审计日志要求完整、不可删改(L250),保留期会截断审计链;② 字段级日志量可控(客户主记录操作频次低);③ 无清理逻辑最简、无运维负担。**票 07-answer 已补保留策略行。** + +--- + +## 7. 本轮复核结论(20260902,fresh 原型第三轮) + +**复核方式**:对 4 个 7-20 过期快照页(a7-3-3-2 查重设置 / a7-3-3-1 超期提醒 / a4-4-1 客户信息 / a4-1-1 公海列表)用本机 Lanhu Python MCP 重新提取 **8-26 fresh** 文本,逐项对照。 + +**§4 九个薄弱点 —— 全部闭环**: + +| 项 | 结论 | 拍板 | +|---|---|---| +| 4.1 D20 重潜触发 | **翻案**:原型=「提交**并报备成功**」两条件,原判「已提交」错误 | **用户选 B**:本期不触发、只留 seam | +| 4.2 D22 直属总监 | **确认**(补全「调动/区域调整=下拉选」,仅离职才阻断) | 维持 | +| 4.3 星级/阶段 | **确认**(阶段 A 口径 / 星级两点确认) | 维持 | +| 4.4 D26 电话查重 | **翻案**:原型查重设置页证明电话=软提示非唯一 | **用户选 A**:可配置软提示、不建唯一索引 | +| 4.5 D18 18 值 | **消号强化**:后端字典架构获 fresh 印证 | 维持 | +| 4.6 D24 重复导入默认 | **确认**(非缺陷) | 维持 | +| 4.7 D21 ngram | **确认**(MySQL 8.4.8) | 维持 | +| 4.8 D17b 保存视图 | **确认**(多 scope_key 不串) | 维持 | +| 4.9 D9 内置视图不入库 | **确认**(逻辑视图) | 维持 | + +**§5 五个漏问点 —— 全部闭环(新增 2 个拍板)**: + +1. 公海/我客户互斥 —— ✅ 消号(领取即移出公海,原型 L327-347) +2. 多负责人 —— ✅ 消号(**单负责人 + 多协同人**,原型 a4-4-8 明文「每个客户只允许 1 名负责人,其余均为协同人」) +3. **客户删除入口** —— **✅ 拍板 A(一期只做归档)**:无前端删除按钮,deleted 仅系统合并置位 +4. 导入电话查重 —— ✅ 已随 D26 解决(软提示) +5. **oplog 保留时长** —— **✅ 拍板 A(无限期保留)**:审计日志不设保留期 + +**本次修正的字段口径(D19 拍板后的遗留同步)**: +- `archive_status` 收敛为 **有效1 / 已归档2** 两值(去掉「已被合并」,合并 Out-of-scope) +- 已归档 = **删除语义(软删)**,不做独立 tab/子页,列表加 `archive_status` 筛选(默认排除) +- 已同步:`crm-customer/CONTEXT.md`(L8/27-28/36/40/56)+ 票 01-answer / 02-answer + map.md D19/D9 + 各 research 子票加顶部废弃注记 + +**结论**:**全部 29 项决策 + 4 个复核翻案(D20/D26)+ 2 个新增拍板(§5.3 删除 / §5.5 oplog)已定稿**,spec 冻结,可进入实现阶段。 + +## 6. 交付状态 + +- `map.md`:D1–D29 全部记录,Frontier = 空,spec 冻结标记。 +- `issues/NN-answer.md` × 11:全部 `Status: resolved`,各⚠挂号/待确认小节已销号。 +- `issues/NN-.md` × 11:research/task 子票,全部 `Status: resolved`。 +- `crm-customer/CONTEXT.md` + `CONTEXT-MAP.md` 行:已建。 +- 未动任何 Java 源码(git status 里的 `Opportunity.java` 变更与本 effort 无关,是既有改动)。 + +**下一步**:交给新会话按第 3、4 节方法复核。若复核对 4.1–4.9 有改动,改对应决策编号 + 重开相应票(如 D20 改 → 重开票 05),而非覆盖原决策(遵循编号+修正链约定)。 diff --git a/.scratch/customer-module/issues/01-answer.md b/.scratch/customer-module/issues/01-answer.md new file mode 100644 index 0000000..f7fb2a7 --- /dev/null +++ b/.scratch/customer-module/issues/01-answer.md @@ -0,0 +1,38 @@ +## Answer + +**Status: resolved** + +### 交付物 + +1. **`crm-customer/CONTEXT.md`** 已创建(ubiquitous language,纯 glossary,对称 crm-lead/CONTEXT.md 写法)。收录 11 个核心术语,每个带 `_Avoid_` 校准: + +| 术语 | 关键点 | +|---|---| +| 客户 | 归属(owner)+档案(archive_status)正交;阶段(customer_stage)只前进 | +| 销售负责人 | `owner_user_id`,唯一持有者 | +| 销售部门 | `owner_dept_id` 快照,随负责人变更(区别于线索 dept_id 不变) | +| 协同人 | 非负责人成员,可看不可归档/合并 | +| 客户阶段 | 潜在/重潜/已成交,外部结果驱动,不可手动改 | +| 档案状态 | 有效/已归档(D19 去掉「已被合并」,合并 Out-of-scope);正交于归属;已归档即删除语义(软删) | +| 客户公海 | owner 空 + 有效的逻辑视图,无独立池实体 | +| 客户总览 | 管理视角,@DataScope 覆盖 | +| 我的客户 | 销售视角,负责人或协同人 + 有效 | +| 联系人 | 子实体,必属客户,无独立 ACL | +| 人脉关系单位 | 纯文本标签,非关联客户 | +| 客户交割 | 两段式:交接→分配,同步商机/项目负责人 | +| 超期未跟进提醒 | crm-rule 全局单例,只落待发通知 | + +2. **`CONTEXT-MAP.md`** 已加一行:`crm-customer` 客户业务。 + +3. **术语校准点**(相比票内初稿的修正): + - 明确「销售部门是**随负责人变更的快照**」——区别于线索 `dept_id` 全生命周期不变(这是本模块与线索的重要语义差异)。 + - 明确「协同人抛公海清空、交割不自动恢复」。 + - 「客户公海 = 逻辑视图」写进定义,杜绝将来误建池表。 + +### 依赖方向(定稿) + +``` +crm-opportunity → crm-customer(商机读客户/建关联) +crm-customer → crm-rule → (crm-auth / crm-dict / crm-file) +crm-customer → crm-opportunity(「关联商机」页签下游反查上游,同库单体可接受,ADR-0029 技术债) +``` diff --git a/.scratch/customer-module/issues/01-domain-context.md b/.scratch/customer-module/issues/01-domain-context.md index e6e64a3..6611f4f 100644 --- a/.scratch/customer-module/issues/01-domain-context.md +++ b/.scratch/customer-module/issues/01-domain-context.md @@ -1,7 +1,8 @@ # 01 — 客户域领域词汇 + crm-customer/CONTEXT.md 骨架 Type: task -Status: open +Status: resolved +> **⚠ 20260902 复核注记**:本文件关于「档案状态」仍写三值(有效/已归档/已被合并)。**D19 已拍板去掉「已被合并」态**(合并 Out-of-scope),`archive_status` 收敛为**有效/已归档**两值。**以 `01-answer.md` 为准。** Blocked by: ## 目标 @@ -26,4 +27,39 @@ Blocked by: ## Answer -_(待 resolve)_ +**Status: resolved** + +### 交付物 + +1. **`crm-customer/CONTEXT.md`** 已创建(ubiquitous language,纯 glossary,对称 crm-lead/CONTEXT.md 写法)。收录 11 个核心术语,每个带 `_Avoid_` 校准: + +| 术语 | 关键点 | +|---|---| +| 客户 | 归属(owner)+档案(archive_status)正交;阶段(customer_stage)只前进 | +| 销售负责人 | `owner_user_id`,唯一持有者 | +| 销售部门 | `owner_dept_id` 快照,随负责人变更(区别于线索 dept_id 不变) | +| 协同人 | 非负责人成员,可看不可归档/合并 | +| 客户阶段 | 潜在/重潜/已成交,外部结果驱动,不可手动改 | +| 档案状态 | 有效/已归档/已被合并,正交于归属 | +| 客户公海 | owner 空 + 有效的逻辑视图,无独立池实体 | +| 客户总览 | 管理视角,@DataScope 覆盖 | +| 我的客户 | 销售视角,负责人或协同人 + 有效 | +| 联系人 | 子实体,必属客户,无独立 ACL | +| 人脉关系单位 | 纯文本标签,非关联客户 | +| 客户交割 | 两段式:交接→分配,同步商机/项目负责人 | +| 超期未跟进提醒 | crm-rule 全局单例,只落待发通知 | + +2. **`CONTEXT-MAP.md`** 已加一行:`crm-customer` 客户业务。 + +3. **术语校准点**(相比票内初稿的修正): + - 明确「销售部门是**随负责人变更的快照**」——区别于线索 `dept_id` 全生命周期不变(这是本模块与线索的重要语义差异)。 + - 明确「协同人抛公海清空、交割不自动恢复」。 + - 「客户公海 = 逻辑视图」写进定义,杜绝将来误建池表。 + +### 依赖方向(定稿) + +``` +crm-opportunity → crm-customer(商机读客户/建关联) +crm-customer → crm-rule → (crm-auth / crm-dict / crm-file) +crm-customer → crm-opportunity(「关联商机」页签下游反查上游,同库单体可接受,ADR-0029 技术债) +``` diff --git a/.scratch/customer-module/issues/02-answer.md b/.scratch/customer-module/issues/02-answer.md new file mode 100644 index 0000000..951ce6b --- /dev/null +++ b/.scratch/customer-module/issues/02-answer.md @@ -0,0 +1,176 @@ + +## Answer + +**Status: resolved** + +### A. 客户主表 `customer`(`crm-customer`) + +#### A1. 字段清单 + +**系统字段(BaseEntity 继承)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint PK | 自增主键 | +| createTime | create_time | datetime | 创建时间 | +| updateTime | update_time | datetime | 最后修改时间 | +| createBy | create_by | bigint | 创建人 user_id | +| updateBy | update_by | bigint | 最后修改人 user_id | +| deleted | deleted | tinyint(1) default 0 | 逻辑删除(**一期:仅系统在合并时置 1,不开放手动删除/无前端删除入口**——§5.3 拍板 A) | + +**业务字段 — 系统生成** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerNo | customer_no | varchar(32) | 系统生成 | 唯一编号,格式 KH+yyyyMMdd+4位序;只读不可改 | + +**业务字段 — 基本信息** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerName | customer_name | varchar(100) | ✅ | 客户名称;停输防抖触发相似查重 | +| customerType | customer_type | varchar(50) | ✅ | 客户类型字典 `customer_type`;见 A3 字典节 | +| provinceCode | province_code | varchar(20) | ✅ | 省(三级地区,与商机对称)| +| cityCode | city_code | varchar(20) | ✅ | 市 | +| districtCode | district_code | varchar(20) | | 区/县(客户有三级,商机只二级) | +| industryCode | industry_code | varchar(50) | ✅ | 行业一级 code(复用 crm-dict `industry` 两级树;客户选到二级,见商机图 D1「行业字典口径」决策:商机选一级、客户选二级)| +| industryChildCode | industry_child_code | varchar(50) | | 行业二级 code(客户额外存二级;商机无此列) | +| customerStarLevel | customer_star_level | tinyint | ✅ | 客户星级 1~5(枚举,非字典;字典值「一星/二星/…/五星」=前端展示用,存数字) | +| relationStarLevel | relation_star_level | tinyint | ✅ | 关系星级 1~5(同上) | +| networkUnits | network_units | text | | 人脉关系单位,JSON 数组存标签名(单个≤50字;非关联 CRM 客户;选填) | + +> ⚠ **JPA/Hibernate 孤立单大写字母规则**(AGENTS.md):`districtCode` / `industryCode` / `industryChildCode` / `provinceCode` / `cityCode` / `customerNo` / `customerStarLevel` / `relationStarLevel` — 均为「完整词+Code/Level/No」,非孤立单大写,**不需加 `@Column(name=...)`**。 + +**业务字段 — 归属(轴1)** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| ownerUserId | owner_user_id | bigint | ✅(新增时) | 销售负责人 user_id;公海时为 NULL | +| ownerUserNameSnapshot | owner_user_name_snapshot | varchar(64) | | 负责人姓名快照(列表免 join) | +| ownerDeptId | owner_dept_id | bigint | ✅(新增时) | 销售部门 dept_id 快照;公海时保留最后部门(供公海可见性锚点,对称商机) | +| ownerDeptNameSnapshot | owner_dept_name_snapshot | varchar(64) | | 部门名快照 | + +> 协同人(团队成员)= 独立子表 `customer_team_member`(不在主表)。 + +**业务字段 — 轴2 阶段 + 档案状态** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerStage | customer_stage | tinyint default 1 | | 客户阶段:潜在1/重潜2/已成交3;只增不减;不可手动改 | +| archiveStatus | archive_status | tinyint default 1 | | 档案状态:**有效1/已归档2**(D19 拍板去掉「已被合并」,合并 Out-of-scope);正交于归属;已归档即删除语义(软删),列表加 `archive_status` 筛选默认排除 | + +**业务字段 — 快照/冗余(列表免 join)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| lastValidFollowTime | last_valid_follow_time | datetime | 最近有效跟进时间(新建时=create_time;每写一条跟进记录刷新;超期提醒计时锚点) | +| strategicAgreementLevel | strategic_agreement_level | tinyint | 战略协议等级快照(1~3级;战略协议子域划出但主表留此冗余供列表展示;NULL=未签);由战略协议子系统写入,本图只读 | +| vipCustomerLevel | vip_customer_level | tinyint | 已成交VIP客户等级快照(1~5级;NULL=无;由 A5 项目模块写入,本图只读/留 seam)| + +> ⚠ `strategicAgreementLevel` → Hibernate 命名策略**孤立单大写字母**:`Level` 前面的 `l` 是小写 → `strategicAgreement` + `Level` 整段。实测 `SpringPhysicalNamingStrategy` 对 `strategicAgreementLevel` 生成 `strategic_agreement_level` ✅(`Level` 是完整词,无问题)。但 `vipCustomerLevel` → `vip` 首字母 `v` 是小写,`C` 是孤立大写?→ 不是,`vip` 是小写词,`Customer` 是完整词,`Level` 是完整词,**不踩坑**。 + +**业务字段 — 关联信息** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| companyDecisionMaker | company_decision_maker | varchar(100) | | 公司决策人(文本) | +| isBizNegotiated | is_biz_negotiated | tinyint | ✅ | 是否商机勾兑(是1/否0) | +| directorVisitTime | director_visit_time | datetime | | 总监近期拜访时间(人工维护,日期时间)| +| isChild | is_child | tinyint default 0 | ✅ | 是否子客户(是1/否0);=1时 parent_customer_id 必填 | +| parentCustomerId | parent_customer_id | bigint | 条件必填 | 父客户 id;服务端防环校验;isChild=0时为 NULL | +| parentCustomerNameSnapshot | parent_customer_name_snapshot | varchar(100) | | 父客户名称快照 | +| cooperationSystem | cooperation_system | varchar(200) | | 合作系统(文本,选填) | +| nonCoopReason | non_coop_reason | varchar(200) | | 不合作原因(文本,选填) | +| joinedPresidentClass | joined_president_class | tinyint | | 是否参加总裁班(是1/否0/NULL=未选) | +| presidentClassPerson | president_class_person | varchar(100) | 条件必填 | 总裁班参加人(joinedPresidentClass=1时必填) | +| presidentClassPhone | president_class_phone | varchar(20) | 条件必填 | 总裁班参加人电话 | +| joinedProductClass | joined_product_class | tinyint | | 是否参加产品经理班 | +| productClassPerson | product_class_person | varchar(100) | 条件必填 | 产品班参加人 | +| productClassPhone | product_class_phone | varchar(20) | 条件必填 | 产品班参加人电话 | + +**业务字段 — 工商信息(企查查回填)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| unifiedCreditCode | unified_credit_code | varchar(18) | 统一社会信用代码;UNIQUE(NULL 除外,未填时不唯一约束);18 位格式校验 | +| legalRepresentative | legal_representative | varchar(100) | 法定代表人 | +| establishedDate | established_date | date | 成立时间(不得晚于当前日期) | +| registeredCapital | registered_capital | varchar(100) | 注册资本(文本,含单位,如「1000万元」) | +| businessScope | business_scope | text | 经营范围(不套 50 字限制) | +| staffSize | staff_size | varchar(50) | 人员规模 | +| annualRevenue | annual_revenue | varchar(50) | 年产值 | +| businessAddress | business_address | varchar(500) | 工商注册地址(不套 50 字限制) | +| bizNameFromLookup | biz_name_from_lookup | varchar(200) | 企查查确认的工商名称(可与 customer_name 不同;只存不覆盖 customer_name,除非用户确认)| + +**业务字段 — 补充资料** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| remark | remark | varchar(500) | 备注(≤500字) | + +> 附件 = 独立子表 `customer_attachment`(复用 crm-file,`biz_type=CUSTOMER`;不在主表)。 + +--- + +#### A2. 需要显式 `@Column(name=...)` 的孤立单大写字母字段 + +检查全部字段——**结论:本表无需加 `@Column(name=...)`**。所有字段均为「完整词 + 完整词」组合,无孤立单大写字母触发 Hibernate 命名策略陷阱。 + +--- + +#### A3. 字典清单(新增/复用) + +| 字典分组 code | 含义 | 状态 | 参考值 | +|---|---|---|---| +| `customer_type` | 客户类型 | 🆕 需新增 | **(grill Q8/D18 定稿,18 值)** 运营商 / 土建总包 / 机电总包 / 装修总包 / 教育局 / 投资(代建)公司 / 设计院 / 集成商 / 集成商(子客户) / 咨询造价公司 / 甲方(企业用户) / 甲方(普教) / 甲方(中高职) / 甲方(政府机关) / 竞争同行 / 个人用户 / 操手 / 招标代理 | +| `industry` | 行业(两级树) | ✅ 已有(商机图票09已建;9一级+51二级) | 复用 | +| `customer_star_level` | 客户星级 | **硬编码枚举 1~5(D4-rev1 定稿,不走字典)** | 1~5 星 | +| `relation_star_level` | 关系星级 | **硬编码枚举 1~5(D4-rev1 定稿,不走字典)** | 一星~五星 | + +> ✅ 已定(grill Q8/D18):`customer_type` = crm-dict 字典分组,18 值(见上表)。含 4 甲方值 + 14 非甲方角色值,是通用角色字典。 + +--- + +#### A4. DataScope 注解 + +```java +@DataScope(module = "customer", ownerColumn = "owner_user_id", deptColumn = "owner_dept_id") +``` + +复用 crm-auth `DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)`,不新建可见性机制。 + +--- + +#### A5. 索引 + +| 索引名 | 列 | 说明 | +|---|---|---| +| `uk_unified_credit_code` | `unified_credit_code` | 信用代码全局唯一(NULL 不参与唯一约束,MySQL 默认行为)| +| `idx_owner_stage` | `owner_user_id, customer_stage, deleted` | 我的客户视图常用过滤 | +| `idx_dept_stage` | `owner_dept_id, archive_status, deleted` | 公海/总览部门维度 | +| `idx_name` | `customer_name` | 相似查重防抖查询 | +| `idx_parent` | `parent_customer_id` | 父子客户查询/防环 | +| `idx_last_follow` | `last_valid_follow_time, owner_user_id` | 超期未跟进提醒扫表 | + +--- + +#### A6. 产品确认项(⚠ P0)— 已随 grill 全部销号 + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | ~~`customer_type` 完整枚举值~~ **已拍板(grill Q8/D18)**:crm-dict 字典,18 值 | 字典种子数据 | +| P0-2 | ~~客户选行业到几级~~ **定稿:二级**(`industry_code` + `industry_child_code` 两列;与商机图 D1 一致) | 两列 | +| P0-3 | ~~星级枚举 vs 字典~~ **已拍板(D4-rev1)**:硬编码枚举 1~5,不走字典 | 存储类型 | +| P1-1 | `vip_customer_level` 等级范围(原型列表列有「已成交VIP客户等级」但详情字段写「1~5级」,待确认范围及触发条件,本图只留 seam) | — | +| P1-2 | `strategic_agreement_level` 战略协议等级范围(原型见「一级/二级/三级」=1~3;战略协议子域划出,主表只存冗余快照,待战略协议 effort 回填写逻辑) | — | + +--- + +#### A7. 回填 ticket 02 的下游影响 + +- **票 03**(三视图):`archiveStatus` 过滤(**有效1/已归档2**,无已被合并——D19),`ownerUserId IS NULL` = 公海视图;`last_valid_follow_time` 排序。 +- **票 04**(查重):`unified_credit_code` UNIQUE 索引 + `customer_name` idx 是查重的存储底座。 +- **票 05**(阶段):`customer_stage` 默认1,只能 UPDATE SET customer_stage=? WHERE id=? AND customer_stage < ?(只前进)。 +- **票 07**(oplog):`last_valid_follow_time` 新建时初始化为 `create_time`。 +- **票 08**(交割):`ownerUserId` + `ownerDeptId` 是交割换主的操作列。 +- **票 10**(超期提醒):`last_valid_follow_time` 是计时锚点;新建时初始化为 `create_time`(grill 挂号确认,见 map.md)。 diff --git a/.scratch/customer-module/issues/02-customer-main-table.md b/.scratch/customer-module/issues/02-customer-main-table.md index b71cd5f..e496352 100644 --- a/.scratch/customer-module/issues/02-customer-main-table.md +++ b/.scratch/customer-module/issues/02-customer-main-table.md @@ -1,7 +1,8 @@ # 02 — 客户主表字段清单 Type: research -Status: open +Status: resolved +> **⚠ 20260902 复核注记**:本文件 `archiveStatus` 仍写三值(有效1/已归档2/已被合并3)。**D19 已拍板去掉「已被合并」**(合并 Out-of-scope),收敛为**有效1/已归档2**。**以 `02-answer.md` 为准。** Blocked by: ## 问题 @@ -39,4 +40,178 @@ Blocked by: ## Answer -_(待 resolve)_ +**Status: resolved**(P0 已全部销号:customer_type=18值字典/D18、行业二级、星级硬编码/D4-rev1;含 grill Q8/Q15 回填) + +**Status: resolved** + +### A. 客户主表 `customer`(`crm-customer`) + +#### A1. 字段清单 + +**系统字段(BaseEntity 继承)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint PK | 自增主键 | +| createTime | create_time | datetime | 创建时间 | +| updateTime | update_time | datetime | 最后修改时间 | +| createBy | create_by | bigint | 创建人 user_id | +| updateBy | update_by | bigint | 最后修改人 user_id | +| deleted | deleted | tinyint(1) default 0 | 逻辑删除 | + +**业务字段 — 系统生成** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerNo | customer_no | varchar(32) | 系统生成 | 唯一编号,格式 KH+yyyyMMdd+4位序;只读不可改 | + +**业务字段 — 基本信息** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerName | customer_name | varchar(100) | ✅ | 客户名称;停输防抖触发相似查重 | +| customerType | customer_type | varchar(50) | ✅ | 客户类型字典 `customer_type`;见 A3 字典节 | +| provinceCode | province_code | varchar(20) | ✅ | 省(三级地区,与商机对称)| +| cityCode | city_code | varchar(20) | ✅ | 市 | +| districtCode | district_code | varchar(20) | | 区/县(客户有三级,商机只二级) | +| industryCode | industry_code | varchar(50) | ✅ | 行业一级 code(复用 crm-dict `industry` 两级树;客户选到二级,见商机图 D1「行业字典口径」决策:商机选一级、客户选二级)| +| industryChildCode | industry_child_code | varchar(50) | | 行业二级 code(客户额外存二级;商机无此列) | +| customerStarLevel | customer_star_level | tinyint | ✅ | 客户星级 1~5(枚举,非字典;字典值「一星/二星/…/五星」=前端展示用,存数字) | +| relationStarLevel | relation_star_level | tinyint | ✅ | 关系星级 1~5(同上) | +| networkUnits | network_units | text | | 人脉关系单位,JSON 数组存标签名(单个≤50字;非关联 CRM 客户;选填) | + +> ⚠ **JPA/Hibernate 孤立单大写字母规则**(AGENTS.md):`districtCode` / `industryCode` / `industryChildCode` / `provinceCode` / `cityCode` / `customerNo` / `customerStarLevel` / `relationStarLevel` — 均为「完整词+Code/Level/No」,非孤立单大写,**不需加 `@Column(name=...)`**。 + +**业务字段 — 归属(轴1)** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| ownerUserId | owner_user_id | bigint | ✅(新增时) | 销售负责人 user_id;公海时为 NULL | +| ownerUserNameSnapshot | owner_user_name_snapshot | varchar(64) | | 负责人姓名快照(列表免 join) | +| ownerDeptId | owner_dept_id | bigint | ✅(新增时) | 销售部门 dept_id 快照;公海时保留最后部门(供公海可见性锚点,对称商机) | +| ownerDeptNameSnapshot | owner_dept_name_snapshot | varchar(64) | | 部门名快照 | + +> 协同人(团队成员)= 独立子表 `customer_team_member`(不在主表)。 + +**业务字段 — 轴2 阶段 + 档案状态** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| customerStage | customer_stage | tinyint default 1 | | 客户阶段:潜在1/重潜2/已成交3;只增不减;不可手动改 | +| archiveStatus | archive_status | tinyint default 1 | | 档案状态:有效1/已归档2/已被合并3;正交于归属 | + +**业务字段 — 快照/冗余(列表免 join)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| lastValidFollowTime | last_valid_follow_time | datetime | 最近有效跟进时间;**超期提醒计时锚点 = 成为「有主」的时刻**(新建即指派=create_time;公海领取时=领取时刻;每写一条跟进记录刷新);公海无负责人=NULL不提醒 | +| strategicAgreementLevel | strategic_agreement_level | tinyint | 战略协议等级快照(1~3级;战略协议子域划出但主表留此冗余供列表展示;NULL=未签);由战略协议子系统写入,本图只读 | +| vipCustomerLevel | vip_customer_level | tinyint | 已成交VIP客户等级快照(1~5级;NULL=无;由 A5 项目模块写入,本图只读/留 seam)| + +> ⚠ `strategicAgreementLevel` → Hibernate 命名策略**孤立单大写字母**:`Level` 前面的 `l` 是小写 → `strategicAgreement` + `Level` 整段。实测 `SpringPhysicalNamingStrategy` 对 `strategicAgreementLevel` 生成 `strategic_agreement_level` ✅(`Level` 是完整词,无问题)。但 `vipCustomerLevel` → `vip` 首字母 `v` 是小写,`C` 是孤立大写?→ 不是,`vip` 是小写词,`Customer` 是完整词,`Level` 是完整词,**不踩坑**。 + +**业务字段 — 关联信息** + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| companyDecisionMaker | company_decision_maker | varchar(100) | | 公司决策人(文本) | +| isBizNegotiated | is_biz_negotiated | tinyint | ✅ | 是否商机勾兑(是1/否0) | +| directorVisitTime | director_visit_time | datetime | | 总监近期拜访时间(人工维护,日期时间)| +| isChild | is_child | tinyint default 0 | ✅ | 是否子客户(是1/否0);=1时 parent_customer_id 必填 | +| parentCustomerId | parent_customer_id | bigint | 条件必填 | 父客户 id;服务端防环校验;isChild=0时为 NULL | +| parentCustomerNameSnapshot | parent_customer_name_snapshot | varchar(100) | | 父客户名称快照 | +| cooperationSystem | cooperation_system | varchar(200) | | 合作系统(文本,选填) | +| nonCoopReason | non_coop_reason | varchar(200) | | 不合作原因(文本,选填) | +| joinedPresidentClass | joined_president_class | tinyint | | 是否参加总裁班(是1/否0/NULL=未选) | +| presidentClassPerson | president_class_person | varchar(100) | 条件必填 | 总裁班参加人(joinedPresidentClass=1时必填) | +| presidentClassPhone | president_class_phone | varchar(20) | 条件必填 | 总裁班参加人电话 | +| joinedProductClass | joined_product_class | tinyint | | 是否参加产品经理班 | +| productClassPerson | product_class_person | varchar(100) | 条件必填 | 产品班参加人 | +| productClassPhone | product_class_phone | varchar(20) | 条件必填 | 产品班参加人电话 | + +**业务字段 — 工商信息(企查查回填)** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| unifiedCreditCode | unified_credit_code | varchar(18) | 统一社会信用代码;UNIQUE(NULL 除外,未填时不唯一约束);18 位格式校验 | +| legalRepresentative | legal_representative | varchar(100) | 法定代表人 | +| establishedDate | established_date | date | 成立时间(不得晚于当前日期) | +| registeredCapital | registered_capital | varchar(100) | 注册资本(文本,含单位,如「1000万元」) | +| businessScope | business_scope | text | 经营范围(不套 50 字限制) | +| staffSize | staff_size | varchar(50) | 人员规模 | +| annualRevenue | annual_revenue | varchar(50) | 年产值 | +| businessAddress | business_address | varchar(500) | 工商注册地址(不套 50 字限制) | +| bizNameFromLookup | biz_name_from_lookup | varchar(200) | 企查查确认的工商名称(可与 customer_name 不同;只存不覆盖 customer_name,除非用户确认)| + +**业务字段 — 补充资料** + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| remark | remark | varchar(500) | 备注(≤500字) | + +> 附件 = 独立子表 `customer_attachment`(复用 crm-file,`biz_type=CUSTOMER`;不在主表)。 + +--- + +#### A2. 需要显式 `@Column(name=...)` 的孤立单大写字母字段 + +检查全部字段——**结论:本表无需加 `@Column(name=...)`**。所有字段均为「完整词 + 完整词」组合,无孤立单大写字母触发 Hibernate 命名策略陷阱。 + +--- + +#### A3. 字典清单(新增/复用) + +| 字典分组 code | 含义 | 状态 | 参考值 | +|---|---|---|---| +| `customer_type` | 客户类型 | 🆕 需新增 | **(grill Q8/D18 定稿,18 值)** 运营商 / 土建总包 / 机电总包 / 装修总包 / 教育局 / 投资(代建)公司 / 设计院 / 集成商 / 集成商(子客户) / 咨询造价公司 / 甲方(企业用户) / 甲方(普教) / 甲方(中高职) / 甲方(政府机关) / 竞争同行 / 个人用户 / 操手 / 招标代理 | +| `industry` | 行业(两级树) | ✅ 已有(商机图票09已建;9一级+51二级) | 复用 | +| `customer_star_level` | 客户星级 | **硬编码枚举 1~5(D4-rev1 定稿,不走字典)** | 1~5 星 | +| `relation_star_level` | 关系星级 | **硬编码枚举 1~5(D4-rev1 定稿,不走字典)** | 一星~五星 | + +> ✅ 已定(grill Q8/D18):`customer_type` = crm-dict 字典分组,18 值(见上表)。注意含 4 个甲方值 + 14 个非甲方角色值,是通用角色字典,非仅甲方。 + +--- + +#### A4. DataScope 注解 + +```java +@DataScope(module = "customer", ownerColumn = "owner_user_id", deptColumn = "owner_dept_id") +``` + +复用 crm-auth `DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)`,不新建可见性机制。 + +--- + +#### A5. 索引 + +| 索引名 | 列 | 说明 | +|---|---|---| +| `uk_unified_credit_code` | `unified_credit_code` | 信用代码全局唯一(NULL 不参与唯一约束,MySQL 默认行为)| +| `idx_owner_stage` | `owner_user_id, customer_stage, deleted` | 我的客户视图常用过滤 | +| `idx_dept_stage` | `owner_dept_id, archive_status, deleted` | 公海/总览部门维度 | +| `idx_name` | `customer_name` | 相似查重防抖查询 | +| `idx_parent` | `parent_customer_id` | 父子客户查询/防环 | +| `idx_last_follow` | `last_valid_follow_time, owner_user_id` | 超期未跟进提醒扫表 | + +--- + +#### A6. 待产品确认(⚠ P0) + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | ~~`customer_type` 完整枚举值~~ **已拍板(grill Q8/D18)**:crm-dict 字典,18 值 | 字典种子数据 | +| P0-2 | ~~客户选行业到几级~~ **定稿:二级**(`industry_code` + `industry_child_code` 两列;与商机图 D1「商机选一级、客户选二级」一致) | 两列 | +| P0-3 | ~~星级枚举 vs 字典~~ **已拍板(D4-rev1)**:硬编码枚举 1~5,不走字典 | 存储类型 | +| P1-1 | `vip_customer_level` 等级范围(原型列表列有「已成交VIP客户等级」但详情字段写「1~5级」,待确认范围及触发条件,本图只留 seam) | — | +| P1-2 | `strategic_agreement_level` 战略协议等级范围(原型见「一级/二级/三级」=1~3;战略协议子域划出,主表只存冗余快照,待战略协议 effort 回填写逻辑) | — | + +--- + +#### A7. 回填 ticket 02 的下游影响 + +- **票 03**(三视图):`archiveStatus` 过滤(有效1/已归档2,归档=删除见 D19),`ownerUserId IS NULL` = 公海视图;`last_valid_follow_time` 排序。 +- **票 04**(查重):`unified_credit_code` UNIQUE 索引 + `customer_name` idx 是查重的存储底座。 +- **票 05**(阶段):`customer_stage` 默认1,只能 UPDATE SET customer_stage=? WHERE id=? AND customer_stage < ?(只前进)。 +- **票 07**(oplog):`last_valid_follow_time` 新建即指派=create_time;公海领取时赋领取时刻;跟进刷新。 +- **票 08**(交割):`ownerUserId` + `ownerDeptId` 是交割换主的操作列。 +- **票 10**(超期提醒):`last_valid_follow_time` 是计时锚点 = **成为有主时刻**(grill Q15 拍板 A):新建即指派=create_time;公海领取=领取时刻;跟进刷新。公海(NULL)不计时。 diff --git a/.scratch/customer-module/issues/03-answer.md b/.scratch/customer-module/issues/03-answer.md new file mode 100644 index 0000000..6b6a6cf --- /dev/null +++ b/.scratch/customer-module/issues/03-answer.md @@ -0,0 +1,94 @@ + +## Answer + +**Status: resolved**(含 grill Q9 归档=删除 + Q15 领取赋锚点回填) + +### A. 三个 workspace 数据范围(定稿) + +| workspace | 数据范围 | 附加状态页 | +|---|---|---| +| **我的客户** | 当前用户为**销售负责人或协同人** 且 `archive_status=有效(1)` 且 **非公海**(owner 非空) | **无独立归档子页**(归档=删除,见 grill Q9/D19;列表加 `archive_status` 筛选默认排除) | +| **客户总览** | `@DataScope` 覆盖组织范围的 **未删除、未进入公海** 客户(默认只列 `archive_status=1`) | **无独立归档 tab**(归档=删除,加筛选条件默认排除,可勾选查询) | +| **客户公海** | `archive_status=有效(1)` **且 `owner_user_id IS NULL`** 逻辑视图 | 无 | + +- **公海不是独立实体**,无池表;对称商机公海 = 逻辑视图(chart D3)。 +- **管理职务不扩大「我的客户」数据范围**(原型 2.1 §6 明文);管理视角走「客户总览」。 +- **协同人视角**:协同人可见客户(进「我的客户」),但**不得归档/合并**(仅负责人可,原型 2.2 §4)。 + +### B. 内置视图 / 常用检索(硬编码,不入库) + +**我的客户** 内置:`我负责的客户`(owner=我) / `我协同的客户`(团队成员表含我) / `待跟进的客户`(有下次跟进时间且 ≤ now) / `我关注的客户`(关注表) / `最近访问`(view_log 倒序)。 + +**客户公海** 内置:`最近访问` / `我关注的客户`(原型 A4-1-1 检索列表只见这两项 + 自定义)。 + +**客户总览** 内置:`最近访问` / `我关注的客户` / `我负责的客户` / `我协同的客户`(原型 A4-2-1 有「我负责的/我协同的」身份筛选)。 + +> 自定义视图 = crm-preference 平台能力,票 11 接入(三个 scope_key)。内置视图**不落库**,口径硬编码(对称商机票 14 拍板)。 + +### C. 三视图模式(定稿) + +| workspace | 视图模式 | +|---|---| +| 我的客户 | 表格 / 列表详情(左表+右详情) / **看板** | +| 客户总览 | 表格 / 列表详情 / **看板** | +| 客户公海 | 表格 / 分屏(左表+右详情) | + +- **看板视图分组页签 = 三个**:`客户阶段` / `客户星级` / `关系星级`(原型 A4-3-1 §1.3 明文「看板视图包含客户阶段、客户星级、关系星级三个分组页签」;A4-2-1 卡片视图顶部即「潜在客户(3)/重潜客户(3)/已成交客户(3)」)。 + - ⚠ **本期注记(D20 改票 B)**:客户阶段本期实际只有「潜在」(重潜/已成交触发链路 seam 不可达),故本期看板「客户阶段」分组下客户均堆在「潜在客户」列,重潜/已成交列恒为空。**这是预期行为、非 bug**:看板仍按 `customer_stage` 正常分组,三列骨架照旧渲染,链路就绪后自然填充;不得为填列而造假阶段数据。 +- **视图切换记住用户偏好**(复用 crm-preference 列偏好能力;对称商机票 14);首次默认表格视图。 +- 勾选跨分页保留;全选仅当前页/当前批次;批量操作按已勾选 ID 集合执行。 + +### D. 共用列(定稿,三 workspace 同款列基础 + 差异列) + +**共用列**:客户名称(固定列不可隐藏) / 客户编号 / 客户类型 / 所属地区 / 行业 / 客户阶段 / 客户星级 / 关系星级 / 战略协议等级(快照) / 已成交VIP客户等级(快照) / 关联业务(商机/项目计数) / 最近跟进时间。 + +**差异列**: +- 我的客户 / 客户总览:+ 销售负责人 / 销售部门。 +- 客户公海:+ **进入公海时间**(`enter_pool_time`,见票 03 回填) / 无销售负责人列(owner 空)。 + +**字段设置** = crm-preference 列偏好(列显隐/排序,用户级);客户名称为固定列不参与设置。 + +### E. 归属迁移动作枚举(定稿) + +| 动作 | 语义 | 前置校验 | 触发方 | +|---|---|---|---| +| 领取 | 公海→个人(owner 空→当前用户) | 客户仍在公海 + 无进行中商机/项目 | 普通销售 | +| 分配 | 公海→个人 / 个人→个人(换 owner) | 目标人员启用 + 有权分配范围 + 非原负责人 | 领导/管理 | +| 抛公海 | 个人→公海(清 owner+协同人) | **无进行中商机/项目**(原型 A4-1-1 §3 明文)+ 有抛公海权限 | 领导/管理 | +| 归档 | 有效→已归档(archive_status 1→2) | 仅销售负责人可;**无进行中商机/项目**(原型 A4-3-1 归档弹窗明说) | 销售/领导 | +| 恢复 | 已归档→有效(2→1) | 仅销售负责人可 | 销售/领导 | +| 交割 | 换 owner(跨模块同步商机/项目负责人) | 票 08 专管 | 销售本人 | + +- 每边**写操作日志**(票 07 oplog);CAS 乐观锁 + 版本号。 +- **批量操作部分失败不回滚**(按客户逐条独立事务,跳过已不可操作项,Toast 报成功/跳过数)——**例外:批量归档整批预检**,任一失败整批不执行(原型 A4-3-1 §3.6 明文)。 +- **「进行中商机/项目」阻止抛公海/归档**:商机侧读 crm-opportunity(状态=推进中 即 opp_status=2);**项目侧留 seam**(A5 未建,先跳过项目那半,等 A5 建好补校验)。 +- **领取/分配 后端副作用(D25,票10 锚点)**:从公海领取或分配到一个负责人时,若 `last_valid_follow_time` 为空(客户此前无主/无锚点)→ 赋当前时刻(超期提醒开始计时)。抛公海/归属迁移不动锚点(仍需有主才计时)。 + +### F. 新增字段(回填票 02) + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| enterPoolTime | enter_pool_time | datetime | 进入公海时间(公海列表列;抛公海时写入,领取/分配时清空) | + +> `enterPoolTime` 无孤立单大写字母陷阱,无需 `@Column(name=...)`。 + +### G. 领导视角 DataScope 映射(对称 crm-auth) + +- 客户总览可见性 = `@DataScope(module="customer")` 套 `DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)`,按用户角色配置决定(ADR-0018 按模块可配)。 +- 普通销售**无客户总览菜单权限**时不可见;总监=DEPT_AND_CHILD 或 ALL(按后台配置)。 +- 公海:普通销售看「可领取」= 其部门集合可见范围内 owner 空的客户;领导看「可分配」= 其管理范围。 + +### H. 产品确认项 — 已随 grill 全部销号 + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | ~~已归档/已被合并 tab 形态~~ **已拍板(grill Q9/D19)**:去掉「已被合并」;**已归档 = 删除**,不做独立 tab/子页,列表加 `archive_status` 筛选条件(默认排除已归档,勾选可查) | 列表查询参数 | +| P1-1 | ~~进入公海时间排序~~ **已拍板(grill Q19)**:保留列 + 加入排序字段池(升降序) | 排序字段池(票 11) | + +### I. 回填 / 下游影响 + +- **票 02**:新增 `enter_pool_time` 字段(本票 F)。 +- **票 08**(交割):owner 换主 = 本票「分配」动作的扩展版(跨模块同步),交割事务复用 owner 变更 oplog 语义。 +- **票 11**(saved-view):三 workspace 各自 scope_key,字段池白名单 = 本票 D 共用列 + 差异列。 +- **票 07**(oplog):归属迁移(领取/分配/抛公海/归档/恢复/交割)都写 oplog。 +- **票 01**(CONTEXT):术语「客户公海」「销售负责人」「协同人」「归档」「已合并」。 diff --git a/.scratch/customer-module/issues/03-ownership-views.md b/.scratch/customer-module/issues/03-ownership-views.md index c979bff..b74a4ce 100644 --- a/.scratch/customer-module/issues/03-ownership-views.md +++ b/.scratch/customer-module/issues/03-ownership-views.md @@ -1,7 +1,8 @@ # 03 — 归属流转 + 三 workspace + 内置视图 + 自定义视图查询口径 Type: research -Status: open +Status: resolved +> **⚠ 20260902 复核注记**:本文件关于「已归档客户/已被合并客户」独立 tab/子页的旧口径**已被推翻**——D19 拍板去掉「已被合并」态、**已归档=删除语义、不做独立 tab**,只加 `archive_status` 筛选条件(默认排除已归档)。**三值归档态/独立子页方案作废,以 `03-answer.md` 为准。** Blocked by: 02 ## 问题 @@ -38,4 +39,92 @@ Blocked by: 02 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 三个 workspace 数据范围(定稿) + +| workspace | 数据范围 | 附加状态页 | +|---|---|---| +| **我的客户** | 当前用户为**销售负责人或协同人** 且 `archive_status=有效(1)` 且 **非公海**(owner 非空) | 子页「已归档客户」= owner 当前用户 且 archive_status=2;「已被合并客户」= owner 当前用户 且 archive_status=3 | +| **客户总览** | `@DataScope` 覆盖组织范围的 **未删除、未进入公海** 客户(含有效/已归档/已被合并三态,但**默认只列有效**,归档/合并走独立 tab) | 顶部 tab「已归档客户」「已被合并客户」 | +| **客户公海** | `archive_status=有效(1)` **且 `owner_user_id IS NULL`** 逻辑视图 | 无 | + +- **公海不是独立实体**,无池表;对称商机公海 = 逻辑视图(chart D3)。 +- **管理职务不扩大「我的客户」数据范围**(原型 2.1 §6 明文);管理视角走「客户总览」。 +- **协同人视角**:协同人可见客户(进「我的客户」),但**不得归档/合并**(仅负责人可,原型 2.2 §4)。 + +### B. 内置视图 / 常用检索(硬编码,不入库) + +**我的客户** 内置:`我负责的客户`(owner=我) / `我协同的客户`(团队成员表含我) / `待跟进的客户`(有下次跟进时间且 ≤ now) / `我关注的客户`(关注表) / `最近访问`(view_log 倒序)。 + +**客户公海** 内置:`最近访问` / `我关注的客户`(原型 A4-1-1 检索列表只见这两项 + 自定义)。 + +**客户总览** 内置:`最近访问` / `我关注的客户` / `我负责的客户` / `我协同的客户`(原型 A4-2-1 有「我负责的/我协同的」身份筛选)。 + +> 自定义视图 = crm-preference 平台能力,票 11 接入(三个 scope_key)。内置视图**不落库**,口径硬编码(对称商机票 14 拍板)。 + +### C. 三视图模式(定稿) + +| workspace | 视图模式 | +|---|---| +| 我的客户 | 表格 / 列表详情(左表+右详情) / **看板** | +| 客户总览 | 表格 / 列表详情 / **看板** | +| 客户公海 | 表格 / 分屏(左表+右详情) | + +- **看板视图分组页签 = 三个**:`客户阶段` / `客户星级` / `关系星级`(原型 A4-3-1 §1.3 明文「看板视图包含客户阶段、客户星级、关系星级三个分组页签」;A4-2-1 卡片视图顶部即「潜在客户(3)/重潜客户(3)/已成交客户(3)」)。 +- **视图切换记住用户偏好**(复用 crm-preference 列偏好能力;对称商机票 14);首次默认表格视图。 +- 勾选跨分页保留;全选仅当前页/当前批次;批量操作按已勾选 ID 集合执行。 + +### D. 共用列(定稿,三 workspace 同款列基础 + 差异列) + +**共用列**:客户名称(固定列不可隐藏) / 客户编号 / 客户类型 / 所属地区 / 行业 / 客户阶段 / 客户星级 / 关系星级 / 战略协议等级(快照) / 已成交VIP客户等级(快照) / 关联业务(商机/项目计数) / 最近跟进时间。 + +**差异列**: +- 我的客户 / 客户总览:+ 销售负责人 / 销售部门。 +- 客户公海:+ **进入公海时间**(`enter_pool_time`,见票 03 回填) / 无销售负责人列(owner 空)。 + +**字段设置** = crm-preference 列偏好(列显隐/排序,用户级);客户名称为固定列不参与设置。 + +### E. 归属迁移动作枚举(定稿) + +| 动作 | 语义 | 前置校验 | 触发方 | +|---|---|---|---| +| 领取 | 公海→个人(owner 空→当前用户) | 客户仍在公海 + 无进行中商机/项目 | 普通销售 | +| 分配 | 公海→个人 / 个人→个人(换 owner) | 目标人员启用 + 有权分配范围 + 非原负责人 | 领导/管理 | +| 抛公海 | 个人→公海(清 owner+协同人) | **无进行中商机/项目**(原型 A4-1-1 §3 明文)+ 有抛公海权限 | 领导/管理 | +| 归档 | 有效→已归档(archive_status 1→2) | 仅销售负责人可;**无进行中商机/项目**(原型 A4-3-1 归档弹窗明说) | 销售/领导 | +| 恢复 | 已归档→有效(2→1) | 仅销售负责人可 | 销售/领导 | +| 交割 | 换 owner(跨模块同步商机/项目负责人) | 票 08 专管 | 销售本人 | + +- 每边**写操作日志**(票 07 oplog);CAS 乐观锁 + 版本号。 +- **批量操作部分失败不回滚**(按客户逐条独立事务,跳过已不可操作项,Toast 报成功/跳过数)——**例外:批量归档整批预检**,任一失败整批不执行(原型 A4-3-1 §3.6 明文)。 +- **「进行中商机/项目」阻止抛公海/归档**:商机侧读 crm-opportunity(状态=推进中 即 opp_status=2);**项目侧留 seam**(A5 未建,先跳过项目那半,等 A5 建好补校验)。 + +### F. 新增字段(回填票 02) + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| enterPoolTime | enter_pool_time | datetime | 进入公海时间(公海列表列;抛公海时写入,领取/分配时清空) | + +> `enterPoolTime` 无孤立单大写字母陷阱,无需 `@Column(name=...)`。 + +### G. 领导视角 DataScope 映射(对称 crm-auth) + +- 客户总览可见性 = `@DataScope(module="customer")` 套 `DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)`,按用户角色配置决定(ADR-0018 按模块可配)。 +- 普通销售**无客户总览菜单权限**时不可见;总监=DEPT_AND_CHILD 或 ALL(按后台配置)。 +- 公海:普通销售看「可领取」= 其部门集合可见范围内 owner 空的客户;领导看「可分配」= 其管理范围。 + +### H. 待产品确认(⚠) + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | 客户总览「已归档客户/已被合并客户」是**独立 tab** 还是**筛选状态**?原型 A4-2-1 顶部有「已归档客户/已被合并客户」两个 tab 且 A4-3-1 也把它们当独立子页——**推荐独立子页**(archive_status 过滤),归档/合并态不进主列表 | 列表查询参数 | +| P1-1 | 公海列表是否需要「进入公海时间」排序?原型列为固定列 → 推荐保留 | 排序字段池(票 11) | + +### I. 回填 / 下游影响 + +- **票 02**:新增 `enter_pool_time` 字段(本票 F)。 +- **票 08**(交割):owner 换主 = 本票「分配」动作的扩展版(跨模块同步),交割事务复用 owner 变更 oplog 语义。 +- **票 11**(saved-view):三 workspace 各自 scope_key,字段池白名单 = 本票 D 共用列 + 差异列。 +- **票 07**(oplog):归属迁移(领取/分配/抛公海/归档/恢复/交割)都写 oplog。 +- **票 01**(CONTEXT):术语「客户公海」「销售负责人」「协同人」「归档」「已合并」。 diff --git a/.scratch/customer-module/issues/04-answer.md b/.scratch/customer-module/issues/04-answer.md new file mode 100644 index 0000000..0bfe7fc --- /dev/null +++ b/.scratch/customer-module/issues/04-answer.md @@ -0,0 +1,87 @@ +## Answer + +**Status: resolved** + +### A. 范围(定稿) + +- ✅ **做**:新增/编辑客户时的**内建查重校验** + **企查查外部服务契约 seam**。 +- ❌ **不做**:A4-6 独立查重页 + 客户合并(chart D6);「客户查重设置」可配规则(chart D4);企查查服务内部实现(只定契约)。 + +### B. 查重校验(内建,写死在新增/编辑流程) + +#### B1. 三层查重(定稿) + +| 层 | 时机 | 规则 | 结果 | +|---|---|---|---| +| L1 自动相似提示 | 客户名称**停输防抖**(~300ms) | 名称模糊匹配全部未删除客户 | 只提示不阻止;无权命中的脱敏为「存在匹配记录」 | +| L2 保存时精确查重 | 点击确认提交 | ① 信用代码全局唯一(硬拦)② 名称相似(弹窗确认) | 信用代码同 → 禁止;名称似 → 弹窗「仍要创建/返回修改」 | +| L3 终查(并发兜底) | 「仍要创建」后、写库前 | 再查一次信用代码 + 名称 | 仍无冲突才写库;DB UNIQUE 兜底 | + +#### B2. 相似判定算法 — 已拍板(grill Q11,seam 化) + +原型明说「使用 CRM 后端统一查重服务,不在前端固化阈值」。**我的推荐(最简可行)**:用 **MySQL 内置 `FULLTEXT` 索引 + `MATCH ... AGAINST` 自然语言模式**,或更简单先上 **`LIKE '%name%'` + 编辑距离(Levenshtein) 后置过滤**。 + +- **✅ 已拍板(grill Q11,用户拍板 = seam 化)**:相似度判定**抽成接口/端口**(可替换策略,如 `CustomerDedupService` 内部策略接口或独立 `CustomerNameSimilarityPort`),一期只做**最简实现**,后续迭代可换算法。 +- **一期最简实现**:`customer_name` 建 FULLTEXT 索引(ngram parser, token_size=2),`MATCH(customer_name) AGAINST(?)` 取 topN 候选,再算相似度判定「相似」;信用代码精确相等判定「重复」。**阈值从「客户查重设置」配置读取(70/80/90%)**,不再写死。 +- **契约稳定、实现可插拔**:调用方不关心具体算法;后续可换 Levenshtein / Jaccard / 外部相似度服务,只替换端口实现。 +- **统一查重服务**:抽 `CustomerDedupService`(crm-customer 内),后续 A4-6 若重启可复用;本期只被新增/编辑流程调用。 + +#### B3. 信用代码校验(定稿) + +- 格式:`^[0-9A-Z]{18}$`(18 位数字或大写字母,原型「18 位数字或字母组合」)。 +- 唯一:`uk_unified_credit_code` 唯一索引(票 02 已建),**NULL 不参与唯一约束**(未填时允许)。 +- 精确相等 → 硬拦禁止创建;命中无权查看的客户只提示「系统已存在该企业」,不显 name/编号/负责人。 + +#### B4. 联系人内联查重(定稿,回填票 06;D26 翻案后校准 20260902) + +- 同一客户内同表格**电话相同两行** → 禁止提交并定位重复行(表单级硬校验)。 +- 无电话 → 按「客户 + 姓名 + 职务」提示疑似重复(只提示)。 +- ~~全库按电话精确查重 → 相同禁止新建~~ **已翻案(D26 用户拍板 A,20260902)**:跨客户电话命中=**软提示疑似重复、不阻断**,受 crm-rule 客户查重设置「联系电话」开关控制(默认开、可关)。不建电话唯一索引。 + +### C. 企查查外部服务契约 seam(定稿) + +#### C1. 端口(不做实现,留 mock) + +```java +// crm-customer 侧出站端口 +public interface CompanyLookupPort { + /** 按企业名查询工商候选列表 */ + List lookup(String companyName); +} + +record CompanyCandidate( + String bizName, String unifiedCreditCode, String legalRepresentative, + LocalDate establishedDate, String registeredCapital, String businessScope, + String staffSize, String annualRevenue, String address +) {} +``` + +#### C2. 回填范围(定稿,原型 §3.3 明文) + +- **允许回填字段(9 项)**:工商名称 / 统一社会信用代码 / 法定代表人 / 成立时间 / 注册资本 / 经营范围 / 人员规模 / 年产值 / 工商注册地址。 +- **只回填当前为空的字段**(首次选择);已填字段再次选择必须先弹「受影响字段确认」才覆盖;服务返回空值不覆盖原值。 +- **永不覆盖**:客户类型 / 行业 / 所属地区 / 关系星级 / 客户星级 / 销售负责人 / 销售部门 / 协同人 / 人脉关系单位 / 联系人 / 备注。 +- 回填后**立即用信用代码执行精确查重**(B1 L2 联动)。 + +#### C3. 异常语义(定稿) + +| 场景 | 行为 | +|---|---| +| 无结果 | 保留表单,提示「未查询到匹配企业」,可手工填写 | +| 服务失败/超时 | 保留表单,提示重试或手工填写;**不阻断手工提交** | +| 请求乱序 | 只处理最后一次有效请求,旧结果不覆盖当前表单 | + +### D. 编辑客户(定稿,对称新增) + +- 编辑页复用新增表单结构 + 查重校验;编辑时**排除自身**出查重范围(改名字不和自己撞)。 +- 历史停用枚举可回显不可重选(原型公共规则)。 + +### E. 回填 / 下游 + +- **票 02**:`unified_credit_code` 唯一索引 + `customer_name` FULLTEXT(ngram) 索引(A5 索引表补一条 `idx_name_fulltext`)。 +- **票 06**:联系人电话查重(B4)。 +- **票 09**(导入):复用 `CustomerDedupService` 做导入查重。 + +### F. 挂号 — 已随 grill 全部销号 + +1. ~~相似判定算法~~ **已拍板(grill Q11)**:seam 化接口,一期最简 = ngram FULLTEXT,后续可换算法。 diff --git a/.scratch/customer-module/issues/04-create-dedup-and-company-lookup.md b/.scratch/customer-module/issues/04-create-dedup-and-company-lookup.md index a7bff74..3ad1f0b 100644 --- a/.scratch/customer-module/issues/04-create-dedup-and-company-lookup.md +++ b/.scratch/customer-module/issues/04-create-dedup-and-company-lookup.md @@ -1,7 +1,7 @@ # 04 — 新增/编辑客户的查重校验 + 企查查工商回填 seam Type: research -Status: open +Status: resolved Blocked by: 02 ## 问题 @@ -36,4 +36,87 @@ Blocked by: 02 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 范围(定稿) + +- ✅ **做**:新增/编辑客户时的**内建查重校验** + **企查查外部服务契约 seam**。 +- ❌ **不做**:A4-6 独立查重页 + 客户合并(chart D6);「客户查重设置」可配规则(chart D4);企查查服务内部实现(只定契约)。 + +### B. 查重校验(内建,写死在新增/编辑流程) + +#### B1. 三层查重(定稿) + +| 层 | 时机 | 规则 | 结果 | +|---|---|---|---| +| L1 自动相似提示 | 客户名称**停输防抖**(~300ms) | 名称模糊匹配全部未删除客户 | 只提示不阻止;无权命中的脱敏为「存在匹配记录」 | +| L2 保存时精确查重 | 点击确认提交 | ① 信用代码全局唯一(硬拦)② 名称相似(弹窗确认) | 信用代码同 → 禁止;名称似 → 弹窗「仍要创建/返回修改」 | +| L3 终查(并发兜底) | 「仍要创建」后、写库前 | 再查一次信用代码 + 名称 | 仍无冲突才写库;DB UNIQUE 兜底 | + +#### B2. 相似判定算法(⚠ 挂号,需拍板) + +原型明说「使用 CRM 后端统一查重服务,不在前端固化阈值」。**我的推荐(最简可行)**:用 **MySQL 内置 `FULLTEXT` 索引 + `MATCH ... AGAINST` 自然语言模式**,或更简单先上 **`LIKE '%name%'` + 编辑距离(Levenshtein) 后置过滤**。 + +- **推荐方案**:`customer_name` 建 FULLTEXT 索引,`MATCH(customer_name) AGAINST(?) ` 取 topN 候选,再用 **Levenshtein 相似度 ≥ 0.7** 判定「相似」,信用代码精确相等判定「重复」。首版阈值 0.7 写死常量(不做后台可配,chart D4)。 +- **备选**:NGram 分词(MySQL ngram parser)+ Jaccard 相似度。中文企业名短,FULLTEXT 默认分词对中文不友好,**ngram parser(token_size=2)更合适**——**最终推荐走 ngram parser**。 +- **统一查重服务**:抽 `CustomerDedupService`(crm-customer 内),后续 A4-6 若重启可复用;本期只被新增/编辑流程调用。 + +#### B3. 信用代码校验(定稿) + +- 格式:`^[0-9A-Z]{18}$`(18 位数字或大写字母,原型「18 位数字或字母组合」)。 +- 唯一:`uk_unified_credit_code` 唯一索引(票 02 已建),**NULL 不参与唯一约束**(未填时允许)。 +- 精确相等 → 硬拦禁止创建;命中无权查看的客户只提示「系统已存在该企业」,不显 name/编号/负责人。 + +#### B4. 联系人内联查重(定稿,回填票 06) + +- 同表格内**电话相同两行** → 禁止提交并定位重复行。 +- 无电话 → 按「客户 + 姓名 + 职务」提示疑似重复(只提示)。 +- 全库按电话精确查重(对已存在联系人)→ 相同禁止新建。 + +### C. 企查查外部服务契约 seam(定稿) + +#### C1. 端口(不做实现,留 mock) + +```java +// crm-customer 侧出站端口 +public interface CompanyLookupPort { + /** 按企业名查询工商候选列表 */ + List lookup(String companyName); +} + +record CompanyCandidate( + String bizName, String unifiedCreditCode, String legalRepresentative, + LocalDate establishedDate, String registeredCapital, String businessScope, + String staffSize, String annualRevenue, String address +) {} +``` + +#### C2. 回填范围(定稿,原型 §3.3 明文) + +- **允许回填字段(9 项)**:工商名称 / 统一社会信用代码 / 法定代表人 / 成立时间 / 注册资本 / 经营范围 / 人员规模 / 年产值 / 工商注册地址。 +- **只回填当前为空的字段**(首次选择);已填字段再次选择必须先弹「受影响字段确认」才覆盖;服务返回空值不覆盖原值。 +- **永不覆盖**:客户类型 / 行业 / 所属地区 / 关系星级 / 客户星级 / 销售负责人 / 销售部门 / 协同人 / 人脉关系单位 / 联系人 / 备注。 +- 回填后**立即用信用代码执行精确查重**(B1 L2 联动)。 + +#### C3. 异常语义(定稿) + +| 场景 | 行为 | +|---|---| +| 无结果 | 保留表单,提示「未查询到匹配企业」,可手工填写 | +| 服务失败/超时 | 保留表单,提示重试或手工填写;**不阻断手工提交** | +| 请求乱序 | 只处理最后一次有效请求,旧结果不覆盖当前表单 | + +### D. 编辑客户(定稿,对称新增) + +- 编辑页复用新增表单结构 + 查重校验;编辑时**排除自身**出查重范围(改名字不和自己撞)。 +- 历史停用枚举可回显不可重选(原型公共规则)。 + +### E. 回填 / 下游 + +- **票 02**:`unified_credit_code` 唯一索引 + `customer_name` FULLTEXT(ngram) 索引(A5 索引表补一条 `idx_name_fulltext`)。 +- **票 06**:联系人电话查重(B4)。 +- **票 09**(导入):复用 `CustomerDedupService` 做导入查重。 + +### F. ⚠ 挂号(需拍板) + +1. **相似判定算法**:ngram parser + 相似度阈值 0.7(推荐)vs 更简单 LIKE+Levenshtein。你选哪个? diff --git a/.scratch/customer-module/issues/05-answer.md b/.scratch/customer-module/issues/05-answer.md new file mode 100644 index 0000000..7ce31d0 --- /dev/null +++ b/.scratch/customer-module/issues/05-answer.md @@ -0,0 +1,56 @@ + +## Answer + +**Status: resolved**(重潜触发点:grill Q10 拍板 A/保守 → 第三轮 fresh 复核翻案,20260902 改票 B = 本期不触发、只留 seam) + +### A. 三态定义(定稿) + +| 值 | 名称 | 驱动源 | 本图 | +|---|---|---|---| +| 1 | 潜在客户 | 新建客户成功默认值 | ✅ | +| 2 | 重潜客户 | 关联的任一方案卡「提交并报备成功」 | ⚠ seam(触发链路本期不可达,见 C;本期不自动触发) | +| 3 | 已成交客户 | 关联的任一项目「首次中标赢单」 | ❌ 留 seam 给 A5 | + +- **只前进不回退**(原型 A4-4-1 §1 明文:方案卡失败/报备失败/商机输单/项目结束**均不改变**阶段)。 +- **不可手动改**:阶段条不可点击;无面向前端的 setter API;仅内部 port + 外部回调触发。 +- **父子客户不联动**:子客户阶段改变不影响父客户(原型无相关规则)。 + +### B. 存储 + 迁移约束(定稿) + +- `customer.customer_stage` tinyint default 1(票 02 已落)。 +- 迁移 SQL 形态:`UPDATE customer SET customer_stage=? WHERE id=? AND customer_stage < ?`(只前进 CAS,天然幂等,无需去重表)。 +- 迁移失败(已是更高阶段)= no-op 静默成功,不报错不写日志。 +- **每次变化**:写操作日志(票 07 oplog,操作人=`系统` + 注明触发来源)+ 刷新公共头部。 + +### C. 重潜触发点 — 已翻案(grill Q10 拍板 A/保守 → 第三轮 fresh 复核,20260902 用户拍板 B) + +原型 A4-4-1(8-26 fresh 复核确认)明文「任一方案卡**提交并报备成功**后更新为重潜」——是**两个条件**(提交 + 报备成功)。「报备/推送成功」在商机图是 **spin-out 本期不可达**(商机图票 07:方案卡状态=草稿1/已提交2/已推送3;推送链路本期不可达,仅 A5 回调特殊路径可达)。 + +**旧拍板(grill Q10=A/保守,已废)**:曾决定本期先按「方案卡状态=已提交(2)」凑合触发重潜,理由是怕客户阶段永久卡在潜在。 + +**✅ 新拍板(第三轮复核,20260902 用户选 B = 严格/不凑合)**:**本期不自动触发潜在→重潜**。理由:①原型要的是「提交**并报备成功**」两个条件,用「已提交」单条件凑合会造出「提交了但报备没成功、客户却已重潜」的**错误状态**,污染阶段派生字段;②`CustomerStagePort.promote` 的 **seam 全部保留**(契约、只前进 CAS、幂等),只是本期无调用方;③ 待报备成功链路就绪(方案卡「已推送(3)」或 A5 回调)后,由该链路调用 `promote(customerId, 2, "scheme_card_reported")` 触发重潜,改动是接入调用点、非改状态机。**本期客户阶段实际只有「潜在」**(字段/枚举/CAS 机制仍按三态建好)。 + +### D. seam 契约(定稿) + +```java +// crm-customer 侧暴露的端口(消费方 = crm-opportunity 方案卡 / 未来 A5 项目) +public interface CustomerStagePort { + /** 提升阶段;只前进,传入更低阶段 = no-op;返回提升后阶段 */ + int promote(Long customerId, int targetStage, String triggerSource); +} +``` + +- `triggerSource` 示例:`scheme_card_reported`(方案卡提交并报备成功,本期链路不可达 seam)/ `project_won`(已成交,A5 seam 占位)。~~`scheme_card_submitted`~~ 已弃用(D20 改票 B,不用「已提交」凑合)。 +- crm-opportunity 方案卡状态机在「已提交」迁移边(或收紧后「已推送」边)**同步调用**此 port(对称 lead→opp port 的同步事务内调用范式;失败抛异常使方案卡事务回滚?→ **不**:阶段提升失败**不阻断**方案卡提交,记日志降级,见 E)。 + +### E. 失败语义(定稿,重要) + +- **阶段提升失败不阻断主业务**:方案卡提交成功但客户阶段提升失败时,方案卡正常提交,阶段提升记日志告警 + 可重试(幂等 no-op 使重试安全)。 +- 理由:客户阶段是**派生冗余视图**(由业务成功结果驱动),不是主业务的一致性边界;用「尽力而为 + 幂等重试」而非「同步强一致回滚」,避免一个派生字段的失败拖垮方案卡主流程。 +- 对称:lead→opp port 是强一致(建商机失败回滚线索),但那是**主业务**;customer_stage 是**派生**,弱一致即可。 + +### F. 回填 / 下游 + +- **票 02**:`customer_stage` 已含。 +- **票 07**:oplog 记录阶段变化(操作人=系统)。 +- **票 01**:CONTEXT.md 术语「客户阶段」。 diff --git a/.scratch/customer-module/issues/05-customer-stage.md b/.scratch/customer-module/issues/05-customer-stage.md index 410e58e..f4374ee 100644 --- a/.scratch/customer-module/issues/05-customer-stage.md +++ b/.scratch/customer-module/issues/05-customer-stage.md @@ -1,7 +1,7 @@ # 05 — 客户阶段(潜在→重潜→已成交,只前进) Type: research -Status: open +Status: resolved Blocked by: 02 ## 问题 @@ -33,4 +33,58 @@ Blocked by: 02 ## Answer -_(待 resolve)_ +**Status: resolved**(含 1 个 ⚠ 产品决策挂号,不阻塞 spec 定稿) + +### A. 三态定义(定稿) + +| 值 | 名称 | 驱动源 | 本图 | +|---|---|---|---| +| 1 | 潜在客户 | 新建客户成功默认值 | ✅ | +| 2 | 重潜客户 | 关联的任一方案卡「提交并报备成功」 | ✅(触发点见 C 挂号) | +| 3 | 已成交客户 | 关联的任一项目「首次中标赢单」 | ❌ 留 seam 给 A5 | + +- **只前进不回退**(原型 A4-4-1 §1 明文:方案卡失败/报备失败/商机输单/项目结束**均不改变**阶段)。 +- **不可手动改**:阶段条不可点击;无面向前端的 setter API;仅内部 port + 外部回调触发。 +- **父子客户不联动**:子客户阶段改变不影响父客户(原型无相关规则)。 + +### B. 存储 + 迁移约束(定稿) + +- `customer.customer_stage` tinyint default 1(票 02 已落)。 +- 迁移 SQL 形态:`UPDATE customer SET customer_stage=? WHERE id=? AND customer_stage < ?`(只前进 CAS,天然幂等,无需去重表)。 +- 迁移失败(已是更高阶段)= no-op 静默成功,不报错不写日志。 +- **每次变化**:写操作日志(票 07 oplog,操作人=`系统` + 注明触发来源)+ 刷新公共头部。 + +### C. ⚠ 挂号:重潜的触发点(需产品拍板) + +原型 A4-4-1 说「任一方案卡**提交并报备成功**后更新为重潜」。但「报备/推送」在商机图是 **spin-out 不可达**(商机图票 07:方案卡状态=草稿1/已提交2/已推送3;推送链路本期不可达,仅 A5 回调特殊路径可达)。 + +**我的推荐(保守)**:本期**先按「方案卡状态=已提交(2)」触发重潜**——理由:①「提交」是方案卡状态机本期内确定可达的一步;②「报备成功」依赖推送链路(spin-out),若死等它,客户阶段会永久卡在潜在,功能形同虚设;③ 报备链路就绪后只需把触发点从「已提交」收紧到「已推送(报备成功)」,改动是一行状态判断。 + +**备选(严格)**:本期不触发重潜,等推送链路建好再放。客户阶段在 push 链路就绪前永远停在潜在。 + +> 你选哪个?选保守(推荐)则票 05 就此冻结;选严格则票 05 标注「重潜触发点 = 待推送链路」。 + +### D. seam 契约(定稿) + +```java +// crm-customer 侧暴露的端口(消费方 = crm-opportunity 方案卡 / 未来 A5 项目) +public interface CustomerStagePort { + /** 提升阶段;只前进,传入更低阶段 = no-op;返回提升后阶段 */ + int promote(Long customerId, int targetStage, String triggerSource); +} +``` + +- `triggerSource` 示例:`scheme_card_submitted` / `project_won`(已成交,A5 seam 占位)。 +- crm-opportunity 方案卡状态机在「已提交」迁移边(或收紧后「已推送」边)**同步调用**此 port(对称 lead→opp port 的同步事务内调用范式;失败抛异常使方案卡事务回滚?→ **不**:阶段提升失败**不阻断**方案卡提交,记日志降级,见 E)。 + +### E. 失败语义(定稿,重要) + +- **阶段提升失败不阻断主业务**:方案卡提交成功但客户阶段提升失败时,方案卡正常提交,阶段提升记日志告警 + 可重试(幂等 no-op 使重试安全)。 +- 理由:客户阶段是**派生冗余视图**(由业务成功结果驱动),不是主业务的一致性边界;用「尽力而为 + 幂等重试」而非「同步强一致回滚」,避免一个派生字段的失败拖垮方案卡主流程。 +- 对称:lead→opp port 是强一致(建商机失败回滚线索),但那是**主业务**;customer_stage 是**派生**,弱一致即可。 + +### F. 回填 / 下游 + +- **票 02**:`customer_stage` 已含。 +- **票 07**:oplog 记录阶段变化(操作人=系统)。 +- **票 01**:CONTEXT.md 术语「客户阶段」。 diff --git a/.scratch/customer-module/issues/06-answer.md b/.scratch/customer-module/issues/06-answer.md new file mode 100644 index 0000000..7e6b528 --- /dev/null +++ b/.scratch/customer-module/issues/06-answer.md @@ -0,0 +1,76 @@ +## Answer + +**Status: resolved** + +### A. 实体 `customer_contact`(crm-customer,独立表) + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| id | id | bigint | | 主键 ASSIGN_ID | +| customerId | customer_id | bigint | ✅ | 所属客户;NOT NULL,无孤立联系人 | +| name | name | varchar(50) | ✅ | 联系人姓名 | +| jobTitleName | job_title_name | varchar(100) | ✅ | 职务名称(文本,始终存) | +| jobTitleId | job_title_id | bigint | | 标准职务 id(字典/预置表),自定义职务时为 NULL | +| jobTitleCategory | job_title_category | varchar(50) | | 职务类别(高层管理/采购/…),自定义职务时为 NULL | +| jobTitleLevel | job_title_level | int | | 职务层级(图谱用),自定义职务时为 NULL | +| phone | phone | varchar(20) | | 联系电话;手机号格式校验 | +| isKeyContact | is_key_contact | tinyint | | 是否关键联系人(是1/否0) | +| isInternal | is_internal | tinyint | | 是否内线(是1/否0) | +| giftRemark | gift_remark | varchar(500) | | 礼品备注 | +| source | source | varchar(20) | | 来源字典 `contact_source`:组会/组局/转介绍/其他 | +| createBy / createTime / updateBy / updateTime | — | — | | BaseEntity | +| deleted | deleted | tinyint default 0 | | 逻辑删除 | + +> ⚠ **孤立单大写字母检查**(AGENTS.md):`isKeyContact`→`is_key_contact`、`isInternal`→`is_internal`、`jobTitleName`/`jobTitleId`/`jobTitleCategory`/`jobTitleLevel`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 + +**索引**: +- `idx_contact_customer` (customer_id, deleted) +- ~~`uk_contact_phone` (phone) 全局唯一~~ **已撤销(D26 第三轮翻案,20260902 用户拍板 A)**:不建电话唯一索引。电话查重=可配置软提示(跟随 crm-rule 客户查重设置「联系电话」开关,默认开),命中只提示疑似重复、不禁止创建。仅保留**同一客户内**两个联系人电话不重复的表单级校验(票 04 B4 内联查重) +- `idx_contact_name` (name) — 全局搜索 + +**权限模型(定稿,原型明文「联系人不设独立 ACL,继承客户权限」)**: +- 无独立 `@DataScope` 注解;服务层显式按 customer 校验(对客户有读权限 → 对其全部联系人有读权限)。 +- 无客户读写权限时隐藏联系人新增/编辑/删除入口;服务端仍校验。 + +### B. 契约(对齐商机硬依赖,promote 自 opp-customer-dependency 分析报告) + +| 优先级 | 接口 | 说明 | +|---|---|---| +| P0 | `POST /api/customer/search` | 客户搜索(名称/联系人/电话/类型 四维模糊 + 分页),商机新增关联客户数据源 | +| P0 | `GET /api/customer/{id}/contacts` | 按客户查联系人列表 | +| P0 | `GET /api/customer/{id}` | 客户基础信息带出 | +| P1 | `GET /api/customer/contact/search` | 联系人全局搜索(带出所属公司名),商机「商机关键联系人」数据源 | +| P1 | `POST /api/customer/quick-create` | 快速创建客户(最小必填集),商机弹窗入口 | +| P2 | `GET /api/customer/{id}/detail` | 客户详情(新窗口跳转),复用票 07 详情页 | + +### C. 联系人 CRUD 契约(定稿) + +``` +POST /api/customer/contact 新增联系人(可指定 customer_id 或独立新增后关联) +PUT /api/customer/contact/{id} 编辑 +DELETE /api/customer/contact/{id} 删除(软删) +GET /api/customer/contact/{id} 详情 +GET /api/customer/contact/page 顶级联系人列表(A4-5-1,跨客户,按 DataScope) +POST /api/customer/contact/import 批量导入(划出本图 → 随图谱 effort,本图只留接口占位) +``` + +### D. 随客户新增的内嵌明细(定稿) + +- 新增客户表单「联系人」Tab = 明细表数组,**客户保存成功后**与客户建立关联(原型 3.5 §1)。 +- 电话内联查重:**同一客户内**两行电话相同禁止提交(表单级硬校验);跨客户电话精确命中=**软提示疑似重复、不阻断**(跟随查重设置「联系电话」开关,D26 翻案后口径,票 04 B4 同步收敛为提示)。 +- 删除仅移除本次未保存行;已输入内容行删除前确认。 + +### E. 产品确认项 — 已随 grill 全部销号 + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | ~~联系人电话唯一约束(grill Q16=B 全局唯一)~~ **已翻案(D26 第三轮复核,20260902 用户拍板 A)**:不建 `uk_contact_phone` 唯一索引;电话查重=可配置软提示(跟随查重设置「联系电话」开关),命中只提示不阻断;仅同客户内联系人电话不重复为表单级校验 | 撤销唯一索引;软提示走查重设置开关 | +| P1-1 | ~~「来源」字典完整值~~ **已拍板(grill Q17)**:种子 = 组会/组局/转介绍/其他 4 项,后续在字典补 | crm-dict 种子 | + +### F. 回填 / 下游 + +- **票 02**:无主表字段变化(联系人是子表)。 +- **票 04**:联系人内联查重(B4)已回填。 +- **票 07**:客户详情「联系人」页签复用 `GET /api/customer/{id}/contacts`。 +- **票 01**:CONTEXT.md 术语「联系人」「关键联系人」。 +- **crm-dict**:新增 `contact_source` 分组 + `job_title` 标准职务预置(图谱 effort 用它,本图只留 jobTitleId 引用位)。 diff --git a/.scratch/customer-module/issues/06-contacts-crud.md b/.scratch/customer-module/issues/06-contacts-crud.md index b101462..e19b02b 100644 --- a/.scratch/customer-module/issues/06-contacts-crud.md +++ b/.scratch/customer-module/issues/06-contacts-crud.md @@ -1,7 +1,7 @@ # 06 — 联系人建模 CRUD + 全局搜索 + 商机侧硬依赖接口 Type: research -Status: open +Status: resolved Blocked by: 02 ## 问题 @@ -54,4 +54,77 @@ Blocked by: 02 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 实体 `customer_contact`(crm-customer,独立表) + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| id | id | bigint | | 主键 ASSIGN_ID | +| customerId | customer_id | bigint | ✅ | 所属客户;NOT NULL,无孤立联系人 | +| name | name | varchar(50) | ✅ | 联系人姓名 | +| jobTitleName | job_title_name | varchar(100) | ✅ | 职务名称(文本,始终存) | +| jobTitleId | job_title_id | bigint | | 标准职务 id(字典/预置表),自定义职务时为 NULL | +| jobTitleCategory | job_title_category | varchar(50) | | 职务类别(高层管理/采购/…),自定义职务时为 NULL | +| jobTitleLevel | job_title_level | int | | 职务层级(图谱用),自定义职务时为 NULL | +| phone | phone | varchar(20) | | 联系电话;手机号格式校验 | +| isKeyContact | is_key_contact | tinyint | | 是否关键联系人(是1/否0) | +| isInternal | is_internal | tinyint | | 是否内线(是1/否0) | +| giftRemark | gift_remark | varchar(500) | | 礼品备注 | +| source | source | varchar(20) | | 来源字典 `contact_source`:组会/组局/转介绍/其他 | +| createBy / createTime / updateBy / updateTime | — | — | | BaseEntity | +| deleted | deleted | tinyint default 0 | | 逻辑删除 | + +> ⚠ **孤立单大写字母检查**(AGENTS.md):`isKeyContact`→`is_key_contact`、`isInternal`→`is_internal`、`jobTitleName`/`jobTitleId`/`jobTitleCategory`/`jobTitleLevel`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 + +**索引**: +- `idx_contact_customer` (customer_id, deleted) +- `uk_contact_phone` (phone) — **仅手机号唯一**;NULL 不参与(无电话时允许,票 04 B4 内联查重兜底) +- `idx_contact_name` (name) — 全局搜索 + +**权限模型(定稿,原型明文「联系人不设独立 ACL,继承客户权限」)**: +- 无独立 `@DataScope` 注解;服务层显式按 customer 校验(对客户有读权限 → 对其全部联系人有读权限)。 +- 无客户读写权限时隐藏联系人新增/编辑/删除入口;服务端仍校验。 + +### B. 契约(对齐商机硬依赖,promote 自 opp-customer-dependency 分析报告) + +| 优先级 | 接口 | 说明 | +|---|---|---| +| P0 | `POST /api/customer/search` | 客户搜索(名称/联系人/电话/类型 四维模糊 + 分页),商机新增关联客户数据源 | +| P0 | `GET /api/customer/{id}/contacts` | 按客户查联系人列表 | +| P0 | `GET /api/customer/{id}` | 客户基础信息带出 | +| P1 | `GET /api/customer/contact/search` | 联系人全局搜索(带出所属公司名),商机「商机关键联系人」数据源 | +| P1 | `POST /api/customer/quick-create` | 快速创建客户(最小必填集),商机弹窗入口 | +| P2 | `GET /api/customer/{id}/detail` | 客户详情(新窗口跳转),复用票 07 详情页 | + +### C. 联系人 CRUD 契约(定稿) + +``` +POST /api/customer/contact 新增联系人(可指定 customer_id 或独立新增后关联) +PUT /api/customer/contact/{id} 编辑 +DELETE /api/customer/contact/{id} 删除(软删) +GET /api/customer/contact/{id} 详情 +GET /api/customer/contact/page 顶级联系人列表(A4-5-1,跨客户,按 DataScope) +POST /api/customer/contact/import 批量导入(划出本图 → 随图谱 effort,本图只留接口占位) +``` + +### D. 随客户新增的内嵌明细(定稿) + +- 新增客户表单「联系人」Tab = 明细表数组,**客户保存成功后**与客户建立关联(原型 3.5 §1)。 +- 电话内联查重:同表电话相同两行禁止提交;全库电话精确查重(票 04 B4)。 +- 删除仅移除本次未保存行;已输入内容行删除前确认。 + +### E. 待产品确认(⚠) + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | 联系人电话唯一约束:**手机号全局唯一** vs 允许一号多人(共号场景)?原型「完全相同禁止新建」倾向唯一——但座机分机号可能重复。**推荐:仅手机号(11位)唯一,座机/短号不做唯一**,`phone` 单列存但唯一索引带条件(`phone IS NOT NULL AND LENGTH(phone)=11` 或用生成列)。 | `uk_contact_phone` 实现 | +| P1-1 | 「来源」字典 `contact_source` 完整值(组会/组局/转介绍/其他,原型明列 4 项,是否还要更多) | crm-dict 种子 | + +### F. 回填 / 下游 + +- **票 02**:无主表字段变化(联系人是子表)。 +- **票 04**:联系人内联查重(B4)已回填。 +- **票 07**:客户详情「联系人」页签复用 `GET /api/customer/{id}/contacts`。 +- **票 01**:CONTEXT.md 术语「联系人」「关键联系人」。 +- **crm-dict**:新增 `contact_source` 分组 + `job_title` 标准职务预置(图谱 effort 用它,本图只留 jobTitleId 引用位)。 diff --git a/.scratch/customer-module/issues/07-answer.md b/.scratch/customer-module/issues/07-answer.md new file mode 100644 index 0000000..fc98f40 --- /dev/null +++ b/.scratch/customer-module/issues/07-answer.md @@ -0,0 +1,86 @@ +## Answer + +**Status: resolved** + +### A. 客户详情页骨架(定稿) + +**公共头部**(8 页签共享):客户名称 / 客户阶段(只读条) / 客户星级 / 关系星级 / 战略协议等级(快照) / 已成交VIP等级(快照) / 客户档案状态 / 销售负责人+部门 / 最近跟进摘要 / 待跟进提醒(下次跟进时间) / 关联业务计数(1商机/1项目)。 + +**汇总卡片**(4 张,均外部模块提供,接口失败显"--"+重试,不显 0):已中标项目总额(A5) / 跟进中方案预估总额(方案卡) / 进行中商机总额(crm-opportunity) / 合同总额·累计回款(预留占位显"--")。 + +**8 页签**(默认进「客户信息」;首次打开才加载,切回保留条件/分页): + +| # | 页签 | 数据源 | 本图 | +|---|---|---|---| +| 1 | 客户信息 | 主表(票 02) | ✅ | +| 2 | 联系人 | `GET /api/customer/{id}/contacts`(票 06) | ✅ | +| 3 | 跟进记录 | `customer_follow`(B) | ✅ | +| 4 | 关联商机 | 反查 crm-opportunity(C) | ✅ | +| 5 | 关联项目 | A5 项目模块 | ❌ 骨架显"--" | +| 6 | 战略协议 | 战略协议子域 | ❌ 骨架显"--" | +| 7 | 团队成员 | 负责人+协同人(D) | ✅ | +| 8 | 操作日志 | `customer_oplog`(E) | ✅ | + +### B. 跟进记录 `customer_follow`(定稿) + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| id | id | bigint | | 主键 | +| customerId | customer_id | bigint | ✅ | 客户 ID | +| followWay | follow_way | varchar(20) | ✅ | 跟进方式字典 `follow_way`:微信/拜访/电话/邮件/会议 | +| followContent | follow_content | text | ✅ | 跟进内容 | +| nextFollowTime | next_follow_time | datetime | | 下次跟进时间(有则进"待跟进"筛选) | +| followBy / followByName / followDeptName | follow_by / follow_by_name / follow_dept_name | — | | 跟进人快照(对称商机跟进范式) | +| createTime | create_time | — | | 跟进时间(=创建时间,不可改) | + +> 无孤立单大写字母陷阱。`nextFollowTime`→`next_follow_time` ✅。 + +**约束**: +- 提交后**不可修改/删除**;纠错 = 新增补充记录(原型 7 明文)。 +- 提交中禁止重复提交(原型公共规则)。 +- **写库副作用(关键)**:`customer.last_valid_follow_time = now`(超期提醒锚点刷新,票 10 回填);更新"待跟进"状态。 + +**契约**: +``` +POST /api/customer/{id}/follow 新增跟进(刷新 last_valid_follow_time) +GET /api/customer/{id}/follow/page 跟进记录分页(时间倒序 + 日期分组;按 follow_way/关键字/时间范围过滤) +``` + +### C. 关联商机页签(定稿,只读反查) + +- 数据源:**反查 crm-opportunity**(按 `customer_id` 查该客户全部商机)。 +- 列:商机名称 / 商机编号 / 阶段 / 预计金额 / 销售负责人 / 最近更新;点商机名跳 crm-opportunity 商机详情(新窗口)。 +- **只读**,不在客户详情内做商机写操作(对称:商机详情反查客户也只是带出)。 +- 接口失败显"--" + 重试,不显 0 条。 + +### D. 团队成员页签(定稿) + +- 数据:销售负责人(owner_user_id 快照)+ 协同人列表(团队成员关联表)。 +- 展示:姓名 / 部门 / 角色(负责人/协同人);`GET /api/customer/{id}/members`。 + +### E. 操作日志 `customer_oplog`(定稿,对称商机 oplog 范式) + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| customerId | customer_id | bigint | 客户 ID | +| action | action | varchar(50) | 动作类型:CREATE/UPDATE/FOLLOW/ARCHIVE/RESTORE/TRANSFER/CLAIM/POOL/STAGE_CHANGE | +| detail | detail | varchar(1000) | 变更描述,形态 `把 {字段} 从 {旧} 修改为 {新}`(原型 8 明文) | +| operatorId / operatorName / operatorDeptName | operator_id / operator_name / operator_dept_name | — | 操作人快照 | +| createTime | create_time | — | 操作时间 | + +- **只追加、不可改删**;`GET /api/customer/{id}/oplog/page`(时间倒序 + 日期分组 + 关键字/时间范围过滤)。 +- **保留策略(§5.5 拍板 A,20260902)**:`customer_oplog` **无限期保留**(审计日志,不设保留期/清理/归档;不做冷热分离)。依据:原型 L250「日志生成后不可修改或删除」= append-only 审计;字段级日志量可控。 +- **写 oplog 的动作**(回填各票):新增/编辑(票 02)/ 归属迁移·归档·恢复·领取·分配·抛公海(票 03)/ 阶段变化 operator=系统(票 05)/ 交割(票 08)。 + +### F. 回填 / 下游 + +- **票 02**:`last_valid_follow_time` 已含(B 刷新)。 +- **票 03**:归属迁移写 oplog(E)。 +- **票 05**:阶段变化写 oplog(operator=系统)。 +- **票 10**:跟进刷新锚点 → 超期提醒计时重启(B 副作用)。 +- **crm-dict**:新增 `follow_way` 分组(微信/拜访/电话/邮件/会议)。 + +### G. 挂号 — 已随 grill 全部销号 + +1. ~~汇总卡片占位渲染~~ **已拍板(grill Q18)**:一律渲染卡片,值显"--"(不显 0/不留白),保持布局稳定。 diff --git a/.scratch/customer-module/issues/07-detail-follow-oplog.md b/.scratch/customer-module/issues/07-detail-follow-oplog.md index 6d85522..74c9e50 100644 --- a/.scratch/customer-module/issues/07-detail-follow-oplog.md +++ b/.scratch/customer-module/issues/07-detail-follow-oplog.md @@ -1,7 +1,7 @@ # 07 — 客户详情 8 页签 + 跟进记录 + 操作日志 Type: research -Status: open +Status: resolved Blocked by: 02, 05 ## 问题 @@ -50,4 +50,86 @@ Blocked by: 02, 05 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 客户详情页骨架(定稿) + +**公共头部**(8 页签共享):客户名称 / 客户阶段(只读条) / 客户星级 / 关系星级 / 战略协议等级(快照) / 已成交VIP等级(快照) / 客户档案状态 / 销售负责人+部门 / 最近跟进摘要 / 待跟进提醒(下次跟进时间) / 关联业务计数(1商机/1项目)。 + +**汇总卡片**(4 张,均外部模块提供,接口失败显"--"+重试,不显 0):已中标项目总额(A5) / 跟进中方案预估总额(方案卡) / 进行中商机总额(crm-opportunity) / 合同总额·累计回款(预留占位显"--")。 + +**8 页签**(默认进「客户信息」;首次打开才加载,切回保留条件/分页): + +| # | 页签 | 数据源 | 本图 | +|---|---|---|---| +| 1 | 客户信息 | 主表(票 02) | ✅ | +| 2 | 联系人 | `GET /api/customer/{id}/contacts`(票 06) | ✅ | +| 3 | 跟进记录 | `customer_follow`(B) | ✅ | +| 4 | 关联商机 | 反查 crm-opportunity(C) | ✅ | +| 5 | 关联项目 | A5 项目模块 | ❌ 骨架显"--" | +| 6 | 战略协议 | 战略协议子域 | ❌ 骨架显"--" | +| 7 | 团队成员 | 负责人+协同人(D) | ✅ | +| 8 | 操作日志 | `customer_oplog`(E) | ✅ | + +### B. 跟进记录 `customer_follow`(定稿) + +| Java 字段 | 列名 | 类型 | 必填 | 说明 | +|---|---|---|---|---| +| id | id | bigint | | 主键 | +| customerId | customer_id | bigint | ✅ | 客户 ID | +| followWay | follow_way | varchar(20) | ✅ | 跟进方式字典 `follow_way`:微信/拜访/电话/邮件/会议 | +| followContent | follow_content | text | ✅ | 跟进内容 | +| nextFollowTime | next_follow_time | datetime | | 下次跟进时间(有则进"待跟进"筛选) | +| followBy / followByName / followDeptName | follow_by / follow_by_name / follow_dept_name | — | | 跟进人快照(对称商机跟进范式) | +| createTime | create_time | — | | 跟进时间(=创建时间,不可改) | + +> 无孤立单大写字母陷阱。`nextFollowTime`→`next_follow_time` ✅。 + +**约束**: +- 提交后**不可修改/删除**;纠错 = 新增补充记录(原型 7 明文)。 +- 提交中禁止重复提交(原型公共规则)。 +- **写库副作用(关键)**:`customer.last_valid_follow_time = now`(超期提醒锚点刷新,票 10 回填);更新"待跟进"状态。 + +**契约**: +``` +POST /api/customer/{id}/follow 新增跟进(刷新 last_valid_follow_time) +GET /api/customer/{id}/follow/page 跟进记录分页(时间倒序 + 日期分组;按 follow_way/关键字/时间范围过滤) +``` + +### C. 关联商机页签(定稿,只读反查) + +- 数据源:**反查 crm-opportunity**(按 `customer_id` 查该客户全部商机)。 +- 列:商机名称 / 商机编号 / 阶段 / 预计金额 / 销售负责人 / 最近更新;点商机名跳 crm-opportunity 商机详情(新窗口)。 +- **只读**,不在客户详情内做商机写操作(对称:商机详情反查客户也只是带出)。 +- 接口失败显"--" + 重试,不显 0 条。 + +### D. 团队成员页签(定稿) + +- 数据:销售负责人(owner_user_id 快照)+ 协同人列表(团队成员关联表)。 +- 展示:姓名 / 部门 / 角色(负责人/协同人);`GET /api/customer/{id}/members`。 + +### E. 操作日志 `customer_oplog`(定稿,对称商机 oplog 范式) + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| customerId | customer_id | bigint | 客户 ID | +| action | action | varchar(50) | 动作类型:CREATE/UPDATE/FOLLOW/ARCHIVE/RESTORE/TRANSFER/CLAIM/POOL/STAGE_CHANGE | +| detail | detail | varchar(1000) | 变更描述,形态 `把 {字段} 从 {旧} 修改为 {新}`(原型 8 明文) | +| operatorId / operatorName / operatorDeptName | operator_id / operator_name / operator_dept_name | — | 操作人快照 | +| createTime | create_time | — | 操作时间 | + +- **只追加、不可改删**;`GET /api/customer/{id}/oplog/page`(时间倒序 + 日期分组 + 关键字/时间范围过滤)。 +- **写 oplog 的动作**(回填各票):新增/编辑(票 02)/ 归属迁移·归档·恢复·领取·分配·抛公海(票 03)/ 阶段变化 operator=系统(票 05)/ 交割(票 08)。 + +### F. 回填 / 下游 + +- **票 02**:`last_valid_follow_time` 已含(B 刷新)。 +- **票 03**:归属迁移写 oplog(E)。 +- **票 05**:阶段变化写 oplog(operator=系统)。 +- **票 10**:跟进刷新锚点 → 超期提醒计时重启(B 副作用)。 +- **crm-dict**:新增 `follow_way` 分组(微信/拜访/电话/邮件/会议)。 + +### G. ⚠ 挂号(需拍板) + +1. 汇总卡片 4 张里「合同总额/累计回款」原型即预留占位——是否本期就显"--",还是连卡片都不渲染?**推荐:渲染卡片但值显"--"**(保持布局稳定)。 diff --git a/.scratch/customer-module/issues/08-answer.md b/.scratch/customer-module/issues/08-answer.md new file mode 100644 index 0000000..1291b90 --- /dev/null +++ b/.scratch/customer-module/issues/08-answer.md @@ -0,0 +1,97 @@ +## Answer + +**Status: resolved**(直属销售总监 = 组织架构推导,已由用户在 grill Q12 拍板 A) + +### A. 流程模型(定稿,两段式) + +``` +发起交接(离职人员→接收总监) → 客户分配(接收总监→具体销售) + owner 变为总监 owner 变为销售 + 生成交接单(待分配) 逐客户/批量分配 +``` + +- **只允许离职/调动/区域调整的销售本人发起**本人名下客户的交接(主管/管理员不可代办,原型二.2 明文)。 +- **可交接客户** = 该销售为**唯一负责人** 且 未删除 且 非公海;仅协同人的客户不纳入。 +- **接收总监**:离职 = 系统自动确定**唯一**直属销售总监;调动/区域调整 = 在本人**启用的**直属销售总监范围内搜索选择。 +- **交接单**:全部变更成功才生成(整批原子);生成唯一交接编号 + 状态「待分配」+ 进度 0/N。 + +### B. 实体(定稿) + +#### B1. 交接单 `customer_transfer` + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| transferNo | transfer_no | varchar(32) | 交接编号(唯一,生成规则对称商机编号) | +| fromUserId / fromUserName | from_user_id / from_user_name | — | 离职/调动人员快照 | +| toDirectorId / toDirectorName | to_director_id / to_director_name | — | 接收总监快照 | +| reason | reason | varchar(20) | 交接原因字典:员工离职/岗位调动/区域调整 | +| remark | remark | varchar(500) | 备注(选填) | +| totalCount | total_count | int | 本次交接客户总数 | +| assignedCount | assigned_count | int | 已分配数(进度 = assigned/total) | +| status | status | tinyint | 0待分配 / 1已完成 | +| createBy / createTime / updateBy / updateTime | — | — | BaseEntity | + +> 无孤立单大写字母陷阱。 + +#### B2. 交接明细 `customer_transfer_detail` + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| transferId | transfer_id | bigint | 交接单 id | +| customerId | customer_id | bigint | 客户 id | +| assignStatus | assign_status | tinyint | 0待分配 / 1已分配 | +| assignUserId / assignUserName | assign_user_id / assign_user_name | — | 分配对象快照(待分配时 NULL) | +| assignTime | assign_time | datetime | 分配时间 | + +- `UNIQUE(transfer_id, customer_id)` 去重。 + +### C. 事务语义(定稿,跨模块) + +1. **发起交接**(整批原子): + - 遍历本次客户,对每个客户:`owner_user_id → to_director_id`;移出 from_user 团队身份;保留原协同人。 + - 对每个客户的**关联商机**(crm-opportunity)`owner_user_id → to_director_id`。 + - **关联项目 owner 同步 = seam**(A5 未建;本图只定义 port 契约,实现时项目侧跳过并标注)。 + - **任一失败 → 全部回滚**,不生成交接单(原型三.确认交接 §7 明文)。 +2. **客户分配**(整批/单个): + - 待分配明细 → 选分配对象(接收总监有权分配范围 + 启用的销售 + 不得为 from_user 本人 + 不得是当前负责人)→ `owner_user_id → assign_user_id`,同步关联商机/项目 owner。 + - 单个失败不整体回滚(批量按逐条独立事务),失败项列原因。 +3. **防重入**:同一 from_user 存在未完成交接单时,禁止再次发起(原型三.确认交接 §1)。 + +### D. 契约(定稿) + +``` +POST /api/customer/transfer/initiate 发起交接(整批原子,返回交接编号) +GET /api/customer/transfer/{id}/preview 发起前客户预览(按原因算可交接客户) +GET /api/customer/transfer/{id} 交接详情 +GET /api/customer/transfer/page 交接记录(交接编号/人员/原因/状态过滤) +POST /api/customer/transfer/{id}/assign 客户分配(单个或批量 customerIds + assignUserId) +GET /api/customer/transfer/{id}/assignable 可分配销售列表(接收总监范围 + 启用) +``` + +### E. 直属销售总监 — 已拍板(grill Q12,组织架构推导) + +原型要求:离职时系统**自动确定唯一直属销售总监**;调动/区域调整时在**启用的直属销售总监范围**选择。核心是「直属销售总监」的确定口径: + +**我的推荐**:**不加 SysUser 字段**,用**组织架构现有层级**推导——按 from_user 所在部门向上找「部门负责人」(`dept.leader_user_id` 或等价),若该负责人是「销售总监」角色则命中;无/停用/多条则按原型阻断并提示「联系管理员维护组织关系」。 + +**理由**:① 组织架构已有部门负责人概念,不引入新字段;② 直属关系是组织属性而非用户属性,加 `superior_user_id` 会带来维护和一致性问题;③ 原型明说「无/停用/多条直属关系 → 阻断」,天然容忍推导失败。 + +**备选**:`SysUser` 加 `superior_user_id`(直属上级),由后台维护;查询 O(1),但需改 auth 模块 + 维护成本。 + +**✅ 已拍板(grill Q12,用户选 A)**:组织架构推导(`dept.leader` 向上找销售总监角色),不改 crm-auth,不加 `superior_user_id`。 + +### F. 回填 / 下游 + +- **票 03**:owner 变更(分配=换主)复用 oplog 语义;交割是分配动作的跨模块扩展版。 +- **票 07**:oplog 记录 `TRANSFER` 动作 + `customer_transfer` 表变更;跨模块同步操作人显「系统」+ 交接编号。 +- **crm-opportunity**:需要商机侧提供「按 customer_id 批量改 owner」的 port(或复用商机分配逻辑)。 +- **A5 项目**:项目 owner 同步 seam(port 契约占位)。 +- **crm-dict**:`transfer_reason` 分组(员工离职/岗位调动/区域调整)。 + +### G. 产品确认项 — 已随 grill 全部销号 + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | ~~交接单「已完成」的判定~~ **已拍板(grill Q13)**:自动完成(assignedCount == totalCount 置为已完成),不设手动「完成交接」按钮 | status 迁移 | diff --git a/.scratch/customer-module/issues/08-transfer.md b/.scratch/customer-module/issues/08-transfer.md index 94fa928..08a009c 100644 --- a/.scratch/customer-module/issues/08-transfer.md +++ b/.scratch/customer-module/issues/08-transfer.md @@ -1,7 +1,7 @@ # 08 — 客户交割(跨模块同步商机/项目负责人) Type: research -Status: open +Status: resolved Blocked by: 02, 03 ## 问题 @@ -44,4 +44,98 @@ Blocked by: 02, 03 ## Answer -_(待 resolve)_ +**Status: resolved**(含 1 个 ⚠ 决策挂号:直属销售总监确定方式) + +### A. 流程模型(定稿,两段式) + +``` +发起交接(离职人员→接收总监) → 客户分配(接收总监→具体销售) + owner 变为总监 owner 变为销售 + 生成交接单(待分配) 逐客户/批量分配 +``` + +- **只允许离职/调动/区域调整的销售本人发起**本人名下客户的交接(主管/管理员不可代办,原型二.2 明文)。 +- **可交接客户** = 该销售为**唯一负责人** 且 未删除 且 非公海;仅协同人的客户不纳入。 +- **接收总监**:离职 = 系统自动确定**唯一**直属销售总监;调动/区域调整 = 在本人**启用的**直属销售总监范围内搜索选择。 +- **交接单**:全部变更成功才生成(整批原子);生成唯一交接编号 + 状态「待分配」+ 进度 0/N。 + +### B. 实体(定稿) + +#### B1. 交接单 `customer_transfer` + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| transferNo | transfer_no | varchar(32) | 交接编号(唯一,生成规则对称商机编号) | +| fromUserId / fromUserName | from_user_id / from_user_name | — | 离职/调动人员快照 | +| toDirectorId / toDirectorName | to_director_id / to_director_name | — | 接收总监快照 | +| reason | reason | varchar(20) | 交接原因字典:员工离职/岗位调动/区域调整 | +| remark | remark | varchar(500) | 备注(选填) | +| totalCount | total_count | int | 本次交接客户总数 | +| assignedCount | assigned_count | int | 已分配数(进度 = assigned/total) | +| status | status | tinyint | 0待分配 / 1已完成 | +| createBy / createTime / updateBy / updateTime | — | — | BaseEntity | + +> 无孤立单大写字母陷阱。 + +#### B2. 交接明细 `customer_transfer_detail` + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | 主键 | +| transferId | transfer_id | bigint | 交接单 id | +| customerId | customer_id | bigint | 客户 id | +| assignStatus | assign_status | tinyint | 0待分配 / 1已分配 | +| assignUserId / assignUserName | assign_user_id / assign_user_name | — | 分配对象快照(待分配时 NULL) | +| assignTime | assign_time | datetime | 分配时间 | + +- `UNIQUE(transfer_id, customer_id)` 去重。 + +### C. 事务语义(定稿,跨模块) + +1. **发起交接**(整批原子): + - 遍历本次客户,对每个客户:`owner_user_id → to_director_id`;移出 from_user 团队身份;保留原协同人。 + - 对每个客户的**关联商机**(crm-opportunity)`owner_user_id → to_director_id`。 + - **关联项目 owner 同步 = seam**(A5 未建;本图只定义 port 契约,实现时项目侧跳过并标注)。 + - **任一失败 → 全部回滚**,不生成交接单(原型三.确认交接 §7 明文)。 +2. **客户分配**(整批/单个): + - 待分配明细 → 选分配对象(接收总监有权分配范围 + 启用的销售 + 不得为 from_user 本人 + 不得是当前负责人)→ `owner_user_id → assign_user_id`,同步关联商机/项目 owner。 + - 单个失败不整体回滚(批量按逐条独立事务),失败项列原因。 +3. **防重入**:同一 from_user 存在未完成交接单时,禁止再次发起(原型三.确认交接 §1)。 + +### D. 契约(定稿) + +``` +POST /api/customer/transfer/initiate 发起交接(整批原子,返回交接编号) +GET /api/customer/transfer/{id}/preview 发起前客户预览(按原因算可交接客户) +GET /api/customer/transfer/{id} 交接详情 +GET /api/customer/transfer/page 交接记录(交接编号/人员/原因/状态过滤) +POST /api/customer/transfer/{id}/assign 客户分配(单个或批量 customerIds + assignUserId) +GET /api/customer/transfer/{id}/assignable 可分配销售列表(接收总监范围 + 启用) +``` + +### E. ⚠ 挂号:直属销售总监怎么定(需拍板) + +原型要求:离职时系统**自动确定唯一直属销售总监**;调动/区域调整时在**启用的直属销售总监范围**选择。核心是「直属销售总监」的确定口径: + +**我的推荐**:**不加 SysUser 字段**,用**组织架构现有层级**推导——按 from_user 所在部门向上找「部门负责人」(`dept.leader_user_id` 或等价),若该负责人是「销售总监」角色则命中;无/停用/多条则按原型阻断并提示「联系管理员维护组织关系」。 + +**理由**:① 组织架构已有部门负责人概念,不引入新字段;② 直属关系是组织属性而非用户属性,加 `superior_user_id` 会带来维护和一致性问题;③ 原型明说「无/停用/多条直属关系 → 阻断」,天然容忍推导失败。 + +**备选**:`SysUser` 加 `superior_user_id`(直属上级),由后台维护;查询 O(1),但需改 auth 模块 + 维护成本。 + +> 你选哪个?选推荐则票 08 冻结;选备选则回填 crm-auth 加字段。 + +### F. 回填 / 下游 + +- **票 03**:owner 变更(分配=换主)复用 oplog 语义;交割是分配动作的跨模块扩展版。 +- **票 07**:oplog 记录 `TRANSFER` 动作 + `customer_transfer` 表变更;跨模块同步操作人显「系统」+ 交接编号。 +- **crm-opportunity**:需要商机侧提供「按 customer_id 批量改 owner」的 port(或复用商机分配逻辑)。 +- **A5 项目**:项目 owner 同步 seam(port 契约占位)。 +- **crm-dict**:`transfer_reason` 分组(员工离职/岗位调动/区域调整)。 + +### G. 待产品确认(⚠) + +| # | 问题 | 影响 | +|---|---|---| +| P0-1 | 交接单「已完成」的判定:assignedCount == totalCount 自动完成,还是总监手动点「完成交接」?原型进度 0/N → **推荐自动完成**(全部分配完即完成) | status 迁移 | diff --git a/.scratch/customer-module/issues/09-answer.md b/.scratch/customer-module/issues/09-answer.md new file mode 100644 index 0000000..fcdcebe --- /dev/null +++ b/.scratch/customer-module/issues/09-answer.md @@ -0,0 +1,60 @@ +## Answer + +**Status: resolved** + +### A. 范围(定稿) + +- ✅ **做**:客户主数据 + 联系人子表 的 Excel 导入(异步任务 + 行级处理 + 结果明细)。 +- ❌ **不做**:OCR 快捷新增(划出);Excel 内图片/附件转存(V1 忽略对象);通过 Excel 清空已有字段(V1 不支持)。 + +### B. 导入方式(定稿,三选一) + +| 方式 | 匹配到已有客户 | 未匹配 | +|---|---|---| +| 仅新增数据 | 不导入,标「客户已存在」 | 创建新客户 | +| 仅更新数据 | 更新 | 不新增,标「未找到可更新客户」 | +| 新增或更新数据 | 更新 | 创建新客户 | + +- **匹配顺序**(定稿):① 客户编号完全一致 → ② 统一社会信用代码完全一致。 +- 编号与信用代码**均缺失或均未匹配** → 不得仅凭名称自动覆盖;名称相似/相同行列「疑似重复」,本次不写入,由用户去新增/编辑流程人工处理(对称票 04 查重口径)。 +- 编号与信用代码**分别匹配到不同客户** → 该行冲突,禁止导入。 +- 文件内**多行匹配同一客户**:`覆盖导入`/`重复时不导入` 互斥,默认「重复时不导入」;覆盖按文件自上而下、最后一条通过校验的为准;不导入则重复组全部失败。 + +### C. 字段语义(定稿) + +- **新增导入**:必填字段空 → 该行失败;非必填空 → 按空值创建。`customer_stage` 固定=潜在(不得指定)。 +- **更新导入**:空白单元格 = 不修改已有值(V1 不支持清空)。 +- **禁止通过导入修改**:销售负责人/协同人(普通销售导入负责人固定=当前用户;有分配权限者可指定授权范围内负责人,超出 → 该行失败)、客户阶段(更新不得回退)、工商字段/战略协议/关联业务汇总等系统计算字段。 +- **联系人子表**(D26 翻案后校准 20260902):电话存在 → 匹配**本客户**已有联系人→更新;匹配**他客户**联系人→**软提示疑似重复、不禁止导入**(列疑似重复明细,跟随查重设置「联系电话」开关;开关关则不提示),**不做跨客户电话唯一硬拦**;电话空 → 按「客户+姓名+职务」疑似重复,不自动覆盖,列疑似重复明细。同一导入文件内同客户电话相同两行 → 该行失败(表单级)。 +- **原子单元** = 单条客户主记录 / 单条联系人子记录;联系人失败不回滚已成功的客户主记录。 + +### D. 任务模型(定稿,对称商机/线索导入范式) + +- **异步任务**:上传 + 服务端预校验 → 展示预计新增/更新/失败/疑似重复数 → 用户确认 → 创建正式导入任务。 +- **预校验**:文件类型(.xls/.xlsx) / 大小(≤20MB) / 模板版本 / 表头(无空单元格才导) / 单 sheet ≤50000 行 / 不计算公式 / 忽略图片附件对象。 +- **行级处理,允许部分成功**:单行内原子(不半行写入)。 +- **结果页**:任务状态 / 总行数 / 新增成功 / 更新成功 / 未变化 / 失败数 / 提交人 / 提交时间 / 完成时间;失败明细(sheet名/行号/客户识别/失败字段/失败原因)可下载,敏感值按字段权限脱敏。 +- **任务超时/异常 → 标记失败**,不得让用户重复提交状态未知的任务。 +- **操作日志**:记录任务 ID/导入方式/模板版本/文件哈希/成功失败数/触发后续动作选择;**不记录文件名、文件内容、敏感字段原值**。 +- **最近导入记录**入口,仅当前用户有权查看的任务。 + +**契约**: +``` +POST /api/customer/import/upload 上传 + 预校验(返回预计数 + 预校验明细) +POST /api/customer/import/{taskId}/confirm 确认创建正式任务(返回 taskId) +GET /api/customer/import/{taskId} 任务结果 +GET /api/customer/import/{taskId}/failures 失败明细下载 +GET /api/customer/import/page 最近导入记录 +GET /api/customer/import/template 下载导入模板(含模板版本) +``` + +### E. 回填 / 下游 + +- **票 04**:复用 `CustomerDedupService`(信用代码精确查重 + 名称相似判定)做导入匹配。 +- **票 03**:导入成功刷新「我的客户」,仍按当前用户数据范围展示。 +- **票 07**:oplog 记录每次导入(任务级)。 +- **crm-file**:文件上传/哈希复用平台能力(对称商机/线索导入)。 + +### F. 挂号 — 已随 grill 全部销号 + +1. ~~文件内多行匹配同一客户默认值~~ **已拍板(grill Q14)**:默认「重复时不导入」(重复组全部失败)。 diff --git a/.scratch/customer-module/issues/09-import.md b/.scratch/customer-module/issues/09-import.md index 6a88465..0c34b48 100644 --- a/.scratch/customer-module/issues/09-import.md +++ b/.scratch/customer-module/issues/09-import.md @@ -1,7 +1,7 @@ # 09 — 客户导入 + 联系人快速新增(OCR 划出) Type: research -Status: open +Status: resolved Blocked by: 02 ## 问题 @@ -33,4 +33,61 @@ Blocked by: 02 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 范围(定稿) + +- ✅ **做**:客户主数据 + 联系人子表 的 Excel 导入(异步任务 + 行级处理 + 结果明细)。 +- ❌ **不做**:OCR 快捷新增(划出);Excel 内图片/附件转存(V1 忽略对象);通过 Excel 清空已有字段(V1 不支持)。 + +### B. 导入方式(定稿,三选一) + +| 方式 | 匹配到已有客户 | 未匹配 | +|---|---|---| +| 仅新增数据 | 不导入,标「客户已存在」 | 创建新客户 | +| 仅更新数据 | 更新 | 不新增,标「未找到可更新客户」 | +| 新增或更新数据 | 更新 | 创建新客户 | + +- **匹配顺序**(定稿):① 客户编号完全一致 → ② 统一社会信用代码完全一致。 +- 编号与信用代码**均缺失或均未匹配** → 不得仅凭名称自动覆盖;名称相似/相同行列「疑似重复」,本次不写入,由用户去新增/编辑流程人工处理(对称票 04 查重口径)。 +- 编号与信用代码**分别匹配到不同客户** → 该行冲突,禁止导入。 +- 文件内**多行匹配同一客户**:`覆盖导入`/`重复时不导入` 互斥,默认「重复时不导入」;覆盖按文件自上而下、最后一条通过校验的为准;不导入则重复组全部失败。 + +### C. 字段语义(定稿) + +- **新增导入**:必填字段空 → 该行失败;非必填空 → 按空值创建。`customer_stage` 固定=潜在(不得指定)。 +- **更新导入**:空白单元格 = 不修改已有值(V1 不支持清空)。 +- **禁止通过导入修改**:销售负责人/协同人(普通销售导入负责人固定=当前用户;有分配权限者可指定授权范围内负责人,超出 → 该行失败)、客户阶段(更新不得回退)、工商字段/战略协议/关联业务汇总等系统计算字段。 +- **联系人子表**:电话存在 → 按电话查重(匹配本客户已有联系人→更新;匹配他客户联系人→冲突禁止转移);电话空 → 按「客户+姓名+职务」疑似重复,不自动覆盖,列失败明细。 +- **原子单元** = 单条客户主记录 / 单条联系人子记录;联系人失败不回滚已成功的客户主记录。 + +### D. 任务模型(定稿,对称商机/线索导入范式) + +- **异步任务**:上传 + 服务端预校验 → 展示预计新增/更新/失败/疑似重复数 → 用户确认 → 创建正式导入任务。 +- **预校验**:文件类型(.xls/.xlsx) / 大小(≤20MB) / 模板版本 / 表头(无空单元格才导) / 单 sheet ≤50000 行 / 不计算公式 / 忽略图片附件对象。 +- **行级处理,允许部分成功**:单行内原子(不半行写入)。 +- **结果页**:任务状态 / 总行数 / 新增成功 / 更新成功 / 未变化 / 失败数 / 提交人 / 提交时间 / 完成时间;失败明细(sheet名/行号/客户识别/失败字段/失败原因)可下载,敏感值按字段权限脱敏。 +- **任务超时/异常 → 标记失败**,不得让用户重复提交状态未知的任务。 +- **操作日志**:记录任务 ID/导入方式/模板版本/文件哈希/成功失败数/触发后续动作选择;**不记录文件名、文件内容、敏感字段原值**。 +- **最近导入记录**入口,仅当前用户有权查看的任务。 + +**契约**: +``` +POST /api/customer/import/upload 上传 + 预校验(返回预计数 + 预校验明细) +POST /api/customer/import/{taskId}/confirm 确认创建正式任务(返回 taskId) +GET /api/customer/import/{taskId} 任务结果 +GET /api/customer/import/{taskId}/failures 失败明细下载 +GET /api/customer/import/page 最近导入记录 +GET /api/customer/import/template 下载导入模板(含模板版本) +``` + +### E. 回填 / 下游 + +- **票 04**:复用 `CustomerDedupService`(信用代码精确查重 + 名称相似判定)做导入匹配。 +- **票 03**:导入成功刷新「我的客户」,仍按当前用户数据范围展示。 +- **票 07**:oplog 记录每次导入(任务级)。 +- **crm-file**:文件上传/哈希复用平台能力(对称商机/线索导入)。 + +### F. ⚠ 挂号(需拍板) + +1. 文件内多行匹配同一客户时,默认「重复时不导入」(原型默认)——确认默认值?**推荐:默认「重复时不导入」**(避免覆盖歧义)。 diff --git a/.scratch/customer-module/issues/10-answer.md b/.scratch/customer-module/issues/10-answer.md new file mode 100644 index 0000000..4812646 --- /dev/null +++ b/.scratch/customer-module/issues/10-answer.md @@ -0,0 +1,116 @@ + +## Answer + +**Status: resolved**(锚点语义 = 成为有主时刻,已由用户在 grill Q15 拍板 A) + +### A. 结论速览 + +客户「超期未跟进提醒」= **crm-rule 全局单例配置 + crm-customer 待发通知 + 调度 Job**,三层对称商机「回收提醒」范式,但配置形态是**单例**(不套 V-CONFIG 版本化,chart D4)。「客户查重设置」不做(chart D6)。 + +| 层 | 落点 | 对称对象 | +|---|---|---| +| 配置 | `crm-rule` `customer_reminder_rule`(单行表,id 固定=1) | `opportunity_pool_rule`(但非版本化) | +| 待发通知 | `crm-customer` `customer_pending_notice` | `opportunity_pending_notice` | +| 调度 | `crm-customer` `CustomerReminderJob`(@Scheduled) | `OpportunityMaintenanceJob` | + +--- + +### B. 配置表 `customer_reminder_rule`(crm-rule) + +**全局单例**:单行表,`id` 固定为 1,种子初始化器 `CustomerReminderRuleInitializer` 植入默认行(对称 `StageTemplateInitializer` 兜底范式)。无版本号、无状态机、无 rule_code。 + +| Java 字段 | 列名 | 类型 | 默认 | 说明 | +|---|---|---|---|---| +| id | id | bigint | 固定 1 | 单例行主键 | +| masterEnabled | master_enabled | tinyint | 1 | 总开关:1启用 0禁用(禁用后 Job 整轮跳过) | +| firstTriggerEnabled | first_trigger_enabled | tinyint | 1 | 首次触发独立开关 | +| firstTriggerDays | first_trigger_days | int | 30 | 首次触发天数(>0) | +| secondIntervalEnabled | second_interval_enabled | tinyint | 1 | 二次提醒独立开关 | +| secondIntervalDays | second_interval_days | int | 7 | 二次提醒间隔天数(>0) | +| updateBy / updateTime | update_by / update_time | — | — | BaseEntity 继承 | + +> ⚠ **孤立单大写字母检查**(AGENTS.md):`masterEnabled`→`master_enabled`、`firstTriggerDays`→`first_trigger_days`、`secondIntervalDays`→`second_interval_days`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 + +**校验规则**(服务层,对称商机规则): +- `firstTriggerDays > 0`、`secondIntervalDays > 0`。 +- 保存 = 全量覆盖(无草稿/发布态,保存即生效)。 +- 只后台管理面板可见(RBAC 功能权限 `customer.reminder.rule`)。 + +**契约**: +``` +GET /api/rule/customer/reminder → 读当前配置(单行) +PUT /api/rule/customer/reminder → 保存配置(全量覆盖,记录操作日志) +``` + +--- + +### C. 触发口径(只前进、确定性提醒链) + +**计时锚点** = `customer.last_valid_follow_time`(票 02 已建)。**锚点 = 成为「有主」的时刻**(grill Q15 拍板 A):① 新建客户直接指派负责人 → 初始化为 `create_time`;② 新建客户进公海(无负责人)→ `last_valid_follow_time = NULL`(无锚点不提醒),被领取/分配时 → 锚点 = 领取时刻;③ 每次写跟进记录刷新锚点(票 07)。 + +**提醒链**(确定性,可从锚点直接推导,无需存「已发首次」状态): + +``` +第 0 次(首次) plan = anchor + first_trigger_days +第 1 次(二次) plan = anchor + first_trigger_days + 1 × second_interval_days +第 k 次(二次) plan = anchor + first_trigger_days + k × second_interval_days +``` + +- 幂等键 = `(customer_id, notice_type, plan_notify_time)`,同计划时刻永不重写(对称商机 `uk_pending_notice_dedupe`)。 +- Job 每轮对每个客户只补**下一条未建的到期提醒**(不批量堆历史)。 +- **跟进刷新锚点** → 旧锚点的未发提醒**置已失效**(对称商机回收时 invalidate 语义);新锚点重新从首次开始计时。 + +**参与范围**(扫表条件): +- `archive_status = 有效(1)`(归档/合并不提醒) +- `owner_user_id IS NOT NULL`(公海无接收人不提醒,chart 票 10 待拍板点 6) +- `last_valid_follow_time IS NOT NULL`(有主客户才有锚点;公海客户领取后赋锚点,天然兜住「公海不计时」) +- `master_enabled = 1` 且(`first_trigger_enabled=1` 或 `second_interval_enabled=1`) + +--- + +### D. 待发通知表 `customer_pending_notice`(crm-customer) + +对称 `OpportunityPendingNotice`,字段集一致: + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | ASSIGN_ID 主键 | +| customerId | customer_id | bigint | 客户 ID | +| noticeType | notice_type | varchar(32) | `OVERDUE_REMIND`(本期唯一类型) | +| targetUserId | target_user_id | bigint | 应通知对象 = 客户销售负责人(快照) | +| planNotifyTime | plan_notify_time | datetime | 应发提醒时刻(= 提醒链推导时刻) | +| payloadJson | payload_json | text | 上下文 JSON(见下) | +| noticeStatus | notice_status | tinyint | 0待发 1已发 2已失效 | + +- **唯一约束**:`UNIQUE(customer_id, notice_type, plan_notify_time)`。 +- **索引**:`idx(customer_id, notice_status)` 供消费者扫待发;`idx(notice_status, plan_notify_time)` 供补发调度。 +- **payload JSON**(对称商机无「剩余天数」伪状态):`{ remindSeq(0=首次/k=第k次二次), overdueDays(已无跟进天数), anchor(last_valid_follow_time), firstTriggerDays, secondIntervalDays }`。 + +> 本期**只落待发记录,不直接发送**;平台通知能力(钉钉 + 应用内通知)spin-out,就绪后由另一消费者扫 `notice_status=待发` 补发(对称商机票 05 拍板)。 + +--- + +### E. 调度 Job `CustomerReminderJob`(crm-customer) + +- **cron**:`${crm.customer.reminder-cron:0 0 4 * * ?}`(默认凌晨 4 点,错开线索 2 点 / 商机 3 点)。 +- **流程**(对称 `OpportunityMaintenanceJob`,但无回收分支): + 1. 读 `customer_reminder_rule` 单行;`master_enabled=0` 或无配置 → 整轮跳过。 + 2. 扫参与范围客户(见 C)。 + 3. 对每个客户:算**下一条未建的到期提醒**(`plan ≤ now`),若有则落 `customer_pending_notice`(幂等:已存在同 plan 时刻跳过)。 + 4. 锚点已被刷新的客户:把旧锚点未发的 OVERDUE_REMIND 置已失效。 +- **不发送**:只落待发记录(E 末尾同)。 + +--- + +### F. 规则生效时机(chart Q4-补 用户强调,已确认) + +> 用户原话「规则在客户新增的时候生效」——最终解读:**新建客户时把 `last_valid_follow_time` 初始化为 `create_time`**,客户创建后即开始计时,30 天(首次触发默认值)无跟进 → 首次提醒。**回填票 02**(已落 `last_valid_follow_time` 字段)+ **票 07**(新建跟进时刷新)。 + +--- + +### G. 回填 / 下游影响 + +- **票 02**:`last_valid_follow_time` 已含(✅ 已落)。 +- **票 07**:跟进记录写入时刷新 `last_valid_follow_time`。 +- **票 01**:CONTEXT.md 增术语「超期未跟进提醒」。 +- **crm-rule/CONTEXT.md**:增客户规则族段(对称商机规则段)。 diff --git a/.scratch/customer-module/issues/10-overdue-reminder-rule.md b/.scratch/customer-module/issues/10-overdue-reminder-rule.md index 2a8522f..4ff41d9 100644 --- a/.scratch/customer-module/issues/10-overdue-reminder-rule.md +++ b/.scratch/customer-module/issues/10-overdue-reminder-rule.md @@ -1,7 +1,7 @@ # 10 — 客户规则族「超期未跟进提醒」(crm-rule 全局单例) Type: research -Status: open +Status: resolved Blocked by: ## 问题 @@ -39,4 +39,116 @@ Blocked by: ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 结论速览 + +客户「超期未跟进提醒」= **crm-rule 全局单例配置 + crm-customer 待发通知 + 调度 Job**,三层对称商机「回收提醒」范式,但配置形态是**单例**(不套 V-CONFIG 版本化,chart D4)。「客户查重设置」不做(chart D6)。 + +| 层 | 落点 | 对称对象 | +|---|---|---| +| 配置 | `crm-rule` `customer_reminder_rule`(单行表,id 固定=1) | `opportunity_pool_rule`(但非版本化) | +| 待发通知 | `crm-customer` `customer_pending_notice` | `opportunity_pending_notice` | +| 调度 | `crm-customer` `CustomerReminderJob`(@Scheduled) | `OpportunityMaintenanceJob` | + +--- + +### B. 配置表 `customer_reminder_rule`(crm-rule) + +**全局单例**:单行表,`id` 固定为 1,种子初始化器 `CustomerReminderRuleInitializer` 植入默认行(对称 `StageTemplateInitializer` 兜底范式)。无版本号、无状态机、无 rule_code。 + +| Java 字段 | 列名 | 类型 | 默认 | 说明 | +|---|---|---|---|---| +| id | id | bigint | 固定 1 | 单例行主键 | +| masterEnabled | master_enabled | tinyint | 1 | 总开关:1启用 0禁用(禁用后 Job 整轮跳过) | +| firstTriggerEnabled | first_trigger_enabled | tinyint | 1 | 首次触发独立开关 | +| firstTriggerDays | first_trigger_days | int | 30 | 首次触发天数(>0) | +| secondIntervalEnabled | second_interval_enabled | tinyint | 1 | 二次提醒独立开关 | +| secondIntervalDays | second_interval_days | int | 7 | 二次提醒间隔天数(>0) | +| updateBy / updateTime | update_by / update_time | — | — | BaseEntity 继承 | + +> ⚠ **孤立单大写字母检查**(AGENTS.md):`masterEnabled`→`master_enabled`、`firstTriggerDays`→`first_trigger_days`、`secondIntervalDays`→`second_interval_days`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 + +**校验规则**(服务层,对称商机规则): +- `firstTriggerDays > 0`、`secondIntervalDays > 0`。 +- 保存 = 全量覆盖(无草稿/发布态,保存即生效)。 +- 只后台管理面板可见(RBAC 功能权限 `customer.reminder.rule`)。 + +**契约**: +``` +GET /api/rule/customer/reminder → 读当前配置(单行) +PUT /api/rule/customer/reminder → 保存配置(全量覆盖,记录操作日志) +``` + +--- + +### C. 触发口径(只前进、确定性提醒链) + +**计时锚点** = `customer.last_valid_follow_time`(票 02 已建;新建客户时初始化为 `create_time`,每次写跟进记录刷新——票 07)。 + +**提醒链**(确定性,可从锚点直接推导,无需存「已发首次」状态): + +``` +第 0 次(首次) plan = anchor + first_trigger_days +第 1 次(二次) plan = anchor + first_trigger_days + 1 × second_interval_days +第 k 次(二次) plan = anchor + first_trigger_days + k × second_interval_days +``` + +- 幂等键 = `(customer_id, notice_type, plan_notify_time)`,同计划时刻永不重写(对称商机 `uk_pending_notice_dedupe`)。 +- Job 每轮对每个客户只补**下一条未建的到期提醒**(不批量堆历史)。 +- **跟进刷新锚点** → 旧锚点的未发提醒**置已失效**(对称商机回收时 invalidate 语义);新锚点重新从首次开始计时。 + +**参与范围**(扫表条件): +- `archive_status = 有效(1)`(归档/合并不提醒) +- `owner_user_id IS NOT NULL`(公海无接收人不提醒,chart 票 10 待拍板点 6) +- `last_valid_follow_time IS NOT NULL`(防御脏数据) +- `master_enabled = 1` 且(`first_trigger_enabled=1` 或 `second_interval_enabled=1`) + +--- + +### D. 待发通知表 `customer_pending_notice`(crm-customer) + +对称 `OpportunityPendingNotice`,字段集一致: + +| Java 字段 | 列名 | 类型 | 说明 | +|---|---|---|---| +| id | id | bigint | ASSIGN_ID 主键 | +| customerId | customer_id | bigint | 客户 ID | +| noticeType | notice_type | varchar(32) | `OVERDUE_REMIND`(本期唯一类型) | +| targetUserId | target_user_id | bigint | 应通知对象 = 客户销售负责人(快照) | +| planNotifyTime | plan_notify_time | datetime | 应发提醒时刻(= 提醒链推导时刻) | +| payloadJson | payload_json | text | 上下文 JSON(见下) | +| noticeStatus | notice_status | tinyint | 0待发 1已发 2已失效 | + +- **唯一约束**:`UNIQUE(customer_id, notice_type, plan_notify_time)`。 +- **索引**:`idx(customer_id, notice_status)` 供消费者扫待发;`idx(notice_status, plan_notify_time)` 供补发调度。 +- **payload JSON**(对称商机无「剩余天数」伪状态):`{ remindSeq(0=首次/k=第k次二次), overdueDays(已无跟进天数), anchor(last_valid_follow_time), firstTriggerDays, secondIntervalDays }`。 + +> 本期**只落待发记录,不直接发送**;平台通知能力(钉钉 + 应用内通知)spin-out,就绪后由另一消费者扫 `notice_status=待发` 补发(对称商机票 05 拍板)。 + +--- + +### E. 调度 Job `CustomerReminderJob`(crm-customer) + +- **cron**:`${crm.customer.reminder-cron:0 0 4 * * ?}`(默认凌晨 4 点,错开线索 2 点 / 商机 3 点)。 +- **流程**(对称 `OpportunityMaintenanceJob`,但无回收分支): + 1. 读 `customer_reminder_rule` 单行;`master_enabled=0` 或无配置 → 整轮跳过。 + 2. 扫参与范围客户(见 C)。 + 3. 对每个客户:算**下一条未建的到期提醒**(`plan ≤ now`),若有则落 `customer_pending_notice`(幂等:已存在同 plan 时刻跳过)。 + 4. 锚点已被刷新的客户:把旧锚点未发的 OVERDUE_REMIND 置已失效。 +- **不发送**:只落待发记录(E 末尾同)。 + +--- + +### F. 规则生效时机(chart Q4-补 用户强调,已确认) + +> 用户原话「规则在客户新增的时候生效」——最终解读:**新建客户时把 `last_valid_follow_time` 初始化为 `create_time`**,客户创建后即开始计时,30 天(首次触发默认值)无跟进 → 首次提醒。**回填票 02**(已落 `last_valid_follow_time` 字段)+ **票 07**(新建跟进时刷新)。 + +--- + +### G. 回填 / 下游影响 + +- **票 02**:`last_valid_follow_time` 已含(✅ 已落)。 +- **票 07**:跟进记录写入时刷新 `last_valid_follow_time`。 +- **票 01**:CONTEXT.md 增术语「超期未跟进提醒」。 +- **crm-rule/CONTEXT.md**:增客户规则族段(对称商机规则段)。 diff --git a/.scratch/customer-module/issues/11-answer.md b/.scratch/customer-module/issues/11-answer.md new file mode 100644 index 0000000..e5477b5 --- /dev/null +++ b/.scratch/customer-module/issues/11-answer.md @@ -0,0 +1,53 @@ +## Answer + +**Status: resolved** + +### A. 接线结论(定稿) + +复用 crm-preference `user_saved_view` 平台能力,**不重写**。rule of three:商机是样本 1,客户是样本 2;样本 3 出现前不抽共享翻译器(各自 `XxxSavedViewFilter`)。 + +### B. scope_key(定稿,3 个) + +| workspace | scope_key | +|---|---| +| 客户总览 | `customer.overview` | +| 我的客户 | `customer.mine` | +| 客户公海 | `customer.pool` | + +- **公海双视角(普通销售 / 领导)共用 `customer.pool`**:字段池相同,视角只影响行内操作(领取 vs 分配),不影响筛选条件。 + +### C. `CustomerSavedViewFilter`(定稿) + +- filter_json → MyBatis-Plus wrapper 翻译器,对称 `OpportunitySavedViewFilter`。 +- **字段池白名单**(票 03 定的可筛选列): + +| 字段 | 支持操作符 | +|---|---| +| 客户名称 / 客户编号 | 包含 / 不包含 | +| 客户类型 / 行业 / 客户阶段 / 客户星级 / 关系星级 / 战略协议等级 / 已成交VIP等级 / 档案状态 / 地区 | 属于任一 / 不属于任一 | +| 最近跟进时间 | 早于 / 晚于 / 从未跟进 / 已有跟进 | +| 进入公海时间(仅 pool) | 早于 / 晚于 | +| 销售负责人 / 销售部门(仅 overview / mine) | 属于任一 / 不属于任一 | +| 创建时间 | 早于 / 晚于 | + +- **未知字段 / 未知操作符 → 跳过**(不报错,对称商机)。 +- **排序字段池**:创建时间 / 最近跟进时间 / 进入公海时间(默认);客户名称等文本字段**不排**(参商机默认口径)。 + +### D. PageParam 接入(定稿) + +- 三 workspace 的 PageParam(`CustomerOverviewPageParam` / `CustomerMinePageParam` / `CustomerPoolPageParam`)均加 `savedViewId`。 +- ViewQueryImpl:有 `savedViewId` → 先调 `SavedViewService.get(viewId, scope_key)` 取 filter → 叠加 wrapper;无 → 不叠加。 +- 前端进入 workspace:先 `GET /api/preference/view/list?scope_key=...` 取 `isDefault` 视图 → 带 `viewId` 调 `/page`(对称商机)。 + +### E. 验收清单(spec 级,不写代码) + +1. scope_key 唯一性:`customer.overview` / `customer.mine` / `customer.pool` 三值互斥,与商机 `opportunity.*` 无碰撞。 +2. filter 白名单:白名单内字段 + 合法操作符生效;白名单外字段/操作符静默跳过。 +3. 默认视图互斥:每 scope 仅一个 `isDefault`(crm-preference 已实现,验证客户三 scope 复用生效)。 +4. 端到端:保存自定义检索 → 列表 → 再次进入带 viewId 恢复条件。 +5. 数据范围不越权:saved-view 只叠加筛选,**不改变** @DataScope 数据范围。 + +### F. 回填 / 下游 + +- **票 03**:字段池白名单来源(票 03 D 节共用列 + 差异列)。 +- **crm-preference**:无改动(纯消费方)。 diff --git a/.scratch/customer-module/issues/11-saved-view-integration.md b/.scratch/customer-module/issues/11-saved-view-integration.md index c60fc62..8fa6695 100644 --- a/.scratch/customer-module/issues/11-saved-view-integration.md +++ b/.scratch/customer-module/issues/11-saved-view-integration.md @@ -1,7 +1,7 @@ # 11 — 自定义视图接入 crm-preference(复用商机 saved-view 平台能力) Type: task -Status: open +Status: resolved Blocked by: 03 ## 目标 @@ -34,4 +34,54 @@ Blocked by: 03 ## Answer -_(待 resolve)_ +**Status: resolved** + +### A. 接线结论(定稿) + +复用 crm-preference `user_saved_view` 平台能力,**不重写**。rule of three:商机是样本 1,客户是样本 2;样本 3 出现前不抽共享翻译器(各自 `XxxSavedViewFilter`)。 + +### B. scope_key(定稿,3 个) + +| workspace | scope_key | +|---|---| +| 客户总览 | `customer.overview` | +| 我的客户 | `customer.mine` | +| 客户公海 | `customer.pool` | + +- **公海双视角(普通销售 / 领导)共用 `customer.pool`**:字段池相同,视角只影响行内操作(领取 vs 分配),不影响筛选条件。 + +### C. `CustomerSavedViewFilter`(定稿) + +- filter_json → MyBatis-Plus wrapper 翻译器,对称 `OpportunitySavedViewFilter`。 +- **字段池白名单**(票 03 定的可筛选列): + +| 字段 | 支持操作符 | +|---|---| +| 客户名称 / 客户编号 | 包含 / 不包含 | +| 客户类型 / 行业 / 客户阶段 / 客户星级 / 关系星级 / 战略协议等级 / 已成交VIP等级 / 档案状态 / 地区 | 属于任一 / 不属于任一 | +| 最近跟进时间 | 早于 / 晚于 / 从未跟进 / 已有跟进 | +| 进入公海时间(仅 pool) | 早于 / 晚于 | +| 销售负责人 / 销售部门(仅 overview / mine) | 属于任一 / 不属于任一 | +| 创建时间 | 早于 / 晚于 | + +- **未知字段 / 未知操作符 → 跳过**(不报错,对称商机)。 +- **排序字段池**:创建时间 / 最近跟进时间 / 进入公海时间(默认);客户名称等文本字段**不排**(参商机默认口径)。 + +### D. PageParam 接入(定稿) + +- 三 workspace 的 PageParam(`CustomerOverviewPageParam` / `CustomerMinePageParam` / `CustomerPoolPageParam`)均加 `savedViewId`。 +- ViewQueryImpl:有 `savedViewId` → 先调 `SavedViewService.get(viewId, scope_key)` 取 filter → 叠加 wrapper;无 → 不叠加。 +- 前端进入 workspace:先 `GET /api/preference/view/list?scope_key=...` 取 `isDefault` 视图 → 带 `viewId` 调 `/page`(对称商机)。 + +### E. 验收清单(spec 级,不写代码) + +1. scope_key 唯一性:`customer.overview` / `customer.mine` / `customer.pool` 三值互斥,与商机 `opportunity.*` 无碰撞。 +2. filter 白名单:白名单内字段 + 合法操作符生效;白名单外字段/操作符静默跳过。 +3. 默认视图互斥:每 scope 仅一个 `isDefault`(crm-preference 已实现,验证客户三 scope 复用生效)。 +4. 端到端:保存自定义检索 → 列表 → 再次进入带 viewId 恢复条件。 +5. 数据范围不越权:saved-view 只叠加筛选,**不改变** @DataScope 数据范围。 + +### F. 回填 / 下游 + +- **票 03**:字段池白名单来源(票 03 D 节共用列 + 差异列)。 +- **crm-preference**:无改动(纯消费方)。 diff --git a/.scratch/customer-module/map.md b/.scratch/customer-module/map.md index 0a7f78a..2b05b04 100644 --- a/.scratch/customer-module/map.md +++ b/.scratch/customer-module/map.md @@ -9,6 +9,8 @@ - **`crm-customer`(新建模块)** — 客户业务域:客户主表 + 归属流转(有主/公海 + 交割换主)+ 客户阶段(潜在→重潜→已成交,只前进)+ 三 workspace(客户总览 / 我的客户 / 客户公海)+ 内置视图 + 自定义视图(复用 crm-preference)+ 新增/编辑(含查重校验 + 企查查工商回填 seam)+ 客户导入 + 客户详情 8 页签 + 跟进记录 + 操作日志 + 客户交割(跨模块同步商机/项目负责人)+ 联系人 CRUD + 全局搜索。 - **`crm-rule`(客户规则族,新增第三族)** — 后台"业务规则"下线索/商机/客户三族并列。客户规则本图**只做「超期未跟进提醒」**(全局单例配置,非版本化)。 +> ⚠️ **待复核**:本 spec 已由用户在 grill Q1–Q19 拍板冻结,但用户要求 “换一个新会话独立复核”。**请先读 `REVIEW-HANDOFF.md`**(含我最担心的 9 个薄弱点 + 5 个我可能漏问的点),再逐条挑战 D1–D29,不要因为 map 里写了“已拍板”就跳过。 + ### 原型出处(蓝湖缓存,已本地提取) - 主文档 `bade4454-52aa-44db-8ba2-dec594732ecb`(`【原型】itc信息化业务中台web端 V1.0`)。 @@ -28,41 +30,94 @@ - **③ 自成子域/复杂特性 → 划出本图**(Not-yet / Out-of-scope 记一笔):战略协议(A4-4-9/10)、联系人图谱(职务二级联动+人脉连线)、**客户查重列表+客户合并(A4-6,grill Q4 拍板不做)**、企查查/OCR 外部服务(只定契约 seam,不深挖服务内部;OCR 快捷新增划出)。 - **[D3] 客户双轴建模(chart grill Q3)** — 对称商机双轴但两轴都更简单: - - **轴1 归属**:**不做显式 tinyint 状态机**,改用两个正交维度:① `owner_user_id`+`owner_dept_id`(快照) 是否为空 = 有主/在公海(公海=owner 空的逻辑视图,对称商机公海);② `archive_status`(有效1/已归档2/已被合并3) 独立字段,归档/合并是它的迁移。迁移动作 = 领取/分配/抛公海/交割(换主)/归档。 - - **轴2 阶段**:主表 `customer_stage`(潜在1/重潜2/已成交3) 单字段,**只增不减、不可配置**(硬编码三态,非商机版本化模板),由外部成功结果驱动。本图**只做潜在→重潜**(重潜由方案卡报备驱动,crm-opportunity 已建);**已成交留 seam** 给 A5。 + - **轴1 归属**:**不做显式 tinyint 状态机**,改用两个正交维度:① `owner_user_id`+`owner_dept_id`(快照) 是否为空 = 有主/在公海(公海=owner 空的逻辑视图,对称商机公海);② `archive_status`(有效1/已归档2) 独立字段,**已归档=删除语义**(软删,用列表筛选条件承载,默认排除已归档,加筛选可查)。迁移动作 = 领取/分配/抛公海/交割(换主)/归档。 + - **轴2 阶段**:主表 `customer_stage`(潜在1/重潜2/已成交3) 单字段,**只增不减、不可配置**(硬编码三态,非商机版本化模板),由外部成功结果驱动。本图定义完整三态字段与只前进 CAS,但**本期两个跃迁均 seam 化不自动触发**:潜在→重潜 由「方案卡提交并报备成功」驱动,报备成功链路本期不可达(D20 改票 B,留 seam);已成交由 A5 项目中标驱动,A5 未建(留 seam)。**本期客户阶段实际只有「潜在」**。 - **[D4] 客户规则落 crm-rule + 全局单例形态(chart grill Q4)** — 客户规则族作 crm-rule 第三族(线索/商机/客户并列)。本图**只做「超期未跟进提醒」**(全局单例配置:总开关 + 首次触发天数(默认30) + 二次提醒间隔(默认7);定时任务 + 钉钉/应用内通知),**不套 V-CONFIG 版本化**(区别于商机规则)。**「客户查重设置」不做**(A4-6 查重执行侧不做,查重口径写死在新增流程)。 +- **[D4-rev1] 客户规则族范围修正(grill Q6,用户拍板)** — 客户规则族只含**两族**:`超期未跟进提醒`(票 10)+ `客户查重设置`(票 04 从「写死」改为**后台可配置**,全局单例形态待 Q7)。**星级设置、客户阶段设置不在客户规则族** → 星级退回硬编码枚举 1~5(票 02 原判),客户阶段退回硬编码三态只前进(票 05 原判)。a7-2-3 里那两页(星级/阶段设置)属于其他族/其他 effort,本图不消费。 + +- **[D4-rev2] 客户查重设置的消费范围(grill Q7,用户拍板 B)** — 客户查重设置做成 crm-rule **全局单例可配置**,本期**只消费于「新增/编辑的内建查重」**(票 04:信用代码硬拦 + 名称相似弹窗改用配置里的匹配方式+相似度 70/80/90%,替换「写死」);**A4-6 查重页留作未来消费者**,本期不做扫描全库/合并。规则字段:总开关、规则组合(任意命中/全部命中)、客户名称(精确/模糊+相似度)、统一社会信用代码(精确)、联系电话(精确)。 + - **[D5] 联系人建模(chart grill Q5)** — **联系人 = `crm-customer` 下独立实体表 `customer_contact`**(不单开 crm-contact 模块);每个联系人 `customer_id` 必属一客户(无孤立),权限继承所属客户(联系人无独立 ACL)。**本图做**:联系人 CRUD(列表/详情/新增/编辑)+ 随客户新增内嵌明细 + 全局搜索接口(商机硬依赖)+ 联系人电话查重。**本图不做**:职务二级联动图谱、人脉关系连线、OCR 快捷新增、联系人导入。职务字段先存「职务名称文本 + 可空 标准职务id/类别/层级」三列占位,不实现联动选择器逻辑。 - **[D6] 新增客户查重口径(chart grill Q4-补)** — 新增/编辑客户的**查重校验做**、内建新增流程:① 统一社会信用代码全局唯一(硬拦),② 客户名称相似→保存时确认弹窗(可"仍要创建")。查重阈值/口径**写死**,不做后台可配。A4-6 独立查重页 + 客户合并**不做**。 +- **[D7] 客户主表字段清单已定稿(票 02 resolved)** — 建 `crm-customer` `customer` 主表,字段分八区:系统字段(BaseEntity)/系统生成(`customer_no`)/基本信息(名称·类型·省市区三级·行业两级·星级×2·人脉关系单位JSON)/归属(owner_user_id+owner_dept_id快照·名快照)/轴2阶段(`customer_stage`)+档案状态(`archive_status`)/快照冗余(`last_valid_follow_time`+战略协议等级+VIP等级)/关联信息(父子客户·总裁班·合作系统)/工商信息(企查查9项+`unified_credit_code` UNIQUE)/补充资料(remark+附件子表)。**无孤立单大写字母陷阱,全表无需 `@Column(name=...)`**。`@DataScope(module="customer", ownerColumn="owner_user_id", deptColumn="owner_dept_id")`。索引:uk_credit_code/idx_owner_stage/idx_dept_stage/idx_name/idx_parent/idx_last_follow。**✅ 已定**:`customer_type` = crm-dict 字典分组 `customer_type`,种子 18 值(见 D18);行业选二级(与商机图D1一致);星级用枚举1~5不走字典(硬编码)。详见 `issues/02`。 + +- **[D8] 客户超期未跟进提醒已定稿(票 10 resolved)** — 三层对称商机「回收提醒」范式但配置形态单例:①`crm-rule` `customer_reminder_rule` 单行表(id固定=1,全局单例,非版本化)存总开关+首次触发天数(默认30)+二次提醒间隔(默认7)+各自独立开关;②`crm-customer` `customer_pending_notice` 待发通知表(UNIQUE(customer_id,notice_type,plan_notify_time)幂等);③`CustomerReminderJob`(@Scheduled cron 默认凌晨4点)只落待发不直接发(平台通知spin-out)。**提醒链确定性**:plan = anchor + firstTriggerDays + k×secondIntervalDays。锚点=`last_valid_follow_time`(**锚点 = 成为「有主」的时刻**:新建即指派=create_time;公海客户被领取=领取时刻;每次跟进刷新)。公海(owner空)/已归档不参与(合并态已按 D19 取消)——公海 `last_valid_follow_time=NULL` 不计时不提醒,领取时赋锚点。**「规则在客户新增时生效」= 新建即指派时初始化 last_valid_follow_time=create_time**。详见 `issues/10`、`issues/02`、`issues/07`。 +- **[D18] 客户类型字典已定稿(grill Q8,用户拍板)** — `customer_type` 落 **crm-dict 字典分组 `customer_type`**(非硬编码枚举),种子 18 值:运营商、土建总包、机电总包、装修总包、教育局、投资(代建)公司、设计院、集成商、集成商(子客户)、咨询造价公司、甲方(企业用户)、甲方(普教)、甲方(中高职)、甲方(政府机关)、竞争同行、个人用户、操手、招标代理。注意含 4 个「甲方(…)」值 + 14 个非甲方角色值,证明客户类型 ≠ 仅甲方,是通用角色字典。主表 `customer_type` 存字典值编码(冗余名快照可选),列表/详情按字典名展示。 + +- **[D19] 归档=删除语义 + 去掉「已被合并」(grill Q9,用户拍板)** — 客户不再有「已被合并」态(合并本就在 Out-of-scope,A4-6 不做,状态值随之取消)。`archive_status` 收敛为 `有效1 / 已归档2` 两值;**已归档即删除**(软删),不做独立 tab/子页,只在列表加一个 `archive_status` 筛选条件:**默认排除已归档**,勾选后查询已归档。总览/我的客户主列表默认只出 `archive_status=1`。票 10 提醒排除条件同步收敛为「公海 + 已归档」。**§5.3 拍板 A(20260902,一期只做归档)**:**不提供任何前端「删除客户」入口**——用户侧只有归档(软删可恢复)+ 合并(源客户系统软删,且合并本体 Out-of-scope);`deleted` 标志仅系统在合并时置 1,不开放手动删除、无前端删除按钮,DB 不做物理删除。详见 `issues/03`。 + +- **[D20] 重潜触发本期不实现、只留 seam(grill Q10 拍板 A → 第三轮 fresh 复核翻案,20260902 改票 B)** — **推翻「已提交(2)触发」的临时口径**。最新原型 `a4-4-1`(8-26)明文「任一方案卡**提交并报备成功**后 → 重潜客户」,是**两个条件**(提交 + 报备成功);而「报备/推送成功」依赖推送链路(spin-out 本期不可达)。据此本期决定:**不自动触发潜在→重潜**,客户阶段实际只有「潜在」;`CustomerStagePort.promote` 的 seam **保留**(契约、CAS 只前进、幂等 全部保留),等报备成功链路(商机方案卡「已推送(3)」或 A5 回调)就绪后再由该链路调用 promote 触发重潜。**不用「已提交」凑合**——避免出现「提交了但报备没成功、客户却已重潜」的错误状态。customer_stage 字段/枚举/三态 CAS 机制不变(D11)。详见 `issues/05`。 + +- **[D21] 相似判定算法 seam 化(grill Q11,用户拍板)** — 客户名称相似度判定**抽成接口/端口**(`CustomerNameSimilarityPort` 或 `CustomerDedupService` 内可替换策略),一期只做**最简实现**,后续迭代可换算法。一期最简实现 = **MySQL ngram 分词器(token_size=2)+ FULLTEXT `MATCH ... AGAINST` 取候选 + 相似度计算**,全在 MySQL 内完成、不引外部服务;阈值从「客户查重设置」配置读取(70/80/90%)。接口契约稳定,算法实现可插拔,后续可换 Levenshtein / Jaccard / 外部相似度服务而不改调用方。详见 `issues/04`。 + +- **[D22] 直属销售总监 = 组织架构推导,不改 crm-auth(grill Q12,用户拍板 A)** — **分两种触发场景**:`transfer_reason=离职` → 系统**自动确定唯一**接收总监(按 from_user 所在部门向上找「部门负责人」`dept.leader_user_id` 或等价,该负责人是「销售总监」角色则命中;无/停用/多条直属关系 → 阻断并提示「联系管理员维护组织关系」);`transfer_reason=岗位调动/区域调整` → **在本人启用的直属销售总监范围内自行搜索选择**(下拉选,**不阻断**)。**不在 SysUser 加 `superior_user_id` 字段**(直属关系是组织属性非用户属性,加字段带来维护/一致性成本)。详见 `issues/08`。 + +- **[D23] 交接单完成判定 = 自动完成(grill Q13,用户拍板 A)** — 交接单状态「已完成」由自定义判定:`assignedCount == totalCount`(全部待分配明细已分配)时自动置为已完成,**不设总监手动「完成交接」按钮**。每行明细独立分配,最后一行分配完自然触发终态;不引入二次确认状态机。详见 `issues/08`。 + +- **[D24] 文件内多行匹配同一客户默认「重复时不导入」(grill Q14,用户拍板 A)** — 导入时文件内多行匹配同一客户,`覆盖导入`/`重复时不导入` 互斥,**默认「重复时不导入」**:重复组全部标失败不写入,用户看失败明细处理。覆盖模式(按文件自上而下、最后一条通过校验的为准)作为可选项存在但不默认。详见 `issues/09`。 + +- **[D25] 超期提醒锚点 = 成为「有主」的时刻(grill Q15,用户拍板 A)** — 修正 D8 锚点语义:`last_valid_follow_time` 锚点在**客户成为有主时**赋初值,**公海不计时不提醒**。① 新建客户**直接指派负责人** → 锚点 = `create_time`(有人负责从建卡起算);② 新建客户**进公海**(无负责人)→ `last_valid_follow_time = NULL`(无锚点,不提醒),**被领取/分配时 → 锚点 = 领取时刻**,从此重计首次提醒天数。`last_valid_follow_time IS NULL` + `owner_user_id IS NOT NULL` 双保险兜住公海。同步影响:票 02(字段初始化语义)、票 07(领取时赋锚点 + 跟进刷新)、票 03(领取/分配动作后置锚点)。详见 `issues/10`。 + +- **[D26] 联系人电话查重 = 可配置软提示(grill Q16 用户拍板 B → 第三轮 fresh 复核翻案,20260902 改票 A)** — **推翻「电话全局唯一索引」硬约束**。最新原型 `a7-3-3-2 客户查重设置`(8-26)明示:**统一社会信用代码**是系统强制规则(重复禁止创建、无关闭开关);**联系电话**仅作「提示疑似重复」,**可被总开关单独关闭**,不禁止创建。据此:① DB **不建** `uk_contact_phone(phone)` 唯一索引;② 电话查重=**可配置软提示**,跟随 crm-rule 客户查重设置单例的「联系电话」开关(默认开);③ 仅保留**表单级校验**——同一客户内两个联系人电话不得重复(来自票 04 B4 内联查重);④ 跨客户电话撞号只提示、不阻断。修正 D12「电话全局唯一」旧口径为「电话查重提示」。详见 `issues/06`。 + +- **[D27] 联系人来源字典 `contact_source` 种子 = 原型 4 项(grill Q17,用户拍板 A)** — crm-dict 字典分组 `contact_source` 种子值:**组会、组局、转介绍、其他**。加值后续在字典补,本期不做额外枚举。详见 `issues/06`。 + +- **[D28] 汇总卡片占位一律渲染 + 显"--"(grill Q18,用户拍板 A)** — 客户详情 4 张汇总卡片(含 A5 预留的「合同总额·累计回款」)**一律渲染卡片**,无数据源/接口失败时值显 `--`(不显 0,不留白),保持布局稳定;A5 建好后自动接数据。与其他外部模块卡片失败显"--"口径统一。详见 `issues/07`。 + +- **[D29] 公海列表「进入公海时间」列保留 + 可排序(grill Q19,用户拍板 A)** — 公海列表 `enter_pool_time` 列保留作固定列,且加入票 11 排序字段池(支持升/降序;降序=先进先看)。复用 saved-view 排序机制,成本极低。详见 `issues/03`。 + +- **[D9] 客户三视图/公海/总览已定稿(票 03 resolved)** — 三个 workspace(公海/总览/我的客户)= 逻辑视图,靠 `@DataScope` + `owner_user_id IS NULL` + `archive_status` 过滤区分,不建独立表。内置视图/常用检索硬编码不入库;三视图模式(列表/卡片/看板),看板分组 = customer_stage / customer_star / relation_star。归属迁移动作枚举(领取/分配/抛公海/交割换主/归档)+ oplog + `enter_pool_time`。已归档走 `archive_status` 筛选(D19),无「已被合并」tab;领取/分配动作后置 `last_valid_follow_time` 锚点(D25)。详见 `issues/03`。 + +- **[D10] 新增/查重/企查查已定稿(票 04 resolved)** — 分层查重(输入相似提示 / 提交精确 / 落库前终检),核心服务 `CustomerDedupService`;信用代码全局唯一硬拦,名称相似弹窗确认。查重口径从「客户查重设置」配置读(D4-rev2)。相似判定算法 seam 化,一期最简 = MySQL ngram FULLTEXT(D21)。企查查 = 外部服务契约 seam(port 占位 + 回填),不深挖服务内部。详见 `issues/04`。 + +- **[D11] 客户阶段只前进已定稿(票 05 resolved)** — `customer_stage` 潜在1/重潜2/已成交3,硬编码三态、只增不减;迁移用 `UPDATE ... WHERE customer_stage < ?` CAS 只前进。重潜触发本期不实现、只留 `CustomerStagePort.promote` seam(D20 改票 B:等「方案卡提交并报备成功」链路就绪再调用;不用「已提交」凑合);seam 契约/CAS/幂等保留。已成交由 A5 项目中标驱动,留 seam。详见 `issues/05`。 + +- **[D12] 联系人 CRUD 已定稿(票 06 resolved)** — `customer_contact`(crm-customer 独立表,权限继承客户,无独立 ACL);契约含 CRUD + 全局搜索(商机硬依赖 promote)+ 随客户新增内嵌明细。电话查重=可配置软提示、不建唯一索引(D26);同客户内电话不重复为表单级校验;`contact_source` 字典种子 = 组会/组局/转介绍/其他(D27)。职务字段三列占位不做联动。详见 `issues/06`。 + +- **[D13] 客户详情/跟进/操作日志已定稿(票 07 resolved)** — 详情页 8 页签骨架(关联项目/战略协议 seam 显 "-");跟进 `customer_follow` append-only,跟进刷新 `last_valid_follow_time`;关联商机页签只读反查 crm-opportunity;团队成员页签;操作日志 `customer_oplog` 字段级(对称商机 oplog 范式,「把 {字段} 从 {旧} 修改为 {新}」);**§5.5 拍板 A(20260902)**:`customer_oplog` **无限期保留**——审计日志不设保留期/清理/冷热分离(原型 L250「日志生成后不可修改或删除」= append-only 审计)。4 张汇总卡片外部模块失败显 "--" + 重试;预留占位卡一律渲染显 "--"(D28)。详见 `issues/07`。 + +- **[D14] crm-customer 领域上下文已建(票 01 resolved)** — 建 `crm-customer/CONTEXT.md` + `CONTEXT-MAP.md` 加行;依赖方向 `crm-opportunity → crm-customer`(商机反查客户/联系人),crm-customer 不反向依赖商机。详见 `issues/01`。 + +- **[D15] 客户交割已定稿(票 08 resolved)** — 两段式(发起交接 / 逐条分配);`customer_transfer` + 明细表,发起时整批原子、分配时每行独立。跨模块副作用:换主同步商机 owner(crm-opportunity),项目 owner 同步留 seam。直属销售总监 = 组织架构推导(D22),完成判定 = 自动完成 assignedCount==totalCount(D23)。`transfer_reason` 字典(离职/岗位调动/区域调整)。详见 `issues/08`。 + +- **[D16] 客户导入已定稿(票 09 resolved)** — 三种导入方式(仅新增 / 仅更新 / 新增或更新);匹配顺序 = 客户编号 → 信用代码,不按名称自动覆盖;异步任务模型(预校验/结果页/失败明细),行级部分成功;模板版本校验。文件内多行匹配同一客户默认「重复时不导入」(D24)。契约 `POST /api/customer/import/upload|/{taskId}/confirm`、`GET /{taskId}|/failures|/page|/template`。详见 `issues/09`。 + +- **[D17] 客户超期提醒规则表 + Job 已定稿(票 10 resolved)** — 见 D8(配置形态/提醒链/锚点)。补充:`customer_pending_notice` 待发通知表(对称 OpportunityPendingNotice,noticeType=OVERDUE_REMIND,notice_status 0待发/1已发/2已失效,payload JSON 含锚点快照);Job 只落待发不直接发(平台通知 spin-out);锚点语义按 D25(成为有主时刻)。详见 `issues/10`。 + +- **[D17b] 客户保存视图接入平台已定稿(票 11 resolved)** — 复用 `user_saved_view` 平台,不新建视图机制;3 个 scope_key(公海/总览/我的客户各一);`CustomerSavedViewFilter` 字段池(未知字段/操作符跳过);默认视图互斥继承平台;`enter_pool_time` 加入排序字段池(D29)。详见 `issues/11`。 + ## Not yet specified | 票 | 主题 | 依赖 | 前沿位置 | |---|---|---|---| -| 01 | 客户域领域词汇 + CONTEXT.md 骨架 (task) | — | ✅ 可开工(无依赖) | -| 02 | **客户主表字段清单** — 扇出最广,多票回填 | — | ✅ 可开工(无依赖) | -| 03 | 归属流转 + 三 workspace + 内置视图查询口径 | 02 | 02 done 后 | -| 04 | 新增查重校验 + 企查查工商回填 seam | 02 | 02 done 后 | -| 05 | 客户阶段(潜在→重潜→已成交,只前进;本图只做前两态) | 02 | 02 done 后 | -| 06 | 联系人 CRUD + 全局搜索 + 商机侧硬依赖接口 | 02 | 02 done 后 | -| 07 | 客户详情 8 页签 + 跟进记录 + 操作日志 | 02, 05 | 02+05 done 后 | -| 08 | 客户交割(跨模块同步商机/项目负责人事务) | 02, 03 | 02+03 done 后 | -| 09 | 客户导入(Excel,OCR 划出) | 02 | 02 done 后 | -| 10 | 客户规则族「超期未跟进提醒」(crm-rule 全局单例) | — | ✅ 可开工(无依赖) | -| 11 | 自定义视图接入 crm-preference(复用商机 saved-view) | 03 | 03 done 后(task) | - -**Frontier(当下可并行开工的票)**:`01`(task,建模块+CONTEXT 骨架)、`02`(主表,扇出最广,优先起 research)、`10`(规则单例,无依赖可并行)。02 resolve 后 03/04/05/06/09 集体解冻,再往下推。 - -**已知 grill 挂号**(不阻塞开票,各票 resolve 时问用户拍板): -- **票 04**:客户名称相似查重的算法/阈值(Trigram / MySQL 全文 / 编辑距离…) -- **票 05**:重潜的触发点到底是「方案卡提交(状态2)」还是「方案卡推送/报备成功(状态3)」? -- **票 08**:直属销售总监怎么定(是否在 SysUser 加 `superior_user_id`)? -- **票 09**:Excel 导入的相似客户处理策略(跳过/创建带标/报错) -- **票 10**:"规则在客户新增时生效"的确认解读——新增客户时初始化 `last_valid_follow_time = create_time`,30 天没跟进首次提醒(本条已在 grill Q4-补 与用户初步对齐,票 resolve 时最终确认) +| 01 | 客户域领域词汇 + CONTEXT.md 骨架 (task) | — | ✅ **resolved**(D14) | +| 02 | **客户主表字段清单** — 扇出最广,多票回填 | — | ✅ **resolved**(D7) | +| 03 | 归属流转 + 三 workspace + 内置视图查询口径 | 02 | ✅ **resolved**(D9) | +| 04 | 新增查重校验 + 企查查工商回填 seam | 02 | ✅ **resolved**(D10) | +| 05 | 客户阶段(潜在→重潜→已成交,只前进;本图只做前两态) | 02 | ✅ **resolved**(D11) | +| 06 | 联系人 CRUD + 全局搜索 + 商机侧硬依赖接口 | 02 | ✅ **resolved**(D12) | +| 07 | 客户详情 8 页签 + 跟进记录 + 操作日志 | 02, 05 | ✅ **resolved**(D13) | +| 08 | 客户交割(跨模块同步商机/项目负责人事务) | 02, 03 | ✅ **resolved**(D15) | +| 09 | 客户导入(Excel,OCR 划出) | 02 | ✅ **resolved**(D16) | +| 10 | 客户规则族「超期未跟进提醒」(crm-rule 全局单例) | — | ✅ **resolved**(D8) | +| 11 | 自定义视图接入 crm-preference(复用商机 saved-view) | 03 | ✅ **resolved**(D17) | + +**Frontier**:**空**——全部 11 张票已 resolve。spec 冻结,交棒 `/implement`。 + +**已知 grill 挂号**(不阻塞开票,各票 resolve 时问用户拍板;已拍板的已销号): +- ~~票 04 相似判定算法~~ **已销号(grill Q11 = seam 化,一期最简)** +- ~~票 05 重潜触发点~~ **已销号(grill Q10 = 保守,方案卡已提交(2))** +- ~~票 03 归档/合并~~ **已销号(grill Q9 = 归档即删除 + 筛选条件)** +- ~~customer_type 枚举~~ **已销号(grill Q8 = 18 值字典)** +- **票 08**:直属销售总监怎么定(是否在 SysUser 加 `superior_user_id`)?【待问】 +- **票 09**:Excel 导入的相似客户处理策略(跳过/创建带标/报错)【待问】 +- **票 10**:"规则在客户新增时生效"的确认解读——新增客户时初始化 `last_valid_follow_time = create_time`(本条已在 grill Q4-补 与用户初步对齐,票 resolve 时最终确认)【待问】 ## Out of scope @@ -73,6 +128,7 @@ - **客户查重列表 + 客户合并**(A4-6-1/2/3)— grill Q4 拍板不做;查重口径写死在新增流程。 - **OCR 快捷新增**(联系人/客户 OCR 识别)— 外部服务,划出;只在新增流程留 seam。 - **企查查等工商数据服务内部**— 只定调用契约 seam(调用点/回填范围/异常/精确查重触发),不深挖服务实现。 -- **客户阶段「已成交客户」**(由 A5 项目首次中标驱动)— 留 seam,本图只做潜在→重潜。 +- **客户阶段「已成交客户」**(由 A5 项目首次中标驱动)— 留 seam,A5 未建。 +- **客户阶段「重潜客户」**(由「方案卡提交并报备成功」驱动)— 留 seam(D20 改票 B),报备成功链路(方案卡已推送3 或 A5 回调)本期不可达;`CustomerStagePort.promote` 契约已定,本期无调用方。**本期客户阶段实际只有「潜在」。** - **「关联项目」详情页签**(读 A5 项目模块)— A5 未建,划出。 - **联系人导入**— 划出(客户导入本图做,联系人导入随图谱 effort)。 diff --git a/.scratch/customer-module/tickets/01-module-skeleton.md b/.scratch/customer-module/tickets/01-module-skeleton.md new file mode 100644 index 0000000..0f0bf4f --- /dev/null +++ b/.scratch/customer-module/tickets/01-module-skeleton.md @@ -0,0 +1,16 @@ +# 01 — 客户模块骨架 + +**What to build:** 创建 `crm-customer` Maven 子模块并挂进根工程与 `crm-app`,使空壳能 `mvn compile` 通过、单体应用能启动、健康检查能通。后续所有客户功能都落在这个模块里;本票只交付"能跑起来的空壳",不含任何业务功能。 + +**Blocked by:** None — can start immediately. + +**Status:** ready-for-agent + +**依赖方向(关键,避免 Maven 循环)**:`crm-customer` 只依赖 `crm-base`/`crm-auth`/`crm-rule`/`crm-dict`/`crm-file`/`crm-preference`。**不依赖 `crm-opportunity`**——「关联商机」反查(票 06)与「商机读客户」(票 11)均通过 customer 侧定义的出站 port 解耦,Maven 依赖只走 `crm-opportunity → crm-customer` 单向,循环依赖靠 Spring DI 解除。 + +- [ ] `crm-customer/pom.xml` 创建,dependencies:`crm-base` / `crm-auth` / `crm-rule` / `crm-dict` / `crm-file` / `crm-preference` + `lombok`(provided) + `spring-boot-starter-test`(test) + `h2`(test)(对称 `crm-opportunity/pom.xml`) +- [ ] 根 `pom.xml` `` 加入 `crm-customer`;`crm-app/pom.xml` 加入 `crm-customer` +- [ ] 包结构建好:`com.crm.customer`(controller / service / mapper / domain.entity / domain.enums / domain.dto / domain.param / port / query / config,对称 opportunity) +- [ ] 最小可行性:`Customer` 实体占位(继承 `BaseEntity` + `@TableName("customer")` + `@Entity @Table`)+ 一个 `GET /api/customer/ping` 健康接口,返回模块名/版本 +- [ ] 验证:`mvn compile` 全工程通过;`crm-app` 启动成功;`@SpringBootApplication(scanBasePackages="com.crm")` + `@MapperScan("com.crm.**.mapper")` + `@EntityScan("com.crm")` 自动扫到 customer 的 controller/mapper/实体 +- [ ] 源码规范:所有 `.java` 为 UTF-8 无 BOM(AGENTS.md);无孤立单大写字母触发 Hibernate 命名策略(命名检查清单) diff --git a/.scratch/customer-module/tickets/02-customer-crud-dedup.md b/.scratch/customer-module/tickets/02-customer-crud-dedup.md new file mode 100644 index 0000000..c20edfb --- /dev/null +++ b/.scratch/customer-module/tickets/02-customer-crud-dedup.md @@ -0,0 +1,19 @@ +# 02 — 客户主表 CRUD + 查重校验 + +**What to build:** 客户主数据能新增/编辑/查询/分页,且新增/编辑时内建三层查重校验(名称相似提示、信用代码唯一硬拦、并发兜底)。这是全模块的扇出底座——字段定义、DataScope、索引、查重端口都会被后续票复用。 + +**Blocked by:** 01 — 客户模块骨架 + +**Status:** ready-for-agent + +**Spec 依据(决策细节详见 .scratch/customer-module/issues/02-answer.md 与 04-answer.md)**:票 02 全字段清单 + 票 04 三层查重口径。本票只做主表实体与查重,**不含**联系人(票 03)、视图/归属(票 04)、详情页(票 06)。 + +- [ ] `customer` 实体全字段(票 02-answer §A1),含 `@DataScope(module="customer", ownerColumn="owner_user_id", deptColumn="owner_dept_id")`;系统字段继承 `BaseEntity`;`deleted` 默认 0 且**一期仅系统在合并时置 1**(§5.3 拍板 A,不开放手动删除/无前端删除入口) +- [ ] `@Table` 索引全部建好(票 02 A5):`uk_unified_credit_code`(unified_credit_code, MySQL NULL 不参与唯一约束) / `idx_owner_stage` / `idx_dept_stage` / `idx_name`(customer_name, FULLTEXT ngram token_size=2,供相似查重) / `idx_parent` / `idx_last_follow` +- [ ] 显式 `@Column(name=...)` 检查:票 02 §A2 结论为**全表无需**——所有字段均完整词+完整词,无孤立单大写陷阱,按清单复核后落实体 +- [ ] CRUD 契约:`POST /api/customer`(新增)/ `PUT /api/customer/{id}`(编辑)/ `GET /api/customer/{id}`(详情带出)/ `GET /api/customer/page`(分页)。编辑走 CAS 乐观锁 +- [ ] **三层查重**(票 04 §B1):L1 名称停输防抖(~300ms)相似提示(只提示不阻止,无权命中脱敏为"存在匹配记录");L2 保存时信用代码精确一致→硬拦禁止 + 名称相似→弹窗"仍要创建/返回修改";L3 仍要创建后写库前再查一次(信用代码+名称)仍冲突才写,DB UNIQUE 兜底 +- [ ] `CustomerNameSimilarityPort` 出站端口 seam(票 04 §B2):契约稳定、实现可插拔;一期最简实现=MySQL `MATCH(customer_name) AGAINST(?)` 取 topN + 判定"相似";阈值从 crm-rule「客户查重设置」读取(70/80/90%,非写死) +- [ ] 信用代码校验(票 04 §B3):格式 `^[0-9A-Z]{18}$`;精确相等→硬拦;命中无权查看的客户只提示"系统已存在该企业",不显 name/编号/负责人 +- [ ] 新增时初始化:`customer_stage`=1(潜在)、`archive_status`=1(有效);若直接指派负责人,`last_valid_follow_time`=create_time;若进公海,`last_valid_follow_time`=NULL(票 10 锚点语义) +- [ ] 服务层集成测试(H2 MySQL 模式):新增/编辑/查重三层的正常与冲突路径全覆盖 diff --git a/.scratch/customer-module/tickets/03-contact-subentity.md b/.scratch/customer-module/tickets/03-contact-subentity.md new file mode 100644 index 0000000..22d1232 --- /dev/null +++ b/.scratch/customer-module/tickets/03-contact-subentity.md @@ -0,0 +1,18 @@ +# 03 — 联系人子表 + +**What to build:** 客户下的联系人能增删改查、全局搜索,且联系人权限继承客户(无独立 ACL)。同客户内电话不重复做表单级校验;跨客户电话命中只软提示、不禁止(D26 口径)。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/06-answer.md)**:联系人独立表 `customer_contact` + 商机侧硬依赖契约。D26 翻案后口径:**不建电话唯一索引**,跨客户电话=可配置软提示。 + +- [ ] `customer_contact` 实体(票 06 §A 全字段):customer_id(NOT NULL,无孤立联系人) / name / jobTitleName / jobTitleId / jobTitleCategory / jobTitleLevel / phone / isKeyContact / isInternal / giftRemark / source + BaseEntity;`deleted` 逻辑删除 +- [ ] 索引:`idx_contact_customer`(customer_id, deleted);`idx_contact_name`(name)。**不建** `uk_contact_phone`(D26 已撤销) +- [ ] 权限模型(票 06):**无独立 `@DataScope`**;服务层按所属客户校验——对客户有读权限→对其全部联系人有读权限;无客户读写权限时隐藏联系人增删改入口、服务端仍校验 +- [ ] CRUD 契约:`POST /api/customer/contact` / `PUT /api/customer/contact/{id}` / `DELETE /api/customer/contact/{id}` / `GET /api/customer/{id}/contacts`(按客户查列表) +- [ ] 全局搜索:`GET /api/customer/contact/search`(带出所属客户名,商机「商机关键联系人」数据源) +- [ ] **电话查重(D26 最终口径)**:同一客户内同表格电话相同两行→禁止提交并定位重复行(表单级硬校验);无电话→按"客户+姓名+职务"提示疑似重复(只提示);跨客户电话命中→**软提示疑似重复、不阻断**,受 crm-rule「客户查重设置→联系电话」开关控制(默认开、可关) +- [ ] **商机侧硬依赖契约**:`POST /api/customer/quick-create`(最小必填集快速创建);`GET /api/customer/search`(名称/联系人/电话/类型四维模糊+分页)——供票 11 对接 +- [ ] 手机号格式校验;联系人不设独立 ACL 的服务层测试覆盖 diff --git a/.scratch/customer-module/tickets/04-workspaces-ownership.md b/.scratch/customer-module/tickets/04-workspaces-ownership.md new file mode 100644 index 0000000..0d6cc0b --- /dev/null +++ b/.scratch/customer-module/tickets/04-workspaces-ownership.md @@ -0,0 +1,24 @@ +# 04 — 三 workspace 视图 + 归属流转 + +**What to build:** 客户能在「我的客户 / 客户总览 / 客户公海」三个 workspace 里按各自数据范围列出、切换视图模式,并支持归属流转动作(领取 / 分配 / 抛公海 / 归档 / 恢复)。三视图是逻辑视图不落池表;每个归属动作写操作日志、走 CAS 乐观锁。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/03-answer.md)**:D9 三视图=逻辑视图;D3 归属⊥档案双轴;D19 归档=删除语义(无独立 tab)。 + +- [ ] 三 workspace 查询(票 03 §A,逻辑视图**不落库**): + - 我的客户 = 当前用户为销售负责人或协同人 且 `archive_status=1` 且 owner 非空 + - 客户总览 = `@DataScope` 覆盖范围内 未删除、未进公海(默认只列 `archive_status=1`) + - 客户公海 = `archive_status=1` 且 `owner_user_id IS NULL` + - 管理职务不扩大「我的客户」范围(走「客户总览」) +- [ ] 内置视图口径(票 03 §B,**硬编码不落库**):我的客户(我负责的/我协同的/待跟进的/我关注的/最近访问);公海(最近访问/我关注的);总览(最近访问/我关注的/我负责的/我协同的) +- [ ] 视图模式(票 03 §C)+ **看板三分组页签**(客户阶段/客户星级/关系星级);⚠ 本期「客户阶段」分组下客户均堆在「潜在客户」列,重潜/已成交列恒空=预期行为(D20 改票 B),不得造假阶段数据 +- [ ] 共用列 + 差异列(票 03 §D):公海列含 `enter_pool_time`「进入公海时间」、无负责人列 +- [ ] **新增字段回填**:`customer.enter_pool_time`(票 03 §F,抛公海写入 / 领取·分配清空) +- [ ] **归属流转动作**(票 03 §E):领取 / 分配 / 抛公海 / 归档(1→2) / 恢复(2→1),各带前置校验("无进行中商机"读 crm-opportunity opp_status=2;**项目侧留 seam** A5 未建先跳过;归档/合并仅销售负责人可) +- [ ] 批量操作部分失败不回滚(逐条独立事务,跳过不可操作项);**例外**:批量归档整批预检,任一失败整批不执行 +- [ ] 领取/分配副作用(D25,票 10 锚点):`last_valid_follow_time` 为空时→赋当前时刻;抛公海/归属迁移不动锚点 +- [ ] 每个归属动作写 `customer_oplog`(票 06);CAS 乐观锁 + version +- [ ] `@DataScope` 映射(票 03 §G):总览套 `DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)`;公海按可见部门范围 diff --git a/.scratch/customer-module/tickets/05-customer-stage-seam.md b/.scratch/customer-module/tickets/05-customer-stage-seam.md new file mode 100644 index 0000000..eef845f --- /dev/null +++ b/.scratch/customer-module/tickets/05-customer-stage-seam.md @@ -0,0 +1,17 @@ +# 05 — 客户阶段(seam 版) + +**What to build:** 客户主记录有一条只前进不回退的阶段指针(潜在→重潜→已成交),字段/枚举/CAS 机制全部按三态建好,但**本期不自动触发**任何进阶——只暴露一个提阶 port 留给未来链路调用。每次阶段变化写系统操作日志。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/05-answer.md)**:D11 只前进 CAS;**D20 第三轮 fresh 复核翻案(20260902 用户拍板 B)= 本期不自动触发重潜、只留 seam**。旧 answer 票曾写"本期做前两态"=已废口径,本票按新口径。 + +- [ ] `customer_stage` 三态枚举(票 05 §A):潜在1(新建默认) / 重潜2 / 已成交3;只增不减、不可手动改(无面向前端 setter API、阶段条不可点);父子客户不联动 +- [ ] 只前进 CAS(票 05 §B):`UPDATE customer SET customer_stage=? WHERE id=? AND customer_stage < ?`(天然幂等);已是更高阶段=no-op 静默成功,不报错不写日志 +- [ ] `CustomerStagePort.promote(customerId, targetStage, triggerSource)` seam(票 05 §D):只前进、传入更低阶段=no-op、返回提升后阶段 +- [ ] **本期无调用方**(D20 改票 B):`triggerSource` 示例 `scheme_card_reported`(方案卡提交并报备成功,本期链路不可达)/`project_won`(A5 seam);~~`scheme_card_submitted`~~ 弃用(不用"已提交"单条件凑合触发,避免脏状态) +- [ ] 失败语义(票 05 §E):阶段提升是**派生冗余视图**,失败**不阻断**主业务(方案卡照常提交);记日志告警 + 幂等重试;弱一致即可(区别于 lead→opp 强一致主业务) +- [ ] 每次阶段变化写 `customer_oplog`(票 06),操作人=`系统` + 注明触发来源 +- [ ] 单测:只前进 CAS 的幂等/no-op/并发路径覆盖 diff --git a/.scratch/customer-module/tickets/06-detail-follow-oplog.md b/.scratch/customer-module/tickets/06-detail-follow-oplog.md new file mode 100644 index 0000000..9f8fd04 --- /dev/null +++ b/.scratch/customer-module/tickets/06-detail-follow-oplog.md @@ -0,0 +1,23 @@ +# 06 — 客户详情页(8 页签 + 跟进记录 + 操作日志) + +**What to build:** 客户详情页能展示公共头部 + 汇总卡片 + 8 个页签;跟进记录能追加(不可改删)且写库刷新超期提醒锚点;操作日志按 append-only 审计记录并可分页查询。外部模块页签(关联项目/战略协议)显"--"占位。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验;05 — 客户阶段(seam 版) + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/07-answer.md)**:D13 详情 8 页签 + append-only 跟进 + 字段级 oplog;§5.5 拍板 A = oplog 无限期保留。 + +**关联商机反查的依赖方向(关键)**:「关联商机」页签需读 crm-opportunity,但 Maven 依赖只能 `opportunity → customer`。故 customer 侧**定义出站 port**(如 `CustomerOpportunityQueryPort`),由 crm-opportunity 实现(对称 ADR-0029 lead→opp port 范式,票 11 落实现);本票只定义 port + 页签消费契约,反查真源接线归票 11。 + +- [ ] 公共头部(票 07 §A,8 页签共享):客户名称/阶段只读条/客户星级/关系星级/战略协议等级(快照)/已成交VIP等级(快照)/档案状态/负责人+部门/最近跟进摘要/待跟进提醒/关联业务计数 +- [ ] 汇总卡片 4 张(票 07 §A):均外部模块提供,接口失败显"--"+重试、不显 0(grill Q18) +- [ ] 8 页签骨架(票 07 §A):1 客户信息(票02) / 2 联系人(票03) / 3 跟进记录 / 4 关联商机(port 反查) / 5 关联项目(A5 显"--") / 6 战略协议(显"--") / 7 团队成员 / 8 操作日志;首次打开才加载,切回保留条件/分页 +- [ ] `customer_follow` 跟进记录(票 07 §B):followWay(字典 follow_way)/followContent/nextFollowTime/跟进人快照;**提交后不可改删**(纠错=新增补充);防重复提交 +- [ ] 跟进写库副作用(关键):`customer.last_valid_follow_time = now`(超期提醒锚点刷新,票 09);更新"待跟进"状态 +- [ ] 跟进契约:`POST /api/customer/{id}/follow` / `GET /api/customer/{id}/follow/page`(时间倒序+日期分组,按 follow_way/关键字/时间范围过滤) +- [ ] 团队成员页签(票 07 §D):销售负责人快照 + 协同人列表;`GET /api/customer/{id}/members` +- [ ] `customer_oplog`(票 07 §E):action 枚举(CREATE/UPDATE/FOLLOW/ARCHIVE/RESTORE/TRANSFER/CLAIM/POOL/STAGE_CHANGE)/detail`把{字段}从{旧}修改为{新}`/操作人快照;**只追加不可改删**;**§5.5 无限期保留**(不设保留期/清理/冷热分离) +- [ ] oplog 契约:`GET /api/customer/{id}/oplog/page`(时间倒序+日期分组+关键字/时间范围过滤) +- [ ] `CustomerOpportunityQueryPort` seam 定义(关联商机页签只读反查,真源归票 11);`GET /api/customer/{id}/detail` +- [ ] crm-dict 新增 `follow_way` 分组(微信/拜访/电话/邮件/会议) diff --git a/.scratch/customer-module/tickets/07-customer-transfer.md b/.scratch/customer-module/tickets/07-customer-transfer.md new file mode 100644 index 0000000..f3dc06a --- /dev/null +++ b/.scratch/customer-module/tickets/07-customer-transfer.md @@ -0,0 +1,19 @@ +# 07 — 客户交割 + +**What to build:** 离职/调动/区域调整的销售本人能发起本人名下客户的两段式交割(发起交接→客户分配),交接单整批原子生成,接收总监再逐客户/批量分配给具体销售;换主时同步 crm-opportunity 商机负责人(项目侧留 seam)。 + +**Blocked by:** 04 — 三 workspace 视图 + 归属流转 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/08-answer.md)**:D15 两段式交割;D22 直属总监组织推导(20260902 复核补:离职=系统自动唯一确定;调动/区域调整=在启用的直属总监范围内下拉选)。 + +- [ ] 流程模型(票 08 §A,两段式):发起交接(owner→接收总监) → 客户分配(总监→具体销售) +- [ ] 权限:只允许离职/调动/区域调整的销售**本人**发起本人名下客户交接(主管/管理员不可代办) +- [ ] 可交接客户 = 该销售为**唯一负责人** 且 未删除 且 非公海(仅协同人的不纳入) +- [ ] 接收总监(D22):离职=系统自动确定唯一直属销售总监;调动/区域调整=在本人**启用的**直属总监范围内搜索选择 +- [ ] `customer_transfer` 交接单实体(票 08 §B1):transferNo(唯一)/from/toDirector 快照/reason(员工离职·岗位调动·区域调整)/totalCount/assignedCount/status(0待分配·1已完成) +- [ ] `customer_transfer_detail` 明细(票 08 §B2):transferId/customerId/assignStatus/assignUser 快照/assignTime;`UNIQUE(transfer_id, customer_id)` +- [ ] 交接单生成:全部变更成功才生成(整批原子);交接编号 + 状态"待分配" + 进度 0/N +- [ ] 事务语义(票 08 §C,跨模块):换 owner 同步 crm-opportunity 商机负责人;**项目侧留 seam**(A5 未建);每步写 `customer_oplog`(TRANSFER) +- [ ] 契约:发起交接 / 交接单列表 / 分配(逐客户+批量)/ 进度查询 diff --git a/.scratch/customer-module/tickets/08-customer-import.md b/.scratch/customer-module/tickets/08-customer-import.md new file mode 100644 index 0000000..4704f51 --- /dev/null +++ b/.scratch/customer-module/tickets/08-customer-import.md @@ -0,0 +1,19 @@ +# 08 — 客户导入 + +**What to build:** 用户能上传 Excel 异步导入客户主数据 + 联系人子表,三选一导入方式(仅新增/仅更新/新增或更新),行级处理允许部分成功,导入前预校验展示预计数、导入后出结果页与失败明细下载。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验;03 — 联系人子表 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/09-answer.md)**:D16 三模式导入 + 异步 + 行级部分成功;OCR 快捷新增划出(Out-of-scope)。D26 翻案后联系人电话口径。 + +- [ ] 三导入方式(票 09 §B):仅新增/仅更新/新增或更新;匹配顺序=① 客户编号完全一致 → ② 统一社会信用代码完全一致 +- [ ] 匹配边界:编号与信用代码均缺失/均未匹配→不凭名称自动覆盖(名称相似列"疑似重复"本次不写入);分别匹配到不同客户→该行冲突禁止导入;文件内多行匹配同一客户→覆盖/不导入互斥(默认不导入) +- [ ] 字段语义(票 09 §C):新增导入必填空→行失败;更新导入空白单元格=不修改(V1 不支持清空);`customer_stage` 固定=潜在不得指定;禁止导入改负责人/协同人/阶段回退/工商字段/战略协议/汇总字段 +- [ ] **联系人子表口径(D26 翻案后)**:电话匹配本客户→更新;匹配他客户→**软提示疑似重复、不禁止导入**(跟随「联系电话」开关,关则不提示)、**不做跨客户电话唯一硬拦**;电话空→按"客户+姓名+职务"疑似重复不自动覆盖;同文件内同客户电话相同两行→该行失败 +- [ ] 原子单元=单条客户主记录/单条联系人子记录;联系人失败不回滚已成功客户主记录 +- [ ] 任务模型(票 09 §D,对称商机/线索导入范式):异步任务;预校验(文件类型.xls/.xlsx、≤20MB、模板版本、表头无空、单sheet≤50000行、忽略公式/图片);行级部分成功 +- [ ] 结果页:任务状态/总行数/新增成功/更新成功/未变化/失败/提交人/时间;失败明细(sheet/行号/客户识别/失败字段/原因)可下载、敏感值按权限脱敏 +- [ ] 操作日志:记任务ID/方式/模板版本/文件哈希/成功失败数;**不记文件名/内容/敏感原值**;超时异常→标失败不让重复提交 +- [ ] 契约(票 09 §D):upload / {taskId}/confirm / {taskId} / {taskId}/failures / import/page / import/template diff --git a/.scratch/customer-module/tickets/09-overdue-reminder.md b/.scratch/customer-module/tickets/09-overdue-reminder.md new file mode 100644 index 0000000..4cc6ac3 --- /dev/null +++ b/.scratch/customer-module/tickets/09-overdue-reminder.md @@ -0,0 +1,18 @@ +# 09 — 超期未跟进提醒 + +**What to build:** 后台可配一条全局单例的「超期未跟进提醒」规则(首次触发天数/二次间隔天数/开关),调度 Job 定期扫描超期客户并落待发通知。计时锚点=客户"成为有主"的时刻,跟进即刷新。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验;06 — 客户详情页(跟进记录刷新锚点) + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/10-answer.md)**:D8 三层对称商机回收提醒但配置=单例(不套版本化);D15/Q15 锚点=成为有主时刻。 + +- [ ] **crm-rule 侧** `customer_reminder_rule` 单例表(票 10 §B,跨模块改动):单行表 id 固定=1;masterEnabled/firstTriggerEnabled/firstTriggerDays(默认30)/secondIntervalEnabled/secondIntervalDays(默认7);无版本号/状态机/rule_code +- [ ] `CustomerReminderRuleInitializer` 种子初始化器(对称 `StageTemplateInitializer` 兜底范式)植入默认行 +- [ ] 校验:`firstTriggerDays>0`、`secondIntervalDays>0`;保存=全量覆盖(保存即生效);RBAC 功能权限 `customer.reminder.rule` +- [ ] 规则契约:`GET /api/rule/customer/reminder`(读单行)/ `PUT /api/rule/customer/reminder`(全量覆盖+记操作日志) +- [ ] **crm-customer 侧** `customer_pending_notice` 待发通知表(对称 `opportunity_pending_notice`) +- [ ] `CustomerReminderJob`(@Scheduled,对称 `OpportunityMaintenanceJob`):masterEnabled=0 整轮跳过;扫 `last_valid_follow_time` 超期客户写待发通知 +- [ ] 触发口径(票 10 §C,锚点=成为有主时刻,Q15 拍板 A):① 新建直接指派负责人→锚点=create_time;② 新建进公海(无负责人)→锚点=NULL 不提醒,被领取/分配时→锚点=领取时刻;③ 每次写跟进刷新锚点(票 06) +- [ ] 只前进、确定性提醒链;索引 `idx_last_follow`(last_valid_follow_time, owner_user_id) 扫表(票 02 已建) diff --git a/.scratch/customer-module/tickets/10-saved-view-integration.md b/.scratch/customer-module/tickets/10-saved-view-integration.md new file mode 100644 index 0000000..f2f6e88 --- /dev/null +++ b/.scratch/customer-module/tickets/10-saved-view-integration.md @@ -0,0 +1,17 @@ +# 10 — 自定义视图接入 crm-preference + +**What to build:** 三个客户 workspace 各自能保存/加载自定义检索视图,复用 crm-preference 的 `user_saved_view` 平台能力(不重写),通过 filter 白名单把保存的条件翻译成查询、且不越权改变数据范围。 + +**Blocked by:** 04 — 三 workspace 视图 + 归属流转 + +**Status:** ready-for-agent + +**Spec 依据(详见 issues/11-answer.md)**:D17 复用 saved-view 平台;rule of three(商机样本1+客户样本2,不抽共享翻译器)。 + +- [ ] 复用 crm-preference `user_saved_view`,**不重写**(各自 `CustomerSavedViewFilter`) +- [ ] 3 个 scope_key(票 11 §B):客户总览=`customer.overview` / 我的客户=`customer.mine` / 客户公海=`customer.pool`;公海双视角共用 `customer.pool`(视角只影响行内操作,不影响筛选) +- [ ] `CustomerSavedViewFilter`(票 11 §C):filter_json → MyBatis-Plus wrapper 翻译器(对称 `OpportunitySavedViewFilter`) +- [ ] 字段池白名单(票 11 §C):客户名称/编号(含/不含);类型/行业/阶段/星级/关系星级/战略等级/VIP等级/档案状态/地区(属于任一/不属于任一);最近跟进时间(早于/晚于/从未/已有);进入公海时间(仅pool);负责人/部门(仅overview/mine);创建时间。**未知字段/操作符→静默跳过** +- [ ] 排序字段池:创建时间/最近跟进时间/进入公海时间;文本字段不排 +- [ ] PageParam 接入(票 11 §D):三 workspace PageParam 加 `savedViewId`;ViewQueryImpl 有 savedViewId→取 filter 叠加 wrapper +- [ ] 验收(票 11 §E):scope_key 三值互斥且与 opportunity.* 无碰撞;白名单内生效/外跳过;每 scope 仅一个 isDefault;端到端保存→恢复;**saved-view 只叠加筛选不改变 @DataScope** diff --git a/.scratch/customer-module/tickets/11-opportunity-customer-wiring.md b/.scratch/customer-module/tickets/11-opportunity-customer-wiring.md new file mode 100644 index 0000000..4da7eed --- /dev/null +++ b/.scratch/customer-module/tickets/11-opportunity-customer-wiring.md @@ -0,0 +1,18 @@ +# 11 — 商机模块客户接口对接(切真源) + +**What to build:** 把 crm-opportunity 里之前 A4 未建时的降级 stub 全部切到真实 customer 数据源,让商机侧「关联客户 / 关键联系人 / 客户搜索 / 快速建客户」真正打通客户模块;同时接上 customer 侧「关联商机」页签的反查 port。这是清偿 A4 未建时留下的技术债。 + +**Blocked by:** 02 — 客户主表 CRUD + 查重校验;03 — 联系人子表;06 — 客户详情页(关联商机 port 定义) + +**Status:** ready-for-agent + +**背景**:A4 未建期间 crm-opportunity 用了多处降级 stub(返回空列表/仅存引用 id)。customer 建成后需切真源,签名不变。依赖方向只走 `opportunity → customer` 单向;customer 侧「关联商机」反查通过 customer 定义、opportunity 实现的出站 port 解耦(ADR-0029 范式),不形成 Maven 循环。 + +- [ ] `crm-opportunity/pom.xml` 增加依赖 `crm-customer`(单向,`opportunity → customer`) +- [ ] `OpportunitySubController` / `IOpportunitySubService#listCustomerContacts(customerId)`:stub 返回空列表 → 改调 customer `GET /api/customer/{id}/contacts` 真源;`CustomerContactDTO` 由 stub 字段占位 → 真源填充(签名不变) +- [ ] `searchCustomers(param)`:票 04 D-02「候选客户池降级=取关联客户快照去重」→ 切 customer `POST /api/customer/search`(四维模糊+分页)真源 +- [ ] `OpportunityCustomer.customerId`("客户模块 A4 未建:引用 id + 名快照并存"):确认关联真实 customer 记录;`customer_name_snapshot` 免 join 展示保留(快照口径不变) +- [ ] `POST /api/customer/quick-create`(商机弹窗快速建客户入口)真源接线 +- [ ] **customer 侧「关联商机」页签**(票 06 定义的 `CustomerOpportunityQueryPort`):在 crm-opportunity 实现该 port(按 customer_id 反查该客户全部商机:名称/编号/阶段/预计金额/负责人/最近更新),供客户详情页签只读展示 +- [ ] 回归:切真源后 crm-opportunity 原有关联客户/联系人相关测试仍绿;跨模块调用无循环依赖、`mvn compile` 全绿 +- [ ] 清理 stub 上遗留的"A4 未建/降级"注释,改为正式实现说明 diff --git a/.scratch/customer-module/tickets/12-prototype-alignment.md b/.scratch/customer-module/tickets/12-prototype-alignment.md new file mode 100644 index 0000000..f9747eb --- /dev/null +++ b/.scratch/customer-module/tickets/12-prototype-alignment.md @@ -0,0 +1,22 @@ +# 12 — 原型页面对齐验收 + +**What to build:** 全模块开发完成后,按蓝湖原型逐页对齐验收:字段/交互/状态/枚举/按钮逐项核对真实实现与原型是否一致,不符处修正。这是整个客户模块的收口验收,确保交付物与设计稿一致。 + +**Blocked by:** 01–11(全部功能票完成,且 `mvn compile` 通过、crm-app 能启动、各票关键接口冒烟通过) + +**Status:** ready-for-agent + +**验收源**:蓝湖 A4 客户模块原型(docId `bade4454`);fresh 提取文本在 `D:/code/crm-需求梳理/lanhu-mcp/data/axure_extract_bade4454_screenshots/`。逐页对照 `.txt` + 必要时 OCR `.png`(OCR 为 fallback,Windows CJK 路径可能 400)。 + +**逐页对齐清单(至少覆盖)**: + +- [ ] `A4-1-1 客户公海列表`:列集(已成交VIP客户等级/进入公海时间)、操作(详情/关注·取消关注/分配)、检索项、分页 +- [ ] `A4-2-1 客户总览` / `我的客户`:身份筛选(我负责的/我协同的)、看板三分组页签(客户阶段/客户星级/关系星级)、视图模式切换 +- [ ] `A4-3-3-1 新增客户`:全字段 + 客户类型下拉(后台配置字典) + 停输防抖相似查重 + 信用代码硬拦 +- [ ] `A4-4-1 客户详情`:公共头部 + 汇总卡片 4 张(失败显"--") + 8 页签;操作区只有归档/标记重点客户(无删除按钮,§5.3);已归档子页有恢复 +- [ ] `A4-4-8 操作日志`:append-only、字段级旧→新、时间倒序+日期分组 +- [ ] `A4-7-1 客户交割`:两段式、接收总监(离职自动/调动·区域调整下拉选)、整批原子、进度 +- [ ] `A4-3-2 客户导入`:三方式、预校验、结果页、失败明细 +- [ ] `A7-3-3-1 超期未跟进提醒` / `A7-3-3-2 客户查重设置`:单例配置项、查重开关(名称/电话/信用代码) +- [ ] 归档/领取/分配/抛公海动作弹窗文案与前置校验(无进行中商机/项目) +- [ ] 逐项记录"符合/不符→已修正",不符处开子任务修复直至对齐 diff --git a/.scratch/customer-module/tickets/13-api-documentation.md b/.scratch/customer-module/tickets/13-api-documentation.md new file mode 100644 index 0000000..e79a8ac --- /dev/null +++ b/.scratch/customer-module/tickets/13-api-documentation.md @@ -0,0 +1,19 @@ +# 13 — 接口文档(对照原型) + +**What to build:** 全模块开发完成后,对照原型界面产出/完善客户模块接口文档,保证每个原型界面用到的接口契约齐全、参数/出参/状态码/权限/脱敏规则齐备且与实现一致。交付一份可供前端与联调直接使用的接口文档。 + +**Blocked by:** 01–11(全部功能票完成);建议在 12 原型对齐后或并行进行 + +**Status:** ready-for-agent + +**文档载体**:复用现有 knife4j/swagger(OpenAPI)注解体系(对称 crm-opportunity 的 `@Schema`/`@Operation` 写法);接口文档以 knife4j 在线文档为主、必要时补一份 Markdown 汇总。 + +- [ ] 按原型界面梳理接口清单(每个界面→其依赖的接口),确保无遗漏、无孤立接口 +- [ ] 客户 CRUD / 查重(票 02):请求/响应 DTO 的 `@Schema` 描述齐全;查重弹窗返回结构(相似候选/脱敏字段)文档化 +- [ ] 联系人 CRUD + 搜索 + quick-create(票 03):含商机侧硬依赖接口 `search`/`contacts`/`quick-create` 契约 +- [ ] 三 workspace 列表 + 归属流转(票 04):分页出参、看板分组出参、各归属动作入参/前置校验错误码 +- [ ] 详情 8 页签 + 跟进 + oplog(票 06):各页签接口、分页/分组/过滤参数、失败显"--"约定 +- [ ] 交割(票 07)/ 导入(票 08)/ 提醒配置(票 09)/ saved-view(票 10)接口契约 +- [ ] 统一标注:权限(RBAC 功能权限 / @DataScope 数据范围)、字段级脱敏规则、错误码与业务含义 +- [ ] 校验:文档中每个接口都能在 knife4j 中打开并与实现签名一致;参数示例可直接联调 +- [ ] 与票 12 对齐结果保持同步——原型对齐中修正的接口同步更新文档 diff --git a/.scratch/customer-module/tickets/README.md b/.scratch/customer-module/tickets/README.md new file mode 100644 index 0000000..572efb9 --- /dev/null +++ b/.scratch/customer-module/tickets/README.md @@ -0,0 +1,46 @@ +# 客户模块(A4)实现票 — 依赖总览 + +从冻结 spec(`../map.md` + `../../crm-customer/CONTEXT.md` + `../issues/NN-answer.md` 决议票)折出的 tracer-bullet 实现票。每张=一条贯穿 schema→API→测试的垂直切片,blocker-first 编号。 + +## 依赖图 + +``` +01 骨架 (无 blocker) + └─ 02 主表CRUD+查重 (⭐扇出最广) + ├─ 03 联系人子表 + ├─ 04 三workspace+归属流转 + │ ├─ 07 客户交割 + │ └─ 10 saved-view接入 + ├─ 05 客户阶段(seam版) + │ └─ 06 详情(8页签+跟进+oplog) ← 也 blocked by 02 + │ └─ 09 超期提醒 ← 也 blocked by 02 + ├─ 08 客户导入 ← blocked by 02 + 03 + └─ 11 商机客户接口对接 ← blocked by 02 + 03 + 06 +12 原型对齐验收 ← blocked by 01–11 全部 +13 接口文档 ← blocked by 01–11 全部(可与 12 并行) +``` + +## 前沿顺序(可开工的票=其 blocker 全完成) + +| 波次 | 可并行开工 | +|---|---| +| 1 | **01** | +| 2 | **02** | +| 3 | **03** / **04** / **05** | +| 4 | **06**(需02+05) / **07**(需04) / **08**(需02+03) / **10**(需04) | +| 5 | **09**(需02+06) / **11**(需02+03+06) | +| 6 | **12** / **13**(需 01–11 全部 + mvn compile 通过 + crm-app 启动 + 关键接口冒烟) | + +## 每张票 `/implement` 时的通用要求 + +- `.java` 一律 UTF-8 无 BOM;新增 entity 过一遍孤立单大写字母检查清单(AGENTS.md) +- 依赖方向单向 `opportunity → customer`;「关联商机」反查走 customer 定义、opportunity 实现的出站 port(ADR-0029 范式),**不建 Maven 循环** +- 每张票收工前 `mvn compile` 全绿 + 服务层集成测试(H2 MySQL 模式)通过 +- 每张票 `/implement` 之间清 context 重新开始;完成后 `/code-review` + +## 已翻案/新拍板口径(实现时以此为准,勿回退旧 answer 正文) + +- **D20 改票 B**:客户阶段本期**不自动触发重潜**,只留 `CustomerStagePort` seam(票 05) +- **D26 拍板 A**:联系人电话=可配置软提示,**不建电话唯一索引**(票 03/08) +- **§5.3 拍板 A**:一期只做归档,**无前端删除入口**,`deleted` 仅系统合并时置位(票 02/04) +- **§5.5 拍板 A**:`customer_oplog` **无限期保留**(票 06) diff --git a/.scratch/opportunity-e2e/demo/shot-12-13-create-result.png b/.scratch/opportunity-e2e/demo/shot-12-13-create-result.png index 46ca8cf..da172df 100644 Binary files a/.scratch/opportunity-e2e/demo/shot-12-13-create-result.png and b/.scratch/opportunity-e2e/demo/shot-12-13-create-result.png differ diff --git a/.scratch/opportunity-e2e/demo/shot-12-14-detail-after-create.png b/.scratch/opportunity-e2e/demo/shot-12-14-detail-after-create.png index 46ca8cf..fa16a79 100644 Binary files a/.scratch/opportunity-e2e/demo/shot-12-14-detail-after-create.png and b/.scratch/opportunity-e2e/demo/shot-12-14-detail-after-create.png differ diff --git a/.scratch/opportunity-e2e/opp-schema-migration.sql b/.scratch/opportunity-e2e/opp-schema-migration.sql new file mode 100644 index 0000000..c2508ba --- /dev/null +++ b/.scratch/opportunity-e2e/opp-schema-migration.sql @@ -0,0 +1,120 @@ +-- ============================================================ +-- 测试环境 crm 库 schema 补齐(opportunity / opportunity_customer) +-- 背景 20260901:导表后两表为残缺混合结构(缺 party_a_clear 等列, +-- 实体映射全列 SELECT/INSERT → SQLSyntaxErrorException → 50001)。 +-- 用法:Navicat 整段执行 / mysql -uroot -p crm < opp-schema-migration.sql +-- 幂等:列已存在自动跳过(information_schema 判存 + 动态 SQL,MySQL 8 无 ADD COLUMN IF NOT EXISTS)。 +-- 对齐实体:Opportunity.java(L40-230)+ OpportunityCustomer.java + BaseEntity.java。 +-- 更省事的替代:重启 crm-app 容器即可,jpa.hibernate.ddl-auto=update 会自动补齐全部缺列。 +-- ============================================================ + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS crm_add_column $$ +CREATE PROCEDURE crm_add_column( + IN p_table VARCHAR(64), IN p_column VARCHAR(64), IN p_ddl TEXT) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table AND COLUMN_NAME = p_column + ) THEN + SET @s = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_ddl); + PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; + SELECT CONCAT('ADD ', p_table, '.', p_column) AS applied; + END IF; +END $$ + +DELIMITER ; + +-- ---------- opportunity 主表 ---------- +CALL crm_add_column('opportunity','opp_name', "varchar(200) NOT NULL COMMENT '商机名称'"); +CALL crm_add_column('opportunity','opp_source', "varchar(64) NOT NULL COMMENT '商机来源字典code'"); +CALL crm_add_column('opportunity','opp_type', "varchar(64) NOT NULL COMMENT '商机类型字典code'"); +CALL crm_add_column('opportunity','industry_code', "varchar(64) NOT NULL COMMENT '行业字典code'"); +CALL crm_add_column('opportunity','bid_form', "varchar(64) NOT NULL COMMENT '招标形式字典code'"); +CALL crm_add_column('opportunity','locality_type', "varchar(64) NOT NULL COMMENT '项目属地字典code'"); +CALL crm_add_column('opportunity','party_a_clear', "tinyint NOT NULL DEFAULT 0 COMMENT '甲方是否明确'"); +CALL crm_add_column('opportunity','party_a', "varchar(200) NULL COMMENT '甲方名称'"); +CALL crm_add_column('opportunity','province_code', "varchar(12) NOT NULL COMMENT '省份国标code'"); +CALL crm_add_column('opportunity','city_code', "varchar(12) NOT NULL COMMENT '市/区国标code'"); +CALL crm_add_column('opportunity','remark', "text NULL COMMENT '备注'"); +CALL crm_add_column('opportunity','project_amount', "decimal(14,2) NULL COMMENT '项目金额'"); +CALL crm_add_column('opportunity','address', "varchar(255) NULL COMMENT '项目详细地址'"); +CALL crm_add_column('opportunity','owner_user_id', "bigint NULL COMMENT '领取人ID'"); +CALL crm_add_column('opportunity','owner_name_snapshot', "varchar(64) NULL COMMENT '领取人姓名快照'"); +CALL crm_add_column('opportunity','owner_dept_id', "bigint NULL COMMENT '领取人部门快照'"); +CALL crm_add_column('opportunity','origin_dept_id', "bigint NULL COMMENT '原始血缘部门'"); +CALL crm_add_column('opportunity','pool_reason', "varchar(32) NULL COMMENT '进入公海原因'"); +CALL crm_add_column('opportunity','creator_user_id', "bigint NOT NULL COMMENT '创建人ID'"); +CALL crm_add_column('opportunity','opp_status', "tinyint NOT NULL COMMENT '商机状态'"); +CALL crm_add_column('opportunity','current_stage_id', "bigint NOT NULL COMMENT '当前阶段节点ID'"); +CALL crm_add_column('opportunity','stage_template_id', "bigint NOT NULL COMMENT '阶段模板版本行ID'"); +CALL crm_add_column('opportunity','stage_template_version', "varchar(20) NOT NULL COMMENT '阶段模板版本号冗余'"); +CALL crm_add_column('opportunity','source_lead_id', "bigint NULL COMMENT '来源线索ID'"); +CALL crm_add_column('opportunity','source_lead_name', "varchar(200) NULL COMMENT '线索名快照'"); +CALL crm_add_column('opportunity','source_phone', "varchar(32) NULL COMMENT '联系电话快照'"); +CALL crm_add_column('opportunity','source_product_code', "varchar(64) NULL COMMENT '需求产品码快照'"); +CALL crm_add_column('opportunity','claim_time', "datetime NULL COMMENT '领取时刻'"); +CALL crm_add_column('opportunity','last_valid_follow_time', "datetime NULL COMMENT '最近有效跟进时间'"); +CALL crm_add_column('opportunity','primary_customer_id', "bigint NULL COMMENT '主要意向客户ID'"); +CALL crm_add_column('opportunity','primary_customer_name_snapshot', "varchar(200) NULL COMMENT '主客户名快照'"); +CALL crm_add_column('opportunity','key_contact_id', "bigint NULL COMMENT '商机关键联系人ID'"); +CALL crm_add_column('opportunity','key_contact_name_snapshot', "varchar(200) NULL COMMENT '商机关键联系人姓名快照'"); +CALL crm_add_column('opportunity','key_contact_company_snapshot', "varchar(200) NULL COMMENT '关键人所在公司快照'"); +CALL crm_add_column('opportunity','scheme_budget', "decimal(18,2) NULL COMMENT '方案卡预算快照'"); +CALL crm_add_column('opportunity','scheme_card_status', "int NULL COMMENT '方案卡状态快照'"); +CALL crm_add_column('opportunity','pause_time', "datetime NULL COMMENT '暂缓开始时刻'"); +CALL crm_add_column('opportunity','pause_reason', "varchar(32) NULL COMMENT '暂缓原因'"); +CALL crm_add_column('opportunity','pause_expected_restart_date', "date NULL COMMENT '预计重启日期'"); +CALL crm_add_column('opportunity','pause_remark', "varchar(500) NULL COMMENT '暂缓补充说明'"); +CALL crm_add_column('opportunity','version', "int NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'"); +-- BaseEntity 公共列(通常都在,幂等兜底) +CALL crm_add_column('opportunity','creator_id', "varchar(50) NULL COMMENT '创建用户id'"); +CALL crm_add_column('opportunity','updater_id', "varchar(50) NULL COMMENT '更新用户id'"); +CALL crm_add_column('opportunity','create_time', "datetime NULL COMMENT '创建时间'"); +CALL crm_add_column('opportunity','update_time', "datetime NULL COMMENT '更新时间'"); +CALL crm_add_column('opportunity','delete_key', "tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除位'"); + +-- ---------- opportunity_customer 关联客户表 ---------- +CALL crm_add_column('opportunity_customer','opportunity_id', "bigint NOT NULL COMMENT '所属商机ID'"); +CALL crm_add_column('opportunity_customer','customer_id', "bigint NOT NULL COMMENT '客户ID'"); +CALL crm_add_column('opportunity_customer','customer_name_snapshot', "varchar(200) NOT NULL COMMENT '客户名快照'"); +CALL crm_add_column('opportunity_customer','customer_role', "varchar(32) NULL COMMENT '客户角色字典code'"); +CALL crm_add_column('opportunity_customer','is_primary_intended', "tinyint NOT NULL DEFAULT 0 COMMENT '是否主要意向客户'"); +CALL crm_add_column('opportunity_customer','delete_key', "bigint NOT NULL DEFAULT 0 COMMENT '软删除复用键'"); +CALL crm_add_column('opportunity_customer','creator_id', "varchar(50) NULL COMMENT '创建用户id'"); +CALL crm_add_column('opportunity_customer','updater_id', "varchar(50) NULL COMMENT '更新用户id'"); +CALL crm_add_column('opportunity_customer','create_time', "datetime NULL COMMENT '创建时间'"); +CALL crm_add_column('opportunity_customer','update_time', "datetime NULL COMMENT '更新时间'"); + +-- ---------- 索引/约束补齐(不阻塞运行,建议顺手执行) ---------- +SET @ix := (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'opportunity' AND INDEX_NAME = 'idx_source_lead'); +SET @sql := IF(@ix = 0, + 'ALTER TABLE `opportunity` ADD UNIQUE INDEX `idx_source_lead` (`source_lead_id`)', + 'SELECT 1'); +PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st; + +SET @ix := (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'opportunity' AND INDEX_NAME = 'idx_owner_status'); +SET @sql := IF(@ix = 0, + 'ALTER TABLE `opportunity` ADD INDEX `idx_owner_status` (`owner_user_id`, `opp_status`)', + 'SELECT 1'); +PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st; + +SET @ix := (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'opportunity' AND INDEX_NAME = 'idx_owner_dept_status'); +SET @sql := IF(@ix = 0, + 'ALTER TABLE `opportunity` ADD INDEX `idx_owner_dept_status` (`owner_dept_id`, `opp_status`)', + 'SELECT 1'); +PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st; + +DROP PROCEDURE crm_add_column; + +-- 执行完核对(应全部返回 0 行 = 缺列已补齐): +-- SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS +-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('opportunity','opportunity_customer') +-- AND COLUMN_NAME IN ('party_a_clear','project_amount','address','key_contact_id', +-- 'key_contact_name_snapshot','key_contact_company_snapshot', +-- 'customer_role','is_primary_intended'); diff --git a/.scratch/patch-naming-trap.md b/.scratch/patch-naming-trap.md new file mode 100644 index 0000000..2f88ff0 --- /dev/null +++ b/.scratch/patch-naming-trap.md @@ -0,0 +1,45 @@ +### JPA/Hibernate 命名策略:孤立单大写字母陷阱 + +Spring Boot 默认的 `SpringPhysicalNamingStrategy` 把驼峰转蛇形时,**会把"孤立的单个大写字母"当成缩略语(同 `URL` / `ID` 处理),不插下划线**。触发条件:某个大写字母的**左侧是小写或字符串开头、右侧是大写或字符串结尾**。 + +这会导致 Hibernate 建表的物理列名与 MyBatis-Plus 生成 SQL 时的列名(走 `camelToUnderscore`)**不一致**,报错形如: + +``` +java.sql.SQLSyntaxErrorException: Unknown column 'party_a_clear' in 'field list' +``` + +已知踩坑记录: + +| Java 字段 | 期望列名 | Hibernate 实际建成 | MyBatis SQL 写的 | +|---|---|---|---| +| `partyA` | `party_a` | `partya` ❌ | `party_a` | +| `partyAClear` | `party_a_clear` | `partyaclear` ❌ | `party_a_clear` | + +**规则**:Java 字段名里出现下述形态的**孤立单大写字母**时,**必须**在 `@Column` 上显式写 `name="蛇形列名"` 钉死: + +- 结尾单大写:`xxxA`、`xxxB`、`xxxZ` +- 夹在小写与大写词之间的单大写:`xxxAYyy`、`xxxBClear` +- 连续单大写:`xxxABC`(会整段当缩略语,不断词) + +反例(**这些形态不会踩坑,无需 `@Column(name=...)`**): + +- `ownerUserId` → `owner_user_id` ✅(`Id` 是两字符段,按普通词处理) +- `bidForm` → `bid_form` ✅(`Form` 是完整词) +- `provinceCode` → `province_code` ✅ + +**修复模板**: + +```java +@Column(name = "party_a_clear", columnDefinition = "tinyint not null default 0 comment '甲方是否明确'") +private Integer partyAClear = 0; +``` + +**为什么不换命名策略**:换策略会影响全部已建对的列(几十列),代价远大于给个位数问题字段加 `@Column(name=...)`。 + +**为什么线上很久才暴露**:`ddl-auto: update` 只加列不改列。老字段可能长期只走 SELECT(`ResultMap` 兜住)不写入,直到某天走 INSERT/UPDATE 才炸。**新加 Java 字段时**如果撞规则,第一次持久化就会报 `Unknown column`。 + +**新增字段检查清单**(写完 entity、跑起来之前): + +1. 字段名里有没有孤立单大写字母?没有 → 直接过。 +2. 有 → 加 `@Column(name = "snake_case_name", columnDefinition = "...")`。 +3. `ddl-auto: update` 环境下,如果表已经存在且字段名撞过坑,需要一条 `ALTER TABLE t CHANGE 旧列名 新列名 类型 ...;` 手工迁移,Hibernate 不会自动改名。 diff --git a/.scratch/patch_ctx_53.md b/.scratch/patch_ctx_53.md new file mode 100644 index 0000000..06ac60e --- /dev/null +++ b/.scratch/patch_ctx_53.md @@ -0,0 +1 @@ +`archive_status` 单字段(有效 1 / 已归档 2),正交于归属与阶段(D19 去掉「已被合并」态,合并 Out-of-scope,A4-6 不做)。**已归档 = 删除语义(软删)**:默认排除于列表筛选、加 `archive_status` 筛选可查,可恢复(恢复回有效)。仅销售负责人可归档。**§5.3 拍板 A(一期只做归档)**:**不提供任何前端「删除客户」入口**——系统级 `deleted` 软删标志仅在合并时由系统置 1,不开放手动删除、无前端删除按钮;DB 不做物理删除。归档与合并是仅有的两种"移除"用户动作,且归档可恢复、合并本体 Out-of-scope。 diff --git a/.scratch/patch_map_53_55.md b/.scratch/patch_map_53_55.md new file mode 100644 index 0000000..171379e --- /dev/null +++ b/.scratch/patch_map_53_55.md @@ -0,0 +1,3 @@ +总览/我的客户主列表默认只出 `archive_status=1`。票 10 提醒排除条件同步收敛为「公海 + 已归档」。**§5.3 拍板 A(20260902,一期只做归档)**:**不提供任何前端「删除客户」入口**——用户侧只有归档(软删可恢复)+ 合并(源客户系统软删,且合并本体 Out-of-scope);`deleted` 标志仅系统在合并时置 1,不开放手动删除、无前端删除按钮,DB 不做物理删除。详见 `issues/03`。 + +操作日志 `customer_oplog` 字段级(对称商机 oplog 范式,「把 {字段} 从 {旧} 修改为 {新}」);**§5.5 拍板 A(20260902)**:`customer_oplog` **无限期保留**——审计日志不设保留期/清理/冷热分离(原型 L250「日志生成后不可修改或删除」= append-only 审计)。 diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/AGENTS.md b/AGENTS.md index 1d1adea..63e84f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,52 @@ All `.java` source files **must be UTF-8 without BOM** (no `EF BB BF` byte prefi ``` - Run a full BOM scan across all `*.java` files after bulk edits (Write/SearchReplace) and before `mvn compile`. +### JPA/Hibernate 命名策略:孤立单大写字母陷阱 + +Spring Boot 默认的 `SpringPhysicalNamingStrategy` 把驼峰转蛇形时,**会把"孤立的单个大写字母"当成缩略语(同 `URL` / `ID` 处理),不插下划线**。触发条件:某个大写字母的**左侧是小写或字符串开头、右侧是大写或字符串结尾**。 + +这会导致 Hibernate 建表的物理列名与 MyBatis-Plus 生成 SQL 时的列名(走 `camelToUnderscore`)**不一致**,报错形如: + +``` +java.sql.SQLSyntaxErrorException: Unknown column 'party_a_clear' in 'field list' +``` + +已知踩坑记录: + +| Java 字段 | 期望列名 | Hibernate 实际建成 | MyBatis SQL 写的 | +|---|---|---|---| +| `partyA` | `party_a` | `partya` ❌ | `party_a` | +| `partyAClear` | `party_a_clear` | `partyaclear` ❌ | `party_a_clear` | + +**规则**:Java 字段名里出现下述形态的**孤立单大写字母**时,**必须**在 `@Column` 上显式写 `name="蛇形列名"` 钉死: + +- 结尾单大写:`xxxA`、`xxxB`、`xxxZ` +- 夹在小写与大写词之间的单大写:`xxxAYyy`、`xxxBClear` +- 连续单大写:`xxxABC`(会整段当缩略语,不断词) + +反例(**这些形态不会踩坑,无需 `@Column(name=...)`**): + +- `ownerUserId` → `owner_user_id` ✅(`Id` 是两字符段,按普通词处理) +- `bidForm` → `bid_form` ✅(`Form` 是完整词) +- `provinceCode` → `province_code` ✅ + +**修复模板**: + +```java +@Column(name = "party_a_clear", columnDefinition = "tinyint not null default 0 comment '甲方是否明确'") +private Integer partyAClear = 0; +``` + +**为什么不换命名策略**:换策略会影响全部已建对的列(几十列),代价远大于给个位数问题字段加 `@Column(name=...)`。 + +**为什么线上很久才暴露**:`ddl-auto: update` 只加列不改列。老字段可能长期只走 SELECT(`ResultMap` 兜住)不写入,直到某天走 INSERT/UPDATE 才炸。**新加 Java 字段时**如果撞规则,第一次持久化就会报 `Unknown column`。 + +**新增字段检查清单**(写完 entity、跑起来之前): + +1. 字段名里有没有孤立单大写字母?没有 → 直接过。 +2. 有 → 加 `@Column(name = "snake_case_name", columnDefinition = "...")`。 +3. `ddl-auto: update` 环境下,如果表已经存在且字段名撞过坑,需要一条 `ALTER TABLE t CHANGE 旧列名 新列名 类型 ...;` 手工迁移,Hibernate 不会自动改名。 + ## Tooling / harness ### File-edit tool degradation strategy (when `edit` silently drops its payload) diff --git a/CONTEXT-MAP.md b/CONTEXT-MAP.md index 9414910..24303a0 100644 --- a/CONTEXT-MAP.md +++ b/CONTEXT-MAP.md @@ -12,6 +12,7 @@ This is a multi-module Maven monorepo. Each `crm-{域}` module is a bounded cont | `crm-base` | 基础包(通用能力,非业务域) | [`crm-base/CONTEXT.md`](./crm-base/CONTEXT.md) | | `crm-lead` | 线索业务(生命周期/状态机/四视图) | [`crm-lead/CONTEXT.md`](./crm-lead/CONTEXT.md) | | `crm-opportunity` | 商机业务(状态+阶段双轴/四视图/转项目) | [`crm-opportunity/CONTEXT.md`](./crm-opportunity/CONTEXT.md) | +| `crm-customer` | 客户业务(三阶段/三 workspace/联系人/交割) | [`crm-customer/CONTEXT.md`](./crm-customer/CONTEXT.md) | | `crm-rule` | 业务规则域(线索公海池 + 商机规则子域) | [`crm-rule/CONTEXT.md`](./crm-rule/CONTEXT.md) | | `crm-preference` | 列偏好(用户级列显隐/排序) | [`crm-preference/CONTEXT.md`](./crm-preference/CONTEXT.md) | | `crm-app` | 唯一启动入口 | _(暂无独立领域语言)_ | diff --git a/crm-app/.classpath b/crm-app/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-app/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-app/.factorypath b/crm-app/.factorypath new file mode 100644 index 0000000..66b2b4f --- /dev/null +++ b/crm-app/.factorypath @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-app/.project b/crm-app/.project new file mode 100644 index 0000000..84d155d --- /dev/null +++ b/crm-app/.project @@ -0,0 +1,34 @@ + + + crm-app + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974709 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-app/.settings/org.eclipse.core.resources.prefs b/crm-app/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..abdea9a --- /dev/null +++ b/crm-app/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding/=UTF-8 diff --git a/crm-app/.settings/org.eclipse.jdt.apt.core.prefs b/crm-app/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-app/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-app/.settings/org.eclipse.jdt.core.prefs b/crm-app/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-app/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-app/.settings/org.eclipse.m2e.core.prefs b/crm-app/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-app/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-auth/.classpath b/crm-auth/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-auth/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-auth/.factorypath b/crm-auth/.factorypath new file mode 100644 index 0000000..5b79b17 --- /dev/null +++ b/crm-auth/.factorypath @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-auth/.project b/crm-auth/.project new file mode 100644 index 0000000..50e483e --- /dev/null +++ b/crm-auth/.project @@ -0,0 +1,34 @@ + + + crm-auth + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974768 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-auth/.settings/org.eclipse.core.resources.prefs b/crm-auth/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..839d647 --- /dev/null +++ b/crm-auth/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-auth/.settings/org.eclipse.jdt.apt.core.prefs b/crm-auth/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-auth/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-auth/.settings/org.eclipse.jdt.core.prefs b/crm-auth/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-auth/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-auth/.settings/org.eclipse.m2e.core.prefs b/crm-auth/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-auth/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-base/.classpath b/crm-base/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-base/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-base/.factorypath b/crm-base/.factorypath new file mode 100644 index 0000000..92efe8d --- /dev/null +++ b/crm-base/.factorypath @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-base/.project b/crm-base/.project new file mode 100644 index 0000000..52609de --- /dev/null +++ b/crm-base/.project @@ -0,0 +1,34 @@ + + + crm-base + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974777 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-base/.settings/org.eclipse.core.resources.prefs b/crm-base/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-base/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-base/.settings/org.eclipse.jdt.apt.core.prefs b/crm-base/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-base/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-base/.settings/org.eclipse.jdt.core.prefs b/crm-base/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-base/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-base/.settings/org.eclipse.m2e.core.prefs b/crm-base/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-base/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-customer/CONTEXT.md b/crm-customer/CONTEXT.md new file mode 100644 index 0000000..b09ff96 --- /dev/null +++ b/crm-customer/CONTEXT.md @@ -0,0 +1,57 @@ +# 客户业务(crm-customer) + +客户的全生命周期业务域:客户实体与三阶段(潜在 / 重潜 / 已成交)、三个 workspace(客户总览 / 我的客户 / 客户公海)、新增 / 编辑 / 导入、联系人子实体、跟进记录与操作日志(`customer_oplog` **无限期保留**,审计日志不设清理/冷热分离,§5.5)、客户交割(离职/调岗批量换主),以及客户规则族「超期未跟进提醒」。依赖方向单向 `crm-opportunity → crm-customer`(商机读客户、建关联);`crm-customer → crm-rule → (crm-auth / crm-dict / crm-file)`;客户读「关联商机」走下游反查上游(同库单体阶段可接受,见 ADR-0029 循环依赖技术债)。访问控制走三机制模型,`@DataScope(module="customer", ownerColumn="owner_user_id", deptColumn="owner_dept_id")`。 + +## Language + +**客户**: +`crm-customer` 的核心聚合根。归属由 `owner_user_id`(销售负责人)+ `owner_dept_id`(销售部门快照)表达,owner 空 = 在客户公海;档案正交维度 `archive_status`(有效 / 已归档)独立于归属(D19 去掉「已被合并」,合并 Out-of-scope)。阶段字段 `customer_stage`(潜在 / 重潜 / 已成交)只前进不回退,由外部成功结果驱动。 +_Avoid_: 线索(转化前)、商机(下游)、联系人(子实体) + +**销售负责人**: +客户的 `owner_user_id`,当前唯一持有该客户的销售。客户「我的客户」视图的第一层过滤依据(负责人 = 当前用户);抛公海 / 交割 / 分配时变更。协同人可见客户但不视为负责人。 +_Avoid_: 领取人(线索术语)、创建人、持有者 + +**销售部门**: +客户的 `owner_dept_id`,负责人所在部门的快照,随负责人变更而更新(非全生命周期不变,区别于线索的 `dept_id`)。是数据范围过滤的部门维度。 +_Avoid_: 归属部门、池归属部门、owner_dept + +**协同人**: +客户团队中非负责人的成员。协同人可见该客户(进「我的客户」),但不得归档 / 合并。抛公海时清空协同人;交割时不自动恢复原协同人(保留原有其他协同人)。 +_Avoid_: 团队成员(泛指)、共享人 + +**客户阶段**: +`customer_stage` 单字段(潜在 1 / 重潜 2 / 已成交 3),**只前进不回退、不可手动改**,由外部成功结果驱动:潜在 →(任一方案卡**提交并报备成功**)→ 重潜 →(任一项目首次中标赢单)→ 已成交。**本期两个跃迁均 seam 化不自动触发**(D20 改票 B):报备成功链路本期不可达(不用「已提交」凑合,避免造出错误的重潜状态),A5 项目模块未建;`CustomerStagePort.promote` 契约/只前进 CAS/幂等 全部建好,等对应链路就绪再由其调用。**本期客户阶段实际只有「潜在」**。 +_Avoid_: 客户状态、归属状态、客户类型 + +**档案状态**: +`archive_status` 单字段(有效 1 / 已归档 2),正交于归属与阶段(D19 去掉「已被合并」态,合并 Out-of-scope,A4-6 不做)。**已归档 = 删除语义(软删)**:默认排除于列表筛选、加 `archive_status` 筛选可查,可恢复(恢复回有效)。仅销售负责人可归档。**§5.3 拍板 A(一期只做归档)**:**不提供任何前端「删除客户」入口**——系统级 `deleted` 软删标志仅在合并时由系统置 1,不开放手动删除、无前端删除按钮;DB 不做物理删除。归档与合并是仅有的两种"移除"用户动作,且归档可恢复、合并本体 Out-of-scope。 +_Avoid_: 删除、禁用、作废 + +**客户公海**: +`owner_user_id` 为空 且 `archive_status=有效` 的客户的**逻辑视图**(无独立池实体,对称商机公海)。进入公海 = 清 owner + 协同人;领取 = 当前用户成为 owner。有进行中商机 / 项目的客户不得抛公海(项目侧留 seam)。 +_Avoid_: 公海池实体(那是线索规则域概念)、池 + +**客户总览**: +管理视角 workspace,`@DataScope` 覆盖组织范围的未删除、未进入公海客户(含有效 / 已归档两态;默认只列 `archive_status=有效`,已归档走筛选)。管理职务本身不扩大「我的客户」数据范围。 +_Avoid_: 全部客户、全局列表 + +**我的客户**: +销售视角 workspace,当前用户为销售负责人**或协同人** 且 `archive_status=有效` 且非公海的客户。已归档客户 = `archive_status=已归档` 的客户(负责人为当前用户),列表加筛选展示、**不做独立子页/独立 tab**(D19:删除了「已被合并」态、「已归档」用筛选条件承载)。 +_Avoid_: 私海(那是线索术语) + +**联系人**: +`customer_contact` 子实体,每条必属一个客户(`customer_id` NOT NULL),无孤立联系人。无独立 ACL,权限继承所属客户。电话查重=可配置软提示(跟随 crm-rule 客户查重设置「联系电话」开关,不建电话唯一索引,D26)、同客户内电话不重复为表单级校验;职务先存文本名 + 可选标准职务 id/类别/层级占位(二级联动图谱留后续)。 +_Avoid_: 线索联系人、决策人(那是客户字段) + +**人脉关系单位**: +客户下的关系单位名标签集(如「XX 集团」),是**纯文本标签**,非关联到 CRM 客户实体。 +_Avoid_: 关联客户、子客户(父客户关联是另一字段) + +**客户交割**: +销售因员工离职 / 岗位调动 / 区域调整,把本人名下客户批量交给直属销售总监,再由总监分配给具体销售。两段式:发起交接(owner → 总监,整批原子)+ 客户分配(owner → 销售)。同步变更关联商机 / 项目负责人(项目侧留 seam)。只允许离职/调动/调整的销售本人发起。 +_Avoid_: 分配(那是公海/日常分配动作)、转交 + +**超期未跟进提醒**: +客户规则族唯一规则(crm-rule 全局单例配置)。客户 `last_valid_follow_time`(新建 = create_time,每次跟进刷新)距今超过首次触发天数(默认 30)触发首次提醒,之后每隔二次提醒间隔(默认 7)重复提醒。只落待发通知 `customer_pending_notice`,不直接发送(平台通知能力 spin-out)。公海 / 已归档客户不参与(「已被合并」态已随 D19 取消)。 +_Avoid_: 超时回收(那是线索/商机公海回收)、逾期 diff --git a/crm-dict/.classpath b/crm-dict/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-dict/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-dict/.factorypath b/crm-dict/.factorypath new file mode 100644 index 0000000..f228cf0 --- /dev/null +++ b/crm-dict/.factorypath @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-dict/.project b/crm-dict/.project new file mode 100644 index 0000000..112ee90 --- /dev/null +++ b/crm-dict/.project @@ -0,0 +1,34 @@ + + + crm-dict + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974789 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-dict/.settings/org.eclipse.core.resources.prefs b/crm-dict/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-dict/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-dict/.settings/org.eclipse.jdt.apt.core.prefs b/crm-dict/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-dict/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-dict/.settings/org.eclipse.jdt.core.prefs b/crm-dict/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-dict/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-dict/.settings/org.eclipse.m2e.core.prefs b/crm-dict/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-dict/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-file/.classpath b/crm-file/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-file/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-file/.factorypath b/crm-file/.factorypath new file mode 100644 index 0000000..c8f5a56 --- /dev/null +++ b/crm-file/.factorypath @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-file/.project b/crm-file/.project new file mode 100644 index 0000000..18014a3 --- /dev/null +++ b/crm-file/.project @@ -0,0 +1,34 @@ + + + crm-file + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974805 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-file/.settings/org.eclipse.core.resources.prefs b/crm-file/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-file/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-file/.settings/org.eclipse.jdt.apt.core.prefs b/crm-file/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-file/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-file/.settings/org.eclipse.jdt.core.prefs b/crm-file/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-file/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-file/.settings/org.eclipse.m2e.core.prefs b/crm-file/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-file/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-lead/.classpath b/crm-lead/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-lead/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-lead/.factorypath b/crm-lead/.factorypath new file mode 100644 index 0000000..30b5320 --- /dev/null +++ b/crm-lead/.factorypath @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-lead/.project b/crm-lead/.project new file mode 100644 index 0000000..62aab29 --- /dev/null +++ b/crm-lead/.project @@ -0,0 +1,34 @@ + + + crm-lead + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974823 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-lead/.settings/org.eclipse.core.resources.prefs b/crm-lead/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-lead/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-lead/.settings/org.eclipse.jdt.apt.core.prefs b/crm-lead/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-lead/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-lead/.settings/org.eclipse.jdt.core.prefs b/crm-lead/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-lead/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-lead/.settings/org.eclipse.m2e.core.prefs b/crm-lead/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-lead/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-opportunity/.classpath b/crm-opportunity/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-opportunity/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-opportunity/.factorypath b/crm-opportunity/.factorypath new file mode 100644 index 0000000..c32a28f --- /dev/null +++ b/crm-opportunity/.factorypath @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-opportunity/.project b/crm-opportunity/.project new file mode 100644 index 0000000..b7ed06f --- /dev/null +++ b/crm-opportunity/.project @@ -0,0 +1,34 @@ + + + crm-opportunity + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974839 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-opportunity/.settings/org.eclipse.core.resources.prefs b/crm-opportunity/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-opportunity/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-opportunity/.settings/org.eclipse.jdt.apt.core.prefs b/crm-opportunity/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-opportunity/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-opportunity/.settings/org.eclipse.jdt.core.prefs b/crm-opportunity/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-opportunity/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-opportunity/.settings/org.eclipse.m2e.core.prefs b/crm-opportunity/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-opportunity/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java b/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java index 05edd30..b886e18 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java @@ -65,12 +65,15 @@ public class Opportunity extends BaseEntity { @Column(columnDefinition = "varchar(64) not null comment '项目属地字典code'") private String localityType = ""; + // 【命名策略坑】partyA / partyAClear 里的孤立单大写字母 'A' 被 SpringPhysicalNamingStrategy + // 当成缩略语(同 URL/ID 处理),不插下划线,会建成 partya / partyaclear。 + // 必须用 @Column(name=...) 显式钉死物理列名。详见 AGENTS.md「命名策略:孤立单大写字母陷阱」。 @Comment("甲方是否明确 0/1") - @Column(columnDefinition = "tinyint not null default 0 comment '甲方是否明确'") + @Column(name = "party_a_clear", columnDefinition = "tinyint not null default 0 comment '甲方是否明确'") private Integer partyAClear = 0; @Comment("甲方名称(party_a_clear=1 时必填)") - @Column(columnDefinition = "varchar(200) comment '甲方名称'") + @Column(name = "party_a", columnDefinition = "varchar(200) comment '甲方名称'") private String partyA; @Comment("省份国标 code(GB/T 2260,6位)") diff --git a/crm-preference/.classpath b/crm-preference/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-preference/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-preference/.factorypath b/crm-preference/.factorypath new file mode 100644 index 0000000..541539a --- /dev/null +++ b/crm-preference/.factorypath @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-preference/.project b/crm-preference/.project new file mode 100644 index 0000000..dfed46a --- /dev/null +++ b/crm-preference/.project @@ -0,0 +1,34 @@ + + + crm-preference + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974854 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-preference/.settings/org.eclipse.core.resources.prefs b/crm-preference/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/crm-preference/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-preference/.settings/org.eclipse.jdt.apt.core.prefs b/crm-preference/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-preference/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-preference/.settings/org.eclipse.jdt.core.prefs b/crm-preference/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-preference/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-preference/.settings/org.eclipse.m2e.core.prefs b/crm-preference/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-preference/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/crm-rule/.classpath b/crm-rule/.classpath new file mode 100644 index 0000000..df66b20 --- /dev/null +++ b/crm-rule/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-rule/.factorypath b/crm-rule/.factorypath new file mode 100644 index 0000000..30b5320 --- /dev/null +++ b/crm-rule/.factorypath @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crm-rule/.project b/crm-rule/.project new file mode 100644 index 0000000..1f58d84 --- /dev/null +++ b/crm-rule/.project @@ -0,0 +1,34 @@ + + + crm-rule + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1788308974867 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/crm-rule/.settings/org.eclipse.core.resources.prefs b/crm-rule/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..839d647 --- /dev/null +++ b/crm-rule/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/crm-rule/.settings/org.eclipse.jdt.apt.core.prefs b/crm-rule/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..dfa4f3a --- /dev/null +++ b/crm-rule/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/crm-rule/.settings/org.eclipse.jdt.core.prefs b/crm-rule/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1de9b43 --- /dev/null +++ b/crm-rule/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,20 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/crm-rule/.settings/org.eclipse.m2e.core.prefs b/crm-rule/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/crm-rule/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1