修复门禁:安全化凭据下发与有效期来源
This commit is contained in:
1 parent
a83a3a642e
commit
0d5df72d8e
10 files changed
+908
-142
No files matched your search
+27
@@ -14,13 +14,16 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.domain.TrainExternalOperation;
|
||||
import com.ruoyi.train.domain.TrainExternalResourceMapping;
|
||||
import com.ruoyi.train.integration.dms.dto.IssueResultVO;
|
||||
import com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
|
||||
import com.ruoyi.train.service.IExternalOperationService;
|
||||
@@ -221,6 +224,30 @@ public class TrainAccessManagementController extends BaseController
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员显式触发下发门禁凭据 (交互端点,单次返回明文密码,严禁入库与记入日志)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:manage')")
|
||||
@Log(title = "门禁授权", businessType = BusinessType.UPDATE, isSaveResponseData = false)
|
||||
@PostMapping("/auth/{authId}/issue")
|
||||
public AjaxResult issueCredential(@PathVariable("authId") Long authId)
|
||||
{
|
||||
IssueResultVO result = accessAuthorizationOrchestrator.issueAuthorization(authId, getUsernameSafe());
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员显式触发注销门禁授权 (统一通过编排器执行)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:manage')")
|
||||
@Log(title = "门禁授权", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/auth/{authId}/revoke")
|
||||
public AjaxResult revokeAuth(@PathVariable("authId") Long authId)
|
||||
{
|
||||
ReconciliationResult result = accessAuthorizationOrchestrator.revokeAuthorization(authId, getUsernameSafe());
|
||||
return success(result);
|
||||
}
|
||||
|
||||
private String sanitizeSecret(String input)
|
||||
{
|
||||
if (input == null)
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.ruoyi.train.integration.dms.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 门禁凭据显式下发响应对象 (Wave 3B / Package A)
|
||||
* <p>
|
||||
* 安全红线:
|
||||
* 1. oneTimePlaintextCredential 为一次性交互展示明文,仅本端点返回一次,严禁存库、严禁记入日志、严禁记入 sys_oper_log;
|
||||
* 2. 交互请求必须配置 @Log(isSaveResponseData = false);
|
||||
* 3. 弹窗关闭后前端内存清除,后续无法再次获取旧明文。
|
||||
*/
|
||||
public class IssueResultVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long authId;
|
||||
private String oneTimePlaintextCredential;
|
||||
private String validStart;
|
||||
private String validUntil;
|
||||
private String status;
|
||||
private String message;
|
||||
|
||||
public IssueResultVO() {
|
||||
}
|
||||
|
||||
public IssueResultVO(Long authId, String oneTimePlaintextCredential, String validStart, String validUntil, String status, String message) {
|
||||
this.authId = authId;
|
||||
this.oneTimePlaintextCredential = oneTimePlaintextCredential;
|
||||
this.validStart = validStart;
|
||||
this.validUntil = validUntil;
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Long getAuthId() {
|
||||
return authId;
|
||||
}
|
||||
|
||||
public void setAuthId(Long authId) {
|
||||
this.authId = authId;
|
||||
}
|
||||
|
||||
public String getOneTimePlaintextCredential() {
|
||||
return oneTimePlaintextCredential;
|
||||
}
|
||||
|
||||
public void setOneTimePlaintextCredential(String oneTimePlaintextCredential) {
|
||||
this.oneTimePlaintextCredential = oneTimePlaintextCredential;
|
||||
}
|
||||
|
||||
public String getValidStart() {
|
||||
return validStart;
|
||||
}
|
||||
|
||||
public void setValidStart(String validStart) {
|
||||
this.validStart = validStart;
|
||||
}
|
||||
|
||||
public String getValidUntil() {
|
||||
return validUntil;
|
||||
}
|
||||
|
||||
public void setValidUntil(String validUntil) {
|
||||
this.validUntil = validUntil;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IssueResultVO{" +
|
||||
"authId=" + authId +
|
||||
", oneTimePlaintextCredential='[PROTECTED]'" +
|
||||
", validStart='" + validStart + '\'' +
|
||||
", validUntil='" + validUntil + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", message='" + message + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+423
-128
@@ -1,42 +1,52 @@
|
||||
package com.ruoyi.train.integration.dms.orchestration;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.TrainExternalOperation;
|
||||
import com.ruoyi.train.domain.TrainExternalResourceMapping;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordCreateRequest;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.integration.dms.config.DmsProperties;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordCreateResponse;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordDeleteRequest;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordDeleteResponse;
|
||||
import com.ruoyi.train.integration.dms.dto.IssueResultVO;
|
||||
import com.ruoyi.train.integration.dms.gateway.ExternalCredentialGateway;
|
||||
import com.ruoyi.train.integration.dms.security.SecurePasswordGenerator;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainClassPlanMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormAuthMapper;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.service.IExternalOperationService;
|
||||
import com.ruoyi.train.service.IExternalResourceMappingService;
|
||||
import com.ruoyi.train.service.ITrainDormAuthService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 宿舍门禁业务授权统一编排器 (Wave 3A)
|
||||
* 宿舍门禁业务授权统一编排器 (Wave 3B / Package A)
|
||||
* <p>
|
||||
* 核心架构与安全准则:
|
||||
* 1. 状态驱动对账 (Reconciliation Model):业务事实 → 期望状态 → 外部操作,与触发顺序完全解耦。
|
||||
* 2. 最小特权主锁策略 (PRIMARY_LOCK_ONLY):房间内仅对且必须对唯一定义的 1 把主门锁下发凭据。
|
||||
* 3. 调房安全策略 (GRANT_NEW_THEN_REVOKE_OLD):先下发新房凭据,新房对账生效后再撤销旧房;新房失败严格保留旧房。
|
||||
* 4. 事务边界清晰 (LONG_DB_TRANSACTION_AROUND_HTTP = NO):严禁长事务包住远程 HTTP 调用,外部调用异常绝不回滚业务主事务。
|
||||
* 5. 密码安全与短暂留存:SecureRandom 强随机生成,明文仅在内存局部变量留存,严禁落库、严禁日志输出。
|
||||
* 6. 操作幂等与防重:业务授权唯一性 + 外部操作幂等键双重防护,重复触发无副作用。
|
||||
* 1. 状态驱动对账 (Reconciliation Model):业务事实 → 期望状态,与触发顺序解耦。
|
||||
* 2. C12 方案 B (AWAITING_MANUAL_ISSUE):自动业务事件(报到、住宿分配、调房)严禁自动生成明文密码并下发 DMS,
|
||||
* 仅确保本地授权记录 (PENDING_GRANT) 并返回 AWAITING_MANUAL_ISSUE。
|
||||
* 3. 显式下发端点 (issueAuthorization):仅交互请求下发,一机一密短暂返回一次性明文,严禁落库、严禁日志输出。
|
||||
* 4. C10 真实手机号:严禁使用伪造手机号,唯一来源为学员主档 (TrainStudent.sPhone)。手机号缺失严格阻断下发。
|
||||
* 5. C09 真实有效期:以开班计划结束时间 (train_class_plan.end_time) 为准,严禁无依据的 +30 天推导。
|
||||
* 6. C01/C11 统一注销与批量注销:严禁直接修改状态模拟注销,严禁 delete(0);退训时遍历所有历史授权,独立处理并生成 BatchReconciliationResult。
|
||||
* 7. 事务边界 (LONG_DB_TRANSACTION_AROUND_HTTP = NO):数据库事务内仅落库 CREATED 状态,经 afterCommit 钩子派发外部 HTTP。
|
||||
*/
|
||||
@Service
|
||||
public class AccessAuthorizationOrchestrator {
|
||||
@@ -44,7 +54,7 @@ public class AccessAuthorizationOrchestrator {
|
||||
|
||||
public static final String VENDOR_DMS = "DMS";
|
||||
public static final String POLICY_PRIMARY_LOCK_ONLY = "PRIMARY_LOCK_ONLY";
|
||||
public static final String VALIDITY_WINDOW_SOURCE = "BUSINESS_POLICY_REQUIRED";
|
||||
public static final String VALIDITY_WINDOW_SOURCE = "PLAN_END_TIME";
|
||||
|
||||
private final TrainEnrollmentMapper enrollmentMapper;
|
||||
private final TrainAccommodationMapper accommodationMapper;
|
||||
@@ -53,9 +63,23 @@ public class AccessAuthorizationOrchestrator {
|
||||
private final IExternalResourceMappingService resourceMappingService;
|
||||
private final IExternalOperationService operationService;
|
||||
private final ExternalCredentialGateway credentialGateway;
|
||||
private TrainStudentMapper studentMapper;
|
||||
private TrainClassPlanMapper classPlanMapper;
|
||||
private DmsProperties dmsProperties;
|
||||
|
||||
protected AccessAuthorizationOrchestrator() {
|
||||
this(null, null, null, null, null, null, null);
|
||||
this(null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public AccessAuthorizationOrchestrator(TrainEnrollmentMapper enrollmentMapper,
|
||||
TrainAccommodationMapper accommodationMapper,
|
||||
TrainDormAuthMapper dormAuthMapper,
|
||||
ITrainDormAuthService dormAuthService,
|
||||
IExternalResourceMappingService resourceMappingService,
|
||||
IExternalOperationService operationService,
|
||||
ExternalCredentialGateway credentialGateway) {
|
||||
this(enrollmentMapper, accommodationMapper, dormAuthMapper, dormAuthService,
|
||||
resourceMappingService, operationService, credentialGateway, null, null, null);
|
||||
}
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -65,7 +89,10 @@ public class AccessAuthorizationOrchestrator {
|
||||
ITrainDormAuthService dormAuthService,
|
||||
IExternalResourceMappingService resourceMappingService,
|
||||
IExternalOperationService operationService,
|
||||
ExternalCredentialGateway credentialGateway) {
|
||||
ExternalCredentialGateway credentialGateway,
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false) TrainStudentMapper studentMapper,
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false) TrainClassPlanMapper classPlanMapper,
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false) DmsProperties dmsProperties) {
|
||||
this.enrollmentMapper = enrollmentMapper;
|
||||
this.accommodationMapper = accommodationMapper;
|
||||
this.dormAuthMapper = dormAuthMapper;
|
||||
@@ -73,6 +100,21 @@ public class AccessAuthorizationOrchestrator {
|
||||
this.resourceMappingService = resourceMappingService;
|
||||
this.operationService = operationService;
|
||||
this.credentialGateway = credentialGateway;
|
||||
this.studentMapper = studentMapper;
|
||||
this.classPlanMapper = classPlanMapper;
|
||||
this.dmsProperties = dmsProperties != null ? dmsProperties : new DmsProperties();
|
||||
}
|
||||
|
||||
public void setStudentMapper(TrainStudentMapper studentMapper) {
|
||||
this.studentMapper = studentMapper;
|
||||
}
|
||||
|
||||
public void setClassPlanMapper(TrainClassPlanMapper classPlanMapper) {
|
||||
this.classPlanMapper = classPlanMapper;
|
||||
}
|
||||
|
||||
public void setDmsProperties(DmsProperties dmsProperties) {
|
||||
this.dmsProperties = dmsProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,16 +128,16 @@ public class AccessAuthorizationOrchestrator {
|
||||
return ReconciliationResult.waiting("Missing enrollmentId");
|
||||
}
|
||||
|
||||
TrainEnrollment enrollment = enrollmentMapper.selectTrainEnrollmentById(enrollmentId);
|
||||
TrainEnrollment enrollment = enrollmentMapper != null ? enrollmentMapper.selectTrainEnrollmentById(enrollmentId) : null;
|
||||
if (enrollment == null) {
|
||||
return ReconciliationResult.waiting("Enrollment not found: " + enrollmentId);
|
||||
}
|
||||
|
||||
Integer enrollmentStatus = enrollment.getEnrollmentStatus();
|
||||
TrainAccommodation activeAcc = accommodationMapper.selectActiveAccommodationByEnrollmentId(enrollmentId);
|
||||
List<TrainAccommodation> accHistory = accommodationMapper.selectAccommodationHistoryByEnrollmentId(enrollmentId);
|
||||
TrainAccommodation activeAcc = accommodationMapper != null ? accommodationMapper.selectActiveAccommodationByEnrollmentId(enrollmentId) : null;
|
||||
List<TrainAccommodation> accHistory = accommodationMapper != null ? accommodationMapper.selectAccommodationHistoryByEnrollmentId(enrollmentId) : Collections.emptyList();
|
||||
|
||||
// 1. 学员退训(3) 或 已完成(4):触发所有在用授权的注销 (REVOKE)
|
||||
// 1. 学员退训(3) 或 已完成(4):触发所有历史/在用授权的注销 (REVOKE)
|
||||
if (Integer.valueOf(3).equals(enrollmentStatus) || Integer.valueOf(4).equals(enrollmentStatus)) {
|
||||
return handleEnrollmentTermination(enrollment, accHistory);
|
||||
}
|
||||
@@ -105,15 +147,14 @@ public class AccessAuthorizationOrchestrator {
|
||||
return ReconciliationResult.waiting("Enrollment status is PENDING_REPORT (0); Grant prerequisites not met");
|
||||
}
|
||||
|
||||
// 3. 请假 (2):政策保持现状 (LEAVE_AUTO_REVOKE_POLICY = OPEN_QUESTION),不主动撤权
|
||||
// 3. 请假 (2):政策保持现状,不主动撤权
|
||||
if (Integer.valueOf(2).equals(enrollmentStatus)) {
|
||||
return ReconciliationResult.noAction("Enrollment on leave (2); maintaining current authorization state");
|
||||
}
|
||||
|
||||
// 4. 已报到 (1):执行发放或调房检查
|
||||
// 4. 已报到 (1):执行发放前置核对或调房后注销检查
|
||||
if (Integer.valueOf(1).equals(enrollmentStatus)) {
|
||||
if (activeAcc == null) {
|
||||
// 已报到但尚未分配有效宿舍
|
||||
return ReconciliationResult.waiting("No active accommodation assigned for reported enrollment; waiting for assignment");
|
||||
}
|
||||
return handleGrantOrTransfer(enrollment, activeAcc, accHistory);
|
||||
@@ -122,39 +163,88 @@ public class AccessAuthorizationOrchestrator {
|
||||
return ReconciliationResult.noAction("No action required for enrollment " + enrollmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 学员退训/结训注销处理 (C11: 遍历注销所有历史授权,独立处理,生成 BatchReconciliationResult)
|
||||
*/
|
||||
private ReconciliationResult handleEnrollmentTermination(TrainEnrollment enrollment, List<TrainAccommodation> accHistory) {
|
||||
BatchReconciliationResult batch = new BatchReconciliationResult(enrollment.getEnrollmentId());
|
||||
|
||||
if (accHistory == null || accHistory.isEmpty()) {
|
||||
return ReconciliationResult.noAction("No accommodation history found for terminated enrollment " + enrollment.getEnrollmentId());
|
||||
batch.setOutcome(BatchReconciliationResult.OUTCOME_ALL_SETTLED);
|
||||
return ReconciliationResult.batch(batch);
|
||||
}
|
||||
|
||||
// 查找所有非 REVOKED 的授权进行注销
|
||||
// 遍历所有相关住宿记录的授权
|
||||
for (TrainAccommodation acc : accHistory) {
|
||||
TrainDormAuth auth = dormAuthMapper.selectTrainDormAuthByAccommodationId(acc.getAccommodationId());
|
||||
if (auth != null && !TrainDormAuth.AUTH_STATUS_REVOKED.equals(auth.getAuthStatus())) {
|
||||
log.info("Terminating authorization for enrollment {} accommodation {}: authId={}",
|
||||
enrollment.getEnrollmentId(), acc.getAccommodationId(), auth.getAuthId());
|
||||
return executeRevokeFlow(auth, "EnrollmentTerminated:" + enrollment.getEnrollmentStatus());
|
||||
if (acc == null || acc.getAccommodationId() == null) {
|
||||
continue;
|
||||
}
|
||||
Long accId = acc.getAccommodationId();
|
||||
TrainDormAuth auth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAccommodationId(accId) : null;
|
||||
|
||||
if (auth == null) {
|
||||
batch.addItem(new ReconciliationItemVO(null, accId, "REVOKE", "NO_ACTION", "No auth record found"));
|
||||
batch.setNoAction(batch.getNoAction() + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 幂等:若已为 REVOKED,记为 NO_ACTION,不得重复撤权
|
||||
if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(auth.getAuthStatus())) {
|
||||
batch.addItem(new ReconciliationItemVO(auth.getAuthId(), accId, "REVOKE", "NO_ACTION", "Already REVOKED"));
|
||||
batch.setNoAction(batch.getNoAction() + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行单项注销流程,一项异常不得阻止其余项继续处理
|
||||
try {
|
||||
ReconciliationResult revokeRes = executeRevokeFlow(auth, "EnrollmentTerminated:" + enrollment.getEnrollmentStatus(), "Orchestrator");
|
||||
if (revokeRes.isSuccess()) {
|
||||
String itemStatus = "DISPATCHED";
|
||||
if (ReconciliationDecision.NO_ACTION.equals(revokeRes.getDecision())) {
|
||||
itemStatus = "REVOKED";
|
||||
}
|
||||
batch.addItem(new ReconciliationItemVO(auth.getAuthId(), accId, "REVOKE", itemStatus, revokeRes.getReason()));
|
||||
batch.setDispatched(batch.getDispatched() + 1);
|
||||
} else {
|
||||
batch.addItem(new ReconciliationItemVO(auth.getAuthId(), accId, "REVOKE", "FAILED", revokeRes.getReason()));
|
||||
batch.setFailed(batch.getFailed() + 1);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to revoke auth {} for enrollment {}: {}", auth.getAuthId(), enrollment.getEnrollmentId(), ex.getMessage());
|
||||
batch.addItem(new ReconciliationItemVO(auth.getAuthId(), accId, "REVOKE", "FAILED", sanitize(ex.getMessage())));
|
||||
batch.setFailed(batch.getFailed() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return ReconciliationResult.noAction("All authorizations already REVOKED for terminated enrollment " + enrollment.getEnrollmentId());
|
||||
batch.computeOutcome();
|
||||
return ReconciliationResult.batch(batch);
|
||||
}
|
||||
|
||||
/**
|
||||
* 住宿分配/报到后授权处理 (C12 方案 B: 自动事件只创建本地记录为 PENDING_GRANT,返回 AWAITING_MANUAL_ISSUE)
|
||||
*/
|
||||
private ReconciliationResult handleGrantOrTransfer(TrainEnrollment enrollment,
|
||||
TrainAccommodation activeAcc,
|
||||
List<TrainAccommodation> accHistory) {
|
||||
Long accommodationId = activeAcc.getAccommodationId();
|
||||
String dormId = activeAcc.getDormId();
|
||||
|
||||
TrainDormAuth currentAuth = dormAuthMapper.selectTrainDormAuthByAccommodationId(accommodationId);
|
||||
TrainDormAuth currentAuth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAccommodationId(accommodationId) : null;
|
||||
|
||||
// 如果新住宿的授权已经生效 (ACTIVE)
|
||||
if (currentAuth != null && TrainDormAuth.AUTH_STATUS_ACTIVE.equals(currentAuth.getAuthStatus())) {
|
||||
// 调房场景第二阶段检查:新房已 ACTIVE,检查是否存在历史退宿的残留旧授权需要注销 (GRANT_NEW_THEN_REVOKE_OLD)
|
||||
TrainDormAuth oldAuthToRevoke = findStaleActiveAuthInHistory(accommodationId, accHistory);
|
||||
if (oldAuthToRevoke != null) {
|
||||
log.info("New room access ACTIVE. Triggering delayed revoke for old accommodation auth: {}", oldAuthToRevoke.getAuthId());
|
||||
return executeRevokeFlow(oldAuthToRevoke, "TransferCompleted");
|
||||
// 调房场景:新房已 ACTIVE,遍历检查所有历史住宿的残留授权并执行注销 (C11: 遍历全部,不单项早退)
|
||||
List<TrainDormAuth> staleAuths = findStaleActiveAuthsInHistory(accommodationId, accHistory);
|
||||
if (!staleAuths.isEmpty()) {
|
||||
log.info("New room access ACTIVE. Triggering revokes for {} stale accommodation auths", staleAuths.size());
|
||||
for (TrainDormAuth oldAuth : staleAuths) {
|
||||
try {
|
||||
executeRevokeFlow(oldAuth, "TransferCompleted", "Orchestrator");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to revoke stale auth {}: {}", oldAuth.getAuthId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return ReconciliationResult.revoking("Triggered revokes for " + staleAuths.size() + " stale accommodation auths");
|
||||
}
|
||||
return ReconciliationResult.noAction("Door authorization is already ACTIVE for accommodation " + accommodationId);
|
||||
}
|
||||
@@ -165,12 +255,12 @@ public class AccessAuthorizationOrchestrator {
|
||||
}
|
||||
|
||||
// 校验外部映射与主锁策略 (PRIMARY_LOCK_ONLY)
|
||||
TrainExternalResourceMapping roomMapping = resourceMappingService.getActiveRoomMapping(VENDOR_DMS, dormId);
|
||||
TrainExternalResourceMapping roomMapping = resourceMappingService != null ? resourceMappingService.getActiveRoomMapping(VENDOR_DMS, dormId) : null;
|
||||
if (roomMapping == null) {
|
||||
return ReconciliationResult.waiting("No ACTIVE room mapping found for dormId " + dormId);
|
||||
}
|
||||
|
||||
List<TrainExternalResourceMapping> lockMappings = resourceMappingService.getActiveLockMappings(VENDOR_DMS, dormId);
|
||||
List<TrainExternalResourceMapping> lockMappings = resourceMappingService != null ? resourceMappingService.getActiveLockMappings(VENDOR_DMS, dormId) : null;
|
||||
if (lockMappings == null || lockMappings.isEmpty()) {
|
||||
return ReconciliationResult.waiting("No door locks mapped for dormId " + dormId);
|
||||
}
|
||||
@@ -186,32 +276,132 @@ public class AccessAuthorizationOrchestrator {
|
||||
return ReconciliationResult.waiting("Dorm " + dormId + " has " + primaryLocks.size() + " primary locks configured; configuration error");
|
||||
}
|
||||
|
||||
TrainExternalResourceMapping primaryLock = primaryLocks.get(0);
|
||||
|
||||
// 推导有效期窗口 (VALIDITY_WINDOW_SOURCE = BUSINESS_POLICY_REQUIRED)
|
||||
Date validStart = activeAcc.getAssignedTime() != null ? activeAcc.getAssignedTime() : new Date();
|
||||
Date validUntil = deriveDefaultValidUntil(validStart);
|
||||
|
||||
return executeGrantFlow(enrollment, activeAcc, primaryLock, currentAuth, validStart, validUntil);
|
||||
}
|
||||
|
||||
private ReconciliationResult executeGrantFlow(TrainEnrollment enrollment,
|
||||
TrainAccommodation activeAcc,
|
||||
TrainExternalResourceMapping primaryLock,
|
||||
TrainDormAuth dormAuth,
|
||||
Date validStart,
|
||||
Date validUntil) {
|
||||
Long accommodationId = activeAcc.getAccommodationId();
|
||||
|
||||
// 步骤 1: 确保本地授权记录存在 (短本地事务)
|
||||
if (dormAuth == null) {
|
||||
dormAuth = dormAuthService.ensureAuthorizationForAccommodation(accommodationId, "Orchestrator");
|
||||
// 确保本地授权记录存在 (短本地事务,初始为 PENDING_GRANT)
|
||||
if (currentAuth == null && dormAuthService != null) {
|
||||
currentAuth = dormAuthService.ensureAuthorizationForAccommodation(accommodationId, "Orchestrator");
|
||||
}
|
||||
|
||||
Long authId = dormAuth.getAuthId();
|
||||
Integer version = dormAuth.getOperationVersion() != null ? dormAuth.getOperationVersion() : 1;
|
||||
// 推导真实有效期并回填本地授权审计 (C09: 真实查询开班计划 end_time,禁止 +30 天)
|
||||
if (currentAuth != null && classPlanMapper != null && enrollment.getPlanId() != null) {
|
||||
TrainClassPlan plan = classPlanMapper.selectTrainClassPlanByPlanId(enrollment.getPlanId());
|
||||
Date now = new Date();
|
||||
Date validStart = activeAcc.getAssignedTime() != null && activeAcc.getAssignedTime().after(now) ? activeAcc.getAssignedTime() : now;
|
||||
Date validUntil = plan != null ? plan.getEndTime() : null;
|
||||
|
||||
// 步骤 2: 创建外部操作流水 (短本地事务,幂等键保护)
|
||||
if (validUntil != null && validUntil.after(validStart)) {
|
||||
currentAuth.setAuthStart(validStart);
|
||||
currentAuth.setAuthEnd(validUntil);
|
||||
dormAuthService.updateTrainDormAuth(currentAuth);
|
||||
}
|
||||
}
|
||||
|
||||
// C12 方案 B: 自动流程绝不向 DMS 下发密码,授权保持 PENDING_GRANT,返回 AWAITING_MANUAL_ISSUE
|
||||
Long authId = currentAuth != null ? currentAuth.getAuthId() : null;
|
||||
log.info("Prerequisites verified for accommodation {} (authId={}). Returning AWAITING_MANUAL_ISSUE", accommodationId, authId);
|
||||
return ReconciliationResult.awaitingManualIssue(authId, "Prerequisites met; waiting for administrator manual issue");
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员显式下发门禁凭据 (交互端点专用,一次性返回明文密码)
|
||||
*
|
||||
* @param authId 门禁授权ID
|
||||
* @param operator 操作人
|
||||
* @return 下发结果 VO (包含一次性明文密码)
|
||||
*/
|
||||
public IssueResultVO issueAuthorization(Long authId, String operator) {
|
||||
if (authId == null) {
|
||||
throw new ServiceException("authId 不能为空");
|
||||
}
|
||||
|
||||
TrainDormAuth auth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAuthId(authId) : null;
|
||||
if (auth == null) {
|
||||
throw new ServiceException("门禁授权记录不存在: " + authId);
|
||||
}
|
||||
|
||||
// 1. Issue 幂等防重守卫 (Req 7): 必须阻止 GRANTING 和 ACTIVE 再次执行 issue
|
||||
String currentStatus = auth.getAuthStatus();
|
||||
if (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(currentStatus) || TrainDormAuth.AUTH_STATUS_GRANTING.equals(currentStatus)) {
|
||||
throw new ServiceException("当前授权处于 " + currentStatus + " 状态,禁止重复下发凭据 (Idempotent Guard)");
|
||||
}
|
||||
if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(currentStatus) || TrainDormAuth.AUTH_STATUS_REVOKING.equals(currentStatus)) {
|
||||
throw new ServiceException("当前授权处于注销流程或已注销,无法重新下发凭据");
|
||||
}
|
||||
|
||||
// 2. ERROR 重试守卫:检查是否存在可能已创建但未知 externalAuthId 的不确定状态
|
||||
if (TrainDormAuth.AUTH_STATUS_ERROR.equals(currentStatus)) {
|
||||
if (auth.getExternalAuthId() != null && !auth.getExternalAuthId().trim().isEmpty()) {
|
||||
throw new ServiceException("该授权记录已存在外部凭据标识 (" + auth.getExternalAuthId() + "),为防止制造孤儿凭据,禁止盲目重发。需先核实或注销旧凭据。");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 关联实体获取
|
||||
TrainAccommodation acc = accommodationMapper != null ? accommodationMapper.selectTrainAccommodationByAccommodationId(auth.getAccommodationId()) : null;
|
||||
if (acc == null) {
|
||||
throw new ServiceException("关联住宿安排不存在: " + auth.getAccommodationId());
|
||||
}
|
||||
|
||||
TrainEnrollment enrollment = enrollmentMapper != null ? enrollmentMapper.selectTrainEnrollmentById(acc.getEnrollmentId()) : null;
|
||||
if (enrollment == null) {
|
||||
throw new ServiceException("关联学员报名档案不存在: " + acc.getEnrollmentId());
|
||||
}
|
||||
|
||||
// 4. C10 真实手机号核验:严禁伪造手机号,唯一来源为 TrainStudent.sPhone
|
||||
String phoneNumber = null;
|
||||
if (studentMapper != null && enrollment.getStudentId() != null) {
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(enrollment.getStudentId());
|
||||
if (student != null) {
|
||||
phoneNumber = student.getsPhone();
|
||||
}
|
||||
}
|
||||
if (phoneNumber == null || !phoneNumber.matches("^1[3-9]\\d{9}$")) {
|
||||
log.warn("Cannot issue credential for auth {}: missing or invalid student phone ({})", authId, sanitize(phoneNumber));
|
||||
return new IssueResultVO(authId, null, null, null, "WAITING_FOR_PREREQUISITE", "MISSING_STUDENT_PHONE: 学员主档缺失有效手机号,暂无法下发门禁");
|
||||
}
|
||||
|
||||
// 5. 门锁配置与主锁策略检查 (PRIMARY_LOCK_ONLY)
|
||||
String dormId = acc.getDormId();
|
||||
TrainExternalResourceMapping roomMapping = resourceMappingService != null ? resourceMappingService.getActiveRoomMapping(VENDOR_DMS, dormId) : null;
|
||||
if (roomMapping == null) {
|
||||
return new IssueResultVO(authId, null, null, null, "WAITING_FOR_PREREQUISITE", "未找到宿舍 " + dormId + " 的有效房间映射");
|
||||
}
|
||||
|
||||
List<TrainExternalResourceMapping> lockMappings = resourceMappingService != null ? resourceMappingService.getActiveLockMappings(VENDOR_DMS, dormId) : null;
|
||||
List<TrainExternalResourceMapping> primaryLocks = lockMappings != null ? lockMappings.stream()
|
||||
.filter(m -> Integer.valueOf(1).equals(m.getIsPrimary()))
|
||||
.collect(Collectors.toList()) : Collections.emptyList();
|
||||
if (primaryLocks.size() != 1) {
|
||||
return new IssueResultVO(authId, null, null, null, "WAITING_FOR_PREREQUISITE", "宿舍主门锁未配置或存在多把主锁 (Primary lock count = " + primaryLocks.size() + ")");
|
||||
}
|
||||
TrainExternalResourceMapping primaryLock = primaryLocks.get(0);
|
||||
|
||||
// 6. C09 有效期窗口推导:以培训计划 end_time 为准,严禁 +30 天
|
||||
TrainClassPlan plan = classPlanMapper != null && enrollment.getPlanId() != null ? classPlanMapper.selectTrainClassPlanByPlanId(enrollment.getPlanId()) : null;
|
||||
Date now = new Date();
|
||||
Date validStart = acc.getAssignedTime() != null && acc.getAssignedTime().after(now) ? acc.getAssignedTime() : now;
|
||||
Date validUntil = plan != null ? plan.getEndTime() : null;
|
||||
|
||||
if (validUntil == null || !validUntil.after(validStart)) {
|
||||
log.warn("Cannot issue credential for auth {}: invalid validity window (validStart={}, validUntil={})", authId, validStart, validUntil);
|
||||
return new IssueResultVO(authId, null, null, null, "WAITING_FOR_PREREQUISITE", "INVALID_VALIDITY_WINDOW: 培训计划结束时间缺失或早于生效开始时间,无法确定凭据有效期窗口");
|
||||
}
|
||||
|
||||
// 回填本地审计时间
|
||||
auth.setAuthStart(validStart);
|
||||
auth.setAuthEnd(validUntil);
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateTrainDormAuth(auth);
|
||||
}
|
||||
|
||||
// 7. DMS disabled 检查 (Req 12)
|
||||
if (dmsProperties != null && !dmsProperties.isEnabled()) {
|
||||
return new IssueResultVO(authId, null, null, null, "WAITING_FOR_PREREQUISITE", "DMS_DISABLED: 共享运行环境门禁对接未启用 (dms.enabled=false),无法向外部厂商下发门禁凭据");
|
||||
}
|
||||
|
||||
// 8. 内存生成 6 位强随机数字密码
|
||||
String plaintextPassword = SecurePasswordGenerator.generateDefaultPassword();
|
||||
|
||||
// 9. 创建外部操作流水 (短本地事务,幂等键保护)
|
||||
Integer version = (auth.getOperationVersion() != null ? auth.getOperationVersion() : 1) + 1;
|
||||
String idempotencyKey = "GRANT:AUTH:" + authId + ":V" + version;
|
||||
TrainExternalOperation op = new TrainExternalOperation();
|
||||
op.setDoorAuthId(authId);
|
||||
@@ -221,15 +411,17 @@ public class AccessAuthorizationOrchestrator {
|
||||
op.setIdempotencyKey(idempotencyKey);
|
||||
op.setExternalRequestType(1); // 1: 下发密码
|
||||
op.setExternalHardwareId(primaryLock.getExternalHardwareId());
|
||||
op.setCreateBy("Orchestrator");
|
||||
op.setCreateBy(operator != null ? operator : "admin");
|
||||
|
||||
TrainExternalOperation createdOp = operationService.createOperation(op);
|
||||
Long operationId = createdOp.getOperationId();
|
||||
TrainExternalOperation createdOp = operationService != null ? operationService.createOperation(op) : null;
|
||||
Long operationId = createdOp != null ? createdOp.getOperationId() : null;
|
||||
|
||||
// 步骤 3: 标记授权记录进入 GRANTING 状态并记录本次操作ID
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_GRANTING, operationId, "Orchestrator");
|
||||
// 10. 标记授权记录进入 GRANTING
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_GRANTING, operationId, operator);
|
||||
}
|
||||
|
||||
// 步骤 4: 构造请求并发起远程凭据下发 (非数据库长事务,LONG_DB_TRANSACTION_AROUND_HTTP = NO)
|
||||
// 11. 调用外部厂商接口
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
Long lockId = null;
|
||||
try {
|
||||
@@ -237,49 +429,135 @@ public class AccessAuthorizationOrchestrator {
|
||||
} catch (Exception e) {
|
||||
log.error("Invalid lockId format: {}", primaryLock.getExternalResourceId());
|
||||
}
|
||||
String holder = enrollment.getStudentId() != null ? enrollment.getStudentId() : "Student";
|
||||
String phoneNumber = "13800000000"; // 缺省测试手机号,后续由业务拓展补充
|
||||
|
||||
// 内存中生成 6 位强随机数字密码
|
||||
String plaintextPassword = SecurePasswordGenerator.generateDefaultPassword();
|
||||
|
||||
try {
|
||||
DmsPasswordCreateResponse resp = credentialGateway.issueOnlinePassword(
|
||||
lockId, holder, phoneNumber, plaintextPassword, sdf.format(validStart), sdf.format(validUntil));
|
||||
lockId, enrollment.getStudentId(), phoneNumber, plaintextPassword,
|
||||
sdf.format(validStart), sdf.format(validUntil));
|
||||
|
||||
// 步骤 5: 成功获取 Vendor Accepted 响应,回填外部流水号并推进为 ACCEPTED
|
||||
String extOpIdStr = resp.getOperationId() != null ? String.valueOf(resp.getOperationId()) : null;
|
||||
String extKeyIdStr = resp.getId() != null ? String.valueOf(resp.getId()) : null;
|
||||
|
||||
operationService.bindExternalReference(operationId, extOpIdStr, resp.getLockHardwareId(), extKeyIdStr, "Orchestrator");
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_ACCEPTED, null, null, "Orchestrator");
|
||||
|
||||
// 回填授权记录上的 externalAuthId (用于后续撤销定位凭据)
|
||||
if (extKeyIdStr != null) {
|
||||
dormAuth.setExternalAuthId(extKeyIdStr);
|
||||
dormAuthService.updateTrainDormAuth(dormAuth);
|
||||
if (operationService != null && operationId != null) {
|
||||
operationService.bindExternalReference(operationId, extOpIdStr, resp.getLockHardwareId(), extKeyIdStr, operator);
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_ACCEPTED, null, null, operator);
|
||||
}
|
||||
|
||||
log.info("DMS password create accepted: localOpId={}, extOpId={}", operationId, extOpIdStr);
|
||||
ReconciliationResult grantResult = ReconciliationResult.grantRequired(authId, operationId, true, true, "Password create accepted by DMS; awaiting poller");
|
||||
grantResult.setOneTimePlaintextCredential(plaintextPassword);
|
||||
return grantResult;
|
||||
if (extKeyIdStr != null && dormAuthService != null) {
|
||||
auth.setExternalAuthId(extKeyIdStr);
|
||||
dormAuthService.updateTrainDormAuth(auth);
|
||||
}
|
||||
|
||||
// 成功后清空历史错误 (Req 16)
|
||||
if (dormAuthMapper != null) {
|
||||
dormAuthMapper.clearLastError(authId);
|
||||
}
|
||||
|
||||
log.info("DMS password create accepted for authId {}: localOpId={}, extOpId={}", authId, operationId, extOpIdStr);
|
||||
|
||||
return new IssueResultVO(
|
||||
authId,
|
||||
plaintextPassword,
|
||||
sdf.format(validStart),
|
||||
sdf.format(validUntil),
|
||||
"ACCEPTED",
|
||||
"凭据已提交下发,设备确认后生效。"
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 外部调用异常 (如读超时、网络异常),严格捕获,绝不回滚业务主事务!
|
||||
log.error("DMS password create failed or timed out for authId {}: {}", authId, e.getMessage());
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_FAILED, "GATEWAY_ERROR", sanitize(e.getMessage()), "Orchestrator");
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, operationId, "Orchestrator");
|
||||
|
||||
return ReconciliationResult.grantRequired(authId, operationId, true, false, "External gateway call failed: " + sanitize(e.getMessage()));
|
||||
log.error("DMS password create failed for authId {}: {}", authId, sanitize(e.getMessage()));
|
||||
if (operationService != null && operationId != null) {
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_FAILED, "GATEWAY_ERROR", sanitize(e.getMessage()), operator);
|
||||
}
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, operationId, operator);
|
||||
}
|
||||
if (dormAuthMapper != null) {
|
||||
dormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_FAILED, "GATEWAY_ERROR: " + sanitize(e.getMessage()), operator, new Date());
|
||||
}
|
||||
throw new ServiceException("门禁凭据下发失败: " + sanitize(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private ReconciliationResult executeRevokeFlow(TrainDormAuth auth, String reason) {
|
||||
Long authId = auth.getAuthId();
|
||||
Integer version = (auth.getOperationVersion() != null ? auth.getOperationVersion() : 1) + 1;
|
||||
/**
|
||||
* 管理员显式触发注销门禁授权
|
||||
*
|
||||
* @param authId 授权ID
|
||||
* @param operator 操作人
|
||||
* @return 注销执行结果
|
||||
*/
|
||||
public ReconciliationResult revokeAuthorization(Long authId, String operator) {
|
||||
if (authId == null) {
|
||||
throw new ServiceException("authId 不能为空");
|
||||
}
|
||||
TrainDormAuth auth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAuthId(authId) : null;
|
||||
if (auth == null) {
|
||||
throw new ServiceException("门禁授权记录不存在: " + authId);
|
||||
}
|
||||
return executeRevokeFlow(auth, "ManualRevoke", operator != null ? operator : "admin");
|
||||
}
|
||||
|
||||
// 步骤 1: 创建外部操作流水 (REVOKE)
|
||||
/**
|
||||
* 单项授权注销核心流程 (安全守卫 + 事务边界隔离)
|
||||
*/
|
||||
private ReconciliationResult executeRevokeFlow(TrainDormAuth auth, String reason, String operator) {
|
||||
Long authId = auth.getAuthId();
|
||||
|
||||
// 1. DMS disabled 守卫 (Req 12): 关闭时不得发送 HTTP
|
||||
if (dmsProperties != null && !dmsProperties.isEnabled()) {
|
||||
if (auth.getExternalAuthId() == null || auth.getExternalAuthId().trim().isEmpty()) {
|
||||
// 从未下发过外部凭据,本地可直接安全标记为 REVOKED
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKED, null, operator);
|
||||
}
|
||||
return ReconciliationResult.noAction("DMS disabled; marked REVOKED locally as no external credential existed");
|
||||
} else {
|
||||
// 存在历史外部凭据,不可谎报 REVOKED,保持待处理状态
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_PENDING_REVOKE, null, operator);
|
||||
}
|
||||
return ReconciliationResult.waiting("DMS disabled; authorization has externalAuthId " + auth.getExternalAuthId() + " awaiting manual settlement");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Revoke 安全守卫 (Req 11): 严禁 externalAuthId == null -> revoke keyId 0
|
||||
if (auth.getExternalAuthId() == null || auth.getExternalAuthId().trim().isEmpty()) {
|
||||
if (TrainDormAuth.AUTH_STATUS_PENDING_GRANT.equals(auth.getAuthStatus())) {
|
||||
// 本地待授权草稿,未曾向厂商下发过
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKED, null, operator);
|
||||
}
|
||||
return ReconciliationResult.noAction("Draft auth never issued to external system; marked REVOKED locally");
|
||||
}
|
||||
// 处于可能已下发的状态但缺失 externalAuthId:严禁发 delete(0),进入明确 ERROR
|
||||
log.warn("Cannot revoke auth {}: externalAuthId is null/empty. Blocking revoke(0)", authId);
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, null, operator);
|
||||
}
|
||||
if (dormAuthMapper != null) {
|
||||
dormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_FAILED,
|
||||
"UNRESOLVABLE_EXTERNAL_CREDENTIAL: externalAuthId缺失,禁止调用delete(0),需人工核对厂商侧凭据", operator, new Date());
|
||||
}
|
||||
return ReconciliationResult.failure("UNRESOLVABLE_EXTERNAL_CREDENTIAL: Missing externalAuthId for auth " + authId + "; manual review required");
|
||||
}
|
||||
|
||||
Long externalKeyId;
|
||||
try {
|
||||
externalKeyId = Long.parseLong(auth.getExternalAuthId().trim());
|
||||
} catch (Exception e) {
|
||||
log.error("Invalid externalAuthId format on auth {}: {}", authId, auth.getExternalAuthId());
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, null, operator);
|
||||
}
|
||||
if (dormAuthMapper != null) {
|
||||
dormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_FAILED,
|
||||
"UNRESOLVABLE_EXTERNAL_CREDENTIAL: externalAuthId格式非法(" + auth.getExternalAuthId() + "),需人工核对", operator, new Date());
|
||||
}
|
||||
return ReconciliationResult.failure("UNRESOLVABLE_EXTERNAL_CREDENTIAL: Invalid externalAuthId for auth " + authId);
|
||||
}
|
||||
|
||||
// 3. 事务内:创建外部操作流水 (STATUS_CREATED) 并推进状态为 REVOKING (Req 17)
|
||||
Integer version = (auth.getOperationVersion() != null ? auth.getOperationVersion() : 1) + 1;
|
||||
String idempotencyKey = "REVOKE:AUTH:" + authId + ":V" + version;
|
||||
TrainExternalOperation op = new TrainExternalOperation();
|
||||
op.setDoorAuthId(authId);
|
||||
@@ -288,64 +566,81 @@ public class AccessAuthorizationOrchestrator {
|
||||
op.setOperationStatus(TrainExternalOperation.STATUS_CREATED);
|
||||
op.setIdempotencyKey(idempotencyKey);
|
||||
op.setExternalRequestType(3); // 3: 删除密码
|
||||
op.setCreateBy("Orchestrator");
|
||||
op.setNextPollTime(new Date());
|
||||
op.setCreateBy(operator);
|
||||
|
||||
TrainExternalOperation createdOp = operationService.createOperation(op);
|
||||
Long operationId = createdOp.getOperationId();
|
||||
TrainExternalOperation createdOp = operationService != null ? operationService.createOperation(op) : null;
|
||||
Long operationId = createdOp != null ? createdOp.getOperationId() : null;
|
||||
|
||||
// 步骤 2: 推进授权状态为 REVOKING
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKING, operationId, "Orchestrator");
|
||||
|
||||
// 步骤 3: 远程调用删除密码
|
||||
Long externalKeyId = null;
|
||||
if (auth.getExternalAuthId() != null && !auth.getExternalAuthId().trim().isEmpty()) {
|
||||
try {
|
||||
externalKeyId = Long.parseLong(auth.getExternalAuthId().trim());
|
||||
} catch (Exception ignored) {}
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKING, operationId, operator);
|
||||
}
|
||||
|
||||
// 4. 事务边界控制 (Req 17: 业务事务内绝不发起远程 HTTP)
|
||||
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
dispatchRevokeCall(authId, operationId, externalKeyId, operator);
|
||||
}
|
||||
});
|
||||
return ReconciliationResult.revokeRequired(authId, operationId, false, true, "Revoke operation created; dispatching after transaction commit");
|
||||
} else {
|
||||
return dispatchRevokeCall(authId, operationId, externalKeyId, operator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 派发远程撤销 HTTP 请求 (事务外执行)
|
||||
*/
|
||||
public ReconciliationResult dispatchRevokeCall(Long authId, Long operationId, Long externalKeyId, String operator) {
|
||||
try {
|
||||
DmsPasswordDeleteResponse resp = credentialGateway.revokeOnlinePassword(externalKeyId != null ? externalKeyId : 0L);
|
||||
DmsPasswordDeleteResponse resp = credentialGateway.revokeOnlinePassword(externalKeyId);
|
||||
String extOpIdStr = resp.getOperationId() != null ? String.valueOf(resp.getOperationId()) : null;
|
||||
|
||||
operationService.bindExternalReference(operationId, extOpIdStr, null, null, "Orchestrator");
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_ACCEPTED, null, null, "Orchestrator");
|
||||
if (operationService != null && operationId != null) {
|
||||
operationService.bindExternalReference(operationId, extOpIdStr, null, null, operator);
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_ACCEPTED, null, null, operator);
|
||||
}
|
||||
|
||||
log.info("DMS password delete accepted: localOpId={}, extOpId={}", operationId, extOpIdStr);
|
||||
return ReconciliationResult.revokeRequired(authId, operationId, true, true, "Password delete accepted by DMS; awaiting poller");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("DMS password delete failed for authId {}: {}", authId, e.getMessage());
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_FAILED, "GATEWAY_ERROR", sanitize(e.getMessage()), "Orchestrator");
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, operationId, "Orchestrator");
|
||||
|
||||
log.error("DMS password delete failed for authId {}: {}", authId, sanitize(e.getMessage()));
|
||||
if (operationService != null && operationId != null) {
|
||||
operationService.transitStatus(operationId, TrainExternalOperation.STATUS_FAILED, "GATEWAY_ERROR", sanitize(e.getMessage()), operator);
|
||||
}
|
||||
if (dormAuthService != null) {
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_ERROR, operationId, operator);
|
||||
}
|
||||
if (dormAuthMapper != null) {
|
||||
dormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_FAILED, "GATEWAY_ERROR: " + sanitize(e.getMessage()), operator, new Date());
|
||||
}
|
||||
return ReconciliationResult.revokeRequired(authId, operationId, true, false, "External revoke failed: " + sanitize(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private TrainDormAuth findStaleActiveAuthInHistory(Long currentAccId, List<TrainAccommodation> history) {
|
||||
if (history == null) return null;
|
||||
private List<TrainDormAuth> findStaleActiveAuthsInHistory(Long currentAccId, List<TrainAccommodation> history) {
|
||||
if (history == null) return Collections.emptyList();
|
||||
List<TrainDormAuth> staleList = new ArrayList<>();
|
||||
for (TrainAccommodation acc : history) {
|
||||
if (!acc.getAccommodationId().equals(currentAccId)) {
|
||||
TrainDormAuth oldAuth = dormAuthMapper.selectTrainDormAuthByAccommodationId(acc.getAccommodationId());
|
||||
if (oldAuth != null && TrainDormAuth.AUTH_STATUS_ACTIVE.equals(oldAuth.getAuthStatus())) {
|
||||
return oldAuth;
|
||||
if (acc != null && acc.getAccommodationId() != null && !acc.getAccommodationId().equals(currentAccId)) {
|
||||
TrainDormAuth oldAuth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAccommodationId(acc.getAccommodationId()) : null;
|
||||
if (oldAuth != null && !TrainDormAuth.AUTH_STATUS_REVOKED.equals(oldAuth.getAuthStatus())) {
|
||||
staleList.add(oldAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Date deriveDefaultValidUntil(Date start) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(start);
|
||||
cal.add(Calendar.DAY_OF_YEAR, 30); // 默认 30 天
|
||||
return cal.getTime();
|
||||
return staleList;
|
||||
}
|
||||
|
||||
private String sanitize(String str) {
|
||||
if (str == null) return null;
|
||||
return str.replaceAll("(?i)(password|token|secret)=[^&\\s]+", "$1=[PROTECTED]");
|
||||
String sanitized = str.replaceAll("(?i)(password|token|secret|appsecret)=[^&\\s]+", "$1=[PROTECTED]")
|
||||
.replaceAll("(?i)(\"password\"\\s*:\\s*\")[^\"]+(\")", "$1[PROTECTED]$2");
|
||||
// 脱敏 11 位手机号 (Req 8)
|
||||
sanitized = sanitized.replaceAll("(?<!\\d)(1[3-9]\\d)\\d{4}(\\d{4})(?!\\d)", "$1****$2");
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package com.ruoyi.train.integration.dms.orchestration;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量门禁对账汇总结果 (Wave 3B / Package A)
|
||||
* <p>
|
||||
* 终态准则:
|
||||
* 1. ALL_SETTLED: 所有相关授权均已确认进入终态 (REVOKED) 或明确无需任何操作 (NO_ACTION);
|
||||
* 2. IN_PROGRESS: 存在已向外部派发但仍在异步处理/轮询中的授权操作;
|
||||
* 3. PARTIAL_FAILURE: 部分授权成功/已处理,部分授权出现不可恢复异常;
|
||||
* 4. ALL_FAILED: 所有需要操作的授权项均处理失败。
|
||||
*/
|
||||
public class BatchReconciliationResult implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String OUTCOME_ALL_SETTLED = "ALL_SETTLED";
|
||||
public static final String OUTCOME_IN_PROGRESS = "IN_PROGRESS";
|
||||
public static final String OUTCOME_PARTIAL_FAILURE = "PARTIAL_FAILURE";
|
||||
public static final String OUTCOME_ALL_FAILED = "ALL_FAILED";
|
||||
|
||||
private Long enrollmentId;
|
||||
private List<ReconciliationItemVO> items = new ArrayList<>();
|
||||
private int total;
|
||||
private int dispatched;
|
||||
private int noAction;
|
||||
private int failed;
|
||||
private String outcome;
|
||||
|
||||
public BatchReconciliationResult() {
|
||||
}
|
||||
|
||||
public BatchReconciliationResult(Long enrollmentId) {
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public void addItem(ReconciliationItemVO item) {
|
||||
if (this.items == null) {
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
this.items.add(item);
|
||||
}
|
||||
|
||||
public void computeOutcome() {
|
||||
this.total = this.items != null ? this.items.size() : 0;
|
||||
if (this.total == 0) {
|
||||
this.outcome = OUTCOME_ALL_SETTLED;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.failed > 0) {
|
||||
if (this.failed == this.total) {
|
||||
this.outcome = OUTCOME_ALL_FAILED;
|
||||
} else {
|
||||
this.outcome = OUTCOME_PARTIAL_FAILURE;
|
||||
}
|
||||
} else if (this.dispatched > 0) {
|
||||
this.outcome = OUTCOME_IN_PROGRESS;
|
||||
} else {
|
||||
this.outcome = OUTCOME_ALL_SETTLED;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getEnrollmentId() {
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
public void setEnrollmentId(Long enrollmentId) {
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public List<ReconciliationItemVO> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<ReconciliationItemVO> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public int getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public int getDispatched() {
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
public void setDispatched(int dispatched) {
|
||||
this.dispatched = dispatched;
|
||||
}
|
||||
|
||||
public int getNoAction() {
|
||||
return noAction;
|
||||
}
|
||||
|
||||
public void setNoAction(int noAction) {
|
||||
this.noAction = noAction;
|
||||
}
|
||||
|
||||
public int getFailed() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public void setFailed(int failed) {
|
||||
this.failed = failed;
|
||||
}
|
||||
|
||||
public String getOutcome() {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
public void setOutcome(String outcome) {
|
||||
this.outcome = outcome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BatchReconciliationResult{" +
|
||||
"enrollmentId=" + enrollmentId +
|
||||
", total=" + total +
|
||||
", dispatched=" + dispatched +
|
||||
", noAction=" + noAction +
|
||||
", failed=" + failed +
|
||||
", outcome='" + outcome + '\'' +
|
||||
", items=" + items +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -22,5 +22,10 @@ public enum ReconciliationDecision {
|
||||
/**
|
||||
* 等待前置业务条件 (未报到、未分房、无门锁映射等)
|
||||
*/
|
||||
WAITING_FOR_PREREQUISITE
|
||||
WAITING_FOR_PREREQUISITE,
|
||||
|
||||
/**
|
||||
* 满足发放条件,等待管理员手动触发凭据下发 (Package A / Option B)
|
||||
*/
|
||||
AWAITING_MANUAL_ISSUE
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.ruoyi.train.integration.dms.orchestration;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 批量门禁对账单项执行结果
|
||||
*/
|
||||
public class ReconciliationItemVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long authId;
|
||||
private Long accommodationId;
|
||||
private String action;
|
||||
private String status;
|
||||
private String message;
|
||||
|
||||
public ReconciliationItemVO() {
|
||||
}
|
||||
|
||||
public ReconciliationItemVO(Long authId, Long accommodationId, String action, String status, String message) {
|
||||
this.authId = authId;
|
||||
this.accommodationId = accommodationId;
|
||||
this.action = action;
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Long getAuthId() {
|
||||
return authId;
|
||||
}
|
||||
|
||||
public void setAuthId(Long authId) {
|
||||
this.authId = authId;
|
||||
}
|
||||
|
||||
public Long getAccommodationId() {
|
||||
return accommodationId;
|
||||
}
|
||||
|
||||
public void setAccommodationId(Long accommodationId) {
|
||||
this.accommodationId = accommodationId;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReconciliationItemVO{" +
|
||||
"authId=" + authId +
|
||||
", accommodationId=" + accommodationId +
|
||||
", action='" + action + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", message='" + message + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+73
@@ -35,6 +35,45 @@ public class ReconciliationResult implements Serializable {
|
||||
return res;
|
||||
}
|
||||
|
||||
public static ReconciliationResult failure(String reason) {
|
||||
ReconciliationResult res = new ReconciliationResult();
|
||||
res.setDecision(ReconciliationDecision.WAITING_FOR_PREREQUISITE);
|
||||
res.setReason(reason);
|
||||
res.setSuccess(false);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static ReconciliationResult awaitingManualIssue(Long doorAuthId, String reason) {
|
||||
ReconciliationResult res = new ReconciliationResult();
|
||||
res.setDecision(ReconciliationDecision.AWAITING_MANUAL_ISSUE);
|
||||
res.setDoorAuthId(doorAuthId);
|
||||
res.setReason(reason);
|
||||
res.setExternalInvoked(false);
|
||||
res.setSuccess(true);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static ReconciliationResult revoking(String reason) {
|
||||
ReconciliationResult res = new ReconciliationResult();
|
||||
res.setDecision(ReconciliationDecision.REVOKE_REQUIRED);
|
||||
res.setReason(reason);
|
||||
res.setSuccess(true);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static ReconciliationResult batch(BatchReconciliationResult batchResult) {
|
||||
ReconciliationResult res = new ReconciliationResult();
|
||||
res.setBatchResult(batchResult);
|
||||
if (batchResult != null) {
|
||||
res.setEnrollmentId(batchResult.getEnrollmentId());
|
||||
res.setDecision(ReconciliationDecision.REVOKE_REQUIRED);
|
||||
res.setReason("Batch reconciliation outcome: " + batchResult.getOutcome());
|
||||
res.setSuccess(!BatchReconciliationResult.OUTCOME_ALL_FAILED.equals(batchResult.getOutcome()));
|
||||
res.setExternalInvoked(batchResult.getDispatched() > 0);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public static ReconciliationResult grantRequired(Long doorAuthId, Long operationId, boolean externalInvoked, boolean success, String reason) {
|
||||
ReconciliationResult res = new ReconciliationResult();
|
||||
res.setDecision(ReconciliationDecision.GRANT_REQUIRED);
|
||||
@@ -57,6 +96,40 @@ public class ReconciliationResult implements Serializable {
|
||||
return res;
|
||||
}
|
||||
|
||||
private BatchReconciliationResult batchResult;
|
||||
|
||||
public BatchReconciliationResult getBatchResult() {
|
||||
return batchResult;
|
||||
}
|
||||
|
||||
public void setBatchResult(BatchReconciliationResult batchResult) {
|
||||
this.batchResult = batchResult;
|
||||
}
|
||||
|
||||
public String getOutcome() {
|
||||
return batchResult != null ? batchResult.getOutcome() : null;
|
||||
}
|
||||
|
||||
public java.util.List<ReconciliationItemVO> getItems() {
|
||||
return batchResult != null ? batchResult.getItems() : null;
|
||||
}
|
||||
|
||||
public int getTotal() {
|
||||
return batchResult != null ? batchResult.getTotal() : 0;
|
||||
}
|
||||
|
||||
public int getDispatched() {
|
||||
return batchResult != null ? batchResult.getDispatched() : 0;
|
||||
}
|
||||
|
||||
public int getNoAction() {
|
||||
return batchResult != null ? batchResult.getNoAction() : 0;
|
||||
}
|
||||
|
||||
public int getFailed() {
|
||||
return batchResult != null ? batchResult.getFailed() : 0;
|
||||
}
|
||||
|
||||
public Long getEnrollmentId() {
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
@@ -66,3 +66,19 @@ export function manualReconcile(enrollmentId) {
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员显式下发门禁凭据 (单次返回明文凭据)
|
||||
export function issueAuth(authId) {
|
||||
return request({
|
||||
url: '/train/access/auth/' + authId + '/issue',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员显式注销门禁授权
|
||||
export function revokeAuth(authId) {
|
||||
return request({
|
||||
url: '/train/access/auth/' + authId + '/revoke',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -107,14 +107,14 @@ export const dynamicRoutes = [
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
redirect: '/train/auth',
|
||||
permissions: ['system:auth:list', 'train:accommodation:list'],
|
||||
permissions: ['train:access:query', 'system:auth:list', 'train:accommodation:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'auth',
|
||||
component: () => import('@/views/train/auth/index'),
|
||||
name: 'DoorAuth',
|
||||
meta: { title: '门禁授权', icon: 'lock' },
|
||||
permissions: ['system:auth:list']
|
||||
permissions: ['train:access:query', 'system:auth:list']
|
||||
},
|
||||
{
|
||||
path: 'accommodation',
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学员学号" align="center" prop="studentId" width="120" />
|
||||
<el-table-column label="手机号" align="center" width="125">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.studentPhone">{{ scope.row.studentPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}</span>
|
||||
<el-tag v-else type="danger" size="mini">缺手机号</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="宿舍安排" align="center" min-width="130">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.dormBuilding || '' }} {{ scope.row.dormRoom || '' }} ({{ scope.row.bedNo || '-' }}床)</span>
|
||||
@@ -108,7 +114,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="重试次数" align="center" prop="retryCount" width="80" />
|
||||
<el-table-column label="异常说明" align="center" prop="lastError" :show-overflow-tooltip="true" min-width="120" />
|
||||
<el-table-column label="管理操作" align="center" width="160" class-name="small-padding fixed-width">
|
||||
<el-table-column label="管理操作" align="center" width="280" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:access:query']"
|
||||
@@ -124,9 +130,31 @@
|
||||
icon="el-icon-refresh"
|
||||
@click="handleManualReconcile(scope.row)"
|
||||
>手动对账</el-button>
|
||||
<span v-if="scope.row.authStatus === 'PENDING_GRANT' || scope.row.authStatus === 'ERROR'">
|
||||
<el-tooltip v-if="!scope.row.studentPhone" content="缺手机号,暂无法下发门禁" placement="top">
|
||||
<span style="display: inline-block; margin-left: 10px;">
|
||||
<el-button
|
||||
v-hasPermi="['train:access:manage']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-key"
|
||||
disabled
|
||||
>下发门禁凭据</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['train:access:manage']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-key"
|
||||
style="color: #409EFF;"
|
||||
@click="handleIssue(scope.row)"
|
||||
>下发门禁凭据</el-button>
|
||||
</span>
|
||||
<el-button
|
||||
v-if="scope.row.authStatus === 'ACTIVE' || scope.row.authStatus === 'GRANTING'"
|
||||
v-hasPermi="['system:auth:edit']"
|
||||
v-if="scope.row.authStatus === 'ACTIVE' || scope.row.authStatus === 'GRANTING' || scope.row.authStatus === 'PENDING_REVOKE'"
|
||||
v-hasPermi="['train:access:manage']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-circle-close"
|
||||
@@ -196,7 +224,7 @@
|
||||
|
||||
<!-- 一次性临时凭据展示对话框 (ADMIN_ONE_TIME_DISPLAY) -->
|
||||
<el-dialog
|
||||
title="一次性临时凭据展示 (ADMIN_ONE_TIME_DISPLAY)"
|
||||
title="一次性门禁凭据展示 (ADMIN_ONE_TIME_DISPLAY)"
|
||||
:visible.sync="oneTimeDialogOpen"
|
||||
width="550px"
|
||||
append-to-body
|
||||
@@ -205,7 +233,7 @@
|
||||
:before-close="handleCloseOneTimeDialog"
|
||||
>
|
||||
<el-alert
|
||||
title="安全合规提示:该临时凭据仅在本次操作创建成功后由内存短暂展示一次!关闭弹窗或刷新页面后将永久清除,系统不持久化亦无法再次查询。"
|
||||
title="凭据已提交下发,设备确认后生效。安全合规提示:该临时凭据仅在本次操作下发成功后由内存短暂展示一次!关闭弹窗或刷新页面后将永久清除,系统不持久化亦无法再次查询旧明文。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@@ -230,8 +258,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listAuth, getOperationHistory, manualReconcile } from '@/api/train/access'
|
||||
import { revokeAuth } from '@/api/train/auth'
|
||||
import { listAuth, getOperationHistory, manualReconcile, issueAuth, revokeAuth } from '@/api/train/access'
|
||||
|
||||
export default {
|
||||
name: 'Auth',
|
||||
@@ -308,18 +335,35 @@ export default {
|
||||
this.getList()
|
||||
if (res.data && res.data.oneTimePlaintextCredential) {
|
||||
this.oneTimeCredential = res.data.oneTimePlaintextCredential
|
||||
this.maskedMode = true
|
||||
this.maskedMode = false
|
||||
this.oneTimeDialogOpen = true
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleIssue(row) {
|
||||
if (!row.studentPhone) {
|
||||
this.$modal.msgWarning('缺手机号,暂无法下发门禁')
|
||||
return
|
||||
}
|
||||
this.$modal.confirm(`确认向学员【${row.studentName || row.studentId}】下发门禁凭据?`).then(() => {
|
||||
return issueAuth(row.authId)
|
||||
}).then(res => {
|
||||
this.$modal.msgSuccess(res.msg || '凭据已提交下发,设备确认后生效。')
|
||||
this.getList()
|
||||
if (res.data && res.data.oneTimePlaintextCredential) {
|
||||
this.oneTimeCredential = res.data.oneTimePlaintextCredential
|
||||
this.maskedMode = false
|
||||
this.oneTimeDialogOpen = true
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleRevoke(row) {
|
||||
const studentDesc = row.studentId || `报名ID:${row.enrollmentId}`
|
||||
const studentDesc = row.studentName ? `${row.studentName} (${row.studentId || row.enrollmentId})` : (row.studentId || `报名ID:${row.enrollmentId}`)
|
||||
const dormDesc = `${row.dormBuilding || ''} ${row.dormRoom || ''}`
|
||||
this.$modal.confirm(`确定要注销学员【${studentDesc}】在宿舍【${dormDesc}】的门禁授权(授权ID: ${row.authId})吗?注销后该学员将无法通行该门禁。`).then(() => {
|
||||
return revokeAuth(row.authId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('注销成功')
|
||||
}).then(res => {
|
||||
this.$modal.msgSuccess(res.msg || '注销操作已提交')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user