|
|
|
|
# CRM 系统开发规范
|
|
|
|
|
|
|
|
|
|
本仓库为 CRM 系统多模块单仓库工程。`crm-base` 是所有业务模块的基础包,**所有业务模块必须依赖 crm-base 并遵守本文档的规范开发**。
|
|
|
|
|
|
|
|
|
|
## 1. 技术栈
|
|
|
|
|
|
|
|
|
|
| 项 | 版本/选型 |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| JDK | 17 |
|
|
|
|
|
| Spring Boot | 3.3.x(父 pom 统一管理) |
|
|
|
|
|
| ORM | MyBatis-Plus(运行时 CRUD)+ JPA 注解(ddl-auto 自动建表,全环境统一) |
|
|
|
|
|
| 认证 | Spring Security + JWT(由 crm-auth 模块实现,base 只依赖 spring-security-core) |
|
|
|
|
|
| 文件存储 | MinIO(对象存储)+ kkFileView(在线预览),由 crm-file 模块实现 |
|
|
|
|
|
| 缓存 | Redis(RedisTemplate 已配置 JSON 序列化) |
|
|
|
|
|
| 文档 | Knife4j(/doc.html) |
|
|
|
|
|
| 工具库 | Hutool、Guava、EasyExcel |
|
|
|
|
|
|
|
|
|
|
## 2. 工程结构与模块命名
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
crm/ # 父工程(本仓库根目录),统一管理依赖版本
|
|
|
|
|
├── crm-base/ # 基础包,不含任何业务
|
|
|
|
|
├── crm-auth/ # 认证与权限:三方扫码登录、JWT、统一用户、部门/角色/菜单、数据权限
|
|
|
|
|
├── crm-file/ # 文件模块:直传/三阶段分片上传(MinIO)、下载、kkFileView 预览
|
|
|
|
|
├── crm-app/ # 唯一启动入口:聚合全部模块打可执行 jar(其他模块不 repackage)
|
|
|
|
|
├── crm-dict/ # 数据字典:两级分组/字典项,业务方按 code 消费
|
|
|
|
|
├── crm-rule/ # 线索公海池配置域:公海池/人员/领取规则/区域(crm-lead 的上游)
|
|
|
|
|
├── crm-lead/ # 线索业务域:7 状态机 + 四视图 + 领取/分配/反馈/转商机/释放
|
|
|
|
|
├── crm-preference/ # 列偏好:用户级列显隐/拖拽排序(与 crm-dict 平行的通用模块)
|
|
|
|
|
├── crm-customer/ # (待建)业务模块示例:客户域
|
|
|
|
|
└── crm-xxx/ # 业务模块命名规范:crm-{业务域},小写中划线
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 新增模块时在根 `pom.xml` 的 `<modules>` 中登记,并在 `<dependencyManagement>` 中管理其版本。
|
|
|
|
|
- 业务模块依赖版本一律继承父 pom,**子模块 pom 中不允许出现 `<version>` 硬编码**(内部模块除外)。
|
|
|
|
|
|
|
|
|
|
## 3. 业务模块内包结构(模板)
|
|
|
|
|
|
|
|
|
|
以客户模块为例,根包 `com.crm.customer`:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
com.crm.customer
|
|
|
|
|
├── controller/ # XxxController,只做参数接收/校验/包装 Result,不写业务逻辑
|
|
|
|
|
├── service/ # 接口:IXxxService extends IBaseService<Xxx>
|
|
|
|
|
│ └── impl/ # 实现:XxxServiceImpl extends BaseServiceImpl<XxxMapper, Xxx>
|
|
|
|
|
├── mapper/ # XxxMapper extends CrmBaseMapper<Xxx>
|
|
|
|
|
├── domain/
|
|
|
|
|
│ ├── entity/ # 实体 Xxx extends BaseEntity(与表一一对应)
|
|
|
|
|
│ ├── dto/ # XxxDTO extends BaseDTO(abstract,出入参双向载体)
|
|
|
|
|
│ ├── vo/ # XxxVO(仅用于视图展示的聚合对象,可选)
|
|
|
|
|
│ ├── param/ # XxxParam extends BaseParam(查询入参)
|
|
|
|
|
│ └── enums/ # 业务枚举,必须实现 HasValueEnum
|
|
|
|
|
├── constant/ # 模块内常量(可选)
|
|
|
|
|
└── task/ # 定时任务(可选,执行前校验 ScheduledTaskProperties.owner)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 启动类约定
|
|
|
|
|
|
|
|
|
|
业务模块的启动类必须扫描到 `com.crm` 下的全部组件与 Mapper:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@SpringBootApplication(scanBasePackages = "com.crm")
|
|
|
|
|
@MapperScan("com.crm.**.mapper")
|
|
|
|
|
@EnableJpaRepositories(considerNestedRepositories = false) // 如不用 JPA Repository 可不加
|
|
|
|
|
public class CustomerApplication { ... }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
建表统一由 JPA 注解 + Hibernate `ddl-auto: update` 完成(全环境一致,不维护 SQL 脚本;运行时 CRUD 全部走 MyBatis-Plus)。
|
|
|
|
|
新增字段的 DEFAULT 值、索引、唯一约束务必通过 `columnDefinition` / `@Table(indexes/uniqueConstraints)` 完整声明:
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
spring:
|
|
|
|
|
jpa:
|
|
|
|
|
hibernate:
|
|
|
|
|
ddl-auto: update
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 4. 类命名规范
|
|
|
|
|
|
|
|
|
|
| 类型 | 命名 | 继承/实现 | 说明 |
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
| 实体 | `Customer` | `BaseEntity` | 与表一一对应,不加后缀 |
|
|
|
|
|
| 传输对象 | `CustomerDTO` | `BaseDTO`(abstract) | 出入参双向载体,业务类继承白拿 id/createTime/updateTime |
|
|
|
|
|
| 视图对象 | `CustomerVO` | - | 多表聚合展示时使用 |
|
|
|
|
|
| 查询参数 | `CustomerParam` | `BaseParam` | 分页/条件查询入参 |
|
|
|
|
|
| 控制器 | `CustomerController` | - | 返回值必须是 `Result<T>` |
|
|
|
|
|
| 服务接口 | `ICustomerService` | `IBaseService<Customer>` | |
|
|
|
|
|
| 服务实现 | `CustomerServiceImpl` | `BaseServiceImpl<CustomerMapper, Customer>` | |
|
|
|
|
|
| Mapper | `CustomerMapper` | `CrmBaseMapper<Customer>` | 自动获得 `batchUpsert` |
|
|
|
|
|
| 枚举 | `CustomerTypeEnum` | `HasValueEnum<T>` | 枚举值常量全大写 |
|
|
|
|
|
| 异常 | `XxxException` | `RuntimeException` | 优先复用 base 的 4 个异常 |
|
|
|
|
|
|
|
|
|
|
## 5. 数据库规范
|
|
|
|
|
|
|
|
|
|
- 表名:`crm_{模块}_{实体}`,全小写下划线,如 `crm_customer_info`、`crm_order_item`。
|
|
|
|
|
- 字段名:全小写下划线(实体字段驼峰自动映射)。
|
|
|
|
|
- **每张表必备公共字段**(由 `BaseEntity` 携带,自动填充,勿手动赋值):
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| `id` | bigint | 雪花 ID 主键(`IdType.ASSIGN_ID`,`CustomIdGenerator` 生成) |
|
|
|
|
|
| `creator_id` | varchar(50) | 创建用户 id,插入时自动填充 |
|
|
|
|
|
| `create_time` | datetime | 创建时间,插入时自动填充 |
|
|
|
|
|
| `updater_id` | varchar(50) | 更新用户 id,插入/更新时自动填充 |
|
|
|
|
|
| `update_time` | datetime | 更新时间,插入/更新时自动填充 |
|
|
|
|
|
| `deleted` | tinyint NOT NULL DEFAULT 0 | 逻辑删除位,删除一律走 `removeById` 系列 |
|
|
|
|
|
|
|
|
|
|
- 多实例部署时必须为每个实例配置不同的雪花机器位:
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
crm:
|
|
|
|
|
snowflake:
|
|
|
|
|
worker-id: 1 # 0~31,实例间互不相同
|
|
|
|
|
datacenter-id: 0 # 0~31
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 6. 接口规范
|
|
|
|
|
|
|
|
|
|
- URL:`/api/{模块}/{资源}`,资源用名词复数或动宾结构,如 `POST /api/customer/customers`、`GET /api/customer/customers/page`。
|
|
|
|
|
- 返回值统一 `Result<T>`;分页返回 `Result<PageResult<T>>`(`Result.page(...)` 一行包装)。
|
|
|
|
|
- HTTP 状态码:业务失败也返回 200,通过 `code`/`success` 判断;仅系统异常 500、路径不存在 404、授权拒绝 403。
|
|
|
|
|
- 入参校验用 `@Valid` + jakarta.validation 注解,校验失败由全局异常处理统一返回 40001。
|
|
|
|
|
- Long 类型(含雪花 ID)已全局序列化为字符串,前端回传时按字符串传即可。
|
|
|
|
|
- 日期时间格式统一 `yyyy-MM-dd HH:mm:ss`(GMT+8)。
|
|
|
|
|
|
|
|
|
|
## 7. 响应码分段(新增须在此登记)
|
|
|
|
|
|
|
|
|
|
| 区间 | 含义 | 归属 |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| 0 | 成功 | base |
|
|
|
|
|
| 400xx | 参数类错误 | base |
|
|
|
|
|
| 401xx | 认证/身份错误 | base / crm-auth |
|
|
|
|
|
| 403xx | 权限错误 | base |
|
|
|
|
|
| 404xx | 资源不存在 | base |
|
|
|
|
|
| 405xx | 请求方式错误 | base |
|
|
|
|
|
| 500xx | 系统/外部调用错误 | base |
|
|
|
|
|
| 60001 | 通用业务错误 | base |
|
|
|
|
|
| 61001~61999 | 认证模块(61001 不支持的登录方式 / 61002 三方授权失败 / 61003 账号禁用 / 61004 部门分配参数非法 / 61005 部门不存在 / 61006 部门下仍有成员拒删 / 61007 部门移动非法防环 / 61008 非本组织成员 / 61010 角色参数非法 / 61011 角色编码已存在 / 61012 内置角色保护 / 61013 该账号已在其他设备在线(带 data 回传 preLoginToken) / 61014 登录确认已失效 / 61015 被顶下线 / 61016 不支持的端类型) | crm-auth |
|
|
|
|
|
| 62001~62999 | 文件模块(62001 大小超限 / 62002 类型禁止 / 62003 上传会话不存在 / 62004 分片不完整 / 62005 预览服务不可用 / 62006 分片大小配置错误) | crm-file |
|
feat(dict): data dictionary module — two-level flat model, CRUD/default/cache/permission/seed (ADR-0015)
- 4 tables (dict_group/dict_item/dict_group_default/dict_ref_count), delete_key soft-delete reuse
- DictGroup/DictItem full CRUD + force-delete + status toggle + enabled-list
- DictReferenceService: increment/decrement/batchApply/isReferenced, same-tx ref_count, no cache
- DictQueryService: Caffeine 10s cache + 2s negative cache for empty results, group.status join
- Default item: INSERT ON DUPLICATE KEY UPDATE, hibernate on group disable, auto-restore
- Permission seeds: 10 perm codes → sys_menu tree, bound to ROLE_ADMIN, no crm-auth import
- Built-in dict initializer: idempotent upsert, builtin=false never touched (US-41), value not overwritten
- 48 tests pass; full project compiles
1 month ago
|
|
|
| 63001~63999 | 数据字典(63001 参数非法 / 63002 分组编码已存在 / 63003 内置字典保护 / 63004 分组下仍有字典项 / 63005 分组不存在或已停用 / 63006 字典编码组内重复 / 63007 字典值同组重复 / 63008 身份字段只读 / 63009 字典项被引用 / 63010 字典项不存在 / 63011 分组不存在 / 63012 仅启用项可设默认) | crm-dict |
|
|
|
|
|
| 64001~64999 | 公海池配置(64001 参数非法 / 64002 池名重复 / 64003 部门已有公海池 / 64004 池不存在 / 64005 池内仍有活跃线索拒删 / 64006 池成员非法 / 64007 区域不存在) | crm-rule |
|
|
|
|
|
| 65001~65999 | 线索业务(65001 线索无效 / 65002 线索不存在 / 65003 起始态不允许迁移 / 65004 非领取人 / 65005 领取规则拒绝 / 65006 日领取上限 / 65007 持有上限 / 65008 转商机失败 / 65009 已转商机不可删改 / 65010 乐观锁 CAS 冲突) | crm-lead |
|
|
|
|
|
|
|
|
|
|
## 8. 异常与断言规范
|
|
|
|
|
|
|
|
|
|
- 业务代码**不要 try-catch 后自己拼 Result**,直接抛异常交给 `GlobalExceptionHandlerAdvice`:
|
|
|
|
|
- 业务规则不满足 → `BusinessErrorException` 或 `AssertUtils.isTrue/notNull(...)`
|
|
|
|
|
- 查不到数据 → `service.getByIdOrThrow(id)` 或 `AssertUtils.exist(...)`(返回 40401)
|
|
|
|
|
- 权限不足 → `PermissionErrorException`
|
|
|
|
|
- 日志:业务异常打 `warn`(无堆栈),系统异常打 `error`(带堆栈)。禁止 `e.printStackTrace()`。
|
|
|
|
|
|
|
|
|
|
## 9. 日志与链路
|
|
|
|
|
|
|
|
|
|
- `TraceIdFilter` 已为每个请求注入 traceId(透传请求头 `X-Trace-Id`,无则生成),并回写响应头。
|
|
|
|
|
- 各模块 logback pattern 必须包含 `%X{traceId}`:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 10. crm-base 能力清单
|
|
|
|
|
|
|
|
|
|
| 包 | 内容 |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| `advice` | `GlobalExceptionHandlerAdvice` 全局异常处理 |
|
|
|
|
|
| `config` | CORS、MyBatis-Plus(分页/防全表更新/批量Upsert注入器)、雪花ID、公共字段填充、Jackson(Long→String、日期格式)、Redis 序列化、Knife4j、定时任务属主 |
|
|
|
|
|
| `constant` | `CommonConstants`(traceId、分页上限、批量条数) |
|
|
|
|
|
| `domain` | `BaseEntity` / `BaseDTO` / `BaseParam` / `Result` / `PageResult` / `ResultCodeEnum` / `HasValueEnum` / `StatusEnum` / 4 个通用异常 |
|
|
|
|
|
| `filter` | `TraceIdFilter` |
|
|
|
|
|
| `mapper` | `CrmBaseMapper`(含 `batchUpsert`) |
|
|
|
|
|
| `security` | `LoginUser` 接口、`SecurityUtils`(当前用户)、`DataVisibility` / `DataVisibilityContext`(数据可见性范围,见 §12) |
|
|
|
|
|
| `annotation` | `@DataScope`(实体级数据权限声明,见 §12) |
|
|
|
|
|
| `service` | `IBaseService` / `BaseServiceImpl`(getByIdOrThrow、pageResult、batchUpsert 分批) |
|
|
|
|
|
| `utils` | `AssertUtils`、`BeanCopyUtils`、`EnumUtils`、`ExcelUtil`(导入/导出)、`PageConverter`(防注入排序)、`ServletUtils`、`TreeUtils` |
|
|
|
|
|
|
|
|
|
|
## 11. 典型用法示例
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
// Controller
|
|
|
|
|
@PostMapping("/page")
|
|
|
|
|
public Result<PageResult<CustomerDTO>> page(@RequestBody CustomerParam param) {
|
|
|
|
|
PageResult<Customer> page = customerService.pageByParam(param);
|
|
|
|
|
return Result.page(page.convert(e -> e.toDTO(CustomerDTO::new)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Service 实现
|
|
|
|
|
@Service
|
|
|
|
|
public class CustomerServiceImpl extends BaseServiceImpl<CustomerMapper, Customer>
|
|
|
|
|
implements ICustomerService {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public PageResult<Customer> pageByParam(CustomerParam param) {
|
|
|
|
|
Page<Customer> page = PageConverter.toMpPage(param);
|
|
|
|
|
lambdaQuery()
|
|
|
|
|
.like(StrUtil.isNotBlank(param.getKeyword()), Customer::getName, param.getKeyword())
|
|
|
|
|
.page(page);
|
|
|
|
|
return new PageResult<>(page);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void changeOwner(Long customerId, String newOwnerId) {
|
|
|
|
|
Customer customer = getByIdOrThrow(customerId);
|
|
|
|
|
AssertUtils.isFalse(newOwnerId.equals(customer.getOwnerId()), "客户已归属该负责人");
|
|
|
|
|
// ...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 12. 认证与权限模块(crm-auth)
|
|
|
|
|
|
|
|
|
|
统一用户 + 三方扫码登录 + 部门/角色/菜单 + 数据权限,业务模块只需依赖 crm-auth 即可获得登录校验与数据权限能力(无需自己配 Security)。
|
|
|
|
|
|
|
|
|
|
### 数据模型
|
|
|
|
|
|
|
|
|
|
| 表 | 说明 |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| `crm_auth_user` | 全 CRM 统一用户,业务表里的负责人/创建人等 id 均指向本表;含 `dept_id` 归属部门(数据权限用) |
|
|
|
|
|
| `crm_auth_identity` | 三方身份绑定,一个用户可绑多个平台身份,唯一约束 (identity_type, union_id) |
|
|
|
|
|
| `sys_dept` | 部门层级(父子关系 + 祖级链) |
|
|
|
|
|
| `sys_menu` | 菜单/按钮资源,目录/菜单/按钮三类型 |
|
|
|
|
|
| `sys_role` | 角色,携带数据范围(DataScopeEnum) |
|
|
|
|
|
| `sys_role_menu` / `sys_user_role` | 角色-菜单、用户-角色多对多关系 |
|
|
|
|
|
|
|
|
|
|
首次启动时 `DataInitializer` 会在 `sys_role` 为空时初始化管理员角色(数据范围=全部)与系统管理菜单。
|
|
|
|
|
|
|
|
|
|
### 登录流程
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
前端扫码拿 authCode ──▶ POST /api/auth/login/dingtalk {authCode}
|
|
|
|
|
后端: authCode 换三方用户(unionId, mobile...)
|
|
|
|
|
├─ 该三方身份已绑定? ─是─▶ 取绑定用户
|
|
|
|
|
├─ 按手机号匹配已有用户? ─是─▶ 复用(跨平台合一) + 补绑身份
|
|
|
|
|
└─ 都没有 ─▶ 首登自动注册 + 绑定身份
|
|
|
|
|
──▶ 签发 JWT(存 Redis) ──▶ 返回 {token, userInfo}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **跨平台身份映射键:手机号**。钉钉、企微用同一手机号视为同一 CRM 用户。要拿到手机号,需在三方开放平台为应用开通手机号权限。
|
|
|
|
|
- **扩展新渠道(如企微)**:实现 `ThirdPartyAuthClient` 接口并注册为 Bean、在 `IdentityTypeEnum` 加枚举、Controller 加一个登录入口即可,登录主流程零改动。
|
|
|
|
|
|
|
|
|
|
### 会话(JWT + Redis)
|
|
|
|
|
|
|
|
|
|
- token 放请求头:`Authorization: Bearer {token}`。
|
|
|
|
|
- 双重校验:先验 JWT 签名,再查 Redis(不存在即已下线/过期);剩余有效期不足一半自动续满(滑动过期)。
|
|
|
|
|
- 强制下线:删除对应 Redis key。
|
|
|
|
|
- 关键配置(生产用环境变量注入):
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
crm:
|
|
|
|
|
auth:
|
|
|
|
|
jwt:
|
|
|
|
|
secret: ${CRM_JWT_SECRET} # 至少 32 位随机串
|
|
|
|
|
ttl-days: 7
|
|
|
|
|
ignore-urls: # 业务模块追加放行路径(默认已放行登录/文档)
|
|
|
|
|
- /api/xxx/callback/**
|
|
|
|
|
dingtalk:
|
|
|
|
|
client-id: ${DINGTALK_CLIENT_ID}
|
|
|
|
|
client-secret: ${DINGTALK_CLIENT_SECRET}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 业务模块如何接入
|
|
|
|
|
|
|
|
|
|
1. pom 依赖 `crm-auth`(会传递带上 crm-base)。
|
|
|
|
|
2. 启动类 `@SpringBootApplication(scanBasePackages = "com.crm")` + `@MapperScan("com.crm.**.mapper")`。
|
|
|
|
|
3. 若需 Hibernate 自动建 auth 表,启动类加 `@EntityScan("com.crm")`(各服务独立部署 auth 时无需关心)。
|
|
|
|
|
4. 业务代码取当前用户:`SecurityUtils.getUserId()` / `getRequiredUserId()`(来自 crm-base)。
|
|
|
|
|
|
|
|
|
|
### 菜单/角色/部门管理接口
|
|
|
|
|
|
|
|
|
|
`SystemController` 提供 `/api/system/**`:菜单树 `GET /menus/tree`(按当前用户角色过滤,前端据此控制路由/UI)、菜单/角色/部门的 save/delete、角色分页 `POST /roles/page`、分配菜单 `POST /roles/assign-menus`、分配角色 `POST /users/assign-roles` 等。
|
|
|
|
|
|
|
|
|
|
### 数据权限(行级过滤)
|
|
|
|
|
|
|
|
|
|
基于 MyBatis 拦截器在 SQL 层自动注入过滤条件,业务代码零侵入:
|
|
|
|
|
|
|
|
|
|
- **声明**:在需要受控的实体类上标 `@DataScope(ownerColumn = "owner_id", deptColumn = "dept_id")`(crm-base 提供),未标注的表不受影响。
|
|
|
|
|
- **生效链路**:JWT 认证通过后 `PermissionService.initDataScopeContext()` 按用户角色取最宽档位、结出部门集合(含子部门档位预先展开子树),整包装入 `DataVisibilityContext`(ThreadLocal,不跳线程);读侧 `DataScopeInterceptor` 拦截 SELECT,把可见范围翻成 WHERE 条件,写侧 `MetaObjectFillHandler` 取同一份身份填归属字段。
|
|
|
|
|
- **档位(`DataScopeLevel`,取值来自 `sys_role.data_scope`)**:1 仅本人(owner_id=当前用户)/ 2 本部门(dept_id IN 部门集合)/ 3 本部门及下属部门(dept_id IN 展开子树后的部门集合)/ 4 全部(不过滤)。多角色取最宽档,无角色按最窄档。
|
|
|
|
|
- **fail-closed**:按部门档位遇上空部门集合则一律不可见(注入 `1=0`),不会退化成不过滤;未知档位等脏配置直接让请求失败而不静默放行。详见 ADR-0006。
|
|
|
|
|
|
|
|
|
|
## 13. 文件模块(crm-file)
|
|
|
|
|
|
|
|
|
|
通用文件上传/下载/预览(MinIO + kkFileView),前端接口与后端内部服务共用。领域术语见 `crm-file/CONTEXT.md`,关键决策见 ADR-0003/0004。
|
|
|
|
|
|
|
|
|
|
### 接口与能力
|
|
|
|
|
|
|
|
|
|
- HTTP(`/api/file/**`):直传 `POST /upload`、详情 `GET /info`、下载 `GET /download`(信封例外:成功二进制流,失败 JSON 信封)、预览地址 `GET /preview-url`、逻辑删除 `POST /delete`、分片三阶段 `POST /multipart/init|upload|complete`。
|
|
|
|
|
- 内部调用:一律注入 `FileApi` 门面(流式/byte[] 上传等),业务表只存 `fileId` 字符串,上传必传业务域 `bizDomain`。
|
|
|
|
|
- 分片上传(中转三阶段,ADR-0004):init 建会话存 Redis(支持带 uploadId 断点续传)→ 逐片写入 MinIO 临时对象 `chunks/{uploadId}/{序号}` → complete 校验齐全后 `composeObject` 合并落库。孤儿分片由 `OrphanChunkCleanupTask` 定时兜底清理。
|
|
|
|
|
- 预览:后端对文件签内网 presigned GET → 拼入 kkFileView onlinePreview 地址返回,浏览器只接触 kkFileView,MinIO 不暴公网。
|
|
|
|
|
- 守门校验:扩展名黑名单、直传上限、单文件总大小上限,越界抛 62xxx(见 §7)。
|
|
|
|
|
|
|
|
|
|
### 关键配置(前缀 `crm.file`,生产凭证用环境变量注入)
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
crm:
|
|
|
|
|
file:
|
|
|
|
|
minio:
|
|
|
|
|
endpoint: http://minio内网地址:9000
|
|
|
|
|
access-key: ${CRM_MINIO_AK}
|
|
|
|
|
secret-key: ${CRM_MINIO_SK}
|
|
|
|
|
bucket: crm # 不存在时启动自动创建
|
|
|
|
|
preview:
|
|
|
|
|
kkfileview-url: ${CRM_KKFILEVIEW_URL} # 必须浏览器可达
|
|
|
|
|
presign-ttl: 10m
|
|
|
|
|
chunk-size: 5MB # 硬下限 5MB(composeObject 约束)
|
|
|
|
|
direct-upload-limit: 10MB # 超过必须走分片
|
|
|
|
|
max-file-size: 1GB
|
|
|
|
|
upload-session-ttl: 24h
|
|
|
|
|
cleanup-cron: 0 0 3 * * ?
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 14. 线索模块(crm-lead)
|
|
|
|
|
|
|
|
|
|
线索全生命周期业务域:7 状态机 + 四视图 + 领取/分配/反馈/转商机/释放,以及超时回收与失效两个定时任务。领域术语与决策见 `crm-lead/CONTEXT.md`,关键决策见 ADR-0018~0022。
|
|
|
|
|
|
|
|
|
|
### 依赖方向
|
|
|
|
|
|
|
|
|
|
单向 `crm-lead → crm-rule → (crm-auth / crm-dict)`;**公海池实体归 crm-rule,本模块只消费**。crm-lead 另依赖 crm-preference(列偏好,单向,crm-preference 不得反向引用,见 §16)。
|
|
|
|
|
|
|
|
|
|
### 接口与能力
|
|
|
|
|
|
|
|
|
|
- HTTP(`/api/lead/**`):四视图分页 `POST /page`、详情 `GET /detail`、历史时间线 `GET /history`,以及领取/分配/反馈/转商机/释放/激活/关注等命令。
|
|
|
|
|
- 四视图(`viewType`,取值域为 `LeadViewType` 枚举):`PUBLIC_POOL`(公海,status=待领取)/ `MY_LEAD`(我的线索,owner=当前用户)/ `MY_FOLLOW`(我的关注)/ `MANAGE`(线索管理,@DataScope 部门天花板);空/非法值回落 `MANAGE`。
|
|
|
|
|
- 批量命令**非原子、逐条 CAS、部分成功**:返回 `BatchResult`(成功 N / 失败 M + 结构化失败原因 `LeadBatchFailReason`)。
|
|
|
|
|
|
|
|
|
|
### 内部模块(读写分离,ADR-0022)
|
|
|
|
|
|
|
|
|
|
`LeadServiceImpl` 已按「深度」拆为三个深模块 + 写侧命令编排:
|
|
|
|
|
|
|
|
|
|
| 深模块 | 藏起的机制 | 契约要点 |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| `LeadTransition` | 7 状态迁移 + 统一 `guard`(起始态/持有人)+ 状态 CAS 乐观锁 | **对 crm-rule/crm-auth 零依赖**;声明式 `allowedFromStatuses()`/`requiresOwner()` |
|
|
|
|
|
| `LeadHistoryRecorder` | 组装 `LeadHistory` + kv 明细序列化 JSON + 落库 | 单方法 `record(...)`,写侧与迁移侧共用;空 kv 存 null |
|
|
|
|
|
| `LeadViewQuery` | 四视图分页 + 详情 + 历史时间线 + 展示字段拼装 | 独占读依赖;自持 `LeadMapper` 用 `selectPage` |
|
|
|
|
|
|
|
|
|
|
- **状态 CAS**:所有写状态的 `UPDATE` 一律带 `AND status IN (合法起始态)`,行数=0 即判「已被他人操作」(ADR-0021)。守卫在 CAS 之上给友好前置(非法态 65003 / 非持有人 65004)。
|
|
|
|
|
- **转商机端口** `OpportunityCreationPort`:商机模块尚不存在,本模块只定接口;首个实现须与 crm-lead 同库共享事务(ADR-0020)。
|
|
|
|
|
|
|
|
|
|
### 定时任务
|
|
|
|
|
|
|
|
|
|
超时回收(RECYCLE)与失效(EXPIRE)两个任务,执行前校验 `ScheduledTaskProperties.owner`;失效任务先于回收任务运行(ADR-0019)。
|
|
|
|
|
|
|
|
|
|
## 15. 公海池配置模块(crm-rule)
|
|
|
|
|
|
|
|
|
|
线索公海池的配置域:公海池实体(归属部门 + 省/市/区)、人员(负责人/协作人/成员)、领取规则、超时回收/失效/领取上限配置、区域字典。领域术语见 `crm-rule/CONTEXT.md`。
|
|
|
|
|
|
|
|
|
|
- **依赖方向**:单向 `crm-rule → (crm-auth / crm-dict)`,**不得反向依赖 crm-lead**——公海池不知道线索存在。公海池表复用 crm-base 数据范围模块。
|
|
|
|
|
- HTTP(`/api/rule/**`):公海池管理 `/api/rule/pool`、区域查询 `/api/rule/region`。
|
|
|
|
|
- 对外读服务:`ISysRegionService`(省市 code→name,供 crm-lead 展示拼装消费)。
|
|
|
|
|
|
|
|
|
|
## 16. 列偏好模块(crm-preference)
|
|
|
|
|
|
|
|
|
|
平台级用户列表列偏好(用户级列显隐 + 拖拽排序),与 crm-dict 平行的通用模块。契约仅 `get / save` 两法。领域术语见 `crm-preference/CONTEXT.md`。
|
|
|
|
|
|
|
|
|
|
- **依赖方向**:只依赖 crm-base。业务方(如 crm-lead)**单向**依赖它;`scope_key` 由业务方约定的稳定 code(如 `lead.public_pool`),crm-preference 仅作不透明字符串存储、不持有字段清单语义。
|
|
|
|
|
- **每菜单独立**:线索域有 4 个 scope(公海 / 我的线索 / 我的关注 / 线索管理),互不同步。候选列清单归业务方定义,本模块只存用户勾选与排序结果(`visible_keys` / `column_order`)。
|
|
|
|
|
- HTTP:`/api/preference/**`。
|