You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

222 lines
11 KiB

package com.crm.project.transfer;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.crm.auth.domain.entity.AuthUser;
import com.crm.auth.service.IAuthUserService;
import com.crm.base.config.MetaObjectFillHandler;
import com.crm.base.domain.exception.BusinessErrorException;
import com.crm.base.service.UserTransferPort;
import com.crm.customer.port.ProjectOwnerSyncPort;
import com.crm.project.constant.ProjectConstants;
import com.crm.project.domain.entity.CrmProject;
import com.crm.project.mapper.CrmProjectMapper;
import com.crm.project.port.inbound.impl.ProjectOwnerSyncPortImpl;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.LocalCacheScope;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.sql.Connection;
import java.sql.Statement;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 移交/同步接缝 H2 集成测试 12
*
* <p>断言点对齐票面</p>
* <ul>
* <li>项目域移交 crm_project owner 三列domain()="project"crm-auth 多态注册表 key
* 与商机域crm-opportunity OpportunityUserTransferPortImpl opportunity 互不串扰</li>
* <li>crm-auth 零改动domain() 返回值即 crm-auth UserTransferService DOMAIN_NAMES
* "project" 本测试钉死契约值注册表自动重挂由 Spring 容器装配保证</li>
* <li>ProjectOwnerSyncPort 真实现customer_id 命中批量刷 owner返回真实影响行数Noop 0 已退役</li>
* </ul>
*/
@DisplayName("移交/同步接缝(H2 集成,票 12)")
class ProjectSeamsH2IntegrationTest {
private static final Long ALICE = 7101L;
private static final Long BOB = 7102L;
private static final Long CUSTOMER_A = 8001L;
private static final Long CUSTOMER_B = 8002L;
private static final AtomicBoolean BOOTSTRAPPED = new AtomicBoolean(false);
private static SqlSessionFactory sqlSessionFactory;
private SqlSession session;
private CrmProjectMapper projectMapper;
private ProjectUserTransferPortImpl transferPort;
private ProjectOwnerSyncPortImpl ownerSyncPort;
@BeforeAll
static void bootstrap() throws Exception {
if (!BOOTSTRAPPED.compareAndSet(false, true)) {
return;
}
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:projseams;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;"
+ "DB_CLOSE_DELAY=-1");
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setEnvironment(new Environment("projseams-test", new JdbcTransactionFactory(), ds));
configuration.setMapUnderscoreToCamelCase(true);
configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
configuration.addInterceptor(mpInterceptor);
GlobalConfigUtils.getGlobalConfig(configuration).setMetaObjectHandler(new MetaObjectFillHandler());
GlobalConfigUtils.getGlobalConfig(configuration)
.setIdentifierGenerator(DefaultIdentifierGenerator.getInstance());
configuration.addMapper(CrmProjectMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
try (SqlSession s = sqlSessionFactory.openSession(true)) {
try (Statement st = s.getConnection().createStatement()) {
for (String sql : SCHEMA) {
st.execute(sql);
}
}
}
}
@BeforeEach
void openSession() throws Exception {
session = sqlSessionFactory.openSession(true);
try (Statement st = session.getConnection().createStatement()) {
st.execute("DELETE FROM crm_project");
for (String sql : SEED) {
st.execute(sql);
}
}
projectMapper = session.getMapper(CrmProjectMapper.class);
IAuthUserService authUserService = Mockito.mock(IAuthUserService.class);
AuthUser bob = new AuthUser();
bob.setId(BOB);
bob.setUsername("李四");
bob.setDeptId(6002L);
Mockito.when(authUserService.getById(BOB)).thenReturn(bob);
transferPort = new ProjectUserTransferPortImpl(projectMapper, authUserService);
ownerSyncPort = new ProjectOwnerSyncPortImpl(projectMapper, authUserService);
}
@AfterEach
void closeSession() {
if (session != null) {
session.close();
}
}
@Test
@DisplayName("项目域移交:domain()=project(crm-auth 注册表 key 契约);count/transfer 查 crm_project 刷 owner 三列")
void transferPortWorks() {
assertThat(transferPort.domain()).isEqualTo("project");
assertThat(transferPort.countOwned(ALICE)).isEqualTo(2L); // P1、P2
int updated = transferPort.transferOwned(ALICE, BOB);
assertThat(updated).isEqualTo(2);
CrmProject p1 = projectMapper.selectById(1L);
assertThat(p1.getOwnerUserId()).isEqualTo(BOB);
assertThat(p1.getOwnerNameSnapshot()).isEqualTo("李四");
assertThat(p1.getOwnerDeptId()).isEqualTo(6002L);
assertThat(transferPort.countOwned(ALICE)).isEqualTo(0L);
assertThat(transferPort.countOwned(BOB)).isEqualTo(3L); // P1、P2 移入 + P3 原本就是 BOB
}
@Test
@DisplayName("移交不串扰:商机域(opportunity 表 opp_status 切域)数据不受项目域移交影响")
void transferPortIsolation() {
// 本测试库无 opportunity 表接入——接缝隔离由「查各自 Mapper/表」保证(结构核查):
// 项目域实现断开 extends(父类就是 Object,未继承商机侧共表抽象类;
// 二者 instanceof 互查甚至无法通过编译——不相关类型,解耦已由编译器证明)
assertThat(transferPort.getClass().getSuperclass()).isEqualTo(Object.class);
int updated = transferPort.transferOwned(ALICE, BOB);
assertThat(updated).isEqualTo(2); // 仅 crm_project 行受影响
}
@Test
@DisplayName("ProjectOwnerSyncPort:customer_id 命中批量刷 owner,返回真实影响行数(Noop 已退役)")
void ownerSyncByCustomer() {
int updated = ownerSyncPort.syncOwnerByCustomer(CUSTOMER_A, BOB);
assertThat(updated).isEqualTo(2); // P1、P3 同属客户 A
CrmProject p3 = projectMapper.selectById(3L);
assertThat(p3.getOwnerUserId()).isEqualTo(BOB);
assertThat(p3.getOwnerNameSnapshot()).isEqualTo("李四");
assertThat(p3.getOwnerDeptId()).isEqualTo(6002L);
// 客户 B(P2 原 owner=ALICE)也随人走
assertThat(ownerSyncPort.syncOwnerByCustomer(CUSTOMER_B, BOB)).isEqualTo(1);
}
@Test
@DisplayName("owner 同步:接收人缺失 → 业务异常(调用方事务回滚)")
void ownerSyncRejectsMissingReceiver() {
assertThatThrownBy(() -> ownerSyncPort.syncOwnerByCustomer(CUSTOMER_A, 99999L))
.isInstanceOf(BusinessErrorException.class)
.hasFieldOrPropertyWithValue("code", ProjectConstants.CODE_PROJECT_INVALID);
}
// ==================== 数据 ====================
private static final String[] SCHEMA = {
"""
CREATE TABLE crm_project (
id BIGINT PRIMARY KEY, creator_id VARCHAR(50), create_time DATETIME,
updater_id VARCHAR(50), update_time DATETIME, deleted TINYINT NOT NULL DEFAULT 0,
project_name VARCHAR(200) NOT NULL,
project_stage TINYINT NOT NULL DEFAULT 0,
project_status TINYINT NOT NULL DEFAULT 1,
filing_status TINYINT NOT NULL DEFAULT 1,
bid_result TINYINT NOT NULL DEFAULT 0,
source_opp_id BIGINT, scheme_card_id BIGINT NOT NULL, customer_id BIGINT NOT NULL,
owner_user_id BIGINT, owner_name_snapshot VARCHAR(64), owner_dept_id BIGINT,
project_amount DECIMAL(14,2), region_code VARCHAR(32), brand VARCHAR(200),
bid_time DATETIME,
scheme_budget DECIMAL(14,2), scheme_version VARCHAR(32), scheme_task_status VARCHAR(32),
quote_total DECIMAL(14,2), quote_status VARCHAR(32),
bid_deadline DATETIME, bid_budget DECIMAL(14,2), brand_control VARCHAR(200),
bid_base_type VARCHAR(32), alloc_ratio DECIMAL(5,2), bid_bond DECIMAL(14,2),
bid_file_id VARCHAR(64), bidder_name VARCHAR(200),
bid_open_time DATETIME, bid_open_place VARCHAR(200), bid_award_method VARCHAR(64),
bid_award_amount DECIMAL(14,2),
close_type VARCHAR(32), close_reason VARCHAR(500), close_time DATETIME,
close_remark VARCHAR(500),
version INT NOT NULL DEFAULT 0)
"""
};
private static final String[] SEED = {
"INSERT INTO crm_project (id, deleted, project_name, project_stage, project_status, filing_status, bid_result, scheme_card_id, customer_id, owner_user_id, owner_name_snapshot, owner_dept_id, version) "
+ "VALUES (1, 0, '项目一', 1, 1, 2, 0, 9001, " + CUSTOMER_A + ", " + ALICE + ", '张三', 6001, 0)",
"INSERT INTO crm_project (id, deleted, project_name, project_stage, project_status, filing_status, bid_result, scheme_card_id, customer_id, owner_user_id, owner_name_snapshot, owner_dept_id, version) "
+ "VALUES (2, 0, '项目二', 3, 1, 2, 0, 9002, " + CUSTOMER_B + ", " + ALICE + ", '张三', 6001, 0)",
"INSERT INTO crm_project (id, deleted, project_name, project_stage, project_status, filing_status, bid_result, scheme_card_id, customer_id, owner_user_id, owner_name_snapshot, owner_dept_id, version) "
+ "VALUES (3, 0, '项目三', 0, 1, 1, 0, 9003, " + CUSTOMER_A + ", " + BOB + ", '王五', 6003, 0)",
};
}