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.
221 lines
11 KiB
221 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)",
|
|
};
|
|
}
|
|
|