✨ 认证记录状态机规则集中为 CertificationRuleSupport 并补测试
把「什么状态能做什么」从持久化里拎出来单独验证。这几条规则是认证改造的地基, 改坏的后果都是线上事故,故先锁住行为再写业务: - prepareForSubmit:新提交一律待审,并清空驳回原因(否则会继承上一次的驳回文案, 出现「审核中」却带着驳回原因的自相矛盾状态); - supersedeIfPending:再次提交只作废前一条待审,已通过/已驳回/已作废都是终态不碰 ——尤其是不得改动已通过的记录,它可能正被生效快照指针指着; - isCertified:唯一判据是生效快照指针,故「曾通过即永久保持」; - isAuditable:只有待审可审,终态再审会把生效快照改坏; - statusText:四种状态各一个中文说法,与列注释、前端文案保持一致。 TransportCertRecord.REJECT_REASON_MAX_LENGTH 与 reject_reason 列宽(200)对齐。 9 个用例覆盖上述规则与边界(null、未知状态、超长原因)。 可离线执行:mvn -o test -pl blade-service/blade-transport -am
This commit is contained in:
+5
@@ -91,6 +91,11 @@ public class TransportCertRecord extends TenantEntity {
|
||||
*/
|
||||
public static final Integer STATUS_SUPERSEDED = 3;
|
||||
|
||||
/**
|
||||
* 驳回原因最大长度,与 {@code reject_reason} 列宽保持一致
|
||||
*/
|
||||
public static final int REJECT_REASON_MAX_LENGTH = 200;
|
||||
|
||||
/**
|
||||
* 认证对象类型:driver-司机,vehicle-车辆
|
||||
* <p>
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.TransportCertRecord;
|
||||
|
||||
/**
|
||||
* 认证记录状态机规则
|
||||
* <p>
|
||||
* 把「什么状态能做什么」集中在一处,与持久化分离,便于单独验证。
|
||||
* 这几条规则是整个认证改造的地基,改坏的后果是线上事故:
|
||||
* 司机重新提交认证后车辆掉出可用池,或同一辆车出现两条待审记录。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class CertificationRuleSupport {
|
||||
|
||||
private static final String STATUS_TEXT_PENDING = "审核中";
|
||||
private static final String STATUS_TEXT_APPROVED = "已通过";
|
||||
private static final String STATUS_TEXT_REJECTED = "已驳回";
|
||||
private static final String STATUS_TEXT_SUPERSEDED = "已作废";
|
||||
private static final String STATUS_TEXT_UNKNOWN = "未知状态";
|
||||
|
||||
private CertificationRuleSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一条新提交的记录准备成待审
|
||||
* <p>
|
||||
* 必须清空驳回原因:新提交是全新一次认证,不能继承上一次的驳回文案,
|
||||
* 否则前端会显示「审核中」却带着旧的驳回原因。
|
||||
*
|
||||
* @param incoming 待入库的记录
|
||||
*/
|
||||
public static void prepareForSubmit(TransportCertRecord incoming) {
|
||||
incoming.setStatus(TransportCertRecord.STATUS_PENDING);
|
||||
incoming.setRejectReason(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 再次提交时,把该认证对象**前一条待审**记录转为已作废
|
||||
* <p>
|
||||
* 只动待审记录:已通过、已驳回、已作废都是终态,一律不碰。
|
||||
* 特别是**不得改动已通过的记录**——它可能正被生效快照指针指着。
|
||||
*
|
||||
* @param previousPending 该对象当前那条待审记录,可为 null
|
||||
* @return 需要更新为已作废的记录;无需更新时返回 null
|
||||
*/
|
||||
public static TransportCertRecord supersedeIfPending(TransportCertRecord previousPending) {
|
||||
if (previousPending == null) {
|
||||
return null;
|
||||
}
|
||||
if (!TransportCertRecord.STATUS_PENDING.equals(previousPending.getStatus())) {
|
||||
return null;
|
||||
}
|
||||
previousPending.setStatus(TransportCertRecord.STATUS_SUPERSEDED);
|
||||
return previousPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定认证对象是否已认证
|
||||
* <p>
|
||||
* 唯一判据是生效快照指针。指针一旦写入永不回退,因此「曾通过即永久保持」,
|
||||
* 重新提交、待审、被驳回都不会让对象变回未认证。
|
||||
*
|
||||
* @param effectiveRecordId 生效快照指针
|
||||
* @return 是否已认证
|
||||
*/
|
||||
public static boolean isCertified(Long effectiveRecordId) {
|
||||
return Func.isNotEmpty(effectiveRecordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定一条记录是否可被审核
|
||||
* <p>
|
||||
* 只有待审记录可审。已通过/已驳回/已作废都是终态,再次审核会把生效快照改坏
|
||||
* (例如把指针指着的那条「已通过」改成「已驳回」,生效值就没了)。
|
||||
*
|
||||
* @param record 认证记录
|
||||
* @return 是否可审核
|
||||
*/
|
||||
public static boolean isAuditable(TransportCertRecord record) {
|
||||
return record != null && TransportCertRecord.STATUS_PENDING.equals(record.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验审核入参是否合法
|
||||
*
|
||||
* @param pass 是否通过
|
||||
* @param rejectReason 驳回原因
|
||||
* @return 是否合法
|
||||
*/
|
||||
public static boolean isRejectReasonValid(boolean pass, String rejectReason) {
|
||||
if (pass) {
|
||||
return true;
|
||||
}
|
||||
String reason = Func.toStr(rejectReason, "").trim();
|
||||
return Func.isNotEmpty(reason)
|
||||
&& reason.length() <= TransportCertRecord.REJECT_REASON_MAX_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过时的驳回原因:必须为 null,避免残留上一次的驳回文案
|
||||
*
|
||||
* @return 恒为 null
|
||||
*/
|
||||
public static String reasonForApprove() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核驳回时的驳回原因:去首尾空白后落库
|
||||
*
|
||||
* @param rejectReason 原始原因
|
||||
* @return 去空白后的原因
|
||||
*/
|
||||
public static String reasonForReject(String rejectReason) {
|
||||
return Func.toStr(rejectReason, "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态对应的中文文案
|
||||
* <p>
|
||||
* 四端(小程序、Web、接口、库注释)必须用同一套说法,不要各造一套。
|
||||
*
|
||||
* @param status 状态值
|
||||
* @return 中文文案
|
||||
*/
|
||||
public static String statusText(Integer status) {
|
||||
if (TransportCertRecord.STATUS_PENDING.equals(status)) {
|
||||
return STATUS_TEXT_PENDING;
|
||||
}
|
||||
if (TransportCertRecord.STATUS_APPROVED.equals(status)) {
|
||||
return STATUS_TEXT_APPROVED;
|
||||
}
|
||||
if (TransportCertRecord.STATUS_REJECTED.equals(status)) {
|
||||
return STATUS_TEXT_REJECTED;
|
||||
}
|
||||
if (TransportCertRecord.STATUS_SUPERSEDED.equals(status)) {
|
||||
return STATUS_TEXT_SUPERSEDED;
|
||||
}
|
||||
return STATUS_TEXT_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验认证对象类型
|
||||
*
|
||||
* @param bizType 类型
|
||||
* @return 是否为合法类型
|
||||
*/
|
||||
public static boolean isValidBizType(String bizType) {
|
||||
return TransportCertRecord.BIZ_TYPE_DRIVER.equals(bizType)
|
||||
|| TransportCertRecord.BIZ_TYPE_VEHICLE.equals(bizType);
|
||||
}
|
||||
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.pojo.entity.TransportCertRecord;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 认证记录状态机规则测试。
|
||||
* <p>
|
||||
* 这几条规则是整个认证改造的地基,一旦被改坏,症状是「司机重新提交认证后车辆掉出可用池」
|
||||
* 或「同一辆车出现两条待审」,都属线上事故,故集中在一处并锁住行为。
|
||||
*/
|
||||
class CertificationRuleSupportTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("提交:前一条待审必须转已作废,而不是被删除")
|
||||
void supersedePending() {
|
||||
TransportCertRecord previousPending = record(TransportCertRecord.STATUS_PENDING);
|
||||
|
||||
TransportCertRecord superseded = CertificationRuleSupport.supersedeIfPending(previousPending);
|
||||
|
||||
// 返回的正是那条旧记录,且只改状态
|
||||
assertEquals(TransportCertRecord.STATUS_SUPERSEDED, superseded.getStatus());
|
||||
assertEquals(previousPending.getId(), superseded.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("提交:前一条不是待审时,不得被改动(已通过/已驳回都是终态)")
|
||||
void doesNotTouchNonPending() {
|
||||
assertNull(CertificationRuleSupport.supersedeIfPending(null));
|
||||
assertNull(CertificationRuleSupport.supersedeIfPending(record(TransportCertRecord.STATUS_APPROVED)));
|
||||
assertNull(CertificationRuleSupport.supersedeIfPending(record(TransportCertRecord.STATUS_REJECTED)));
|
||||
assertNull(CertificationRuleSupport.supersedeIfPending(record(TransportCertRecord.STATUS_SUPERSEDED)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("提交:新记录一律是待审,且清空驳回原因(避免继承上一条的驳回文案)")
|
||||
void newRecordIsPending() {
|
||||
TransportCertRecord incoming = record(null);
|
||||
incoming.setStatus(TransportCertRecord.STATUS_REJECTED);
|
||||
incoming.setRejectReason("上一次的驳回原因");
|
||||
|
||||
CertificationRuleSupport.prepareForSubmit(incoming);
|
||||
|
||||
assertEquals(TransportCertRecord.STATUS_PENDING, incoming.getStatus());
|
||||
assertNull(incoming.getRejectReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生效快照判定:只看指针,与记录表里任何一条的状态都无关")
|
||||
void effectiveIsDecidedByPointerOnly() {
|
||||
assertFalse(CertificationRuleSupport.isCertified(null));
|
||||
assertTrue(CertificationRuleSupport.isCertified(123456789L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("可审核判定:只有待审记录可被审核,终态一律拒绝(防止把生效快照改坏)")
|
||||
void onlyPendingIsAuditable() {
|
||||
assertTrue(CertificationRuleSupport.isAuditable(record(TransportCertRecord.STATUS_PENDING)));
|
||||
assertFalse(CertificationRuleSupport.isAuditable(record(TransportCertRecord.STATUS_APPROVED)));
|
||||
assertFalse(CertificationRuleSupport.isAuditable(record(TransportCertRecord.STATUS_REJECTED)));
|
||||
assertFalse(CertificationRuleSupport.isAuditable(record(TransportCertRecord.STATUS_SUPERSEDED)));
|
||||
assertFalse(CertificationRuleSupport.isAuditable(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核入参:驳回必须给原因,通过时原因无意义需清空")
|
||||
void auditPayload() {
|
||||
// 驳回无原因 -> 拒绝
|
||||
assertFalse(CertificationRuleSupport.isRejectReasonValid(false, null));
|
||||
assertFalse(CertificationRuleSupport.isRejectReasonValid(false, " "));
|
||||
assertTrue(CertificationRuleSupport.isRejectReasonValid(false, "证件照片模糊"));
|
||||
|
||||
// 通过时不需要原因
|
||||
assertTrue(CertificationRuleSupport.isRejectReasonValid(true, null));
|
||||
|
||||
// 通过必须把原因清空,否则会残留上一次驳回文案
|
||||
assertNull(CertificationRuleSupport.reasonForApprove());
|
||||
assertEquals("证件照片模糊", CertificationRuleSupport.reasonForReject(" 证件照片模糊 "));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("驳回原因长度:超过 200 字拒绝(与列宽一致)")
|
||||
void rejectReasonLength() {
|
||||
assertTrue(CertificationRuleSupport.isRejectReasonValid(false, "短".repeat(200)));
|
||||
assertFalse(CertificationRuleSupport.isRejectReasonValid(false, "长".repeat(201)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("状态文案:四种状态各一个中文说法,未知状态不静默给空")
|
||||
void statusText() {
|
||||
assertEquals("审核中", CertificationRuleSupport.statusText(TransportCertRecord.STATUS_PENDING));
|
||||
assertEquals("已通过", CertificationRuleSupport.statusText(TransportCertRecord.STATUS_APPROVED));
|
||||
assertEquals("已驳回", CertificationRuleSupport.statusText(TransportCertRecord.STATUS_REJECTED));
|
||||
assertEquals("已作废", CertificationRuleSupport.statusText(TransportCertRecord.STATUS_SUPERSEDED));
|
||||
assertEquals("未知状态", CertificationRuleSupport.statusText(99));
|
||||
assertEquals("未知状态", CertificationRuleSupport.statusText(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("认证对象类型:只认 driver / vehicle,其它一律拒绝")
|
||||
void bizTypeValidation() {
|
||||
assertTrue(CertificationRuleSupport.isValidBizType("driver"));
|
||||
assertTrue(CertificationRuleSupport.isValidBizType("vehicle"));
|
||||
assertFalse(CertificationRuleSupport.isValidBizType("DRIVER"));
|
||||
assertFalse(CertificationRuleSupport.isValidBizType("truck"));
|
||||
assertFalse(CertificationRuleSupport.isValidBizType(null));
|
||||
assertFalse(CertificationRuleSupport.isValidBizType(""));
|
||||
}
|
||||
|
||||
private TransportCertRecord record(Integer status) {
|
||||
TransportCertRecord record = new TransportCertRecord();
|
||||
record.setId(1000L);
|
||||
record.setStatus(status);
|
||||
return record;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user