| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- package com.dms.service;
- import com.dms.common.DmsException;
- import com.dms.common.Errors;
- import com.dms.common.IpAddresses;
- import com.dms.common.Page;
- import com.dms.common.Payloads;
- import com.dms.mapper.IpEntryMapper;
- import com.dms.mapper.OrganizationMapper;
- import com.dms.model.IpEntryEntity;
- import com.dms.model.OrgIpAssignmentEntity;
- import com.dms.model.OrganizationEntity;
- import com.dms.security.AuthContext;
- import jakarta.servlet.http.HttpServletRequest;
- import java.net.InetAddress;
- import java.time.LocalDateTime;
- import java.time.ZoneOffset;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TreeSet;
- import org.springframework.dao.DuplicateKeyException;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- /**
- * 部门IP登录限制:IP条目池维护与部门分配,
- * 对齐 backend/dms/services/ip_entry_service.py。
- */
- @Service
- public class IpEntryService {
- private static final Set<String> IP_ENTRY_CREATE_FIELDS = Set.of("ipValue", "remark");
- private static final Set<String> IP_ENTRY_UPDATE_FIELDS = Set.of("ipValue", "remark", "rowVersion");
- private static final Set<String> ORG_ASSIGNMENT_FIELDS = Set.of("ipEntryIds", "rowVersion");
- private static final int IP_VALUE_MAX_LENGTH = 64;
- private static final int REMARK_MAX_LENGTH = 128;
- private final IpEntryMapper ipEntryMapper;
- private final OrganizationMapper organizationMapper;
- private final AuditService auditService;
- public IpEntryService(IpEntryMapper ipEntryMapper, OrganizationMapper organizationMapper,
- AuditService auditService) {
- this.ipEntryMapper = ipEntryMapper;
- this.organizationMapper = organizationMapper;
- this.auditService = auditService;
- }
- // ------------------------------------------------------------ 条目池查询
- @Transactional(readOnly = true)
- public Map<String, Object> listIpEntries(String keyword, Page.Params pageRequest) {
- String keywordValue = keyword != null && !keyword.strip().isEmpty()
- ? keyword.strip() : null;
- long total = ipEntryMapper.countByFilters(keywordValue);
- List<IpEntryEntity> entries = ipEntryMapper.pageByFilters(
- keywordValue, pageRequest.pageSize(), pageRequest.offset());
- List<Map<String, Object>> items = new ArrayList<>(entries.size());
- for (IpEntryEntity entry : entries) {
- items.add(entrySummary(entry));
- }
- return Page.result(items, pageRequest, total);
- }
- // ------------------------------------------------------------ 条目池 CRUD
- /** 对齐 create_ip_entry。 */
- @Transactional
- public Map<String, Object> create(Object payload, HttpServletRequest request) {
- Map<String, Object> body = Payloads.exactPayload(payload, IP_ENTRY_CREATE_FIELDS);
- String ipValue = normalizeIpValue(body.get("ipValue"));
- String remark = remark(body.get("remark"));
- Long actorId = AuthContext.get().userId();
- try {
- if (ipEntryMapper.findIdByIpValueExclude(ipValue, 0L) != null) {
- throw conflict("该IP已存在", null);
- }
- IpEntryEntity entry = new IpEntryEntity();
- entry.setIpValue(ipValue);
- entry.setRemark(remark);
- entry.setCreatedBy(actorId);
- entry.setUpdatedBy(actorId);
- entry.setRowVersion(0);
- ipEntryMapper.insert(entry);
- Map<String, Object> detail = Payloads.orderedMap();
- detail.put("ipValue", entry.getIpValue());
- detail.put("remark", entry.getRemark());
- audit(request, AuditService.Action.CREATE_IP_ENTRY,
- entry.getId(), entry.getIpValue(), detail);
- return entrySummary(entry);
- } catch (DuplicateKeyException ex) {
- throw conflict("该IP已存在", null);
- }
- }
- /** 对齐 update_ip_entry。 */
- @Transactional
- public Map<String, Object> update(long entryId, Object payload, HttpServletRequest request) {
- Map<String, Object> body = Payloads.exactPayload(payload, IP_ENTRY_UPDATE_FIELDS);
- String ipValue = normalizeIpValue(body.get("ipValue"));
- String remark = remark(body.get("remark"));
- int expectedVersion = Payloads.rowVersion(body.get("rowVersion"));
- Long actorId = AuthContext.get().userId();
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- IpEntryEntity entry = ipEntryMapper.findActiveByIdForUpdate(entryId);
- if (entry == null) {
- throw notFound("IP条目不存在");
- }
- checkVersion(entry, expectedVersion);
- if (ipEntryMapper.findIdByIpValueExclude(ipValue, entryId) != null) {
- throw conflict("该IP已存在", null);
- }
- Map<String, Object> before = Payloads.orderedMap();
- before.put("ipValue", entry.getIpValue());
- before.put("remark", entry.getRemark());
- entry.setIpValue(ipValue);
- entry.setRemark(remark);
- entry.setUpdatedBy(actorId);
- entry.setUpdatedAt(now);
- entry.setRowVersion(entry.getRowVersion() + 1);
- ipEntryMapper.update(entry);
- Map<String, Object> after = Payloads.orderedMap();
- after.put("ipValue", entry.getIpValue());
- after.put("remark", entry.getRemark());
- Map<String, Object> detail = Payloads.orderedMap();
- detail.put("before", before);
- detail.put("after", after);
- audit(request, AuditService.Action.EDIT_IP_ENTRY,
- entry.getId(), entry.getIpValue(), detail);
- return entrySummary(entry);
- }
- /** 对齐 delete_ip_entry:被部门引用时拒绝删除。 */
- @Transactional
- public Map<String, Object> delete(long entryId, int rowVersion, HttpServletRequest request) {
- Long actorId = AuthContext.get().userId();
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- IpEntryEntity entry = ipEntryMapper.findActiveByIdForUpdate(entryId);
- if (entry == null) {
- throw notFound("IP条目不存在");
- }
- checkVersion(entry, rowVersion);
- if (ipEntryMapper.countActiveAssignmentsByEntry(entryId) > 0) {
- throw conflict("该IP已分配给部门,请先取消分配", null);
- }
- String targetName = entry.getIpValue();
- ipEntryMapper.softDelete(entryId, now, actorId);
- entry.setRowVersion(entry.getRowVersion() + 1);
- Map<String, Object> detail = Payloads.orderedMap();
- detail.put("ipValue", targetName);
- detail.put("rowVersion", entry.getRowVersion());
- audit(request, AuditService.Action.DELETE_IP_ENTRY, entryId, targetName, detail);
- Map<String, Object> result = Payloads.orderedMap();
- result.put("id", serializeId(entryId));
- result.put("deleted", true);
- return result;
- }
- // ------------------------------------------------------------ 部门分配
- @Transactional(readOnly = true)
- public Map<String, Object> listOrgIpAssignments(long organizationId) {
- if (organizationMapper.findByIdNotDeleted(organizationId) == null) {
- throw notFound("机构不存在");
- }
- Map<String, Object> result = Payloads.orderedMap();
- result.put("ipEntryIds", ipEntryMapper.listAssignedEntryIdsByOrganization(organizationId)
- .stream().map(String::valueOf).toList());
- return result;
- }
- /** 对齐 update_org_ip_assignments:整体替换 + 组织 row_version 联动。 */
- @Transactional
- public Map<String, Object> updateOrgIpAssignments(long organizationId, Object payload,
- HttpServletRequest request) {
- Map<String, Object> body = Payloads.exactPayload(payload, ORG_ASSIGNMENT_FIELDS);
- List<Long> entryIds = Payloads.idList(body.get("ipEntryIds"), "ipEntryIds");
- int expectedVersion = Payloads.rowVersion(body.get("rowVersion"));
- Long actorId = AuthContext.get().userId();
- LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
- OrganizationEntity organization =
- organizationMapper.findByIdNotDeletedForUpdate(organizationId);
- if (organization == null) {
- throw notFound("机构不存在");
- }
- checkOrgVersion(organization, expectedVersion);
- Set<Long> targetIds = new HashSet<>(entryIds);
- List<IpEntryEntity> rows = List.of();
- if (!targetIds.isEmpty()) {
- rows = ipEntryMapper.listActiveByIds(new TreeSet<>(targetIds));
- Set<Long> validIds = new HashSet<>();
- for (IpEntryEntity row : rows) {
- validIds.add(row.getId());
- }
- Set<Long> invalid = new HashSet<>(targetIds);
- invalid.removeAll(validIds);
- if (!invalid.isEmpty()) {
- throw Errors.invalidArgument("ipEntryIds包含无效IP条目");
- }
- }
- List<OrgIpAssignmentEntity> existingRows =
- ipEntryMapper.listActiveAssignmentsByOrganization(organizationId);
- Map<Long, OrgIpAssignmentEntity> existingMap = new HashMap<>();
- for (OrgIpAssignmentEntity row : existingRows) {
- existingMap.put(row.getIpEntryId(), row);
- }
- for (Map.Entry<Long, OrgIpAssignmentEntity> entry : existingMap.entrySet()) {
- if (!targetIds.contains(entry.getKey())) {
- ipEntryMapper.softDeleteAssignment(entry.getValue().getId(), now, actorId);
- }
- }
- for (Long ipEntryId : new TreeSet<>(targetIds)) {
- if (!existingMap.containsKey(ipEntryId)) {
- OrgIpAssignmentEntity row = new OrgIpAssignmentEntity();
- row.setOrganizationId(organizationId);
- row.setIpEntryId(ipEntryId);
- row.setCreatedBy(actorId);
- row.setUpdatedBy(actorId);
- ipEntryMapper.insertAssignment(row);
- }
- }
- organization.setUpdatedBy(actorId);
- organization.setUpdatedAt(now);
- organizationMapper.update(organization);
- organization.setRowVersion(organization.getRowVersion() + 1);
- List<Map<String, Object>> ipEntries = new ArrayList<>();
- for (IpEntryEntity row : rows) {
- Map<String, Object> item = Payloads.orderedMap();
- item.put("id", serializeId(row.getId()));
- item.put("ipValue", row.getIpValue());
- ipEntries.add(item);
- }
- Map<String, Object> detail = Payloads.orderedMap();
- detail.put("ipEntries", ipEntries);
- detail.put("rowVersion", organization.getRowVersion());
- audit(request, AuditService.Action.ASSIGN_ORG_IP,
- organizationId, organization.getOrgName(), detail);
- Map<String, Object> result = Payloads.orderedMap();
- result.put("id", serializeId(organizationId));
- result.put("ipEntryIds", new TreeSet<>(targetIds).stream().map(String::valueOf).toList());
- result.put("rowVersion", organization.getRowVersion());
- return result;
- }
- // ------------------------------------------------------------ IP 规范化
- /**
- * 对齐 normalize_ip_value:精确 IP、CIDR(主机位必须为 0)或「起IP-止IP」范围
- * (两端同版本且起≤止)。
- *
- * <p>IPv4 手动解析以完全对齐 Python ipaddress(拒绝前导零且不做 DNS 查询);
- * IPv6 走 InetAddress 字面量解析(Java 输出全展开形式,与 Python 压缩形式存在
- * 已知展示差异,仅影响 IPv6 场景的存储文本)。</p>
- */
- public static String normalizeIpValue(Object value) {
- if (!(value instanceof String text)) {
- throw Errors.invalidArgument("ipValue必须是字符串");
- }
- String stripped = text.strip();
- if (stripped.isEmpty()) {
- throw Errors.invalidArgument("ipValue不能为空");
- }
- if (stripped.length() > IP_VALUE_MAX_LENGTH) {
- throw Errors.invalidArgument("ipValue长度不能超过" + IP_VALUE_MAX_LENGTH);
- }
- int slash = stripped.indexOf('/');
- String normalized = slash >= 0
- ? normalizeCidr(stripped, slash)
- : stripped.indexOf('-') >= 0
- ? normalizeRange(stripped, stripped.indexOf('-'))
- : normalizeAddress(stripped);
- // IPv6 等形式的规范化展开可能长于输入文本,统一复查落库长度
- if (normalized.length() > IP_VALUE_MAX_LENGTH) {
- throw Errors.invalidArgument("ipValue长度不能超过" + IP_VALUE_MAX_LENGTH);
- }
- return normalized;
- }
- private static String normalizeCidr(String text, int slashIndex) {
- String addressPart = text.substring(0, slashIndex);
- String prefixPart = text.substring(slashIndex + 1);
- if (addressPart.indexOf(':') >= 0) {
- byte[] bytes = parseIpv6(addressPart, text);
- int prefix = parsePrefix(prefixPart, 128, text);
- if (!hostBitsZero(bytes, prefix)) {
- throw hostBitsError(text);
- }
- return inetText(bytes) + "/" + prefix;
- }
- int[] octets = parseIpv4(addressPart, text);
- int prefix = parsePrefix(prefixPart, 32, text);
- int address = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3];
- int hostBits = 32 - prefix;
- if (hostBits < 32 && (address & ((1 << hostBits) - 1)) != 0) {
- throw hostBitsError(text);
- }
- return octets[0] + "." + octets[1] + "." + octets[2] + "." + octets[3] + "/" + prefix;
- }
- /** 对齐范围分支:「起IP-止IP」两端同版本且起≤止,规范化为 起文本-止文本。 */
- private static String normalizeRange(String text, int dashIndex) {
- String startPart = text.substring(0, dashIndex);
- String endPart = text.substring(dashIndex + 1);
- if (startPart.isEmpty() || endPart.isEmpty() || endPart.indexOf('-') >= 0) {
- throw invalidIp(text);
- }
- boolean startV6 = startPart.indexOf(':') >= 0;
- if (startV6 != (endPart.indexOf(':') >= 0)) {
- throw rangeVersionError(text);
- }
- byte[] start = startV6
- ? parseIpv6(startPart, text)
- : ipv4ToBytes(parseIpv4(startPart, text));
- byte[] end = startV6
- ? parseIpv6(endPart, text)
- : ipv4ToBytes(parseIpv4(endPart, text));
- if (IpAddresses.unsignedCompare(start, end) > 0) {
- throw rangeOrderError(text);
- }
- return inetText(start) + "-" + inetText(end);
- }
- private static byte[] ipv4ToBytes(int[] octets) {
- return new byte[]{
- (byte) octets[0], (byte) octets[1], (byte) octets[2], (byte) octets[3]
- };
- }
- private static String normalizeAddress(String text) {
- if (text.indexOf(':') >= 0) {
- return inetText(parseIpv6(text, text));
- }
- int[] octets = parseIpv4(text, text);
- return octets[0] + "." + octets[1] + "." + octets[2] + "." + octets[3];
- }
- /** 手动解析 IPv4:4 段、1~3 位、无前导零、0~255。 */
- private static int[] parseIpv4(String text, String original) {
- String[] parts = text.split("\\.", -1);
- if (parts.length != 4) {
- throw invalidIp(original);
- }
- int[] octets = new int[4];
- for (int i = 0; i < 4; i++) {
- String part = parts[i];
- if (part.isEmpty() || part.length() > 3 || !isDigits(part)) {
- throw invalidIp(original);
- }
- if (part.length() > 1 && part.charAt(0) == '0') {
- throw invalidIp(original);
- }
- int value = Integer.parseInt(part);
- if (value > 255) {
- throw invalidIp(original);
- }
- octets[i] = value;
- }
- return octets;
- }
- /** IPv6 字面量解析(含 ':' 一律按 IPv6 处理,不触发 DNS 查询)。 */
- private static byte[] parseIpv6(String text, String original) {
- try {
- return InetAddress.getByName(text).getAddress();
- } catch (Exception ex) {
- throw invalidIp(original);
- }
- }
- private static int parsePrefix(String prefix, int maxValue, String original) {
- if (prefix.isEmpty() || !isDigits(prefix)) {
- throw invalidIp(original);
- }
- int value;
- try {
- value = Integer.parseInt(prefix);
- } catch (NumberFormatException ex) {
- throw invalidIp(original);
- }
- if (value > maxValue) {
- throw invalidIp(original);
- }
- return value;
- }
- /** 校验 IPv6 字节序列低 (128-prefix) 位全为 0。 */
- private static boolean hostBitsZero(byte[] bytes, int prefix) {
- for (int bit = prefix; bit < 128; bit++) {
- int byteIndex = bit / 8;
- int bitInByte = 7 - (bit % 8);
- if (((bytes[byteIndex] >> bitInByte) & 1) != 0) {
- return false;
- }
- }
- return true;
- }
- /** 字节序列渲染为地址文本(4字节为IPv4点分、16字节为IPv6全展开形式)。 */
- private static String inetText(byte[] bytes) {
- try {
- return InetAddress.getByAddress(bytes).getHostAddress();
- } catch (Exception ex) {
- throw new IllegalStateException("IP 地址渲染失败", ex);
- }
- }
- private static DmsException invalidIp(String text) {
- return Errors.invalidArgument("IP地址、网段或IP范围格式非法:" + text);
- }
- private static DmsException hostBitsError(String text) {
- return Errors.invalidArgument("网段要求主机位为0,请输入如192.168.1.0/24的形式:" + text);
- }
- private static DmsException rangeVersionError(String text) {
- return Errors.invalidArgument("IP范围两端必须同为IPv4或IPv6:" + text);
- }
- private static DmsException rangeOrderError(String text) {
- return Errors.invalidArgument("IP范围起始不能大于结束:" + text);
- }
- private static boolean isDigits(String value) {
- for (int i = 0; i < value.length(); i++) {
- char c = value.charAt(i);
- if (c < '0' || c > '9') {
- return false;
- }
- }
- return true;
- }
- // ------------------------------------------------------------ 内部工具
- /** 对齐 _remark:null → "",strip,长度上限 128。 */
- private static String remark(Object value) {
- if (value == null) {
- return "";
- }
- if (!(value instanceof String text)) {
- throw Errors.invalidArgument("remark必须是字符串");
- }
- String normalized = text.strip();
- if (normalized.length() > REMARK_MAX_LENGTH) {
- throw Errors.invalidArgument("remark长度不能超过" + REMARK_MAX_LENGTH);
- }
- return normalized;
- }
- private static Map<String, Object> entrySummary(IpEntryEntity entry) {
- Map<String, Object> result = Payloads.orderedMap();
- result.put("id", serializeId(entry.getId()));
- result.put("ipValue", entry.getIpValue());
- result.put("remark", entry.getRemark());
- result.put("rowVersion", entry.getRowVersion());
- return result;
- }
- private static void checkVersion(IpEntryEntity entry, int expectedVersion) {
- if (entry.getRowVersion() == null || entry.getRowVersion() != expectedVersion) {
- throw conflict("数据已被其他用户修改,请刷新后重试",
- Map.of("currentRowVersion", entry.getRowVersion() == null
- ? 0 : entry.getRowVersion()));
- }
- }
- private static void checkOrgVersion(OrganizationEntity entity, int expectedVersion) {
- if (entity.getRowVersion() == null || entity.getRowVersion() != expectedVersion) {
- throw conflict("数据已被其他用户修改,请刷新后重试",
- Map.of("currentRowVersion", entity.getRowVersion() == null
- ? 0 : entity.getRowVersion()));
- }
- }
- private void audit(HttpServletRequest request, String action, long targetId,
- String targetName, Map<String, Object> detail) {
- auditService.insert(auditService.businessAudit(
- request, action, "IP_ENTRY", targetId, targetName,
- auditService.toJson(detail)));
- }
- private static String serializeId(Long id) {
- return id == null ? null : String.valueOf(id);
- }
- private static DmsException conflict(String message, Object details) {
- return new DmsException("DATA_VERSION_CONFLICT", 409, message, details);
- }
- private static DmsException notFound(String message) {
- return new DmsException("RESOURCE_NOT_FOUND", 404, message);
- }
- }
|