From 4c6ca52e7efe95da8625c06a8fe84a78556736ab Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 17 Apr 2026 10:18:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=B4=A6=E6=88=B7=E5=BA=93?= =?UTF-8?q?=E4=B8=BA=E5=8D=95=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- ccdi-info-collection/pom.xml | 6 + .../collection/domain/CcdiAccountInfo.java | 37 ++++ .../collection/domain/CcdiAccountResult.java | 86 ---------- .../mapper/CcdiAccountResultMapper.java | 13 -- .../impl/CcdiAccountInfoServiceImpl.java | 98 ++++------- .../info/collection/CcdiAccountInfoMapper.xml | 25 ++- .../mapper/CcdiAccountInfoMapperTest.java | 109 ++++++++++++ .../CcdiAccountInfoServiceImplTest.java | 124 ++++++++++++++ .../lsfx/client/CreditParseClientTest.java | 90 ---------- .../sql/CcdiAccountInfoMergeSqlTest.java | 33 ++++ deploy/deploy-to-nas-tongweb.sh | 101 +++++++++++ deploy/remote-deploy-tongweb.py | 136 +++++++++++++++ ...-run-package-conventions-implementation.md | 94 ++++++++++ ...as-tongweb-deploy-script-implementation.md | 62 +++++++ ...ccount-columns-migration-implementation.md | 26 +++ ...unt-library-single-table-implementation.md | 53 ++++++ ...unt-library-single-table-implementation.md | 62 +++++++ ...-backend-package-and-deploy-conventions.sh | 70 ++++++++ .../tests/test_schema_migration_scripts.py | 19 +++ ...d-ccdi-account-info-external-scenarios.sql | 160 ++++++------------ ...-account-info-abnormal-account-columns.sql | 31 ++++ ...16-merge-ccdi-account-result-into-info.sql | 156 +++++++++++++++++ tests/deploy/test_deploy_to_nas_tongweb.py | 67 ++++++++ 24 files changed, 1285 insertions(+), 377 deletions(-) delete mode 100644 ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java delete mode 100644 ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java create mode 100644 ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapperTest.java create mode 100644 ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAccountInfoServiceImplTest.java delete mode 100644 ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiAccountInfoMergeSqlTest.java create mode 100755 deploy/deploy-to-nas-tongweb.sh create mode 100644 deploy/remote-deploy-tongweb.py create mode 100644 docs/plans/backend/2026-04-14-backend-run-package-conventions-implementation.md create mode 100644 docs/plans/backend/2026-04-14-nas-tongweb-deploy-script-implementation.md create mode 100644 docs/plans/backend/2026-04-15-lsfx-mock-server-ccdi-account-info-abnormal-account-columns-migration-implementation.md create mode 100644 docs/plans/backend/2026-04-16-account-library-single-table-implementation.md create mode 100644 docs/reports/implementation/2026-04-16-account-library-single-table-implementation.md create mode 100644 docs/tests/scripts/test-backend-package-and-deploy-conventions.sh create mode 100644 lsfx-mock-server/tests/test_schema_migration_scripts.py create mode 100644 sql/migration/2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql create mode 100644 sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql create mode 100644 tests/deploy/test_deploy_to_nas_tongweb.py diff --git a/.gitignore b/.gitignore index 49669283..b79b2fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,6 @@ output/ logs/ -.DS_Store \ No newline at end of file +.DS_Store + +ruoyi-ui/vue.config.js diff --git a/ccdi-info-collection/pom.xml b/ccdi-info-collection/pom.xml index 8158c290..5c99e018 100644 --- a/ccdi-info-collection/pom.xml +++ b/ccdi-info-collection/pom.xml @@ -57,6 +57,12 @@ spring-boot-starter-test test + + com.ruoyi + ccdi-lsfx + 3.9.1 + compile + diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java index 4709db89..6d5467c6 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountInfo.java @@ -9,6 +9,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; +import java.math.BigDecimal; import java.util.Date; /** @@ -56,6 +57,42 @@ public class CcdiAccountInfo implements Serializable { /** 币种 */ private String currency; + /** 是否实控账户:0-否 1-是 */ + @TableField("is_self_account") + private Integer isActualControl; + + /** 月均交易笔数 */ + @TableField("monthly_avg_trans_count") + private Integer avgMonthTxnCount; + + /** 月均交易金额 */ + @TableField("monthly_avg_trans_amount") + private BigDecimal avgMonthTxnAmount; + + /** 交易频率等级 */ + @TableField("trans_freq_type") + private String txnFrequencyLevel; + + /** 借方单笔最高额 */ + @TableField("dr_max_single_amount") + private BigDecimal debitSingleMaxAmount; + + /** 贷方单笔最高额 */ + @TableField("cr_max_single_amount") + private BigDecimal creditSingleMaxAmount; + + /** 借方日累计最高额 */ + @TableField("dr_max_daily_amount") + private BigDecimal debitDailyMaxAmount; + + /** 贷方日累计最高额 */ + @TableField("cr_max_daily_amount") + private BigDecimal creditDailyMaxAmount; + + /** 风险等级 */ + @TableField("trans_risk_level") + private String txnRiskLevel; + /** 状态:1-正常 2-已销户 */ private Integer status; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java deleted file mode 100644 index 0f76037e..00000000 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiAccountResult.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.ruoyi.info.collection.domain; - -import com.baomidou.mybatisplus.annotation.FieldFill; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.io.Serial; -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.Date; - -/** - * 账户分析结果对象 ccdi_account_result - * - * @author ruoyi - * @date 2026-04-13 - */ -@Data -@TableName("ccdi_account_result") -public class CcdiAccountResult implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - /** 主键ID */ - @TableId(value = "result_id", type = IdType.AUTO) - private Long resultId; - - /** 账户号码 */ - private String accountNo; - - /** 是否实控账户:0-否 1-是 */ - @TableField("is_self_account") - private Integer isActualControl; - - /** 月均交易笔数 */ - @TableField("monthly_avg_trans_count") - private Integer avgMonthTxnCount; - - /** 月均交易金额 */ - @TableField("monthly_avg_trans_amount") - private BigDecimal avgMonthTxnAmount; - - /** 交易频率等级 */ - @TableField("trans_freq_type") - private String txnFrequencyLevel; - - /** 借方单笔最高额 */ - @TableField("dr_max_single_amount") - private BigDecimal debitSingleMaxAmount; - - /** 贷方单笔最高额 */ - @TableField("cr_max_single_amount") - private BigDecimal creditSingleMaxAmount; - - /** 借方日累计最高额 */ - @TableField("dr_max_daily_amount") - private BigDecimal debitDailyMaxAmount; - - /** 贷方日累计最高额 */ - @TableField("cr_max_daily_amount") - private BigDecimal creditDailyMaxAmount; - - /** 风险等级 */ - @TableField("trans_risk_level") - private String txnRiskLevel; - - /** 创建者 */ - @TableField(fill = FieldFill.INSERT) - private String createBy; - - /** 创建时间 */ - @TableField(fill = FieldFill.INSERT) - private Date createTime; - - /** 更新者 */ - @TableField(fill = FieldFill.INSERT_UPDATE) - private String updateBy; - - /** 更新时间 */ - @TableField(fill = FieldFill.INSERT_UPDATE) - private Date updateTime; -} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java deleted file mode 100644 index 29c84f71..00000000 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAccountResultMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ruoyi.info.collection.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.info.collection.domain.CcdiAccountResult; - -/** - * 账户分析结果数据层 - * - * @author ruoyi - * @date 2026-04-13 - */ -public interface CcdiAccountResultMapper extends BaseMapper { -} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java index 3383b5ad..afb1e1c4 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAccountInfoServiceImpl.java @@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.info.collection.domain.CcdiAccountInfo; -import com.ruoyi.info.collection.domain.CcdiAccountResult; import com.ruoyi.info.collection.domain.CcdiBaseStaff; import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation; import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO; @@ -16,7 +15,6 @@ import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO; import com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO; import com.ruoyi.info.collection.domain.vo.ImportResult; import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper; -import com.ruoyi.info.collection.mapper.CcdiAccountResultMapper; import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper; import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper; import com.ruoyi.info.collection.service.ICcdiAccountInfoService; @@ -56,9 +54,6 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService { @Resource private CcdiAccountInfoMapper accountInfoMapper; - @Resource - private CcdiAccountResultMapper accountResultMapper; - @Resource private CcdiBaseStaffMapper baseStaffMapper; @@ -87,9 +82,8 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService { CcdiAccountInfo accountInfo = new CcdiAccountInfo(); BeanUtils.copyProperties(addDTO, accountInfo); - int result = accountInfoMapper.insert(accountInfo); - syncAccountResult(accountInfo.getBankScope(), null, accountInfo.getAccountNo(), addDTO); - return result; + prepareAnalysisFields(accountInfo); + return accountInfoMapper.insert(accountInfo); } @Override @@ -110,26 +104,13 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService { CcdiAccountInfo accountInfo = new CcdiAccountInfo(); BeanUtils.copyProperties(editDTO, accountInfo); - int result = accountInfoMapper.updateById(accountInfo); - syncAccountResult(accountInfo.getBankScope(), existing, accountInfo.getAccountNo(), editDTO); - return result; + prepareAnalysisFields(accountInfo); + return accountInfoMapper.updateById(accountInfo); } @Override @Transactional public int deleteAccountInfoByIds(Long[] ids) { - List accountList = accountInfoMapper.selectBatchIds(Arrays.asList(ids)); - if (!accountList.isEmpty()) { - List accountNos = accountList.stream() - .map(CcdiAccountInfo::getAccountNo) - .filter(StringUtils::isNotEmpty) - .toList(); - if (!accountNos.isEmpty()) { - LambdaQueryWrapper resultWrapper = new LambdaQueryWrapper<>(); - resultWrapper.in(CcdiAccountResult::getAccountNo, accountNos); - accountResultMapper.delete(resultWrapper); - } - } return accountInfoMapper.deleteBatchIds(Arrays.asList(ids)); } @@ -250,51 +231,38 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService { } } - private void syncAccountResult(String newBankScope, CcdiAccountInfo existing, String accountNo, Object dto) { - String oldBankScope = existing == null ? null : existing.getBankScope(); - String oldAccountNo = existing == null ? null : existing.getAccountNo(); - - if (existing != null && "EXTERNAL".equals(oldBankScope) - && (!"EXTERNAL".equals(newBankScope) || !StringUtils.equals(oldAccountNo, accountNo))) { - LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); - deleteWrapper.eq(CcdiAccountResult::getAccountNo, oldAccountNo); - accountResultMapper.delete(deleteWrapper); - } - - if (!"EXTERNAL".equals(newBankScope)) { + private void prepareAnalysisFields(CcdiAccountInfo accountInfo) { + if (!"EXTERNAL".equals(accountInfo.getBankScope())) { + clearAnalysisFields(accountInfo); return; } + if (accountInfo.getIsActualControl() == null) { + accountInfo.setIsActualControl(1); + } + if (accountInfo.getAvgMonthTxnCount() == null) { + accountInfo.setAvgMonthTxnCount(0); + } + if (accountInfo.getAvgMonthTxnAmount() == null) { + accountInfo.setAvgMonthTxnAmount(BigDecimal.ZERO); + } + if (StringUtils.isEmpty(accountInfo.getTxnFrequencyLevel())) { + accountInfo.setTxnFrequencyLevel("MEDIUM"); + } + if (StringUtils.isEmpty(accountInfo.getTxnRiskLevel())) { + accountInfo.setTxnRiskLevel("LOW"); + } + } - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiAccountResult::getAccountNo, accountNo); - CcdiAccountResult existingResult = accountResultMapper.selectOne(wrapper); - - CcdiAccountResult accountResult = new CcdiAccountResult(); - BeanUtils.copyProperties(dto, accountResult); - accountResult.setAccountNo(accountNo); - if (accountResult.getIsActualControl() == null) { - accountResult.setIsActualControl(1); - } - if (accountResult.getAvgMonthTxnCount() == null) { - accountResult.setAvgMonthTxnCount(0); - } - if (accountResult.getAvgMonthTxnAmount() == null) { - accountResult.setAvgMonthTxnAmount(BigDecimal.ZERO); - } - if (StringUtils.isEmpty(accountResult.getTxnFrequencyLevel())) { - accountResult.setTxnFrequencyLevel("MEDIUM"); - } - if (StringUtils.isEmpty(accountResult.getTxnRiskLevel())) { - accountResult.setTxnRiskLevel("LOW"); - } - - if (existingResult == null) { - accountResultMapper.insert(accountResult); - return; - } - - accountResult.setResultId(existingResult.getResultId()); - accountResultMapper.updateById(accountResult); + private void clearAnalysisFields(CcdiAccountInfo accountInfo) { + accountInfo.setIsActualControl(null); + accountInfo.setAvgMonthTxnCount(null); + accountInfo.setAvgMonthTxnAmount(null); + accountInfo.setTxnFrequencyLevel(null); + accountInfo.setDebitSingleMaxAmount(null); + accountInfo.setCreditSingleMaxAmount(null); + accountInfo.setDebitDailyMaxAmount(null); + accountInfo.setCreditDailyMaxAmount(null); + accountInfo.setTxnRiskLevel(null); } private void validateAmount(BigDecimal amount, String fieldLabel) { diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml index 29297ae7..51d0729a 100644 --- a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml @@ -67,15 +67,15 @@ ai.status AS status, ai.effective_date AS effectiveDate, ai.invalid_date AS invalidDate, - ar.is_self_account AS isActualControl, - ar.monthly_avg_trans_count AS avgMonthTxnCount, - ar.monthly_avg_trans_amount AS avgMonthTxnAmount, - ar.trans_freq_type AS txnFrequencyLevel, - ar.dr_max_single_amount AS debitSingleMaxAmount, - ar.cr_max_single_amount AS creditSingleMaxAmount, - ar.dr_max_daily_amount AS debitDailyMaxAmount, - ar.cr_max_daily_amount AS creditDailyMaxAmount, - ar.trans_risk_level AS txnRiskLevel, + ai.is_self_account AS isActualControl, + ai.monthly_avg_trans_count AS avgMonthTxnCount, + ai.monthly_avg_trans_amount AS avgMonthTxnAmount, + ai.trans_freq_type AS txnFrequencyLevel, + ai.dr_max_single_amount AS debitSingleMaxAmount, + ai.cr_max_single_amount AS creditSingleMaxAmount, + ai.dr_max_daily_amount AS debitDailyMaxAmount, + ai.cr_max_daily_amount AS creditDailyMaxAmount, + ai.trans_risk_level AS txnRiskLevel, ai.create_by AS createBy, ai.create_time AS createTime, ai.update_by AS updateBy, @@ -107,10 +107,10 @@ AND ai.account_type = #{query.accountType} - AND ar.is_self_account = #{query.isActualControl} + AND ai.is_self_account = #{query.isActualControl} - AND ar.trans_risk_level = #{query.riskLevel} + AND ai.trans_risk_level = #{query.riskLevel} AND ai.status = #{query.status} @@ -121,7 +121,6 @@ SELECT FROM ccdi_account_info ai - LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card @@ -133,7 +132,6 @@ SELECT FROM ccdi_account_info ai - LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card @@ -145,7 +143,6 @@ SELECT FROM ccdi_account_info ai - LEFT JOIN ccdi_account_result ar ON ai.account_no = ar.account_no LEFT JOIN ccdi_base_staff bs ON ai.owner_type = 'EMPLOYEE' AND ai.owner_id = bs.id_card LEFT JOIN ccdi_staff_fmy_relation fr ON ai.owner_type = 'RELATION' AND ai.owner_id = fr.relation_cert_no LEFT JOIN ccdi_base_staff bsRel ON fr.person_id = bsRel.id_card diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapperTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapperTest.java new file mode 100644 index 00000000..3863c4b5 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiAccountInfoMapperTest.java @@ -0,0 +1,109 @@ +package com.ruoyi.info.collection.mapper; + +import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.apache.ibatis.type.TypeAliasRegistry; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiAccountInfoMapperTest { + + private static final String RESOURCE = "mapper/info/collection/CcdiAccountInfoMapper.xml"; + + @Test + void selectAccountInfoPage_shouldReadAnalysisColumnsFromAccountInfoTableOnly() throws Exception { + MappedStatement mappedStatement = loadMappedStatement( + "com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper.selectAccountInfoPage"); + + String sql = renderSql(mappedStatement, Map.of("query", new CcdiAccountInfoQueryDTO())).toLowerCase(); + + assertTrue(sql.contains("from ccdi_account_info ai"), sql); + assertFalse(sql.contains("ccdi_account_result"), sql); + assertTrue(sql.contains("ai.is_self_account as isactualcontrol"), sql); + assertTrue(sql.contains("ai.monthly_avg_trans_count as avgmonthtxncount"), sql); + assertTrue(sql.contains("ai.trans_risk_level as txnrisklevel"), sql); + } + + private MappedStatement loadMappedStatement(String statementId) throws Exception { + Configuration configuration = new Configuration(); + configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource())); + registerTypeAliases(configuration.getTypeAliasRegistry()); + configuration.getLanguageRegistry().register(XMLLanguageDriver.class); + configuration.addMapper(CcdiAccountInfoMapper.class); + + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { + XMLMapperBuilder xmlMapperBuilder = + new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments()); + xmlMapperBuilder.parse(); + } + return configuration.getMappedStatement(statementId); + } + + private String renderSql(MappedStatement mappedStatement, Map params) { + BoundSql boundSql = mappedStatement.getBoundSql(new HashMap<>(params)); + return boundSql.getSql().replaceAll("\\s+", " ").trim(); + } + + private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) { + typeAliasRegistry.registerAlias("map", Map.class); + } + + private static class NoOpDataSource implements DataSource { + + @Override + public java.sql.Connection getConnection() { + throw new UnsupportedOperationException("Not required for SQL rendering tests"); + } + + @Override + public java.sql.Connection getConnection(String username, String password) { + throw new UnsupportedOperationException("Not required for SQL rendering tests"); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter out) { + } + + @Override + public void setLoginTimeout(int seconds) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return java.util.logging.Logger.getGlobal(); + } + + @Override + public T unwrap(Class iface) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAccountInfoServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAccountInfoServiceImplTest.java new file mode 100644 index 00000000..965f40a0 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAccountInfoServiceImplTest.java @@ -0,0 +1,124 @@ +package com.ruoyi.info.collection.service; + +import com.ruoyi.info.collection.domain.CcdiAccountInfo; +import com.ruoyi.info.collection.domain.CcdiBaseStaff; +import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO; +import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper; +import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper; +import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper; +import com.ruoyi.info.collection.service.impl.CcdiAccountInfoServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.BeanWrapperImpl; + +import java.math.BigDecimal; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiAccountInfoServiceImplTest { + + @InjectMocks + private CcdiAccountInfoServiceImpl service; + + @Mock + private CcdiAccountInfoMapper accountInfoMapper; + + @Mock + private CcdiBaseStaffMapper baseStaffMapper; + + @Mock + private CcdiStaffFmyRelationMapper staffFmyRelationMapper; + + @Test + void insertExternalAccount_shouldPersistAnalysisFieldsOnAccountInfo() { + CcdiAccountInfoAddDTO dto = buildBaseAddDto(); + dto.setOwnerType("EXTERNAL"); + dto.setOwnerId("330101199001010011"); + dto.setBankScope("EXTERNAL"); + dto.setIsActualControl(0); + dto.setAvgMonthTxnCount(6); + dto.setAvgMonthTxnAmount(new BigDecimal("1234.56")); + dto.setTxnFrequencyLevel("HIGH"); + dto.setDebitSingleMaxAmount(new BigDecimal("100.00")); + dto.setCreditSingleMaxAmount(new BigDecimal("200.00")); + dto.setDebitDailyMaxAmount(new BigDecimal("300.00")); + dto.setCreditDailyMaxAmount(new BigDecimal("400.00")); + dto.setTxnRiskLevel("MEDIUM"); + + when(accountInfoMapper.selectCount(any())).thenReturn(0L); + when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1); + + service.insertAccountInfo(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CcdiAccountInfo.class); + verify(accountInfoMapper).insert(captor.capture()); + BeanWrapperImpl wrapper = new BeanWrapperImpl(captor.getValue()); + assertEquals(0, wrapper.getPropertyValue("isActualControl")); + assertEquals(6, wrapper.getPropertyValue("avgMonthTxnCount")); + assertEquals(new BigDecimal("1234.56"), wrapper.getPropertyValue("avgMonthTxnAmount")); + assertEquals("HIGH", wrapper.getPropertyValue("txnFrequencyLevel")); + assertEquals("MEDIUM", wrapper.getPropertyValue("txnRiskLevel")); + } + + @Test + void insertInternalAccount_shouldClearAnalysisFieldsOnAccountInfo() { + CcdiAccountInfoAddDTO dto = buildBaseAddDto(); + dto.setOwnerType("EMPLOYEE"); + dto.setOwnerId("330101199001010022"); + dto.setBankScope("INTERNAL"); + dto.setIsActualControl(1); + dto.setAvgMonthTxnCount(8); + dto.setAvgMonthTxnAmount(new BigDecimal("9988.66")); + dto.setTxnFrequencyLevel("HIGH"); + dto.setDebitSingleMaxAmount(new BigDecimal("111.11")); + dto.setCreditSingleMaxAmount(new BigDecimal("222.22")); + dto.setDebitDailyMaxAmount(new BigDecimal("333.33")); + dto.setCreditDailyMaxAmount(new BigDecimal("444.44")); + dto.setTxnRiskLevel("HIGH"); + + CcdiBaseStaff staff = new CcdiBaseStaff(); + staff.setIdCard(dto.getOwnerId()); + + when(baseStaffMapper.selectOne(any())).thenReturn(staff); + when(accountInfoMapper.selectCount(any())).thenReturn(0L); + when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1); + + service.insertAccountInfo(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CcdiAccountInfo.class); + verify(accountInfoMapper).insert(captor.capture()); + BeanWrapperImpl wrapper = new BeanWrapperImpl(captor.getValue()); + assertNull(wrapper.getPropertyValue("isActualControl")); + assertNull(wrapper.getPropertyValue("avgMonthTxnCount")); + assertNull(wrapper.getPropertyValue("avgMonthTxnAmount")); + assertNull(wrapper.getPropertyValue("txnFrequencyLevel")); + assertNull(wrapper.getPropertyValue("debitSingleMaxAmount")); + assertNull(wrapper.getPropertyValue("creditSingleMaxAmount")); + assertNull(wrapper.getPropertyValue("debitDailyMaxAmount")); + assertNull(wrapper.getPropertyValue("creditDailyMaxAmount")); + assertNull(wrapper.getPropertyValue("txnRiskLevel")); + } + + private CcdiAccountInfoAddDTO buildBaseAddDto() { + CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO(); + dto.setAccountNo("6222024000000001"); + dto.setAccountType("BANK"); + dto.setAccountName("测试账户"); + dto.setOpenBank("中国银行"); + dto.setBankCode("BOC"); + dto.setCurrency("CNY"); + dto.setStatus(1); + dto.setEffectiveDate(new Date()); + return dto; + } +} diff --git a/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java b/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java deleted file mode 100644 index 99da5353..00000000 --- a/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.ruoyi.lsfx.client; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.ruoyi.lsfx.domain.response.CreditParseResponse; -import com.ruoyi.lsfx.exception.LsfxApiException; -import com.ruoyi.lsfx.util.HttpUtil; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.argThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CreditParseClientTest { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Mock - private HttpUtil httpUtil; - - @InjectMocks - private CreditParseClient client; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(client, "creditParseUrl", "http://credit-host/xfeature-mngs/conversation/htmlEval"); - } - - @Test - void shouldDeserializeCreditParseResponse() throws Exception { - String json = """ - { - "message": "成功", - "status_code": "0", - "payload": { - "lx_header": {"query_cert_no": "3301"}, - "lx_debt": {"uncle_bank_house_bal": "12.00"}, - "lx_publictype": {"civil_cnt": 1} - } - } - """; - - CreditParseResponse response = objectMapper.readValue(json, CreditParseResponse.class); - - assertEquals("0", response.getStatusCode()); - assertEquals("3301", response.getPayload().getLxHeader().get("query_cert_no")); - } - - @Test - void shouldCallConfiguredUrlWithMultipartParams() { - File file = new File("sample.html"); - CreditParseResponse response = new CreditParseResponse(); - response.setStatusCode("0"); - - when(httpUtil.uploadFile(eq("http://credit-host/xfeature-mngs/conversation/htmlEval"), anyMap(), isNull(), eq(CreditParseResponse.class))) - .thenReturn(response); - - CreditParseResponse actual = client.parse("LXCUSTALL", "PERSON", file); - - assertEquals("0", actual.getStatusCode()); - verify(httpUtil).uploadFile(eq("http://credit-host/xfeature-mngs/conversation/htmlEval"), argThat(params -> - "LXCUSTALL".equals(params.get("model")) - && "PERSON".equals(params.get("hType")) - && file.equals(params.get("file")) - ), isNull(), eq(CreditParseResponse.class)); - } - - @Test - void shouldWrapHttpErrorsAsLsfxApiException() { - when(httpUtil.uploadFile(anyString(), anyMap(), isNull(), eq(CreditParseResponse.class))) - .thenThrow(new LsfxApiException("网络失败")); - - assertThrows(LsfxApiException.class, - () -> client.parse("LXCUSTALL", "PERSON", new File("sample.html"))); - } -} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiAccountInfoMergeSqlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiAccountInfoMergeSqlTest.java new file mode 100644 index 00000000..d0059199 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiAccountInfoMergeSqlTest.java @@ -0,0 +1,33 @@ +package com.ruoyi.ccdi.project.sql; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiAccountInfoMergeSqlTest { + + @Test + void accountInfoMergeSql_shouldAddColumnsMigrateDataAndDropLegacyTable() throws IOException { + Path path = Path.of("..", "sql", "migration", + "2026-04-16-merge-ccdi-account-result-into-info.sql"); + + assertTrue(Files.exists(path), "账户库合表迁移脚本应存在"); + + String sql = Files.readString(path, StandardCharsets.UTF_8).toLowerCase(); + assertAll( + () -> assertTrue(sql.contains("bin/mysql_utf8_exec.sh")), + () -> assertTrue(sql.contains("ccdi_account_info")), + () -> assertTrue(sql.contains("add column `is_self_account`")), + () -> assertTrue(sql.contains("monthly_avg_trans_count")), + () -> assertTrue(sql.contains("update `ccdi_account_info` ai")), + () -> assertTrue(sql.contains("join `ccdi_account_result` ar")), + () -> assertTrue(sql.contains("drop table `ccdi_account_result`")) + ); + } +} diff --git a/deploy/deploy-to-nas-tongweb.sh b/deploy/deploy-to-nas-tongweb.sh new file mode 100755 index 00000000..6f70371d --- /dev/null +++ b/deploy/deploy-to-nas-tongweb.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +SERVER_HOST="116.62.17.81" +SERVER_PORT="9444" +SERVER_USERNAME="wkc" +SERVER_PASSWORD="wkc@0825" +REMOTE_ROOT="/volume1/webapp/ccdi" +TONGWEB_HOME="${TONGWEB_HOME:-/opt/TongWeb}" +APP_NAME="${APP_NAME:-ruoyi-admin}" +DRY_RUN="false" + +ensure_command() { + local command_name="$1" + if ! command -v "${command_name}" >/dev/null 2>&1; then + echo "缺少命令: ${command_name}" >&2 + exit 1 + fi +} + +ensure_paramiko() { + if python3 - <<'PY' +import importlib.util +import sys + +sys.exit(0 if importlib.util.find_spec("paramiko") else 1) +PY + then + return + fi + + python3 -m pip install --user paramiko +} + +POSITION=0 +for arg in "$@"; do + if [[ "${arg}" == "--dry-run" ]]; then + DRY_RUN="true" + continue + fi + + POSITION=$((POSITION + 1)) + case "${POSITION}" in + 1) SERVER_HOST="${arg}" ;; + 2) SERVER_PORT="${arg}" ;; + 3) SERVER_USERNAME="${arg}" ;; + 4) SERVER_PASSWORD="${arg}" ;; + 5) REMOTE_ROOT="${arg}" ;; + 6) TONGWEB_HOME="${arg}" ;; + 7) APP_NAME="${arg}" ;; + *) + echo "仅支持 [host] [port] [username] [password] [remoteRoot] [tongwebHome] [appName] [--dry-run]" >&2 + exit 1 + ;; + esac +done + +if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DryRun] TongWeb NAS 部署参数预览" + echo "Host: ${SERVER_HOST}" + echo "Port: ${SERVER_PORT}" + echo "Username: ${SERVER_USERNAME}" + echo "RemoteRoot: ${REMOTE_ROOT}" + echo "TongWebHome: ${TONGWEB_HOME}" + echo "AppName: ${APP_NAME}" + exit 0 +fi + +echo "[1/4] 检查本地环境" +ensure_command "mvn" +ensure_command "python3" + +echo "[2/4] 打包后端 war" +( + cd "${REPO_ROOT}" + mvn -pl ruoyi-admin -am package -DskipTests +) + +WAR_PATH="${REPO_ROOT}/ruoyi-admin/target/ruoyi-admin.war" +if [[ ! -f "${WAR_PATH}" ]]; then + echo "未找到后端 war 包: ${WAR_PATH}" >&2 + exit 1 +fi + +echo "[3/4] 检查远端执行依赖" +ensure_paramiko + +echo "[4/4] 上传 war 并重启 TongWeb" +python3 "${SCRIPT_DIR}/remote-deploy-tongweb.py" \ + --host "${SERVER_HOST}" \ + --port "${SERVER_PORT}" \ + --username "${SERVER_USERNAME}" \ + --password "${SERVER_PASSWORD}" \ + --local-war "${WAR_PATH}" \ + --remote-root "${REMOTE_ROOT}" \ + --tongweb-home "${TONGWEB_HOME}" \ + --app-name "${APP_NAME}" diff --git a/deploy/remote-deploy-tongweb.py b/deploy/remote-deploy-tongweb.py new file mode 100644 index 00000000..5c467976 --- /dev/null +++ b/deploy/remote-deploy-tongweb.py @@ -0,0 +1,136 @@ +import argparse +import posixpath +import shlex +import sys +from pathlib import Path + +import paramiko + + +def parse_args(): + parser = argparse.ArgumentParser(description="Upload backend war to NAS and restart TongWeb.") + parser.add_argument("--host", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--username", required=True) + parser.add_argument("--password", required=True) + parser.add_argument("--local-war", required=True) + parser.add_argument("--remote-root", required=True) + parser.add_argument("--tongweb-home", required=True) + parser.add_argument("--app-name", required=True) + return parser.parse_args() + + +def run_command(ssh, command): + stdin, stdout, stderr = ssh.exec_command(command) + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode("utf-8", errors="ignore") + error = stderr.read().decode("utf-8", errors="ignore") + return exit_code, output, error + + +def sudo_prefix(password): + return f"printf '%s\\n' {shlex.quote(password)} | sudo -S -p '' " + + +def detect_command_prefix(ssh, password, command): + plain_exit_code, _, _ = run_command(ssh, f"{command} >/dev/null 2>&1") + if plain_exit_code == 0: + return "" + + sudo_probe = f"{sudo_prefix(password)}{command} >/dev/null 2>&1" + sudo_exit_code, _, _ = run_command(ssh, sudo_probe) + if sudo_exit_code == 0: + return sudo_prefix(password) + + raise RuntimeError(f"Remote command is not accessible: {command}") + + +def ensure_remote_path(ssh, prefix, remote_path): + command = f"{prefix}mkdir -p {shlex.quote(remote_path)}" + exit_code, output, error = run_command(ssh, command) + if exit_code != 0: + raise RuntimeError(f"Failed to create remote directory {remote_path}:\n{output}\n{error}") + + +def upload_file(sftp, local_file, remote_file): + parent_dir = posixpath.dirname(remote_file) + try: + sftp.listdir(parent_dir) + except OSError: + raise RuntimeError(f"SFTP remote directory not found: {parent_dir}") + sftp.put(str(local_file), remote_file) + + +def build_deploy_command(args, prefix): + app_war_name = f"{args.app_name}.war" + remote_war_path = posixpath.join(args.remote_root.rstrip("/"), "backend", app_war_name) + autodeploy_dir = posixpath.join(args.tongweb_home.rstrip("/"), "autodeploy") + deployed_war_path = posixpath.join(autodeploy_dir, app_war_name) + deployed_dir_path = posixpath.join(autodeploy_dir, args.app_name) + stop_script = posixpath.join(args.tongweb_home.rstrip("/"), "bin", "stopserver.sh") + start_script = posixpath.join(args.tongweb_home.rstrip("/"), "bin", "startservernohup.sh") + + return ( + "set -e;" + f"test -d {shlex.quote(args.tongweb_home)};" + f"test -x {shlex.quote(stop_script)};" + f"test -x {shlex.quote(start_script)};" + f"{prefix}mkdir -p {shlex.quote(autodeploy_dir)};" + f"{prefix}sh {shlex.quote(stop_script)} >/dev/null 2>&1 || true;" + f"{prefix}rm -rf {shlex.quote(deployed_dir_path)};" + f"{prefix}rm -f {shlex.quote(deployed_war_path)};" + f"{prefix}cp {shlex.quote(remote_war_path)} {shlex.quote(deployed_war_path)};" + f"{prefix}sh {shlex.quote(start_script)};" + "sleep 5;" + f"ls -l {shlex.quote(autodeploy_dir)};" + ) + + +def main(): + args = parse_args() + local_war = Path(args.local_war).resolve() + if not local_war.exists(): + raise FileNotFoundError(f"Local war does not exist: {local_war}") + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect( + hostname=args.host, + port=args.port, + username=args.username, + password=args.password, + timeout=20, + ) + + sftp = ssh.open_sftp() + try: + remote_root = args.remote_root.rstrip("/") + remote_backend_dir = posixpath.join(remote_root, "backend") + remote_war_path = posixpath.join(remote_backend_dir, f"{args.app_name}.war") + + ensure_remote_path(ssh, "", remote_root) + ensure_remote_path(ssh, "", remote_backend_dir) + upload_file(sftp, local_war, remote_war_path) + + command_prefix = detect_command_prefix(ssh, args.password, f"test -d {shlex.quote(args.tongweb_home)}") + deploy_command = build_deploy_command(args, command_prefix) + exit_code, output, error = run_command(ssh, deploy_command) + if exit_code != 0: + raise RuntimeError(f"Remote TongWeb deploy failed:\n{output}\n{error}") + + print("=== DEPLOY OUTPUT ===") + print(output.strip()) + if error.strip(): + print("=== DEPLOY STDERR ===") + print(error.strip()) + finally: + sftp.close() + ssh.close() + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/docs/plans/backend/2026-04-14-backend-run-package-conventions-implementation.md b/docs/plans/backend/2026-04-14-backend-run-package-conventions-implementation.md new file mode 100644 index 00000000..0114e704 --- /dev/null +++ b/docs/plans/backend/2026-04-14-backend-run-package-conventions-implementation.md @@ -0,0 +1,94 @@ +# 2026-04-14 后端运行与打包约定实施记录 + +## 1. 改动目标 + +- 固化本地后端继续走 `ruoyi-admin.jar + 内嵌 Tomcat` 启动链路 +- 固化 `mvn -pl ruoyi-admin -am package -DskipTests` 同时产出 `jar` 与 `war` +- 固化部署脚本统一消费 `ruoyi-admin.war` +- 固化 `bin/restart_java_backend.sh` 默认跟随后端日志,并支持 `FOLLOW_LOGS=false` + +## 2. 实施内容 + +### 2.1 Maven 打包链路 + +涉及文件: + +- `ruoyi-admin/pom.xml` + +实施内容: + +- 保持 `ruoyi-admin` 的 `jar` 不变,确保本地运行仍使用可执行 `jar` +- 为 `maven-war-plugin` 增加 `package` 阶段显式执行 `war` 目标,确保执行 `mvn -pl ruoyi-admin -am package -DskipTests` 时额外生成 `ruoyi-admin.war` +- 保留 `spring-boot-maven-plugin repackage`,继续生成可执行 `ruoyi-admin.jar` + +### 2.2 本地后端重启脚本 + +涉及文件: + +- `bin/restart_java_backend.sh` + +实施内容: + +- 新增 `FOLLOW_LOGS="${FOLLOW_LOGS:-true}"` 默认开关 +- `start`、`restart` 成功后默认执行 `tail -F` 持续输出后端日志 +- 当外部传入 `FOLLOW_LOGS=false` 时,仅启动后端,不进入日志跟随 + +### 2.3 部署产物切换 + +涉及文件: + +- `deploy/deploy-to-nas.sh` +- `deploy/deploy.ps1` +- `docker/backend/Dockerfile` + +实施内容: + +- 部署目录组装时由复制 `ruoyi-admin.jar` 改为复制 `ruoyi-admin.war` +- Docker 后端镜像改为消费 `ruoyi-admin.war` +- 保证部署脚本不再把 `ruoyi-admin.jar` 当作生产部署产物 + +### 2.4 项目约定同步 + +涉及文件: + +- `AGENTS.md` + +实施内容: + +- 补充本地运行、双产物打包、部署使用 `war`、`FOLLOW_LOGS` 开关等仓库级约定 +- 在 Build / Run / Test Commands 中补充主应用定向打包命令 + +## 3. 验证记录 + +### 3.1 脚本检查 + +执行: + +```bash +sh docs/tests/scripts/test-restart-java-backend.sh +sh docs/tests/scripts/test-backend-package-and-deploy-conventions.sh +``` + +结果: + +- 两个脚本均通过 + +### 3.2 Maven 双产物验证 + +执行: + +```bash +mvn -pl ruoyi-admin -am package -DskipTests +``` + +结果: + +- 构建成功 +- 生成 `ruoyi-admin/target/ruoyi-admin.jar` +- 生成 `ruoyi-admin/target/ruoyi-admin.war` + +## 4. 结论 + +- 本地开发链路继续保持 `jar + 内嵌 Tomcat` +- 部署链路统一切换为 `war` +- 后端重启脚本默认跟日志,且支持显式关闭 diff --git a/docs/plans/backend/2026-04-14-nas-tongweb-deploy-script-implementation.md b/docs/plans/backend/2026-04-14-nas-tongweb-deploy-script-implementation.md new file mode 100644 index 00000000..1f645b5f --- /dev/null +++ b/docs/plans/backend/2026-04-14-nas-tongweb-deploy-script-implementation.md @@ -0,0 +1,62 @@ +# 2026-04-14 NAS TongWeb 部署脚本实施记录 + +## 1. 目标 + +- 新增一套独立于 Docker 的 NAS 部署脚本 +- 部署链路固定使用 `ruoyi-admin.war` +- 远端通过 `TongWeb` 自动部署目录发布应用,并使用 `stopserver.sh` / `startservernohup.sh` 重启服务 + +## 2. 实施内容 + +### 2.1 新增 TongWeb NAS 部署入口 + +涉及文件: + +- `deploy/deploy-to-nas-tongweb.sh` + +实施内容: + +- 提供与现有 NAS 脚本一致的 SSH 连接参数风格 +- 默认执行 `mvn -pl ruoyi-admin -am package -DskipTests` +- 本地仅校验并上传 `ruoyi-admin/target/ruoyi-admin.war` +- 支持 `--dry-run` 预览参数 + +### 2.2 新增 TongWeb 远端执行器 + +涉及文件: + +- `deploy/remote-deploy-tongweb.py` + +实施内容: + +- 通过 SFTP 将 `war` 上传到 NAS 临时目录 `${remoteRoot}/backend/` +- 远端复制 `war` 到 `${TONGWEB_HOME}/autodeploy/${appName}.war` +- 清理 `${TONGWEB_HOME}/autodeploy/${appName}` 旧解压目录 +- 依次执行 `stopserver.sh`、`startservernohup.sh` + +### 2.3 新增脚本回归测试 + +涉及文件: + +- `tests/deploy/test_deploy_to_nas_tongweb.py` + +实施内容: + +- 覆盖默认参数 `dry-run` +- 覆盖自定义参数 `dry-run` +- 校验部署入口已调用 `remote-deploy-tongweb.py` +- 校验远端执行器包含 `autodeploy`、`stopserver.sh`、`startservernohup.sh` + +## 3. 验证命令 + +```bash +python3 -m pytest tests/deploy/test_deploy_to_nas_tongweb.py -q +bash -n deploy/deploy-to-nas-tongweb.sh +bash deploy/deploy-to-nas-tongweb.sh --dry-run +``` + +## 4. 说明 + +- 默认 `TongWebHome` 取 `/opt/TongWeb`,可通过第 6 个位置参数或环境变量 `TONGWEB_HOME` 覆盖 +- 默认应用名为 `ruoyi-admin`,可通过第 7 个位置参数或环境变量 `APP_NAME` 覆盖 +- 本次只新增 `TongWeb` 后端部署链路,不改动现有 Docker NAS 部署脚本 diff --git a/docs/plans/backend/2026-04-15-lsfx-mock-server-ccdi-account-info-abnormal-account-columns-migration-implementation.md b/docs/plans/backend/2026-04-15-lsfx-mock-server-ccdi-account-info-abnormal-account-columns-migration-implementation.md new file mode 100644 index 00000000..f40b58ca --- /dev/null +++ b/docs/plans/backend/2026-04-15-lsfx-mock-server-ccdi-account-info-abnormal-account-columns-migration-implementation.md @@ -0,0 +1,26 @@ +# LSFX Mock Server `ccdi_account_info` 异常账户字段补迁移后端实施文档 + +## 背景 + +- `lsfx-mock-server` 上传接口 `/watson/api/project/remoteUploadSplitFile` 在写入 `ccdi_account_info` 时使用了 `is_self_account`、`trans_risk_level` 字段。 +- 当前开发库中的 `ccdi_account_info` 为历史表结构,不包含这两列,导致 `AbnormalAccountBaselineService.apply(...)` 执行 upsert 时依次抛出 `Unknown column 'is_self_account' in 'field list'`、`Unknown column 'trans_risk_level' in 'field list'`,上传接口直接返回 500。 + +## 本次修改 + +- 新增增量脚本 `sql/migration/2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql`。 +- 脚本以最短路径为已有 `ccdi_account_info` 表补齐异常账户同步当前必需的字段,并保持可重复执行: + - 使用 `information_schema.columns` 判断字段是否已存在 + - 通过 `PREPARE / EXECUTE` 仅在缺列时执行 `ALTER TABLE` + - 补齐 `is_self_account` 与 `trans_risk_level` + - 列位置与当前写库 SQL 保持一致 +- 新增回归测试 `lsfx-mock-server/tests/test_schema_migration_scripts.py`,锁定该增量脚本必须存在且包含两条补列语句。 + +## 验证 + +- `python3 -m pytest /Users/wkc/Desktop/ccdi/ccdi/lsfx-mock-server/tests/test_schema_migration_scripts.py -q` +- 使用 `bin/mysql_utf8_exec.sh` 执行增量脚本后,复查 `SHOW COLUMNS FROM ccdi_account_info`,确认存在 `is_self_account`、`trans_risk_level` 字段。 + +## 影响范围 + +- 仅影响 `lsfx-mock-server` 依赖的 `ccdi_account_info` 历史表结构补齐。 +- 不修改接口协议,不改动前端。 diff --git a/docs/plans/backend/2026-04-16-account-library-single-table-implementation.md b/docs/plans/backend/2026-04-16-account-library-single-table-implementation.md new file mode 100644 index 00000000..df71bbbb --- /dev/null +++ b/docs/plans/backend/2026-04-16-account-library-single-table-implementation.md @@ -0,0 +1,53 @@ +# 账户库双表合单表后端实施计划 + +## 1. 目标 + +将账户库由 `ccdi_account_info` + `ccdi_account_result` 双表结构收敛为单表 `ccdi_account_info`,迁移完成后删除旧表,同时保持现有账户库接口、字段名和前端交互不变。 + +## 2. 实施范围 + +- 数据库增量迁移脚本 +- 账户库后端实体、Mapper XML、服务层 +- 外部场景种子脚本 +- 账户库相关回归测试 + +本次不调整前端页面、接口路径和接口字段名。 + +## 3. 实施步骤 + +### 3.1 数据库迁移 + +1. 新增 `sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql` +2. 在脚本中先校验 `ccdi_account_info.account_no` 无重复 +3. 为 `ccdi_account_info` 补齐分析字段 +4. 按 `account_no` 从 `ccdi_account_result` 回填数据 +5. 回填完成后删除 `ccdi_account_result` + +### 3.2 后端代码调整 + +1. `CcdiAccountInfo` 实体吸收分析字段映射 +2. 删除 `CcdiAccountResult` 实体与 `CcdiAccountResultMapper` +3. `CcdiAccountInfoMapper.xml` 去掉对 `ccdi_account_result` 的联表 +4. `CcdiAccountInfoServiceImpl` 去掉结果表双写逻辑 +5. 保持原有业务语义: + - `bankScope = EXTERNAL` 时补齐默认分析字段 + - `bankScope != EXTERNAL` 时清空分析字段,避免误写 + +### 3.3 配套脚本与测试 + +1. 将 `2026-04-13` 外部账户场景种子脚本改为单表写入 +2. 新增 SQL 脚本文本断言测试 +3. 新增账户库服务层与 Mapper SQL 结构测试 + +## 4. 验证要点 + +- 迁移脚本包含“补字段、回填、删旧表”三步 +- 账户库列表/详情/导出查询均只读 `ccdi_account_info` +- 行外账户保存分析字段 +- 行内账户清空分析字段 +- 外部场景种子脚本不再写入 `ccdi_account_result` + +## 5. 风险说明 + +- 仓库当前 `ccdi-info-collection` 模块存在既有依赖缺失问题,可能影响常规 Maven 全量编译与测试执行 +- 本次需要将“账户库改动验证结果”和“仓库原有构建阻塞”分开记录 diff --git a/docs/reports/implementation/2026-04-16-account-library-single-table-implementation.md b/docs/reports/implementation/2026-04-16-account-library-single-table-implementation.md new file mode 100644 index 00000000..415f5268 --- /dev/null +++ b/docs/reports/implementation/2026-04-16-account-library-single-table-implementation.md @@ -0,0 +1,62 @@ +# 账户库双表合单表实施记录 + +## 1. 本次实施内容 + +### 1.1 单表模型收敛 + +- 在 `CcdiAccountInfo` 中补齐以下分析字段映射: + - `is_self_account` + - `monthly_avg_trans_count` + - `monthly_avg_trans_amount` + - `trans_freq_type` + - `dr_max_single_amount` + - `cr_max_single_amount` + - `dr_max_daily_amount` + - `cr_max_daily_amount` + - `trans_risk_level` +- 删除 `CcdiAccountResult` 实体与 `CcdiAccountResultMapper` + +### 1.2 查询与写入逻辑调整 + +- `CcdiAccountInfoMapper.xml` 已移除 `ccdi_account_result` 联表 +- 账户库列表、详情、导出统一从 `ccdi_account_info` 读取分析字段 +- `CcdiAccountInfoServiceImpl` 已移除结果表双写逻辑 +- 新增单表分析字段处理规则: + - 行外账户默认补齐分析字段缺省值 + - 行内账户统一清空分析字段 + +### 1.3 数据迁移与种子脚本 + +- 新增增量脚本: + - `sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql` +- 更新外部场景种子脚本: + - `sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql` +- 种子脚本已改为直接写入 `ccdi_account_info`,不再依赖旧表 + +### 1.4 测试补充 + +- 新增 `CcdiAccountInfoServiceImplTest` +- 新增 `CcdiAccountInfoMapperTest` +- 新增 `CcdiAccountInfoMergeSqlTest` + +## 2. 验证记录 + +### 2.1 已完成验证 + +- `ccdi-project` 模块执行 `mvn -pl ccdi-project -DskipTests compile` 成功 +- 文件级检查确认: + - 账户库主链路代码已无 `CcdiAccountResult` / `accountResultMapper` 引用 + - `CcdiAccountInfoMapper.xml` 已无 `ccdi_account_result` 联表 + - 新增迁移脚本包含补字段、按 `account_no` 回填、删除旧表逻辑 + +### 2.2 现存仓库阻塞 + +- `ccdi-info-collection` 模块常规编译失败,失败原因为仓库已有依赖/类缺失,与本次账户库改动不直接相关 +- 典型阻塞包括: + - `com.ruoyi.common.annotation` 下若干注解类缺失 + - 多个服务类依赖 `org.springframework.data.redis.core`,当前模块未解析 + - 既有测试代码与当前依赖版本存在不一致 + +## 3. 结论 + +本次账户库已按方案完成“双表合单表”代码与 SQL 收敛,后续若要做完整 Maven 回归,需要先处理仓库当前已有的模块依赖与测试编译问题。 diff --git a/docs/tests/scripts/test-backend-package-and-deploy-conventions.sh b/docs/tests/scripts/test-backend-package-and-deploy-conventions.sh new file mode 100644 index 00000000..b842da37 --- /dev/null +++ b/docs/tests/scripts/test-backend-package-and-deploy-conventions.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd) +POM_FILE="$ROOT_DIR/ruoyi-admin/pom.xml" +DEPLOY_SH="$ROOT_DIR/deploy/deploy-to-nas.sh" +DEPLOY_PS1="$ROOT_DIR/deploy/deploy.ps1" +DOCKERFILE="$ROOT_DIR/docker/backend/Dockerfile" +TARGET_DIR="$ROOT_DIR/ruoyi-admin/target" + +echo "[检查] 后端打包必须同时产出 jar 与 war,部署脚本只能使用 war" + +if ! grep -Fq 'jar' "$POM_FILE"; then + echo "失败: ruoyi-admin 仍需保持 jar 打包类型以支持本地内嵌 Tomcat 运行" + exit 1 +fi + +if ! grep -Fq 'war' "$POM_FILE"; then + echo "失败: 未显式执行 war 打包目标" + exit 1 +fi + +if ! grep -Fq 'ruoyi-admin.war' "$DEPLOY_SH"; then + echo "失败: deploy-to-nas.sh 未改为使用 ruoyi-admin.war" + exit 1 +fi + +if grep -Fq 'ruoyi-admin.jar' "$DEPLOY_SH"; then + echo "失败: deploy-to-nas.sh 仍引用 ruoyi-admin.jar" + exit 1 +fi + +if ! grep -Fq 'ruoyi-admin.war' "$DEPLOY_PS1"; then + echo "失败: deploy.ps1 未改为使用 ruoyi-admin.war" + exit 1 +fi + +if grep -Fq 'ruoyi-admin.jar' "$DEPLOY_PS1"; then + echo "失败: deploy.ps1 仍引用 ruoyi-admin.jar" + exit 1 +fi + +if ! grep -Fq 'COPY backend/ruoyi-admin.war /app/ruoyi-admin.war' "$DOCKERFILE"; then + echo "失败: Dockerfile 未改为复制 ruoyi-admin.war" + exit 1 +fi + +if grep -Fq 'ruoyi-admin.jar' "$DOCKERFILE"; then + echo "失败: Dockerfile 仍引用 ruoyi-admin.jar" + exit 1 +fi + +echo "[检查] 执行 Maven 打包产物校验" +( + cd "$ROOT_DIR" + mvn -pl ruoyi-admin -am package -DskipTests +) + +if [ ! -f "$TARGET_DIR/ruoyi-admin.jar" ]; then + echo "失败: 未生成 $TARGET_DIR/ruoyi-admin.jar" + exit 1 +fi + +if [ ! -f "$TARGET_DIR/ruoyi-admin.war" ]; then + echo "失败: 未生成 $TARGET_DIR/ruoyi-admin.war" + exit 1 +fi + +echo "通过" diff --git a/lsfx-mock-server/tests/test_schema_migration_scripts.py b/lsfx-mock-server/tests/test_schema_migration_scripts.py new file mode 100644 index 00000000..374cccda --- /dev/null +++ b/lsfx-mock-server/tests/test_schema_migration_scripts.py @@ -0,0 +1,19 @@ +from pathlib import Path + + +def test_ccdi_account_info_should_have_incremental_migration_for_abnormal_account_columns(): + project_root = Path(__file__).resolve().parents[2] + migration_path = ( + project_root + / "sql" + / "migration" + / "2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql" + ) + + assert migration_path.exists(), "缺少 ccdi_account_info 异常账户字段补迁移脚本" + + sql = migration_path.read_text(encoding="utf-8") + + assert "information_schema.columns" in sql + assert "ALTER TABLE `ccdi_account_info` ADD COLUMN `is_self_account`" in sql + assert "ALTER TABLE `ccdi_account_info` ADD COLUMN `trans_risk_level`" in sql diff --git a/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql b/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql index 5d68f6e0..7ac46022 100644 --- a/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql +++ b/sql/migration/2026-04-13-seed-ccdi-account-info-external-scenarios.sql @@ -11,6 +11,15 @@ INSERT INTO ccdi_account_info ( bank, bank_code, currency, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, status, effective_date, invalid_date, @@ -28,32 +37,6 @@ SELECT 'HZLH001', 'CNY', 1, - '2026-04-13', - NULL, - 'system', - 'system' -FROM dual -WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_info WHERE account_no = '622202440000010001' -); - -INSERT INTO ccdi_account_result ( - account_no, - is_self_account, - monthly_avg_trans_count, - monthly_avg_trans_amount, - trans_freq_type, - dr_max_single_amount, - cr_max_single_amount, - dr_max_daily_amount, - cr_max_daily_amount, - trans_risk_level, - create_by, - update_by -) -SELECT - '622202440000010001', - 1, 12, 28600.00, 'MEDIUM', @@ -62,11 +45,14 @@ SELECT 16000.00, 22000.00, 'MEDIUM', + 1, + '2026-04-13', + NULL, 'system', 'system' FROM dual WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_result WHERE account_no = '622202440000010001' + SELECT 1 FROM ccdi_account_info WHERE account_no = '622202440000010001' ); INSERT INTO ccdi_account_info ( @@ -79,6 +65,15 @@ INSERT INTO ccdi_account_info ( bank, bank_code, currency, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, status, effective_date, invalid_date, @@ -95,32 +90,6 @@ SELECT '国泰君安杭州营业部', 'GTJAHZ01', 'CNY', - 1, - '2026-04-13', - NULL, - 'system', - 'system' -FROM dual -WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_info WHERE account_no = 'ZQ330101199104010101' -); - -INSERT INTO ccdi_account_result ( - account_no, - is_self_account, - monthly_avg_trans_count, - monthly_avg_trans_amount, - trans_freq_type, - dr_max_single_amount, - cr_max_single_amount, - dr_max_daily_amount, - cr_max_daily_amount, - trans_risk_level, - create_by, - update_by -) -SELECT - 'ZQ330101199104010101', 0, 6, 152000.00, @@ -130,11 +99,14 @@ SELECT 98000.00, 116000.00, 'HIGH', + 1, + '2026-04-13', + NULL, 'system', 'system' FROM dual WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_result WHERE account_no = 'ZQ330101199104010101' + SELECT 1 FROM ccdi_account_info WHERE account_no = 'ZQ330101199104010101' ); INSERT INTO ccdi_account_info ( @@ -147,6 +119,15 @@ INSERT INTO ccdi_account_info ( bank, bank_code, currency, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, status, effective_date, invalid_date, @@ -163,32 +144,6 @@ SELECT '支付宝', 'ALIPAY', 'CNY', - 1, - '2026-04-13', - NULL, - 'system', - 'system' -FROM dual -WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_info WHERE account_no = '13700000035' -); - -INSERT INTO ccdi_account_result ( - account_no, - is_self_account, - monthly_avg_trans_count, - monthly_avg_trans_amount, - trans_freq_type, - dr_max_single_amount, - cr_max_single_amount, - dr_max_daily_amount, - cr_max_daily_amount, - trans_risk_level, - create_by, - update_by -) -SELECT - '13700000035', 0, 18, 46800.00, @@ -198,11 +153,14 @@ SELECT 18800.00, 21600.00, 'LOW', + 1, + '2026-04-13', + NULL, 'system', 'system' FROM dual WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_result WHERE account_no = '13700000035' + SELECT 1 FROM ccdi_account_info WHERE account_no = '13700000035' ); INSERT INTO ccdi_account_info ( @@ -215,6 +173,15 @@ INSERT INTO ccdi_account_info ( bank, bank_code, currency, + is_self_account, + monthly_avg_trans_count, + monthly_avg_trans_amount, + trans_freq_type, + dr_max_single_amount, + cr_max_single_amount, + dr_max_daily_amount, + cr_max_daily_amount, + trans_risk_level, status, effective_date, invalid_date, @@ -231,32 +198,6 @@ SELECT '微信支付', 'WXPAY', 'CNY', - 1, - '2026-04-13', - NULL, - 'system', - 'system' -FROM dual -WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_info WHERE account_no = 'wx-ext-20260413-001' -); - -INSERT INTO ccdi_account_result ( - account_no, - is_self_account, - monthly_avg_trans_count, - monthly_avg_trans_amount, - trans_freq_type, - dr_max_single_amount, - cr_max_single_amount, - dr_max_daily_amount, - cr_max_daily_amount, - trans_risk_level, - create_by, - update_by -) -SELECT - 'wx-ext-20260413-001', 0, 9, 9800.00, @@ -266,9 +207,12 @@ SELECT 5600.00, 7000.00, 'LOW', + 1, + '2026-04-13', + NULL, 'system', 'system' FROM dual WHERE NOT EXISTS ( - SELECT 1 FROM ccdi_account_result WHERE account_no = 'wx-ext-20260413-001' + SELECT 1 FROM ccdi_account_info WHERE account_no = 'wx-ext-20260413-001' ); diff --git a/sql/migration/2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql b/sql/migration/2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql new file mode 100644 index 00000000..7bd80853 --- /dev/null +++ b/sql/migration/2026-04-15-sync-ccdi-account-info-abnormal-account-columns.sql @@ -0,0 +1,31 @@ +SET @ccdi_account_info_is_self_account_sql = IF( + EXISTS( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'is_self_account' + ), + 'SELECT 1', + 'ALTER TABLE `ccdi_account_info` ADD COLUMN `is_self_account` TINYINT NOT NULL DEFAULT 1 COMMENT ''是否本人账户'' AFTER `currency`' +); + +PREPARE ccdi_account_info_is_self_account_stmt FROM @ccdi_account_info_is_self_account_sql; +EXECUTE ccdi_account_info_is_self_account_stmt; +DEALLOCATE PREPARE ccdi_account_info_is_self_account_stmt; + +SET @ccdi_account_info_trans_risk_level_sql = IF( + EXISTS( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'trans_risk_level' + ), + 'SELECT 1', + 'ALTER TABLE `ccdi_account_info` ADD COLUMN `trans_risk_level` VARCHAR(32) DEFAULT NULL COMMENT ''交易风险等级'' AFTER `is_self_account`' +); + +PREPARE ccdi_account_info_trans_risk_level_stmt FROM @ccdi_account_info_trans_risk_level_sql; +EXECUTE ccdi_account_info_trans_risk_level_stmt; +DEALLOCATE PREPARE ccdi_account_info_trans_risk_level_stmt; diff --git a/sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql b/sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql new file mode 100644 index 00000000..d84406b5 --- /dev/null +++ b/sql/migration/2026-04-16-merge-ccdi-account-result-into-info.sql @@ -0,0 +1,156 @@ +-- 合并账户库双表:将 ccdi_account_result 分析字段并回 ccdi_account_info,并删除旧表。 +-- 执行说明:涉及中文内容时请使用 bin/mysql_utf8_exec.sh 执行,确保会话字符集为 utf8mb4。 + +DELIMITER // + +DROP PROCEDURE IF EXISTS `merge_ccdi_account_result_into_info`// +CREATE PROCEDURE `merge_ccdi_account_result_into_info`() +BEGIN + DECLARE duplicate_count INT DEFAULT 0; + DECLARE result_table_exists INT DEFAULT 0; + + SELECT COUNT(*) + INTO duplicate_count + FROM ( + SELECT account_no + FROM ccdi_account_info + WHERE account_no IS NOT NULL + AND account_no <> '' + GROUP BY account_no + HAVING COUNT(*) > 1 + ) duplicated_accounts; + + IF duplicate_count > 0 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'ccdi_account_info.account_no 存在重复数据,禁止执行账户库合表迁移。'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'is_self_account' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `is_self_account` TINYINT NOT NULL DEFAULT 1 COMMENT '是否本人账户' AFTER `currency`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'monthly_avg_trans_count' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `monthly_avg_trans_count` INT DEFAULT NULL COMMENT '月均交易笔数' AFTER `is_self_account`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'monthly_avg_trans_amount' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `monthly_avg_trans_amount` DECIMAL(18, 2) DEFAULT NULL COMMENT '月均交易金额' AFTER `monthly_avg_trans_count`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'trans_freq_type' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `trans_freq_type` VARCHAR(32) DEFAULT NULL COMMENT '交易频率类型' AFTER `monthly_avg_trans_amount`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'dr_max_single_amount' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `dr_max_single_amount` DECIMAL(18, 2) DEFAULT NULL COMMENT '最大单笔支出金额' AFTER `trans_freq_type`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'cr_max_single_amount' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `cr_max_single_amount` DECIMAL(18, 2) DEFAULT NULL COMMENT '最大单笔收入金额' AFTER `dr_max_single_amount`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'dr_max_daily_amount' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `dr_max_daily_amount` DECIMAL(18, 2) DEFAULT NULL COMMENT '最大单日支出金额' AFTER `cr_max_single_amount`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'cr_max_daily_amount' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `cr_max_daily_amount` DECIMAL(18, 2) DEFAULT NULL COMMENT '最大单日收入金额' AFTER `dr_max_daily_amount`; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_info' + AND column_name = 'trans_risk_level' + ) THEN + ALTER TABLE `ccdi_account_info` + ADD COLUMN `trans_risk_level` VARCHAR(32) DEFAULT NULL COMMENT '交易风险等级' AFTER `cr_max_daily_amount`; + END IF; + + SELECT COUNT(*) + INTO result_table_exists + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'ccdi_account_result'; + + IF result_table_exists > 0 THEN + UPDATE `ccdi_account_info` ai + JOIN `ccdi_account_result` ar + ON ai.account_no = ar.account_no + SET ai.is_self_account = ar.is_self_account, + ai.monthly_avg_trans_count = ar.monthly_avg_trans_count, + ai.monthly_avg_trans_amount = ar.monthly_avg_trans_amount, + ai.trans_freq_type = ar.trans_freq_type, + ai.dr_max_single_amount = ar.dr_max_single_amount, + ai.cr_max_single_amount = ar.cr_max_single_amount, + ai.dr_max_daily_amount = ar.dr_max_daily_amount, + ai.cr_max_daily_amount = ar.cr_max_daily_amount, + ai.trans_risk_level = ar.trans_risk_level, + ai.update_by = COALESCE(ar.update_by, ai.update_by), + ai.update_time = COALESCE(ar.update_time, ai.update_time); + + DROP TABLE `ccdi_account_result`; + END IF; +END// + +CALL `merge_ccdi_account_result_into_info`()// +DROP PROCEDURE IF EXISTS `merge_ccdi_account_result_into_info`// + +DELIMITER ; diff --git a/tests/deploy/test_deploy_to_nas_tongweb.py b/tests/deploy/test_deploy_to_nas_tongweb.py new file mode 100644 index 00000000..f792ad5b --- /dev/null +++ b/tests/deploy/test_deploy_to_nas_tongweb.py @@ -0,0 +1,67 @@ +from pathlib import Path +import subprocess + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_PATH = REPO_ROOT / "deploy" / "deploy-to-nas-tongweb.sh" +REMOTE_DEPLOY_PATH = REPO_ROOT / "deploy" / "remote-deploy-tongweb.py" + + +def test_tongweb_sh_dry_run_uses_default_target(): + result = subprocess.run( + ["bash", str(SCRIPT_PATH), "--dry-run"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + assert "Host: 116.62.17.81" in result.stdout + assert "Port: 9444" in result.stdout + assert "Username: wkc" in result.stdout + assert "RemoteRoot: /volume1/webapp/ccdi" in result.stdout + assert "TongWebHome: /opt/TongWeb" in result.stdout + assert "AppName: ruoyi-admin" in result.stdout + + +def test_tongweb_sh_dry_run_accepts_override_arguments(): + result = subprocess.run( + [ + "bash", + str(SCRIPT_PATH), + "10.0.0.8", + "2222", + "deploy-user", + "secret", + "/volume2/custom/app", + "/data/TongWeb7", + "ccdi-console", + "--dry-run", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + assert "Host: 10.0.0.8" in result.stdout + assert "Port: 2222" in result.stdout + assert "Username: deploy-user" in result.stdout + assert "RemoteRoot: /volume2/custom/app" in result.stdout + assert "TongWebHome: /data/TongWeb7" in result.stdout + assert "AppName: ccdi-console" in result.stdout + + +def test_tongweb_sh_script_should_call_remote_deploy_helper(): + script_text = SCRIPT_PATH.read_text(encoding="utf-8") + + assert 'remote-deploy-tongweb.py' in script_text + assert 'ruoyi-admin/target/ruoyi-admin.war' in script_text + + +def test_remote_deploy_tongweb_should_use_autodeploy_and_tongweb_scripts(): + script_text = REMOTE_DEPLOY_PATH.read_text(encoding="utf-8") + + assert "autodeploy" in script_text + assert "startservernohup.sh" in script_text + assert "stopserver.sh" in script_text