You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
luoweijian e888aeeb42 chore: mark tickets 02/03/04 as resolved (InitBinder + Dict IO) 4 weeks ago
.qoder/repowiki commit 1 month ago
.scratch chore: mark tickets 02/03/04 as resolved (InitBinder + Dict IO) 4 weeks ago
.vscode commit 1 month ago
crm-app chore: stop tracking build artifacts (target/, .idea/, test-output.txt) 4 weeks ago
crm-auth chore: stop tracking build artifacts (target/, .idea/, test-output.txt) 4 weeks ago
crm-base chore: stop tracking build artifacts (target/, .idea/, test-output.txt) 4 weeks ago
crm-dict refactor: align dict tests with DTO return types (ADR-0017 tickets 03/04) 4 weeks ago
crm-file chore: stop tracking build artifacts (target/, .idea/, test-output.txt) 4 weeks ago
docs commit 4 weeks ago
logs feat(auth): role management — ancestor completion, CRUD, permission assignment, cascade delete (ADR-0012) 1 month ago
nginx commit 4 weeks ago
.dockerignore commit 1 month ago
.gitignore chore: stop tracking build artifacts (target/, .idea/, test-output.txt) 4 weeks ago
AGENTS.md first-commit 1 month ago
CONTEXT-MAP.md feat(auth,dict,base): extract PermissionSeeder seam to replace shadow entities (ADR-0016) 4 weeks ago
Checksum commit 1 month ago
Dockerfile commit 4 weeks ago
Downloading commit 1 month ago
Extracting commit 1 month ago
Fetching commit 1 month ago
Installing commit 1 month ago
Latest commit 1 month ago
README.md refactor: BaseDTO 重构为 abstract 基类 + IBaseService 清理死代码 (ADR-0017 ticket 01) 4 weeks ago
Verifying commit 1 month ago
bruno-sync.config.json feat(auth): role management — ancestor completion, CRUD, permission assignment, cascade delete (ADR-0012) 1 month ago
compile-output.txt feat(auth): role management — ancestor completion, CRUD, permission assignment, cascade delete (ADR-0012) 1 month ago
docker-compose.yml commit 4 weeks ago
install.cmd commit 1 month ago
mcp_call.py commit 1 month ago
pom.xml refactor(crm-dict): judgement call #6 #7 — pom version extraction + VO independent fields 1 month ago
settings.xml commit 1 month ago

README.md

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-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:

@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) 完整声明:

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_infocrm_order_item
  • 字段名:全小写下划线(实体字段驼峰自动映射)。
  • 每张表必备公共字段(由 BaseEntity 携带,自动填充,勿手动赋值):
字段 类型 说明
id bigint 雪花 ID 主键(IdType.ASSIGN_IDCustomIdGenerator 生成)
creator_id varchar(50) 创建用户 id,插入时自动填充
create_time datetime 创建时间,插入时自动填充
updater_id varchar(50) 更新用户 id,插入/更新时自动填充
update_time datetime 更新时间,插入/更新时自动填充
deleted tinyint NOT NULL DEFAULT 0 逻辑删除位,删除一律走 removeById 系列
  • 多实例部署时必须为每个实例配置不同的雪花机器位:
crm:
  snowflake:
    worker-id: 1        # 0~31,实例间互不相同
    datacenter-id: 0    # 0~31

6. 接口规范

  • URL:/api/{模块}/{资源},资源用名词复数或动宾结构,如 POST /api/customer/customersGET /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 部门移动非法防环) crm-auth
62001~62999 文件模块(62001 大小超限 / 62002 类型禁止 / 62003 上传会话不存在 / 62004 分片不完整 / 62005 预览服务不可用 / 62006 分片大小配置错误) crm-file
63001~63999 数据字典(63001 参数非法 / 63002 分组编码已存在 / 63003 内置字典保护 / 63004 分组下仍有字典项 / 63005 分组不存在或已停用 / 63006 字典编码组内重复 / 63007 字典值同组重复 / 63008 身份字段只读 / 63009 字典项被引用 / 63010 字典项不存在 / 63011 分组不存在 / 63012 仅启用项可设默认) crm-dict

8. 异常与断言规范

  • 业务代码不要 try-catch 后自己拼 Result,直接抛异常交给 GlobalExceptionHandlerAdvice
    • 业务规则不满足 → BusinessErrorExceptionAssertUtils.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 AssertUtilsBeanCopyUtilsEnumUtilsExcelUtil(导入/导出)、PageConverter(防注入排序)、ServletUtilsTreeUtils

11. 典型用法示例

// 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。
  • 关键配置(生产用环境变量注入):
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,生产凭证用环境变量注入)

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 * * ?