20260922172938
This commit is contained in:
1 parent
583984c24f
commit
c8dad8d4a6
46 files changed
+3366
-1246
No files matched your search
@@ -30,6 +30,9 @@ import java.util.Set;
|
||||
* 不读模块关系、不跑模块 SQL 规则。普通模块列表不允许传表名。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>另外,{@code saveobjt} 的删除行可以带 {@code dependent_deletes} 声明,把引用这些行的
|
||||
* 依赖行一并删除(见 {@link #deleteDependents}):依赖关系由调用方声明,服务端不按表名猜。
|
||||
*
|
||||
* <p>计划、检查、删除在同一事务中完成(设计 §5.3):任一步骤失败整批回滚。
|
||||
*/
|
||||
@Service
|
||||
@@ -178,6 +181,110 @@ public class DataDeleteService {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖行删除:本表被删的行被别的表引用时,连引用行一起删(《FMS删除策略重构设计》§6.2)。
|
||||
*
|
||||
* <p>例如删除权限点({@code s_power})要连引用它的用户授权行({@code s_user_power})
|
||||
* 一起删:授权行只记录“拥有”,没有外键约束,权限点没了、授权行留着就是幽灵授权 ——
|
||||
* 权限判定会读到它,授权界面也无从解释。这类关系不登记在 {@code s_relation} 里
|
||||
* (那是有模块归属的业务关系),也不由服务端按表名反查:调用方在请求里声明依赖表、
|
||||
* 键列和取值列,这里按声明执行。表名只出现在请求里,服务端不认识具体业务表。
|
||||
*
|
||||
* <p>声明格式 {@code {table, key_field, source_field}}:对本表本次要删的每一行,
|
||||
* 用取值列的值得出要删的键值,再删依赖表里键列等于该值的行。取值用本表这一行的列值
|
||||
* 条件查出(与物理删除的 WHERE 同口径),所以调用方不需要先查一遍再拼删除行。
|
||||
*
|
||||
* @param parentTable 本表(正在删除的表)的元数据
|
||||
* @param parentRows 本次要删除的行,列值即删除条件
|
||||
* @param dependents 依赖声明列表
|
||||
* @return 实际删除的依赖行数
|
||||
*/
|
||||
public int deleteDependents(
|
||||
Connection connection,
|
||||
DbUtils.TableMetadata parentTable,
|
||||
List<Map<String, Object>> parentRows,
|
||||
List<Map<String, Object>> dependents
|
||||
) throws SQLException {
|
||||
int total = 0;
|
||||
for (Map<String, Object> dependent : dependents) {
|
||||
String table = text(dependent.get("table"));
|
||||
String keyField = text(dependent.get("key_field"));
|
||||
String sourceField = text(dependent.get("source_field"));
|
||||
// 声明不完整时直接拒绝:静默跳过等于把该清的依赖行留下,成了没人解释得了的孤儿行
|
||||
if (table == null || keyField == null || sourceField == null) {
|
||||
throw new BusinessException(
|
||||
"依赖删除声明不完整,需要 table / key_field / source_field");
|
||||
}
|
||||
|
||||
DbUtils.TableMetadata dependentTable = dbUtils.loadTableMetadata(connection, table);
|
||||
DbUtils.ColumnMetadata keyColumn = requireKeyColumn(dependentTable, keyField);
|
||||
DbUtils.ColumnMetadata sourceColumn = dbUtils.getColumn(parentTable, sourceField);
|
||||
if (sourceColumn == null) {
|
||||
throw new BusinessException("依赖删除的取值列不存在: "
|
||||
+ parentTable.requestedName() + "." + sourceField);
|
||||
}
|
||||
|
||||
List<Object> values = querySourceValues(
|
||||
connection, parentTable, parentRows, sourceColumn);
|
||||
if (values.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int deleted = deleteRecords(connection, dependentTable, keyColumn, values);
|
||||
total += deleted;
|
||||
writeDeleteLog(null, parentTable.requestedName(), table,
|
||||
keyColumn.name(), values.size(), deleted);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出本表待删行在取值列上的值(去重)。
|
||||
*
|
||||
* <p>匹配条件与物理删除同口径:行内有本表可写列的列值做 AND;一行里没有任何本表列时
|
||||
* 拒绝执行,和 {@code DbUtils.delete} 一样不允许从这条路径形成无条件匹配。
|
||||
*/
|
||||
private List<Object> querySourceValues(
|
||||
Connection connection,
|
||||
DbUtils.TableMetadata table,
|
||||
List<Map<String, Object>> rows,
|
||||
DbUtils.ColumnMetadata sourceColumn
|
||||
) throws SQLException {
|
||||
Set<Object> values = new LinkedHashSet<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
List<String> conditions = new ArrayList<>();
|
||||
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
||||
DbUtils.ColumnMetadata column = columnOf(table, entry.getKey());
|
||||
if (column == null || !column.writable()) {
|
||||
continue;
|
||||
}
|
||||
conditions.add(dbUtils.quoteQualifiedIdentifier(column.name())
|
||||
+ " = " + literal(entry.getValue(), column));
|
||||
}
|
||||
if (conditions.isEmpty()) {
|
||||
throw new BusinessException("删除条件不能为空: " + table.requestedName());
|
||||
}
|
||||
|
||||
String sql = "select distinct " + dbUtils.quoteQualifiedIdentifier(sourceColumn.name())
|
||||
+ " from " + table.quotedName()
|
||||
+ " where " + String.join(" and ", conditions);
|
||||
for (Map<String, Object> found : dbUtils.loadDataBySql(connection, sql)) {
|
||||
Object value = found.get(sourceColumn.name());
|
||||
if (value != null) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(values);
|
||||
}
|
||||
|
||||
/** 按列名取元数据(大小写不敏感);不是本表列时返回 null,与物理删除忽略未知列同口径 */
|
||||
private DbUtils.ColumnMetadata columnOf(DbUtils.TableMetadata table, String fieldName) {
|
||||
if (fieldName == null) {
|
||||
return null;
|
||||
}
|
||||
return table.columnsByName().get(fieldName.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
// ── target=module:计划 → 检查 → 执行 ──────────────────────────────────
|
||||
|
||||
private Map<String, Integer> executeModuleTarget(
|
||||
|
||||
@@ -25,6 +25,13 @@ import java.util.StringJoiner;
|
||||
@Service
|
||||
public class DataSaveService {
|
||||
|
||||
/**
|
||||
* 依赖行删除声明(请求可选):本表本次要删的行被别的表引用时,连引用行一起删。
|
||||
* 声明为 {@code [{table, key_field, source_field}]},由删除服务按声明执行
|
||||
* (《FMS删除策略重构设计》§6.2)。表名只出现在请求里,服务端不按表名猜关系。
|
||||
*/
|
||||
private static final String DEPENDENT_DELETES = "dependent_deletes";
|
||||
|
||||
@Resource
|
||||
private DataSource dataSource;
|
||||
|
||||
@@ -430,6 +437,15 @@ public class DataSaveService {
|
||||
"update",
|
||||
rows(request, "updates")
|
||||
);
|
||||
// 依赖行随本表删除:请求显式声明依赖表(如删权限点连引用它的授权行一起删),
|
||||
// 由删除服务按声明查出引用行的键值再删(设计《删除策略》§6.2)。声明通用 ——
|
||||
// 服务端不认识具体表名,依赖关系由调用方声明,不做“按表名猜关系”的特殊处理。
|
||||
// 依赖行先于父行删除(设计 §5.3),所以放在物理删除之前;本表没有删除行时
|
||||
// 没有依赖可清,直接跳过。与本次删除同事务,任一步失败整体回滚。
|
||||
List<Map<String, Object>> dependentDeletes = rows(request, DEPENDENT_DELETES);
|
||||
if (!deleteRows.isEmpty() && !dependentDeletes.isEmpty()) {
|
||||
executeDependentDeletes(connection, table, deleteRows, dependentDeletes);
|
||||
}
|
||||
if (deleteTarget != null && !deleteTarget.isBlank() && !deleteRows.isEmpty()) {
|
||||
executeDeletes(connection, request, tableName, keyField, deleteTarget, deleteRows);
|
||||
} else {
|
||||
@@ -450,6 +466,25 @@ public class DataSaveService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行请求声明的依赖行删除(设计《删除策略》§6.2)。
|
||||
*
|
||||
* <p>具体匹配与删除走删除服务:那里有物理删除执行器、主键类型校验和删除日志,
|
||||
* 与其它删除路径同一套实现,不在这里再写一份。
|
||||
*/
|
||||
private void executeDependentDeletes(
|
||||
Connection connection,
|
||||
DbUtils.TableMetadata table,
|
||||
List<Map<String, Object>> deleteRows,
|
||||
List<Map<String, Object>> dependentDeletes
|
||||
) {
|
||||
try {
|
||||
dataDeleteService.deleteDependents(connection, table, deleteRows, dependentDeletes);
|
||||
} catch (SQLException exception) {
|
||||
throw new SaveObjectException(table.requestedName(), "delete", -1, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过删除服务执行 saveobjt 的删除行(设计 §6.2)。
|
||||
*
|
||||
|
||||
@@ -29,7 +29,9 @@ import java.util.regex.Pattern;
|
||||
* 三层,与《FMS新系统核心表结构设计》第 14 节一一对应:
|
||||
* 1. 动作权限(14.1 / 14.2):s_power(action.{模块}.{动作})+ s_user_power,白名单语义;
|
||||
* 2. 字段权限(14.3):s_user_field_power 的 b_view / b_export,无记录按模块默认(可看可导出);
|
||||
* 3. 数据范围(14.4):s_user_data_power,按操作精确匹配优先、多条规则 OR、整体再与其它条件 AND。
|
||||
* 3. 数据范围(14.4):s_user_data_power,按操作精确匹配优先、多条规则 OR、整体再与其它条件 AND;
|
||||
* 按范围判的规则所用的字段取自模块配置 s_module.b_scope_field(记录归属人字段),
|
||||
* self 直接比该字段,dept / dept_tree 由当前用户反查部门后再比。
|
||||
*/
|
||||
@Service
|
||||
public class SqlPermissionService {
|
||||
@@ -107,7 +109,7 @@ public class SqlPermissionService {
|
||||
* 无生效规则返回 null,表示该模块无数据范围限制。
|
||||
*/
|
||||
public String buildDataScopeCondition(String moduleId, String userId, String operation) {
|
||||
String sql = "SELECT [b_operation], [b_scope_type], [b_scope_field], [b_condition_sql] "
|
||||
String sql = "SELECT [b_operation], [b_scope_type], [b_condition_sql] "
|
||||
+ "FROM [s_user_data_power] "
|
||||
+ "WHERE [b_user_id] = ? AND [b_module_id] = ? AND [b_canuse] = 1 "
|
||||
+ "AND ([b_operation] = ? OR [b_operation] = '*') "
|
||||
@@ -128,20 +130,29 @@ public class SqlPermissionService {
|
||||
List<Map<String, Object>> effective = exact.isEmpty() ? wildcard : exact;
|
||||
|
||||
String deptId = null;
|
||||
// 判断字段只来自模块配置(s_module.b_scope_field,记录归属人字段):同一个模块的
|
||||
// 所有用户必须用同一字段判范围,所以授权行上的同名列不参与判定。按需读一次,
|
||||
// 只有按范围判的规则(self / dept / dept_tree)才需要它
|
||||
String moduleScopeField = null;
|
||||
List<String> fragments = new ArrayList<>();
|
||||
for (Map<String, Object> row : effective) {
|
||||
String scopeType = text(row.get("b_scope_type")).toLowerCase(Locale.ROOT);
|
||||
String scopeField = text(row.get("b_scope_field"));
|
||||
switch (scopeType) {
|
||||
case "all", "" -> {
|
||||
// 全部数据:不追加条件
|
||||
}
|
||||
case "self" -> {
|
||||
String field = requireScopeField(scopeField);
|
||||
if (moduleScopeField == null) {
|
||||
moduleScopeField = loadModuleScopeField(connection, moduleId);
|
||||
}
|
||||
String field = requireScopeField(moduleScopeField);
|
||||
fragments.add(quote(field) + " = " + dbUtils.toSqlStringLiteral(userId));
|
||||
}
|
||||
case "dept", "dept_tree" -> {
|
||||
String field = requireScopeField(scopeField);
|
||||
if (moduleScopeField == null) {
|
||||
moduleScopeField = loadModuleScopeField(connection, moduleId);
|
||||
}
|
||||
String field = requireScopeField(moduleScopeField);
|
||||
if (deptId == null) {
|
||||
deptId = loadDeptId(connection, userId);
|
||||
}
|
||||
@@ -197,6 +208,20 @@ public class SqlPermissionService {
|
||||
return rows.isEmpty() ? null : text(rows.get(0).get("b_dept_id"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块的数据范围字段(s_module.b_scope_field,记录归属人字段如 b_inputuser_id)。
|
||||
* 配在模块上而不是授权行上:同一个模块的所有用户必须用同一字段判范围。
|
||||
* 未配置时返回空串,由 requireScopeField 明确报错,不静默放行。
|
||||
*/
|
||||
private String loadModuleScopeField(Connection connection, String moduleId) throws SQLException {
|
||||
List<Map<String, Object>> rows = query(
|
||||
connection,
|
||||
"SELECT [b_scope_field] FROM [s_module] WHERE [b_id] = ?",
|
||||
moduleId
|
||||
);
|
||||
return rows.isEmpty() ? "" : text(rows.get(0).get("b_scope_field"));
|
||||
}
|
||||
|
||||
private String loadDeptPath(Connection connection, String deptId) throws SQLException {
|
||||
List<Map<String, Object>> rows = query(
|
||||
connection,
|
||||
|
||||
Reference in new issue
Block a user