
权限系统做到第二版时我彻底放弃在框架里继续堆角色判断的if-else转用php-casbin统一做RBAC并且为多租户场景把整套表结构重新设计了一遍。这篇文章就是把这套“用户租户角色权限”的表结构完整拆开每张表为什么留这些字段、Casbin规则表和业务表怎么咬合、以及上线前我踩出来的几个坑。如果你正在做SaaS化改造或者想把框架自带的简单角色判断换成独立可扩展的权限引擎这份设计可以直接给你参考。1. 多租户隔离方案选型共用库加tenant_id为什么最省心1.1 三种隔离方式的账本对比在动笔画表之前第一个要敲定的是“租户的数据到底怎么隔”。我见过不少人上来就选独立库理由是“租户数据必须物理隔离”结果团队被运维成本拖垮。其实主流方案就三种各有各的适用范围方案隔离强度部署与运维成本查询成本典型适用阶段独立数据库一租户一库最高高每个库都要跑迁移、备份、账号管理连接管理复杂跨租户汇总几乎无法做大客户定制化、合规要求极高的场景独立Schema一租户一个Schema较高中迁移脚本要循环执行共享连接池但仍要动态拼Schema中型SaaS有一定隔离要求共用库tenant_id字段低但够用低一套表结构、一套迁移最低所有查询走同一张表绝大多数SaaS起步和增长期我最后选了第三种共用库tenant_id。原因不是“偷懒”而是现实里多数SaaS在初期租户量、单个租户的数据量都不大单表加索引完全扛得住。共用库还能让你随便写跨租户的统计报表运营想看“哪些租户活跃度高”时不用绕一大圈。真正需要独立库时再拆也来得及——只要你在业务层把tenant_id一直带着后面拆库只是数据搬运问题。1.2 共用库方案下必须定下的三条底线共用库节省了运维却把压力转移到了编码自控力上。如果不立规矩半年后你就会被跨租户数据事故折磨。我在设计表结构之前先定了三条底线所有业务表必须有tenant_id字段并且单独建索引。这里的“所有”包括基础数据表连配置表都不要漏。漏掉一张将来某条SQL就会被“忘了”加租户条件。所有SQL查询都要显式带租户条件不依赖所谓“全局过滤器”。全局过滤器看起来省事但一旦有人用原生SQL、或者在联合查询里绕过了过滤器越权数据就出来了。显式写在语句里review代码时一眼能看出来。唯一性约束必须把tenant_id纳入。比如用户名租户A里叫admin和租户B里叫admin是完全合法的两个人唯一索引就必须是(tenant_id, username)而不是全局username唯一。这三条看起来简单但从根上决定了共用库方案能不能安全落地。权限系统本身是为了防越权如果数据隔离的桶底漏了规则再严密也没用。1.3 主键类型和业务code规则表里尽量存“人话”表主键用什么类型我建议分两层考虑数据库自增主键多数表继续用int unsigned或bigint unsigned自增没必要为了“微服务化焦虑”上来就上雪花ID。但Casbin规则里不要存数据库自增ID而是给租户、角色、权限各加一个业务code字段规则文本里存code。为什么因为规则表要给人看、要导出、要跨环境迁移。一条规则长这样p: role_code_tenant_001, tenant_001, user:create, *如果全存自增ID规则会变成p: 1287, 332, 8873, *你完全没法从文本里读懂这条规则代表什么。更麻烦的是自增ID一旦删除重用规则语义就会被污染。所以我规定用户的规则标识用主键ID可以接受因为用户通常禁用不删除但租户、角色、权限的规则标识一律用业务code。这条经验在后面每个字段设计里都会反复出现。2. 七张表的逐字段拆解与完整DDL2.1 可直接抄作业的整套DDL下面是这套设计对应的MySQL DDL包含七张表租户表、用户表、角色表、权限表、用户角色关系表、角色权限关系表、Casbin规则表。字符集统一utf8mb4引擎用InnoDB。-- 租户表 CREATE TABLE tenants ( id int unsigned NOT NULL AUTO_INCREMENT COMMENT 租户ID, code varchar(64) NOT NULL COMMENT 租户业务编码规则表中使用, name varchar(128) NOT NULL COMMENT 租户名称, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, expired_at datetime DEFAULT NULL COMMENT 到期时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租户表; -- 用户表 CREATE TABLE users ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 用户ID, tenant_id int unsigned NOT NULL COMMENT 所属租户ID, username varchar(64) NOT NULL COMMENT 登录账号, phone varchar(32) DEFAULT NULL, email varchar(128) DEFAULT NULL, password_hash varchar(255) NOT NULL, nickname varchar(64) NOT NULL DEFAULT , is_super_admin tinyint NOT NULL DEFAULT 0 COMMENT 全局超管不走Casbin, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_tenant_username (tenant_id, username), KEY idx_tenant (tenant_id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 角色表 CREATE TABLE roles ( id int unsigned NOT NULL AUTO_INCREMENT COMMENT 角色ID, tenant_id int unsigned NOT NULL COMMENT 所属租户ID, code varchar(64) NOT NULL COMMENT 角色编码规则表中使用, name varchar(64) NOT NULL COMMENT 角色名称, description varchar(255) NOT NULL DEFAULT , status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_tenant_code (tenant_id, code), KEY idx_tenant (tenant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; -- 权限表 CREATE TABLE permissions ( id int unsigned NOT NULL AUTO_INCREMENT COMMENT 权限ID, parent_id int unsigned NOT NULL DEFAULT 0 COMMENT 父权限ID用于组织权限树, tenant_id int unsigned NOT NULL DEFAULT 0 COMMENT 0系统公共权限其他为租户自定义权限, code varchar(128) NOT NULL COMMENT 权限点编码如user:create, name varchar(64) NOT NULL COMMENT 权限名称, type tinyint NOT NULL DEFAULT 1 COMMENT 1菜单 2按钮 3API, sort smallint NOT NULL DEFAULT 0 COMMENT 排序, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code (code), KEY idx_parent (parent_id), KEY idx_tenant (tenant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT权限表; -- 用户角色关系表 CREATE TABLE user_roles ( id bigint unsigned NOT NULL AUTO_INCREMENT, user_id bigint unsigned NOT NULL COMMENT 用户ID, role_id int unsigned NOT NULL COMMENT 角色ID, tenant_id int unsigned NOT NULL COMMENT 冗余租户ID利于隔离查询与审计, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_role (user_id, role_id), KEY idx_role (role_id), KEY idx_tenant (tenant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表; -- 角色权限关系表 CREATE TABLE role_permissions ( id bigint unsigned NOT NULL AUTO_INCREMENT, role_id int unsigned NOT NULL COMMENT 角色ID, permission_id int unsigned NOT NULL COMMENT 权限ID, tenant_id int unsigned NOT NULL COMMENT 冗余租户ID利于隔离查询与审计, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_role_permission (role_id, permission_id), KEY idx_permission (permission_id), KEY idx_tenant (tenant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色权限关联表; -- Casbin规则表 CREATE TABLE casbin_rule ( id bigint unsigned NOT NULL AUTO_INCREMENT, ptype varchar(32) NOT NULL COMMENT 规则类型p策略规则 / g分组规则, v0 varchar(255) NOT NULL DEFAULT , v1 varchar(255) NOT NULL DEFAULT , v2 varchar(255) NOT NULL DEFAULT , v3 varchar(255) NOT NULL DEFAULT , v4 varchar(255) NOT NULL DEFAULT , v5 varchar(255) NOT NULL DEFAULT , PRIMARY KEY (id), KEY idx_ptype (ptype), KEY idx_v0_v1_v2 (v0, v1, v2) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTCasbin规则表;2.2 租户表与用户表数据的根和归属租户表很简单核心是把code从自增ID里独立出来。这个code会成为Casbin规则里的domain参数所以要短、要稳定、要可读。我习惯用类似tenant_001这种编码也见过用UUID的不建议规则表里一长串UUID看着就头疼。用户表要说的事情比较多。第一我用tenant_id直接挂在用户表上等价于“一个用户只属于一个租户”。这是绝大多数SaaS的默认模型用户注册时选定一个租户企业/团队以后就是这家的人。如果你业务里确实有“一个人可以同时加入多个企业切换身份后进入不同工作台”的需求那就需要把users.tenant_id抽出去加一张user_tenant_rel关系表。标题里“用户租户”就是围绕最常见的一对一关系来设计的一对多属于变体设计思路相通。第二登录账号的唯一性。用户名在全局不需要唯一但在一个租户内必须唯一所以唯一索引是(tenant_id, username)。手机号和邮箱我刻意没加唯一约束因为真实业务里同一个手机号被两个租户各注册一次实在太常见了全局唯一反而会误伤。如果业务要求手机号全局唯一再往users表上补一个uk_phone即可。第三用户表的规则标识用主键ID还是单独code我上文说过规则里尽量存code但用户是个例外——用户通常只增不删禁用代替删除自增ID相对稳定而且用户体量大再造一层user_code徒增复杂度。如果你团队确实接受“删用户再重建”的操作那就给users表加一个user_code字段规则表里用user_code别用自增ID。2.3 角色表与权限表权限点要全局唯一还是租户内唯一角色表的逻辑很直白每个租户里的角色独立管理所以code只要求在租户内唯一唯一索引(tenant_id, code)。这样租户A和租户B可以各有一个admin角色他们在各自租户语境下语义清晰也不会撞车。权限表我这里花了较多心思。先解释几个字段parent_id用来组织权限树给后台管理界面生成勾选树用的。比如“用户管理”下面挂“用户新增”“用户删除”。Casbin规则本身不感知树结构它只认权限点code。tenant_id权限表默认允许平台统一维护一套公共权限tenant_id0代表系统级公共权限。如果业务允许租户自定义扩展权限点就再插入一条指定租户ID的权限记录。code这是权限点的最终形态格式上我推荐“资源:动作”比如user:create、order:update。不要用中文规则表、代码、日志里都要传递同一个字符串。type菜单、按钮、API三种类型主要是后台展示区分。菜单权限控制“能不能看到这个入口”按钮权限控制“能不能点”API权限控制“能不能调”。到Casbin层三种类型最后都被翻译成perm code没有本质区别。一个值得注意的设计决策权限code我做了全局唯一uk_code而不是租户内唯一。原因是权限点是平台级资源语义一旦定下来就不该在不同租户之间有歧义。如果允许不同租户定义相同code但不同含义规则表就乱了一条p: role_x, tenant_001, user:create, *到底代表哪个租户的user:create所以我们强制权限code全局唯一租户自定义权限如果和公共权限语义冲突直接不允许。2.4 两张关系表和casbin_rule表冗余字段不是浪费user_roles和role_permissions两张关系表字段结构看起来几乎一样一对ID加一个tenant_id。这个tenant_id是冗余的因为通过role_id反查roles表一定能拿到租户。为什么还要冗余两个理由一是隔离查询。权限配置页面上要展示“当前租户的所有权限”如果关系表没有tenant_id就得让role_permissions先join roles再用租户过滤多一次join有了tenant_id一条where就完事索引也简单。二是审计。某人改坏了数据排查时直接看关系表里的租户字段就能定位范围不用顺着ID链去猜。关系表上我没有加物理外键。不是偷懒是生产环境不建议加外键外键会在高频写入时占用额外锁影响性能和扩展性。关系完整性交给应用层去维护如果担心脏数据定时任务扫一遍孤儿记录就够了。casbin_rule表的字段是php-casbin的标准结构ptype区分规则类型v0到v5存储规则参数。这里解释一下这套表在这套设计里的约定ptype p的策略规则v0角色codev1租户codev2权限codev3动作本设计统一为*这就是role_permissions的投影。ptype g的分组规则v0用户IDv1角色codev2租户code这就是user_roles的投影。业务管理后台永远操作前五张业务表casbin_rule表只是运行时投影通过同步逻辑保持一致不直接手工改。3. model.conf怎么配置才能让Casbin认识“租户”3.1 多域RBAC模型让domain承载租户ID表结构定了关键一步是让Casbin“理解”这套表。Casbin的强项之一是原生支持带域的RBAC模型domain这个概念恰好可以映射为租户ID。一个最简单可用的model.conf长这样[request_definition] r sub, dom, obj, act [policy_definition] p sub, dom, obj, act [role_definition] g _, _, _ [policy_effect] e some(where (p.eft allow)) [matchers] m g(r.sub, p.sub, r.dom) r.dom p.dom r.obj p.obj (r.act p.act || p.act *)逐行拆解r sub, dom, obj, act请求由四个元素组成分别代表“谁”“在哪个租户”“操作什么资源”“做什么动作”。p sub, dom, obj, act策略规则同样是四个元素我们在规则表里传入的实参就是角色code租户code权限code*。g _, _, _分组规则支持三个参数贴合的语义是用户角色租户域。m g(r.sub, p.sub, r.dom) r.dom p.dom r.obj p.obj (r.act p.act || p.act *)匹配逻辑是请求者在请求租户域内必须属于某个角色这个角色在该租户域下拥有对应权限动作要么精确匹配要么规则里用通配*。有了这个模型权限判断就统一了。以“租户tenant_001的管理员admin给用户user_1001授予用户管理权限”为例g规则user_1001, admin, tenant_001p规则admin, tenant_001, user:create, *请求enforce(user_1001, tenant_001, user:create, *)匹配过程是先查分组确定user_1001在tenant_001里属于admin角色再查策略确定admin角色在tenant_001下对user:create有允许规则最后动作对得上。整套查询都在casbin_rule表上发生速度快。3.2 规则数据与业务数据的同步逻辑业务表和casbin_rule表不是天然一致的需要代码去同步。刚接触Casbin的人最容易犯的错是在管理后台改了角色权限结果enforce不生效因为casbin_rule还是旧的。我的做法是封装一个PermissionSyncService凡是业务表变更的入口都统一走它// 给用户分配角色 $enforcer-addGroupingPolicy($userId, $roleCode, $tenantCode); // 移除用户角色 $enforcer-deleteGroupingPolicy($userId, $roleCode, $tenantCode); // 给角色挂权限点 $enforcer-addPolicy($roleCode, $tenantCode, $permissionCode, *); // 移除角色权限点 $enforcer-removePolicy($roleCode, $tenantCode, $permissionCode, *); // 整租户权限重建仅初始化或修复数据时用 foreach ($userRolePairs as $pair) { $enforcer-addGroupingPolicy($pair[user_id], $pair[role_code], $pair[tenant_code]); }这里有个工程问题要提前想好业务表和casbin_rule表不在同一个事务里。如果第一步insert user_roles成功第二步同步g规则失败就会出现业务表有角色但鉴权不过的“假掉线”。我的处理是业务表变更成功后立刻触发同步同时把整个租户的权限版本号递增后面缓存一节会细说保证即使同步暂时失败也不会让旧缓存一直生效再拉一条队列任务做兜底重试。这属于最终一致性思路权限系统可以接受短时间内的规则同步延迟但绝不能接受“永久不一致”。3.3 一次完整鉴权的数据链路实际请求进来时大致走这么几步中间件解析Token拿到$userId从域名、请求头或path前缀解析出$tenantCode。初始化Enforcer实例。php-casbin用法是new Enforcer(/path/to/model.conf, new DatabaseAdapter($pdo))。Laravel环境下建议把Enforcer注册成单例避免每次请求重新加载模型和策略。从路由映射表拿到当前接口的权限code例如user:create。调用$enforcer-enforce($userId, $tenantCode, user:create, *)返回true或false。返回false时抛异常让前端跳403页面或弹出无权限提示。第3步的路由映射表是本设计中很关键的一环每个接口都要和权限code建立对应关系。可以做成一张路由权限表也可以直接写在路由注解里。我最开始只在按钮层面控制后来发现API层不控制等于白搭——有人绕过前端直接curl接口按钮藏得再好也没用。权限code最终必须落在服务端路由层做强制校验。4. 上线前必须想清楚的四个隐患4.1 超级管理员别硬塞进规则表多租户系统里通常有两类管理员平台级超管和租户内管理员。平台超管要能进任意租户操作常见错误做法是给它配一条通配规则比如p: super_admin, *, *, *。极其不建议这么做。原因有三通配规则会让所有租户的enforce都先命中超管规则表里这条“万能钥匙”一旦导出、打印、泄露排查时必须全局找业务去核对。租户管理后台如果需要展示“哪些角色拥有哪些权限”通配规则会和租户自己的角色配置混在一起引起各种误展示。超管的鉴权路径本应是“绝对信任”不需要经过规则匹配器跑一遍。正确做法是users表里直接放is_super_admin字段在权限中间件里优先判断if (!empty($user[is_super_admin])) { return $next($request); } if (!$enforcer-enforce($userId, $tenantCode, $permissionCode, *)) { throw new AccessDeniedException(没有操作权限); }这样超管的放行逻辑零规则依赖租户内的管理员则通过给角色挂权限点的正常方式管理。两者的边界非常清晰。4.2 权限变更后的缓存失效怎么设计Casbin查询规则本身很快但每次请求都读一次数据库还是会有压力。我在上线前给Enforcer的enforce结果加了缓存单纯加缓存容易难的是权限变更后怎么失效。逐条删除缓存太脆弱一个租户改了3个角色、每个角色挂了20个权限点你得精确算出哪些用户影响哪些权限稍微漏一个就有人“权限没刷新”。我的方案是“租户级版本号”$version cache(tenant:.$tenantCode.:perm_version, 0); $cacheKey perm:{$tenantId}:{$version}:{$userId}:{$permissionCode}:*; if (cache()-has($cacheKey)) { return cache()-get($cacheKey); } $allowed $enforcer-enforce($userId, $tenantCode, $permissionCode, *); cache()-set($cacheKey, $allowed, 600); return $allowed;权限变更时除了同步规则还执行cache()-increment(tenant:.$tenantCode.:perm_version);版本号一变同一个用户的缓存key就变了旧缓存自然过期新请求会重新读取规则。这个方案的关键优势是无论规则变更多复杂一条increment解决所有失效问题不用去扫描哪些key要删。我上线后遇到的最大好处是让“权限改完立刻生效”这件事变成了确定性行为再也不用靠猜。4.3 接口数据查询的租户边界无法靠Casbin兜底这是很多权限系统的通病Casbin判断“你有没有权限调用订单列表接口”但它根本不知道“你能看到哪些订单”。数据范围必须由SQL层来保证。举个例子查询订单详情// 错误示范只按订单ID查可能看到其他租户的订单 $order Order::find($orderId); // 正确做法租户条件必须写进查询 $order Order::where(tenant_id, $currentTenantId)-find($orderId);如果你的代码里大量出现“只按主键ID查询”的接口在多租户模式下就是重大隐患。因为主键ID是全局自增的租户A的订单ID未必在租户B范围里但拦不住有人遍历ID一个个试。我在code review时定了硬规矩带数据返回的查询SQL里必须出现tenant_id条件。少数确实不需要租户隔离的数据比如国家码表要在代码注释里特别说明并走白名单流程。4.4 软删除、唯一索引与账号复用很多团队习惯用软删除deleted_at字段来管理用户和角色结果和唯一索引撞车。举个典型场景租户A有用户名admin后来这个人离职被“删除”软删除过段时间新员工入职想注册admin如果唯一索引是(tenant_id, username)插入会直接冲突如果唯一索引是(tenant_id, username, deleted_at)由于软删记录deleted_at不是NULL看上去能插入成功后但如果两个被删账号恰好同一秒删除再次插入还会撞索引。我的处理建议分两种用户和角色尽量不物理删除用status0禁用。这样唯一索引不会变复杂权限规则也可以保留——被禁用的人反正登录不了规则在不在无所谓。确需保留“删除”语义时删除操作顺带把账号改成username_del_{timestamp}释放原名同时保持唯一索引干净。之后再清理掉casbin_rule里对应的g规则和p规则避免规则表积累垃圾。这套思路里“禁用代替删除”是首选。权限系统里历史关系本身有审计价值删除留痕比物理清空更有意义。5. 演进过程中的复盘什么时候不需要Casbin5.1 从框架自带RBAC迁移的真实感受迁移之前我们项目用框架自带的角色判断一个用户表、一个角色表、一张用户角色表权限判断靠控制器里手写if ($user-can(xxx))。初期没问题但随着权限点增多出现两个痛点每次新增一个资源都要改菜单表、角色表、关系表代码模板复制粘贴权限校验逻辑散落各处。没有统一的规则存储格式无法导出、无法对比差异更不要说支持复杂的多域模型。换成php-casbin之后权限点管理收敛到permissions表规则管理收敛到casbin_rule表控制器里全部变成一行enforce()。新增权限点时我只需要加一条permission记录再给对应角色挂上代码层面零改动。这种“配置化权限”带来的长期收益比一开始多写的那些同步代码大得多。5.2 不建议上Casbin的场景说了这么多优点也得泼盆冷水。如果你的项目满足以下任一条件我建议再等等权限结构十几年不变角色固定、权限点固定、没有动态配置需求。这种情况框架自带的简单RBAC已经够用引入Casbin属于杀鸡用牛刀。只有菜单显示控制没有API层校验权限只控制前端显示不显示某个按钮后端接口压根没做权限判断。这种情况问题不在权限引擎先补API层校验再说。团队对这个引擎不熟悉且不打算维护Casbin的模型配置和规则同步有一定学习成本接手的人如果没搞懂原理改一处权限可能埋一个雷。一个简单的判断标准如果你没法在运行期动态调整权限而不用发版那就不需要Casbin。Casbin解决的是动态、复杂、可扩展的权限模型为了省那一行if-else而上它性价比不高。5.3 后续扩展数据权限、资源属主与审计日志这套表结构还有一个容易被忽略的好处为后续从RBAC扩展到ABAC留了路。Casbin的request定义是可以自己加的如果你想管“某个人只能操作自己创建的订单”可以在request里加上owner资源属主在matcher里判断r.owner p.sub之类想加数据范围只看本部门、只看本租户可以在策略里增加数据范围标记字段。权限审计方面最简单的做法是在中间件里把每次enforce的user_id、tenant_code、permission_code、结果、时间都记一条日志。这不是为了复杂而是线上出问题时你能回答“谁在什么时间尝试了什么越权操作”。多租户系统一旦出安全事故这个日志就是第一手的排查材料。我自己在实际项目中最受益的设计决策有两个一是权限点规范化成user:create这种全局唯一code让规则表始终可读可审计二是租户级版本号的缓存失效方案让权限变更“改了立刻生效绝不错乱”。如果你打算按这套结构开工先把7张表建起来再写个简单的PermissionSyncService跑通一个权限点后面按这个模式批量迁移即可。