项目:ERP-CRM | 版本:v1.1 | 生成日期:2026-10-05 | 依据:源码分析(v1.1:2026-10-06 一致性回刷,与 16-20 篇对齐)
后端共 45 个 @RestController(含登录/注册/公共上传两个无前缀 Controller),接口为 RuoYi 风格的 REST-ish 设计:路径按"业务域/资源/动作"三层组织,列表 GET /xxx/list、详情 GET /xxx/{id}、新增 POST /xxx、修改 PUT /xxx、删除 DELETE /xxx/{ids}、导出 POST /xxx/export、特殊动作用动词子路径(如 PUT /erp/stock/stock-in/{id}、PUT /erp/transfer/execute/{id}、POST /crm/approval/submit/{contractId})。接口文档由 springdoc 自动生成,开发期访问 http://localhost:9090/swagger-ui/index.html(application.yml springdoc 段;prod 激活时 EcProdConfigVerifier 强制 springdoc.api-docs.enabled=false)。
全局约定(前后端共同遵守的"隐形协议",代码依据标注在括号内):
tenant 请求头——TenantInterceptor 对一切路径生效,缺失即抛"请求缺少租户信息"。前端 utils/request.js 从 cookie 自动注入。Authorization: Bearer <JWT>——JwtAuthenticationTokenFilter 解析后从 Redis login_tokens:{uuid} 取 LoginUser。{code, msg, ...}——成功 code=200;列表额外带 rows/total(TableDataInfo);单体数据在 data 键(AjaxResult.success(data))。AuthenticationEntryPointImpl + ServletUtils.renderString(response.setStatus(200) 硬编码);403 才是真 HTTP 语义之外的 body code(GlobalExceptionHandler 返回 AjaxResult.error(403, "没有权限,请联系管理员授权"),HTTP 仍 200)。pageNum/pageSize(query)——PageUtils.startPage() 从请求参数取,pageSize 被钳制 ≤500;响应 total 为 PageHelper 统计的全量行数。ServiceException,GlobalExceptionHandler 将其 message 原样放入 msg(如"仅待入库状态的采购单可执行入库")。@Log(title, businessType) 异步落 sys_oper_log;@RepeatSubmit + 前端 1 秒窗口防重复提交。POST /common/upload(multipart,单文件 10MB/请求 20MB 上限,扩展名白名单 MimeTypeUtils)与 GET /common/download、GET /common/download/resource;上传产物经 /profile/** 匿名直出(SecurityConfig GET 放行)。以下按 Controller 归组列出全部主要端点("库"列标注该接口数据落在主库 MASTER 还是当前租户库 TENANT;通用 CRUD 五件套以 "CRUD" 缩写表示 list/get/POST/PUT/DELETE)。
| 方法 | 路径 | 用途 | 关键入参 | 响应要点 | 库 |
|---|---|---|---|---|---|
| POST | /login | 登录 | header tenant;body {username,password}(LoginBody);限流 30 次/分/IP | {code:200, token:"..."} | TENANT(用户校验)+ MASTER(租户状态) |
| POST | /logout | 登出 | token | LogoutSuccessHandlerImpl 删除 Redis 会话 | REDIS |
| GET | getInfo | 当前用户信息+权限 | token | {user, roles[], permissions[]} | TENANT |
| GET | getRouters | 动态菜单树 | token | data: RouterVo[](SysMenuServiceImpl.buildMenus) | TENANT |
| POST | /register | 租户注册(真实建库) | body {tenantName, adminName, adminPass, packageId?}(TenantRegisterBody,JSR-303 校验);限流 5 次/时/IP | 成功登记主库并返回 | MASTER+新建 TENANT |
| GET | /captchaImage | 图形验证码 | 已确认移除:验证码链路曾短期恢复后被产品决策移除(git 8339bee→b036eec→前端 e3f64b4,考古见《16-决策记录ADR》ADR-006)。当前源码无该端点与任何 captcha 控制器(全库 grep captcha 仅剩未使用的工具类 common/utils/VerifyCodeUtils.java),LoginBody 仅 username/password 两字段;登录防护保留 IP 限流(30 次/分)+ 密码错误锁定(5 次/10 分钟) | — | |
| GET | /saas/package/list | 套餐列表(注册页匿名可用) | — | data: TenantPackage[] | MASTER |
| GET | /system/tenant/list | 租户管理列表 | 管理端鉴权 | 分页租户登记 | MASTER |
| PUT | /system/tenant | 修改租户(改套餐/状态/有效期) | MasterTenant | 经 updateMasterTenant(XML 含 package_id) | MASTER |
| DELETE | /system/tenant/{id} | 删除租户登记 | id | 存在性校验 | MASTER |
| GET | /system/tenant/quota | 当前租户配额查询 | token | {maxUsers, used} 语义(TenantQuotaService) | MASTER+TENANT |
| POST | /common/upload | 文件上传 | multipart file | {url, fileName, newFileName} | TENANT(本地盘) |
| GET | /common/download、/common/download/resource | 文件下载 | fileName / resource | 文件流 | — |
| 方法 | 路径 | 用途 | 关键入参/要点 |
|---|---|---|---|
| GET/POST/PUT/DELETE | /system/user(list、{userId}、新增、修改、/{userIds}) | 用户 CRUD | 新增/导入先过 TenantQuotaService.checkUserQuota;GET /system/user/ 与 /{userId} 复用同一映射(SysUserController 第 101 行 value = {"/", "/{userId}"}) |
| POST/PUT | /system/user/importData、/importTemplate、/resetPwd、/changeStatus、/authRole、GET /authRole/{userId} | 导入(受配额)、模板、重置密码、状态、分配角色 | Excel 走 ExcelUtil |
| GET | /system/user/profile、PUT /updatePwd、POST /avatar | 个人中心 | 头像限 IMAGE_EXTENSION |
| CRUD | /system/role(+ /dataScope、/authUser/* 等) | 角色 CRUD/数据权限/分配用户 | @DataScope 部门过滤 |
| GET/POST/PUT/DELETE | /system/menu(+ /treeselect、/roleMenuTreeselect/{roleId}) | 菜单管理 | 修改会联动子菜单状态 |
| CRUD | /system/dept(+ /list/exclude/{deptId}) | 部门管理 | 树形(ancestors) |
| CRUD | /system/post(+ /optionselect) | 岗位管理 | — |
| CRUD | /system/dict/type(+ /optionselect、/refreshCache)、/system/dict/data | 字典类型/数据 | 字典缓存 Redis |
| CRUD | /system/config(+ /configKey/{configKey}、/refreshCache) | 参数配置 | — |
| CRUD | /system/notice | 通知公告 | XSS 过滤范围含 /system(excludes /system/notice 例外见 yml) |
| 方法 | 路径 | 用途 | 要点 |
|---|---|---|---|
| GET/DELETE | /monitor/operlog/list、/{operIds}、/clean | 操作日志 | @Log 注解异步写入(LogAspect + AsyncManager) |
| GET/DELETE | /monitor/logininfor/list、/{infoIds}、/clean | 登录日志 | 主库表(登录全局一条链) |
| GET/DELETE | /monitor/online/list、/{tokenId} | 在线用户/强踢 | 遍历 Redis login_tokens:*,删除即下线 |
| GET | /monitor/server | 服务器监控 | 当前源码无此端点,请求得 404(oshi 域对象仍在但无调用方,前端 monitor/server.vue 为遗留死页,见 15 篇第 3 节、17 篇 3.1) |
| GET | /actuator/health、/actuator/prometheus | 健康与指标 | 仅 9091 端口(management.server.port) |
| 方法 | 路径 | 用途 | 关键入参 | 备注 |
|---|---|---|---|---|
| GET | /crm/customer/list | 客户列表 | name/phone/customerRank/customerStatus/createTime + 分页 | crm:customer:list |
| GET | /crm/customer/person-list、/person-customers、/deal-list | 我的客户/成交客户 | owner 维度过滤 | |
| POST | /crm/customer/transfer | 转移客户 | {ids[], newOwner} | 非管理员强制限本人名下 |
| POST | /crm/customer/to-pool | 移入公海 | {ids[], poolType} | 仅客户状态可入池 |
| POST | /crm/customer/export | 导出 | 查询条件 | ExcelUtil,download() blob |
| CRUD | /crm/customer | 客户 CRUD | — | 删除走 deleteCrmCustomerByIdsForOwner 归属校验 |
| GET/POST | /crm/clues/list、/person-list、/export | 线索列表 | — | status=0 线索态 |
| DELETE | /crm/clues/person/{ids} | 删除个人线索 | ids | 归属校验 |
| POST/PUT | /crm/clues/transfer、/to-customer | 线索转移/转客户 | {ids,newOwner} / {ids,newOwner} | 转客户即 CustomerFolder 状态翻转 |
| GET/POST | /crm/pool/list、/receive/{id} | 公海/领取 | id | 领取置 owner=当前用户 |
| CRUD | /crm/comment(+ /customer/{customerId}) | 跟进记录 | customerId | 同步刷新客户 last_followup_time 等(XML) |
| CRUD | /crm/opportunity、/crm/contract、/crm/receivable | 商机/合同/回款 | — | 生成器产物,left join 客户名/合同号 |
| CRUD | /crm/order | CRM 订单 | — | 订单号唯一键 |
| GET | /crm/approval/list?status=&title=&pageNum=&pageSize= | 审批待办/已办 | status(0 待办,1/2 已办过滤结果) | Flowable 查询 + 租户库合同聚合 |
| GET | /crm/approval/{id} | 审批详情 | taskId | — |
| POST | /crm/approval/submit/{contractId} | 提交合同审批 | contractId | crm:contract:edit;重复提交按 review_status 拒绝 |
| POST | /crm/approval/approve | 审批通过/驳回 | body {taskId, status:"1"/"2", opinion} | crm:approval:approve;驳回必填意见 |
| GET | /crm/index-data/info、/stats | 首页看板 | — | crm:index:data / 无按钮权限(登录即可) |
| GET | /crm/owner/list | 负责人下拉 | — | 供转移/筛选用 |
| 方法 | 路径 | 用途 | 关键入参 | 备注 |
|---|---|---|---|---|
| CRUD | /erp/product(+ POST /querPorductByTags) | 产品 CRUD/按标签查询 | — | 含 safety_stock 安全库存 |
| CRUD | /erp/supplier、/erp/warehouse(+ GET /options)、/erp/client、/erp/info(开票)、/erp/tags、/erp/map、/erp/sn | 主数据七组 | — | sn 主键为 snUuid |
| CRUD | /erp/stock(+ PUT /stock-in/{id}) | 采购单/入库 | 明细 items[] | 入库 CAS 认领防重复;已入库单禁删 |
| CRUD | /erp/payment | 采购付款 | stockOrderId | — |
| CRUD | /erp/order | 销售订单 | — | 详情聚合客户/开票/商品 |
| CRUD | /erp/retail | 零售出库 | snUuid | SN 状态置出库 |
| CRUD | /erp/flow | 库存流水 | warehouseId | 0 出库/1 入库/2 期初 |
| CRUD | /erp/transfer(+ PUT /execute/{id}) | 调拨单/执行 | fromWarehouseId ≠ toWarehouseId | 执行:CAS + 负库存复检回滚 |
| GET/POST | /erp/adjust/list、/erp/adjust | 盘点调整 | adjust_type 2 盘盈/3 盘亏 | 记录 before/after_qty |
| GET | /erp/finance/overview、/stock-summary | 财务总览/库存汇总 | — | 聚合查询 |
| 方法 | 路径 | 用途 | 备注 |
|---|---|---|---|
CRUD + POST /export | /ai/knowledgeBase | 知识条目管理 | ai:knowledgeBase:* 权限串;表 knowledge_base 在租户模板(init-sql-script/erp-crm-tenant-sample.sql 第 699 行) |
认证时序(SysLoginService.login,erp-crm/src/main/java/com/ec/auth/web/service/SysLoginService.java):
pwd_lock_time:{tenant}:{username}——命中则报"密码错误次数过多,账号已锁定,请约 N 分钟后再试";AuthenticationManager.authenticate → UserDetailsServiceImpl.loadUserByUsername 查当前租户库 sys_user(BCrypt 比对);BadCredentialsException)则计数 pwd_err_cnt:{tenant}:{username}(10 分钟窗口),累计 5 次写锁定键并清计数;AsyncManager + AsyncFactory.recordLogininfor)、更新登录 IP/时间,TokenService.createToken 签发 JWT。令牌结构:JWT(HS512,密钥 EC_TOKEN_SECRET ≥64 字节)payload 仅含 login_user_key(随机 UUID)与 login_tenant_key(租户名);真实会话对象 LoginUser 存 Redis login_tokens:{uuid},TTL=token.expireTime(120 分钟)。JwtAuthenticationTokenFilter 每请求解析 JWT→取 Redis→verifyToken 判断剩余有效期 ≤20 分钟则滑动续期(TokenService.MILLIS_MINUTE_TEN)。JWT 被盗用无法单独作废,但 Redis 键删除即可强制下线(监控-在线用户页即此原理)。
鉴权模型:接口级 @PreAuthorize("@ss.hasPermi('crm:customer:add')")(PermissionService 比对 LoginUser.permissions);数据级 @DataScope(按角色配置拼 dept_id in (...) SQL 片段);行级归属校验在 service 显式编码(客户转移/公海/删除均含 owner 判定)。菜单权限的"源头"是租户库 sys_menu.perms 列,角色-菜单经 sys_role_menu 关联。
GlobalExceptionHandler(auth/web/exception/GlobalExceptionHandler.java)统一兜底,全部返回 HTTP 200 + 业务码:
| 业务码 | 场景 | 产出位置 |
|---|---|---|
| 200 | 成功;TableDataInfo 列表 | AjaxResult.success / getDataTable |
| 401 | 未认证/token 失效 | AuthenticationEntryPointImpl(消息含请求 URI) |
| 403 | 无权限(AccessDeniedException) | GlobalExceptionHandler |
| 500 | 兜底:ServiceException(msg 原样透传)、RuntimeException(记录日志,返回"系统异常")、Exception、参数绑定 BindException/MethodArgumentNotValidException(返回首条字段错误)、DemoModeException(演示模式拒绝写操作) | GlobalExceptionHandler 各 @ExceptionHandler |
前端 utils/errorHandler.js 的 resolveApiMessage/notifyApiError/promptRelogin 按同一套业务码映射提示行为;utils/errorCode.js 保有 code→默认文案表。
POST /login请求(tenant 头必须;body 字段与 LoginBody 一一对应):
POST /login HTTP/1.1
Content-Type: application/json;charset=utf-8
tenant: tenant1
{ "username": "admin", "password": "admin123" }成功响应(SysLoginController.login:ajax.put(Constants.TOKEN, token)):
{ "msg": "操作成功", "code": 200, "token": "eyJhbGciOiJIUzUxMiJ9..." }失败响应示例(密码错误 3 次时,SysLoginService 第 148 行):
{ "msg": "用户名或密码错误,还可尝试 2 次", "code": 500 }内置逻辑:IP 限流(30 次/分,@RateLimiter(limitType = LimitType.IP, time = 60, count = 30))→ 租户状态校验 → 锁定检查 → AuthenticationManager 认证(租户库 sys_user)→ 计数/锁定 → 签发 JWT + Redis 会话;成功与失败均打点 erp.login.total{tenant,result}(ErpMetrics.login)。
PUT /erp/stock/stock-in/{id}请求:
PUT /erp/stock/stock-in/13 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
tenant: tenant1处理链(ErpStockOrderServiceImpl.stockIn,erp-crm/src/main/java/com/ec/erp/service/impl/ErpStockOrderServiceImpl.java 第 105-135 行):① 采购单存在性校验;② warehouseId 非空校验("采购单未指定入库仓库,请先补充仓库信息");③ 明细非空校验;④ CAS 认领 claimForStockIn(id)(UPDATE erp_stock_order SET status='1' WHERE id=? AND status='0',返回 0 即抛"仅待入库状态的采购单可执行入库");⑤ 按明细逐条写 erp_storage_flow(type=1 入库、带 warehouse_id 与订单号备注)。整个方法 @Transactional,任一步失败全部回滚。响应:
{ "msg": "操作成功", "code": 200 }GET /crm/customer/list(分页契约)请求:GET /crm/customer/list?pageNum=1&pageSize=10&name=华&customerRank=1。处理:startPage() → PageHelper 拦截 SQL 拼 LIMIT → TableDataInfo:
{ "total": 42, "rows": [ { "id": 1, "name": "华信集团", "linkman": "王总", "phone": "13800000000", "region": "华东", "customerRank": "1", "customerStatus": "1", "owner": "admin", "lastFollowupTime": "2026-09-30 10:12:00" } ], "code": 200, "msg": "查询成功" }前端 useTable 直接消费 rows/total;导出则将同样查询条件 POST 到 /crm/customer/export 返回 xlsx blob(ExcelUtil 按 @Excel 注解列头)。
gen_business_modules.py 的 controller 模板补齐五件套端点,并同步在租户模板库 sys_menu 加菜单与按钮权限(参照 db-backup/upgrade_business_modules.sql 末段的 INSERT INTO sys_menu/sys_role_menu 写法),否则前端页面拿不到菜单与 v-hasPermi 权限串。@PreAuthorize;未声明则仅要求登录(authenticated),任何租户内用户均可调用。@DataSource(MASTER) 已封装在 saas service 层,web 层不要自行切换数据源。crm_approval.id,两者不可混用(CrmApprovalController.getInfo 的 {id} 传任务 ID)。