ai-engineering-init 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/leniu-java-export/SKILL.md +389 -95
- package/.codex/skills/leniu-java-export/SKILL.md +389 -95
- package/.cursor/skills/bug-detective/SKILL.md +19 -19
- package/.cursor/skills/leniu-java-export/SKILL.md +389 -95
- package/.cursor/skills/project-navigator/SKILL.md +164 -258
- package/package.json +7 -1
- package/scripts/build-skills.js +180 -0
- package/src/platform-map.json +56 -0
- package/src/skills/add-skill/SKILL.md +488 -0
- package/src/skills/add-todo/SKILL.md +269 -0
- package/src/skills/api-development/SKILL.md +266 -0
- package/src/skills/architecture-design/SKILL.md +262 -0
- package/src/skills/backend-annotations/SKILL.md +302 -0
- package/src/skills/banana-image/CHANGELOG.md +37 -0
- package/src/skills/banana-image/README.md +146 -0
- package/src/skills/banana-image/SKILL.md +171 -0
- package/src/skills/banana-image/assets/logo.png +0 -0
- package/src/skills/banana-image/references/advanced-usage.md +189 -0
- package/src/skills/banana-image/scripts/apply_template.py +125 -0
- package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/src/skills/banana-image/scripts/batch_prep.py +82 -0
- package/src/skills/banana-image/scripts/package-lock.json +1437 -0
- package/src/skills/banana-image/scripts/package.json +18 -0
- package/src/skills/banana-image/scripts/requirements.txt +10 -0
- package/src/skills/banana-image/templates/poster.json +22 -0
- package/src/skills/banana-image/templates/product.json +17 -0
- package/src/skills/banana-image/templates/social.json +22 -0
- package/src/skills/banana-image/templates/thumbnail.json +17 -0
- package/src/skills/brainstorm/SKILL.md +216 -0
- package/src/skills/bug-detective/SKILL.md +256 -0
- package/src/skills/bug-detective/references/error-patterns.md +242 -0
- package/src/skills/check/SKILL.md +367 -0
- package/src/skills/code-patterns/SKILL.md +280 -0
- package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/src/skills/codex-code-review/SKILL.md +135 -0
- package/src/skills/collaborating-with-codex/SKILL.md +174 -0
- package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/src/skills/crud/SKILL.md +265 -0
- package/src/skills/crud-development/SKILL.md +409 -0
- package/src/skills/data-permission/SKILL.md +292 -0
- package/src/skills/data-permission/references/custom-data-scope.md +90 -0
- package/src/skills/database-ops/SKILL.md +407 -0
- package/src/skills/dev/SKILL.md +187 -0
- package/src/skills/error-handler/SKILL.md +371 -0
- package/src/skills/file-oss-management/SKILL.md +255 -0
- package/src/skills/file-oss-management/references/entities.md +105 -0
- package/src/skills/file-oss-management/references/service-impl.md +104 -0
- package/src/skills/git-workflow/SKILL.md +397 -0
- package/src/skills/init-docs/SKILL.md +194 -0
- package/src/skills/json-serialization/SKILL.md +357 -0
- package/src/skills/leniu-api-development/SKILL.md +319 -0
- package/src/skills/leniu-api-development/references/real-examples.md +273 -0
- package/src/skills/leniu-architecture-design/SKILL.md +383 -0
- package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/src/skills/leniu-brainstorm/SKILL.md +242 -0
- package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/src/skills/leniu-code-patterns/SKILL.md +411 -0
- package/src/skills/leniu-crud-development/SKILL.md +404 -0
- package/src/skills/leniu-crud-development/references/templates.md +597 -0
- package/src/skills/leniu-customization-location/SKILL.md +410 -0
- package/src/skills/leniu-data-permission/SKILL.md +341 -0
- package/src/skills/leniu-database-ops/SKILL.md +426 -0
- package/src/skills/leniu-error-handler/SKILL.md +462 -0
- package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/src/skills/leniu-java-code-style/SKILL.md +510 -0
- package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/src/skills/leniu-java-entity/SKILL.md +237 -0
- package/src/skills/leniu-java-entity/references/templates.md +237 -0
- package/src/skills/leniu-java-export/SKILL.md +570 -0
- package/src/skills/leniu-java-logging/SKILL.md +229 -0
- package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/src/skills/leniu-java-mq/SKILL.md +338 -0
- package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/src/skills/leniu-java-task/SKILL.md +367 -0
- package/src/skills/leniu-java-total-line/SKILL.md +196 -0
- package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/src/skills/leniu-mealtime/SKILL.md +215 -0
- package/src/skills/leniu-redis-cache/SKILL.md +331 -0
- package/src/skills/leniu-report-customization/SKILL.md +335 -0
- package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/src/skills/leniu-security-guard/SKILL.md +306 -0
- package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/src/skills/mysql-debug/SKILL.md +364 -0
- package/src/skills/next/SKILL.md +137 -0
- package/src/skills/openspec-apply-change/SKILL.md +165 -0
- package/src/skills/openspec-archive-change/SKILL.md +122 -0
- package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/src/skills/openspec-continue-change/SKILL.md +126 -0
- package/src/skills/openspec-explore/SKILL.md +299 -0
- package/src/skills/openspec-ff-change/SKILL.md +109 -0
- package/src/skills/openspec-new-change/SKILL.md +82 -0
- package/src/skills/openspec-onboard/SKILL.md +414 -0
- package/src/skills/openspec-sync-specs/SKILL.md +146 -0
- package/src/skills/openspec-verify-change/SKILL.md +176 -0
- package/src/skills/performance-doctor/SKILL.md +303 -0
- package/src/skills/progress/SKILL.md +193 -0
- package/src/skills/project-navigator/SKILL.md +211 -0
- package/src/skills/redis-cache/SKILL.md +333 -0
- package/src/skills/redis-cache/references/listeners.md +23 -0
- package/src/skills/scheduled-jobs/SKILL.md +314 -0
- package/src/skills/security-guard/SKILL.md +353 -0
- package/src/skills/security-guard/references/encrypt-config.md +103 -0
- package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/src/skills/sms-mail/SKILL.md +308 -0
- package/src/skills/sms-mail/references/mail-config.md +88 -0
- package/src/skills/sms-mail/references/sms-config.md +74 -0
- package/src/skills/social-login/SKILL.md +266 -0
- package/src/skills/social-login/references/provider-configs.md +118 -0
- package/src/skills/start/SKILL.md +154 -0
- package/src/skills/store-pc/SKILL.md +366 -0
- package/src/skills/sync/SKILL.md +149 -0
- package/src/skills/task-tracker/SKILL.md +307 -0
- package/src/skills/tech-decision/SKILL.md +393 -0
- package/src/skills/tenant-management/SKILL.md +288 -0
- package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/src/skills/test-development/SKILL.md +301 -0
- package/src/skills/test-development/references/parameterized-examples.md +119 -0
- package/src/skills/ui-pc/SKILL.md +438 -0
- package/src/skills/update-status/SKILL.md +159 -0
- package/src/skills/utils-toolkit/SKILL.md +362 -0
- package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/src/skills/websocket-sse/SKILL.md +271 -0
- package/src/skills/workflow-engine/SKILL.md +321 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crud-development
|
|
3
|
+
description: |
|
|
4
|
+
后端 CRUD 开发规范。基于 RuoYi-Vue-Plus 三层架构(Controller -> Service -> Mapper),无独立 DAO 层。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 新建业务模块的 CRUD 功能
|
|
8
|
+
- 创建 Entity、BO、VO、Service、Mapper、Controller
|
|
9
|
+
- 分页查询、新增、修改、删除、导出
|
|
10
|
+
- 查询条件构建(buildQueryWrapper)
|
|
11
|
+
|
|
12
|
+
触发词:CRUD、增删改查、新建模块、Entity、BO、VO、Service、Mapper、Controller、分页查询、buildQueryWrapper、@AutoMapper、BaseMapperPlus、TenantEntity
|
|
13
|
+
|
|
14
|
+
注意:
|
|
15
|
+
- 三层架构,Service 直接注入 Mapper,无 DAO 层。
|
|
16
|
+
- 查询条件在 Service 层构建(buildQueryWrapper)。
|
|
17
|
+
- 使用 @AutoMapper(单数)而非 @AutoMappers。
|
|
18
|
+
- API 路径使用标准 RESTful 格式(/list、/{id})。
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# CRUD 开发规范(RuoYi-Vue-Plus 三层架构)
|
|
22
|
+
|
|
23
|
+
## 核心架构特征
|
|
24
|
+
|
|
25
|
+
| 项 | 规范 |
|
|
26
|
+
|----|------|
|
|
27
|
+
| **包名前缀** | `org.dromara.*` |
|
|
28
|
+
| **架构** | Controller -> Service -> Mapper(无 DAO 层) |
|
|
29
|
+
| **查询构建** | Service 层 `buildQueryWrapper()` |
|
|
30
|
+
| **Mapper** | 继承 `BaseMapperPlus<Entity, VO>` |
|
|
31
|
+
| **对象转换** | `MapstructUtils.convert()` |
|
|
32
|
+
| **Entity 基类** | `TenantEntity`(多租户) |
|
|
33
|
+
| **BO 映射** | `@AutoMapper`(单数) |
|
|
34
|
+
| **API 路径** | RESTful:`/list`、`/{id}` |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. Entity
|
|
39
|
+
|
|
40
|
+
```java
|
|
41
|
+
package org.dromara.demo.domain;
|
|
42
|
+
|
|
43
|
+
import org.dromara.common.tenant.core.TenantEntity;
|
|
44
|
+
import com.baomidou.mybatisplus.annotation.*;
|
|
45
|
+
import lombok.Data;
|
|
46
|
+
import lombok.EqualsAndHashCode;
|
|
47
|
+
import java.io.Serial;
|
|
48
|
+
|
|
49
|
+
@Data
|
|
50
|
+
@EqualsAndHashCode(callSuper = true)
|
|
51
|
+
@TableName("test_xxx")
|
|
52
|
+
public class Xxx extends TenantEntity {
|
|
53
|
+
|
|
54
|
+
@Serial
|
|
55
|
+
private static final long serialVersionUID = 1L;
|
|
56
|
+
|
|
57
|
+
@TableId(value = "id")
|
|
58
|
+
private Long id;
|
|
59
|
+
|
|
60
|
+
private String xxxName;
|
|
61
|
+
|
|
62
|
+
private String status;
|
|
63
|
+
|
|
64
|
+
@TableLogic
|
|
65
|
+
private Long delFlag;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 2. BO
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
package org.dromara.demo.domain.bo;
|
|
73
|
+
|
|
74
|
+
import io.github.linpeilie.annotations.AutoMapper;
|
|
75
|
+
import org.dromara.demo.domain.Xxx;
|
|
76
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
77
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
78
|
+
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
|
79
|
+
import lombok.Data;
|
|
80
|
+
import lombok.EqualsAndHashCode;
|
|
81
|
+
import jakarta.validation.constraints.*;
|
|
82
|
+
|
|
83
|
+
@Data
|
|
84
|
+
@EqualsAndHashCode(callSuper = true)
|
|
85
|
+
@AutoMapper(target = Xxx.class, reverseConvertGenerate = false)
|
|
86
|
+
public class XxxBo extends BaseEntity {
|
|
87
|
+
|
|
88
|
+
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
|
|
89
|
+
private Long id;
|
|
90
|
+
|
|
91
|
+
@NotBlank(message = "名称不能为空", groups = {AddGroup.class, EditGroup.class})
|
|
92
|
+
private String xxxName;
|
|
93
|
+
|
|
94
|
+
private String status;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 3. VO
|
|
99
|
+
|
|
100
|
+
```java
|
|
101
|
+
package org.dromara.demo.domain.vo;
|
|
102
|
+
|
|
103
|
+
import io.github.linpeilie.annotations.AutoMapper;
|
|
104
|
+
import org.dromara.demo.domain.Xxx;
|
|
105
|
+
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
|
106
|
+
import cn.idev.excel.annotation.ExcelProperty;
|
|
107
|
+
import lombok.Data;
|
|
108
|
+
import java.io.Serial;
|
|
109
|
+
import java.io.Serializable;
|
|
110
|
+
import java.util.Date;
|
|
111
|
+
|
|
112
|
+
@Data
|
|
113
|
+
@ExcelIgnoreUnannotated
|
|
114
|
+
@AutoMapper(target = Xxx.class)
|
|
115
|
+
public class XxxVo implements Serializable {
|
|
116
|
+
|
|
117
|
+
@Serial
|
|
118
|
+
private static final long serialVersionUID = 1L;
|
|
119
|
+
|
|
120
|
+
@ExcelProperty(value = "主键")
|
|
121
|
+
private Long id;
|
|
122
|
+
|
|
123
|
+
@ExcelProperty(value = "名称")
|
|
124
|
+
private String xxxName;
|
|
125
|
+
|
|
126
|
+
@ExcelProperty(value = "状态")
|
|
127
|
+
private String status;
|
|
128
|
+
|
|
129
|
+
@ExcelProperty(value = "创建时间")
|
|
130
|
+
private Date createTime;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 4. Service 接口
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
package org.dromara.demo.service;
|
|
138
|
+
|
|
139
|
+
import org.dromara.demo.domain.bo.XxxBo;
|
|
140
|
+
import org.dromara.demo.domain.vo.XxxVo;
|
|
141
|
+
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
142
|
+
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
143
|
+
import java.util.Collection;
|
|
144
|
+
import java.util.List;
|
|
145
|
+
|
|
146
|
+
public interface IXxxService {
|
|
147
|
+
XxxVo queryById(Long id);
|
|
148
|
+
List<XxxVo> queryList(XxxBo bo);
|
|
149
|
+
TableDataInfo<XxxVo> queryPageList(XxxBo bo, PageQuery pageQuery);
|
|
150
|
+
Boolean insertByBo(XxxBo bo);
|
|
151
|
+
Boolean updateByBo(XxxBo bo);
|
|
152
|
+
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 5. Service 实现(核心)
|
|
157
|
+
|
|
158
|
+
```java
|
|
159
|
+
package org.dromara.demo.service.impl;
|
|
160
|
+
|
|
161
|
+
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
162
|
+
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
163
|
+
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
164
|
+
import lombok.RequiredArgsConstructor;
|
|
165
|
+
import org.springframework.stereotype.Service;
|
|
166
|
+
import org.dromara.common.core.exception.ServiceException;
|
|
167
|
+
import org.dromara.common.core.utils.MapstructUtils;
|
|
168
|
+
import org.dromara.common.core.utils.StringUtils;
|
|
169
|
+
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
170
|
+
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
171
|
+
import org.dromara.demo.domain.Xxx;
|
|
172
|
+
import org.dromara.demo.domain.bo.XxxBo;
|
|
173
|
+
import org.dromara.demo.domain.vo.XxxVo;
|
|
174
|
+
import org.dromara.demo.mapper.XxxMapper;
|
|
175
|
+
import org.dromara.demo.service.IXxxService;
|
|
176
|
+
import java.util.Collection;
|
|
177
|
+
import java.util.List;
|
|
178
|
+
import java.util.Map;
|
|
179
|
+
|
|
180
|
+
@Service
|
|
181
|
+
@RequiredArgsConstructor
|
|
182
|
+
public class XxxServiceImpl implements IXxxService {
|
|
183
|
+
|
|
184
|
+
private final XxxMapper baseMapper; // 直接注入 Mapper(无 DAO 层)
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
public XxxVo queryById(Long id) {
|
|
188
|
+
return baseMapper.selectVoById(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Override
|
|
192
|
+
public List<XxxVo> queryList(XxxBo bo) {
|
|
193
|
+
return baseMapper.selectVoList(buildQueryWrapper(bo));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Override
|
|
197
|
+
public TableDataInfo<XxxVo> queryPageList(XxxBo bo, PageQuery pageQuery) {
|
|
198
|
+
LambdaQueryWrapper<Xxx> lqw = buildQueryWrapper(bo);
|
|
199
|
+
Page<XxxVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
|
200
|
+
return TableDataInfo.build(result);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@Override
|
|
204
|
+
public Boolean insertByBo(XxxBo bo) {
|
|
205
|
+
Xxx add = MapstructUtils.convert(bo, Xxx.class);
|
|
206
|
+
validEntityBeforeSave(add);
|
|
207
|
+
return baseMapper.insert(add) > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@Override
|
|
211
|
+
public Boolean updateByBo(XxxBo bo) {
|
|
212
|
+
Xxx update = MapstructUtils.convert(bo, Xxx.class);
|
|
213
|
+
validEntityBeforeSave(update);
|
|
214
|
+
return baseMapper.updateById(update) > 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@Override
|
|
218
|
+
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
|
219
|
+
if (isValid) {
|
|
220
|
+
List<Xxx> list = baseMapper.selectByIds(ids);
|
|
221
|
+
if (list.size() != ids.size()) {
|
|
222
|
+
throw new ServiceException("您没有删除权限!");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return baseMapper.deleteByIds(ids) > 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private LambdaQueryWrapper<Xxx> buildQueryWrapper(XxxBo bo) {
|
|
229
|
+
Map<String, Object> params = bo.getParams();
|
|
230
|
+
LambdaQueryWrapper<Xxx> lqw = Wrappers.lambdaQuery();
|
|
231
|
+
lqw.eq(bo.getId() != null, Xxx::getId, bo.getId());
|
|
232
|
+
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), Xxx::getStatus, bo.getStatus());
|
|
233
|
+
lqw.like(StringUtils.isNotBlank(bo.getXxxName()), Xxx::getXxxName, bo.getXxxName());
|
|
234
|
+
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
|
|
235
|
+
Xxx::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
|
|
236
|
+
lqw.orderByAsc(Xxx::getId);
|
|
237
|
+
return lqw;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private void validEntityBeforeSave(Xxx entity) {
|
|
241
|
+
// TODO 做一些数据校验,如唯一约束
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 6. Mapper
|
|
247
|
+
|
|
248
|
+
```java
|
|
249
|
+
package org.dromara.demo.mapper;
|
|
250
|
+
|
|
251
|
+
import org.dromara.demo.domain.Xxx;
|
|
252
|
+
import org.dromara.demo.domain.vo.XxxVo;
|
|
253
|
+
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
|
254
|
+
|
|
255
|
+
public interface XxxMapper extends BaseMapperPlus<Xxx, XxxVo> {
|
|
256
|
+
// 已提供 selectVoById、selectVoPage、selectVoList 等方法
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 7. Controller
|
|
261
|
+
|
|
262
|
+
```java
|
|
263
|
+
package org.dromara.demo.controller;
|
|
264
|
+
|
|
265
|
+
import java.util.Arrays;
|
|
266
|
+
import java.util.List;
|
|
267
|
+
import lombok.RequiredArgsConstructor;
|
|
268
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
269
|
+
import jakarta.validation.constraints.*;
|
|
270
|
+
import cn.dev33.satoken.annotation.SaCheckPermission;
|
|
271
|
+
import org.springframework.web.bind.annotation.*;
|
|
272
|
+
import org.springframework.validation.annotation.Validated;
|
|
273
|
+
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
|
274
|
+
import org.dromara.common.log.annotation.Log;
|
|
275
|
+
import org.dromara.common.log.enums.BusinessType;
|
|
276
|
+
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
277
|
+
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
278
|
+
import org.dromara.common.web.core.BaseController;
|
|
279
|
+
import org.dromara.common.core.domain.R;
|
|
280
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
281
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
282
|
+
import org.dromara.common.excel.utils.ExcelUtil;
|
|
283
|
+
import org.dromara.demo.domain.vo.XxxVo;
|
|
284
|
+
import org.dromara.demo.domain.bo.XxxBo;
|
|
285
|
+
import org.dromara.demo.service.IXxxService;
|
|
286
|
+
|
|
287
|
+
@Validated
|
|
288
|
+
@RequiredArgsConstructor
|
|
289
|
+
@RestController
|
|
290
|
+
@RequestMapping("/demo/xxx")
|
|
291
|
+
public class XxxController extends BaseController {
|
|
292
|
+
|
|
293
|
+
private final IXxxService xxxService;
|
|
294
|
+
|
|
295
|
+
@SaCheckPermission("demo:xxx:list")
|
|
296
|
+
@GetMapping("/list")
|
|
297
|
+
public TableDataInfo<XxxVo> list(XxxBo bo, PageQuery pageQuery) {
|
|
298
|
+
return xxxService.queryPageList(bo, pageQuery);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@SaCheckPermission("demo:xxx:query")
|
|
302
|
+
@GetMapping("/{id}")
|
|
303
|
+
public R<XxxVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
|
|
304
|
+
return R.ok(xxxService.queryById(id));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@SaCheckPermission("demo:xxx:add")
|
|
308
|
+
@Log(title = "XXX管理", businessType = BusinessType.INSERT)
|
|
309
|
+
@RepeatSubmit()
|
|
310
|
+
@PostMapping()
|
|
311
|
+
public R<Void> add(@Validated(AddGroup.class) @RequestBody XxxBo bo) {
|
|
312
|
+
return toAjax(xxxService.insertByBo(bo));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@SaCheckPermission("demo:xxx:edit")
|
|
316
|
+
@Log(title = "XXX管理", businessType = BusinessType.UPDATE)
|
|
317
|
+
@RepeatSubmit()
|
|
318
|
+
@PutMapping()
|
|
319
|
+
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XxxBo bo) {
|
|
320
|
+
return toAjax(xxxService.updateByBo(bo));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@SaCheckPermission("demo:xxx:remove")
|
|
324
|
+
@Log(title = "XXX管理", businessType = BusinessType.DELETE)
|
|
325
|
+
@DeleteMapping("/{ids}")
|
|
326
|
+
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
|
|
327
|
+
return toAjax(xxxService.deleteWithValidByIds(Arrays.asList(ids), true));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@SaCheckPermission("demo:xxx:export")
|
|
331
|
+
@Log(title = "XXX管理", businessType = BusinessType.EXPORT)
|
|
332
|
+
@PostMapping("/export")
|
|
333
|
+
public void export(@Validated XxxBo bo, HttpServletResponse response) {
|
|
334
|
+
List<XxxVo> list = xxxService.queryList(bo);
|
|
335
|
+
ExcelUtil.exportExcel(list, "XXX数据", XxxVo.class, response);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## 8. 建表 SQL
|
|
341
|
+
|
|
342
|
+
```sql
|
|
343
|
+
CREATE TABLE demo_xxx (
|
|
344
|
+
id BIGINT(20) NOT NULL COMMENT '主键(雪花ID,不用AUTO_INCREMENT)',
|
|
345
|
+
tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户ID',
|
|
346
|
+
xxx_name VARCHAR(100) NOT NULL COMMENT '名称',
|
|
347
|
+
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
|
348
|
+
create_dept BIGINT(20) DEFAULT NULL COMMENT '创建部门',
|
|
349
|
+
create_by BIGINT(20) DEFAULT NULL COMMENT '创建人',
|
|
350
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
351
|
+
update_by BIGINT(20) DEFAULT NULL COMMENT '更新人',
|
|
352
|
+
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
353
|
+
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
|
354
|
+
del_flag BIGINT(20) DEFAULT 0 COMMENT '删除标志(0正常 1已删除)',
|
|
355
|
+
PRIMARY KEY (id)
|
|
356
|
+
) ENGINE=InnoDB COMMENT='XXX表';
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 常见错误速查
|
|
362
|
+
|
|
363
|
+
```java
|
|
364
|
+
// ---- 错误写法 ----
|
|
365
|
+
private final IXxxDao xxxDao; // 本项目没有 DAO 层
|
|
366
|
+
BeanUtil.copyProperties(bo, entity); // 必须用 MapstructUtils.convert()
|
|
367
|
+
class XxxServiceImpl extends ServiceImpl<...> {} // Service 不继承基类
|
|
368
|
+
@AutoMappers({@AutoMapper(...)}) // 用单数 @AutoMapper
|
|
369
|
+
@GetMapping("/pageXxxs") // 应该是 /list
|
|
370
|
+
@GetMapping("/getXxx/{id}") // 应该是 /{id}
|
|
371
|
+
|
|
372
|
+
// ---- 正确写法 ----
|
|
373
|
+
private final XxxMapper baseMapper; // 直接注入 Mapper
|
|
374
|
+
MapstructUtils.convert(bo, Xxx.class); // MapstructUtils 转换
|
|
375
|
+
class XxxServiceImpl implements IXxxService {} // 只实现接口
|
|
376
|
+
@AutoMapper(target = Xxx.class) // 单数注解
|
|
377
|
+
@GetMapping("/list") // RESTful 路径
|
|
378
|
+
@GetMapping("/{id}") // RESTful 路径
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 检查清单
|
|
384
|
+
|
|
385
|
+
- [ ] 包名是 `org.dromara.*`?
|
|
386
|
+
- [ ] Service 只实现接口,不继承基类?
|
|
387
|
+
- [ ] Service 直接注入 Mapper(无 DAO)?
|
|
388
|
+
- [ ] `buildQueryWrapper()` 在 Service 层?
|
|
389
|
+
- [ ] Entity 继承 `TenantEntity`?
|
|
390
|
+
- [ ] BO 使用 `@AutoMapper`(单数)?
|
|
391
|
+
- [ ] 使用 `MapstructUtils.convert()` 转换?
|
|
392
|
+
- [ ] Mapper 继承 `BaseMapperPlus<Entity, VO>`?
|
|
393
|
+
- [ ] Controller 使用 RESTful 路径?
|
|
394
|
+
- [ ] SQL 使用 `del_flag`(0正常 1删除)?
|
|
395
|
+
- [ ] 主键使用雪花 ID(无 AUTO_INCREMENT)?
|
|
396
|
+
- [ ] 代码注释和 SQL COMMENT 使用中文?
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 参考实现
|
|
401
|
+
|
|
402
|
+
| 类型 | 类名 |
|
|
403
|
+
|------|------|
|
|
404
|
+
| Entity | `org.dromara.demo.domain.TestDemo` |
|
|
405
|
+
| BO | `org.dromara.demo.domain.bo.TestDemoBo` |
|
|
406
|
+
| VO | `org.dromara.demo.domain.vo.TestDemoVo` |
|
|
407
|
+
| Service | `org.dromara.demo.service.impl.TestDemoServiceImpl` |
|
|
408
|
+
| Mapper | `org.dromara.demo.mapper.TestDemoMapper` |
|
|
409
|
+
| Controller | `org.dromara.demo.controller.TestDemoController` |
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-permission
|
|
3
|
+
description: |
|
|
4
|
+
数据权限开发指南。实现行级数据隔离,支持部门权限、本人权限、自定义权限等 6 种权限类型。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 为业务模块添加数据权限过滤
|
|
8
|
+
- 配置部门级数据隔离
|
|
9
|
+
- 扩展自定义数据权限类型
|
|
10
|
+
- 临时忽略数据权限查询全量数据
|
|
11
|
+
- 排查数据权限不生效问题
|
|
12
|
+
|
|
13
|
+
触发词:数据权限、@DataPermission、DataScope、行级权限、数据隔离、部门权限、本人权限、自定义权限、权限过滤、数据过滤、按部门过滤、按创建人过滤
|
|
14
|
+
|
|
15
|
+
注意:如果是认证授权(登录、Token、Sa-Token)或菜单/按钮权限,请使用 security-guard。
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# 数据权限开发指南
|
|
19
|
+
|
|
20
|
+
> 通过 MyBatis 拦截器自动注入 WHERE 条件,实现行级数据过滤。
|
|
21
|
+
|
|
22
|
+
## 1. 六种权限类型
|
|
23
|
+
|
|
24
|
+
| 类型 | 字典值 | SQL 效果 |
|
|
25
|
+
|------|--------|---------|
|
|
26
|
+
| 全部数据 | 1 | 不拼接条件 |
|
|
27
|
+
| 自定义权限 | 2 | `dept_id IN (角色关联的部门ID)` |
|
|
28
|
+
| 本部门 | 3 | `dept_id = 100` |
|
|
29
|
+
| 本部门及以下 | 4 | `dept_id IN (100,101,102)` |
|
|
30
|
+
| 仅本人 | 5 | `create_by = 1` |
|
|
31
|
+
| 部门及以下或本人 | 6 | `dept_id IN (...) OR create_by = 1` |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. 快速上手
|
|
36
|
+
|
|
37
|
+
### 步骤 1:Service 方法加注解
|
|
38
|
+
|
|
39
|
+
```java
|
|
40
|
+
import org.dromara.common.mybatis.annotation.DataPermission;
|
|
41
|
+
import org.dromara.common.mybatis.annotation.DataColumn;
|
|
42
|
+
|
|
43
|
+
@Service
|
|
44
|
+
@RequiredArgsConstructor
|
|
45
|
+
public class OrderServiceImpl implements IOrderService {
|
|
46
|
+
|
|
47
|
+
private final OrderMapper baseMapper;
|
|
48
|
+
|
|
49
|
+
@DataPermission({
|
|
50
|
+
@DataColumn(key = "deptName", value = "create_dept"),
|
|
51
|
+
@DataColumn(key = "userName", value = "create_by")
|
|
52
|
+
})
|
|
53
|
+
@Override
|
|
54
|
+
public TableDataInfo<OrderVo> pageWithPermission(OrderBo bo, PageQuery pageQuery) {
|
|
55
|
+
LambdaQueryWrapper<Order> lqw = buildQueryWrapper(bo);
|
|
56
|
+
Page<OrderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
|
57
|
+
return TableDataInfo.build(result);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 步骤 2:确保表有权限字段
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
CREATE TABLE m_order (
|
|
66
|
+
id BIGINT(20) NOT NULL COMMENT '主键ID',
|
|
67
|
+
-- 业务字段 ...
|
|
68
|
+
create_dept BIGINT(20) DEFAULT NULL COMMENT '创建部门', -- 必须
|
|
69
|
+
create_by BIGINT(20) DEFAULT NULL COMMENT '创建人', -- 必须
|
|
70
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
71
|
+
PRIMARY KEY (id)
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 步骤 3:角色管理中配置数据权限范围
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. 使用场景
|
|
80
|
+
|
|
81
|
+
### 按部门过滤(最常见)
|
|
82
|
+
|
|
83
|
+
```java
|
|
84
|
+
@DataPermission({
|
|
85
|
+
@DataColumn(key = "deptName", value = "create_dept")
|
|
86
|
+
})
|
|
87
|
+
public List<Order> listWithPermission(OrderBo bo) {
|
|
88
|
+
return list(buildQueryWrapper(bo));
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 按创建人过滤
|
|
93
|
+
|
|
94
|
+
```java
|
|
95
|
+
@DataPermission({
|
|
96
|
+
@DataColumn(key = "userName", value = "create_by")
|
|
97
|
+
})
|
|
98
|
+
public List<Task> listMyTasks(TaskBo bo) {
|
|
99
|
+
return list(buildQueryWrapper(bo));
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 部门 + 创建人混合
|
|
104
|
+
|
|
105
|
+
```java
|
|
106
|
+
@DataPermission({
|
|
107
|
+
@DataColumn(key = "deptName", value = "create_dept"),
|
|
108
|
+
@DataColumn(key = "userName", value = "create_by")
|
|
109
|
+
})
|
|
110
|
+
public TableDataInfo<ProjectVo> pageWithPermission(ProjectBo bo, PageQuery pageQuery) {
|
|
111
|
+
// ...
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 多表关联(使用表别名)
|
|
116
|
+
|
|
117
|
+
```java
|
|
118
|
+
// SQL: SELECT u.*, d.dept_name FROM sys_user u LEFT JOIN sys_dept d ON ...
|
|
119
|
+
@DataPermission({
|
|
120
|
+
@DataColumn(key = "deptName", value = "u.dept_id"),
|
|
121
|
+
@DataColumn(key = "userName", value = "u.user_id")
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 临时忽略数据权限
|
|
126
|
+
|
|
127
|
+
```java
|
|
128
|
+
import org.dromara.common.mybatis.helper.DataPermissionHelper;
|
|
129
|
+
|
|
130
|
+
// 忽略数据权限,查全量
|
|
131
|
+
Long total = DataPermissionHelper.ignore(() -> orderService.count());
|
|
132
|
+
|
|
133
|
+
// 无返回值
|
|
134
|
+
DataPermissionHelper.ignore(() -> {
|
|
135
|
+
List<Config> configs = configService.list();
|
|
136
|
+
return null;
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 指定权限标识跳过过滤
|
|
141
|
+
|
|
142
|
+
```java
|
|
143
|
+
// 拥有 order:all 权限的角色不过滤
|
|
144
|
+
@DataPermission({
|
|
145
|
+
@DataColumn(key = "deptName", value = "create_dept", permission = "order:all")
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. Mapper XML 中使用
|
|
152
|
+
|
|
153
|
+
```java
|
|
154
|
+
// Mapper 接口
|
|
155
|
+
@DataPermission({
|
|
156
|
+
@DataColumn(key = "deptName", value = "o.create_dept")
|
|
157
|
+
})
|
|
158
|
+
List<OrderVo> selectOrderReport(@Param("bo") OrderBo bo);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```xml
|
|
162
|
+
<select id="selectOrderReport" resultType="OrderVo">
|
|
163
|
+
SELECT o.*, u.user_name
|
|
164
|
+
FROM m_order o
|
|
165
|
+
LEFT JOIN sys_user u ON o.create_by = u.user_id
|
|
166
|
+
WHERE o.status = #{bo.status}
|
|
167
|
+
<!-- 数据权限自动追加到这里 -->
|
|
168
|
+
</select>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. 扩展自定义权限类型
|
|
174
|
+
|
|
175
|
+
> 详细步骤见 `references/custom-data-scope.md`
|
|
176
|
+
|
|
177
|
+
**步骤 1**:修改 `DataScopeType` 枚举
|
|
178
|
+
|
|
179
|
+
```java
|
|
180
|
+
REGION("7", "按区域", "#{#regionName} IN ( #{@sdss.getUserRegions( #user.userId )} )"),
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**步骤 2**:在 `ISysDataScopeService` 添加方法
|
|
184
|
+
|
|
185
|
+
```java
|
|
186
|
+
@Service("sdss")
|
|
187
|
+
public class SysDataScopeServiceImpl implements ISysDataScopeService {
|
|
188
|
+
@Override
|
|
189
|
+
@Cacheable(cacheNames = CacheNames.SYS_USER_REGIONS, key = "#userId")
|
|
190
|
+
public String getUserRegions(Long userId) {
|
|
191
|
+
List<Long> regionIds = userRegionMapper.selectRegionIdsByUserId(userId);
|
|
192
|
+
return CollUtil.isEmpty(regionIds) ? "-1" : StringUtils.join(regionIds, ",");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**步骤 3**:使用
|
|
198
|
+
|
|
199
|
+
```java
|
|
200
|
+
@DataPermission({
|
|
201
|
+
@DataColumn(key = "regionName", value = "region_id")
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 自定义变量
|
|
206
|
+
|
|
207
|
+
```java
|
|
208
|
+
// 设置自定义变量(请求结束后自动清理)
|
|
209
|
+
DataPermissionHelper.setVariable("shopId", shopId);
|
|
210
|
+
|
|
211
|
+
@DataPermission({
|
|
212
|
+
@DataColumn(key = "shopId", value = "shop_id")
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 6. 多角色权限计算
|
|
219
|
+
|
|
220
|
+
- **SELECT 查询**:多角色权限用 `OR` 连接(并集)
|
|
221
|
+
- **UPDATE/DELETE**:多角色权限用 `AND` 连接(交集)
|
|
222
|
+
- 可通过 `joinStr` 参数自定义:
|
|
223
|
+
|
|
224
|
+
```java
|
|
225
|
+
@DataPermission(value = {
|
|
226
|
+
@DataColumn(key = "deptName", value = "create_dept")
|
|
227
|
+
}, joinStr = "AND")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 7. 禁止项
|
|
233
|
+
|
|
234
|
+
```java
|
|
235
|
+
// ❌ 在 ISysDataScopeService 内调用带权限的方法(死循环)
|
|
236
|
+
public String getDeptAndChild(Long deptId) {
|
|
237
|
+
deptService.list(wrapper); // 如果带 @DataPermission 会死循环
|
|
238
|
+
// ✅ 直接用 Mapper 或 DataPermissionHelper.ignore()
|
|
239
|
+
deptMapper.selectList(wrapper);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ❌ 表别名不匹配
|
|
243
|
+
@DataColumn(key = "deptName", value = "user.dept_id") // SQL 别名是 u
|
|
244
|
+
// ✅ @DataColumn(key = "deptName", value = "u.dept_id")
|
|
245
|
+
|
|
246
|
+
// ❌ 在 Controller 层使用 @DataPermission(无效!)
|
|
247
|
+
// ✅ 必须在 Service 实现类或 Mapper 接口上
|
|
248
|
+
|
|
249
|
+
// ✅ Entity 必须继承 TenantEntity(包含 create_dept、create_by)
|
|
250
|
+
// ✅ 多表查询时使用正确的表别名
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 8. 问题排查
|
|
256
|
+
|
|
257
|
+
| 检查项 | 可能原因 | 解决方案 |
|
|
258
|
+
|--------|---------|---------|
|
|
259
|
+
| 超级管理员? | 超管自动跳过权限 | 用普通用户测试 |
|
|
260
|
+
| 角色数据范围? | 范围为"全部数据" | 修改角色数据权限 |
|
|
261
|
+
| 注解位置? | 不在 Service/Mapper 层 | 移动到 Service 实现类 |
|
|
262
|
+
| 表别名? | value 别名与 SQL 不一致 | 检查修正别名 |
|
|
263
|
+
| Unknown column? | 表别名不存在 | 检查 value 中的别名 |
|
|
264
|
+
| dept_id IN ()? | 权限服务返回空 | 检查 ISysDataScopeService |
|
|
265
|
+
|
|
266
|
+
**调试**:开启 SQL 日志查看拼接结果
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
mybatis-plus:
|
|
270
|
+
configuration:
|
|
271
|
+
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 9. 核心类位置
|
|
277
|
+
|
|
278
|
+
| 类 | 路径 |
|
|
279
|
+
|---|------|
|
|
280
|
+
| `@DataPermission` | `ruoyi-common/ruoyi-common-mybatis/.../annotation/DataPermission.java` |
|
|
281
|
+
| `@DataColumn` | `ruoyi-common/ruoyi-common-mybatis/.../annotation/DataColumn.java` |
|
|
282
|
+
| `DataScopeType` | `ruoyi-common/ruoyi-common-mybatis/.../enums/DataScopeType.java` |
|
|
283
|
+
| `DataPermissionHelper` | `ruoyi-common/ruoyi-common-mybatis/.../helper/DataPermissionHelper.java` |
|
|
284
|
+
| `PlusDataPermissionHandler` | `ruoyi-common/ruoyi-common-mybatis/.../handler/PlusDataPermissionHandler.java` |
|
|
285
|
+
| 使用示例 | `ruoyi-modules/ruoyi-system/.../impl/SysUserServiceImpl.java` |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 多项目适配说明
|
|
290
|
+
|
|
291
|
+
- 如果需要 leniu-tengyun-core 项目的数据权限开发规范,请使用 `leniu-data-permission` skill
|
|
292
|
+
- leniu-tengyun-core 使用物理库隔离架构,与 RuoYi-Vue-Plus 的逻辑隔离方式不同
|