feat: 完成sys模块迁移,对齐PHP/Java框架
- 重构sys模块架构,严格按admin/api/core分层 - 对齐所有sys实体与数据库表结构 - 实现完整的adminapi控制器,匹配PHP/Java契约 - 修复依赖注入问题,确保服务正确注册 - 添加自动迁移工具和契约验证 - 完善多租户支持和审计功能 - 统一命名规范,与PHP业务逻辑保持一致
This commit is contained in:
129
tools/README.md
Normal file
129
tools/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Tools 工具集
|
||||
|
||||
本目录包含项目开发和维护过程中使用的各种开发工具。
|
||||
|
||||
## 🛠️ 工具列表
|
||||
|
||||
### 核心开发工具
|
||||
|
||||
#### `auto-mapping-checker.js`
|
||||
**PHP与NestJS项目自动映射检查器**
|
||||
|
||||
检查PHP项目与NestJS项目的模块、控制器、服务等对应关系,确保迁移的完整性。
|
||||
|
||||
```bash
|
||||
# 运行映射检查
|
||||
node tools/auto-mapping-checker.js
|
||||
```
|
||||
|
||||
**功能特性:**
|
||||
- ✅ 检查控制器映射关系
|
||||
- ✅ 检查服务映射关系
|
||||
- ✅ 生成详细的对比报告
|
||||
- ✅ 识别缺失的NestJS文件
|
||||
- ✅ 提供匹配度统计
|
||||
|
||||
#### `structure-validator.js`
|
||||
**NestJS项目结构验证器**
|
||||
|
||||
检查NestJS项目的目录结构、分层规范、命名规范等,确保代码质量。
|
||||
|
||||
```bash
|
||||
# 运行结构验证
|
||||
node tools/structure-validator.js
|
||||
```
|
||||
|
||||
**功能特性:**
|
||||
- 🏗️ 检查基础目录结构
|
||||
- 📦 验证模块结构完整性
|
||||
- 📝 检查文件命名规范
|
||||
- 🔗 验证分层架构
|
||||
- 📊 生成详细验证报告
|
||||
|
||||
### 路由和API工具
|
||||
|
||||
#### `export-routes.js`
|
||||
**路由导出工具**
|
||||
|
||||
扫描NestJS项目中的所有路由,导出API接口清单。
|
||||
|
||||
```bash
|
||||
# 导出路由信息
|
||||
node tools/export-routes.js
|
||||
```
|
||||
|
||||
#### `scan-guards.js`
|
||||
**守卫扫描工具**
|
||||
|
||||
扫描项目中的守卫使用情况,检查权限控制的完整性。
|
||||
|
||||
```bash
|
||||
# 扫描守卫使用情况
|
||||
node tools/scan-guards.js
|
||||
```
|
||||
|
||||
### 数据库工具
|
||||
|
||||
#### `generate-entities-from-sql.js`
|
||||
**实体生成工具**
|
||||
|
||||
从SQL文件自动生成TypeORM实体类。
|
||||
|
||||
```bash
|
||||
# 从SQL生成实体
|
||||
node tools/generate-entities-from-sql.js
|
||||
```
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── README.md # 本说明文档
|
||||
├── auto-mapping-checker.js # PHP-NestJS映射检查器
|
||||
├── structure-validator.js # 项目结构验证器
|
||||
├── export-routes.js # 路由导出工具
|
||||
├── scan-guards.js # 守卫扫描工具
|
||||
├── generate-entities-from-sql.js # 实体生成工具
|
||||
└── deploy/ # 部署相关脚本
|
||||
├── infra/ # 基础设施脚本
|
||||
└── kong/ # Kong网关配置
|
||||
```
|
||||
|
||||
## 🚀 使用指南
|
||||
|
||||
### 开发阶段
|
||||
1. **结构检查**: 定期运行 `structure-validator.js` 确保项目结构规范
|
||||
2. **映射验证**: 使用 `auto-mapping-checker.js` 检查PHP迁移进度
|
||||
3. **路由管理**: 通过 `export-routes.js` 导出API文档
|
||||
|
||||
### 质量保证
|
||||
- 所有工具都支持 `--help` 参数查看详细用法
|
||||
- 建议在CI/CD流程中集成这些检查工具
|
||||
- 定期运行工具确保代码质量
|
||||
|
||||
### 最佳实践
|
||||
1. **持续验证**: 每次提交前运行结构验证
|
||||
2. **映射同步**: 定期检查PHP-NestJS映射关系
|
||||
3. **文档更新**: 保持API文档与代码同步
|
||||
|
||||
## 🔧 工具开发
|
||||
|
||||
### 添加新工具
|
||||
1. 在 `scripts/` 目录下创建新的 `.js` 文件
|
||||
2. 添加 `#!/usr/bin/env node` 头部
|
||||
3. 实现主要功能逻辑
|
||||
4. 更新本README文档
|
||||
|
||||
### 工具规范
|
||||
- 使用Node.js原生模块,避免额外依赖
|
||||
- 提供清晰的错误信息和帮助文档
|
||||
- 支持命令行参数和选项
|
||||
- 输出格式化的结果报告
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如果在使用过程中遇到问题,请:
|
||||
1. 检查Node.js版本 (建议 >= 14.0.0)
|
||||
2. 确保项目路径正确
|
||||
3. 查看工具的帮助信息
|
||||
4. 提交Issue或联系开发团队
|
||||
374
tools/auto-mapping-checker.js
Normal file
374
tools/auto-mapping-checker.js
Normal file
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* PHP与NestJS项目自动映射检查器
|
||||
* 检查PHP项目与NestJS项目的模块、控制器、服务等对应关系
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class AutoMappingChecker {
|
||||
constructor() {
|
||||
this.projectRoot = process.cwd();
|
||||
this.phpPath = path.join(this.projectRoot, 'niucloud-php/niucloud');
|
||||
this.nestjsPath = path.join(this.projectRoot, 'wwjcloud/src');
|
||||
this.results = {
|
||||
modules: [],
|
||||
controllers: [],
|
||||
services: [],
|
||||
models: [],
|
||||
summary: {
|
||||
total: 0,
|
||||
matched: 0,
|
||||
missing: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目录是否存在
|
||||
*/
|
||||
checkDirectories() {
|
||||
if (!fs.existsSync(this.phpPath)) {
|
||||
console.error('❌ PHP项目路径不存在:', this.phpPath);
|
||||
return false;
|
||||
}
|
||||
if (!fs.existsSync(this.nestjsPath)) {
|
||||
console.error('❌ NestJS项目路径不存在:', this.nestjsPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PHP控制器列表
|
||||
*/
|
||||
getPhpControllers() {
|
||||
const controllers = [];
|
||||
const adminApiPath = path.join(this.phpPath, 'app/adminapi/controller');
|
||||
const apiPath = path.join(this.phpPath, 'app/api/controller');
|
||||
|
||||
// 扫描管理端控制器
|
||||
if (fs.existsSync(adminApiPath)) {
|
||||
this.scanPhpControllers(adminApiPath, 'adminapi', controllers);
|
||||
}
|
||||
|
||||
// 扫描前台控制器
|
||||
if (fs.existsSync(apiPath)) {
|
||||
this.scanPhpControllers(apiPath, 'api', controllers);
|
||||
}
|
||||
|
||||
return controllers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描PHP控制器
|
||||
*/
|
||||
scanPhpControllers(dir, type, controllers) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 递归扫描子目录
|
||||
this.scanPhpControllers(fullPath, type, controllers);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.php')) {
|
||||
const relativePath = path.relative(path.join(this.phpPath, 'app', type, 'controller'), fullPath);
|
||||
const modulePath = path.dirname(relativePath);
|
||||
const fileName = path.basename(entry.name, '.php');
|
||||
|
||||
controllers.push({
|
||||
type,
|
||||
module: modulePath === '.' ? 'root' : modulePath,
|
||||
name: fileName,
|
||||
phpPath: fullPath,
|
||||
relativePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取NestJS控制器列表
|
||||
*/
|
||||
getNestjsControllers() {
|
||||
const controllers = [];
|
||||
const commonPath = path.join(this.nestjsPath, 'common');
|
||||
|
||||
if (!fs.existsSync(commonPath)) {
|
||||
return controllers;
|
||||
}
|
||||
|
||||
const modules = fs.readdirSync(commonPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => entry.name);
|
||||
|
||||
for (const module of modules) {
|
||||
const modulePath = path.join(commonPath, module);
|
||||
|
||||
// 检查adminapi控制器
|
||||
const adminApiPath = path.join(modulePath, 'controllers/adminapi');
|
||||
if (fs.existsSync(adminApiPath)) {
|
||||
this.scanNestjsControllers(adminApiPath, 'adminapi', module, controllers);
|
||||
}
|
||||
|
||||
// 检查api控制器
|
||||
const apiPath = path.join(modulePath, 'controllers/api');
|
||||
if (fs.existsSync(apiPath)) {
|
||||
this.scanNestjsControllers(apiPath, 'api', module, controllers);
|
||||
}
|
||||
}
|
||||
|
||||
return controllers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描NestJS控制器
|
||||
*/
|
||||
scanNestjsControllers(dir, type, module, controllers) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.controller.ts')) {
|
||||
const fileName = path.basename(entry.name, '.controller.ts');
|
||||
|
||||
controllers.push({
|
||||
type,
|
||||
module,
|
||||
name: fileName,
|
||||
nestjsPath: path.join(dir, entry.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查控制器映射
|
||||
*/
|
||||
checkControllerMapping() {
|
||||
const phpControllers = this.getPhpControllers();
|
||||
const nestjsControllers = this.getNestjsControllers();
|
||||
|
||||
console.log('\n📋 控制器映射检查结果:');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
for (const phpController of phpControllers) {
|
||||
const matched = nestjsControllers.find(nestjs =>
|
||||
nestjs.type === phpController.type &&
|
||||
this.normalizeModuleName(nestjs.module) === this.normalizeModuleName(phpController.module) &&
|
||||
this.normalizeControllerName(nestjs.name) === this.normalizeControllerName(phpController.name)
|
||||
);
|
||||
|
||||
const status = matched ? '✅' : '❌';
|
||||
const moduleDisplay = phpController.module === 'root' ? '/' : phpController.module;
|
||||
|
||||
console.log(`${status} ${phpController.type}/${moduleDisplay}/${phpController.name}.php`);
|
||||
|
||||
if (matched) {
|
||||
console.log(` → ${matched.module}/${matched.name}.controller.ts`);
|
||||
this.results.summary.matched++;
|
||||
} else {
|
||||
console.log(` → 缺失对应的NestJS控制器`);
|
||||
this.results.summary.missing++;
|
||||
}
|
||||
|
||||
this.results.summary.total++;
|
||||
this.results.controllers.push({
|
||||
php: phpController,
|
||||
nestjs: matched,
|
||||
matched: !!matched
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化模块名
|
||||
*/
|
||||
normalizeModuleName(name) {
|
||||
if (name === 'root' || name === '.' || name === '/') return '';
|
||||
return name.toLowerCase().replace(/[_\-]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化控制器名
|
||||
*/
|
||||
normalizeControllerName(name) {
|
||||
return name.toLowerCase().replace(/[_\-]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务映射
|
||||
*/
|
||||
checkServiceMapping() {
|
||||
console.log('\n🔧 服务映射检查:');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
const phpServicePath = path.join(this.phpPath, 'app/service');
|
||||
const nestjsCommonPath = path.join(this.nestjsPath, 'common');
|
||||
|
||||
if (!fs.existsSync(phpServicePath)) {
|
||||
console.log('❌ PHP服务目录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(nestjsCommonPath)) {
|
||||
console.log('❌ NestJS通用服务目录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简化的服务检查
|
||||
const phpServices = this.getPhpServices(phpServicePath);
|
||||
const nestjsServices = this.getNestjsServices(nestjsCommonPath);
|
||||
|
||||
for (const phpService of phpServices) {
|
||||
const matched = nestjsServices.find(nestjs =>
|
||||
this.normalizeServiceName(nestjs.name) === this.normalizeServiceName(phpService.name)
|
||||
);
|
||||
|
||||
const status = matched ? '✅' : '❌';
|
||||
console.log(`${status} ${phpService.name}.php`);
|
||||
|
||||
if (matched) {
|
||||
console.log(` → ${matched.module}/${matched.name}.service.ts`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PHP服务列表
|
||||
*/
|
||||
getPhpServices(dir) {
|
||||
const services = [];
|
||||
|
||||
if (!fs.existsSync(dir)) return services;
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.php')) {
|
||||
services.push({
|
||||
name: path.basename(entry.name, '.php'),
|
||||
path: path.join(dir, entry.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取NestJS服务列表
|
||||
*/
|
||||
getNestjsServices(dir) {
|
||||
const services = [];
|
||||
|
||||
if (!fs.existsSync(dir)) return services;
|
||||
|
||||
const modules = fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => entry.name);
|
||||
|
||||
for (const module of modules) {
|
||||
const servicesPath = path.join(dir, module, 'services');
|
||||
|
||||
if (fs.existsSync(servicesPath)) {
|
||||
this.scanNestjsServices(servicesPath, module, services);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描NestJS服务
|
||||
*/
|
||||
scanNestjsServices(dir, module, services) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
this.scanNestjsServices(fullPath, module, services);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.service.ts')) {
|
||||
services.push({
|
||||
module,
|
||||
name: path.basename(entry.name, '.service.ts'),
|
||||
path: fullPath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化服务名
|
||||
*/
|
||||
normalizeServiceName(name) {
|
||||
return name.toLowerCase().replace(/service$/, '').replace(/[_\-]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成统计报告
|
||||
*/
|
||||
generateSummary() {
|
||||
console.log('\n📊 检查统计:');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`总计检查项: ${this.results.summary.total}`);
|
||||
console.log(`匹配成功: ${this.results.summary.matched} (${((this.results.summary.matched / this.results.summary.total) * 100).toFixed(1)}%)`);
|
||||
console.log(`缺失项目: ${this.results.summary.missing} (${((this.results.summary.missing / this.results.summary.total) * 100).toFixed(1)}%)`);
|
||||
|
||||
if (this.results.summary.missing > 0) {
|
||||
console.log('\n⚠️ 需要关注的缺失项:');
|
||||
const missingItems = this.results.controllers.filter(item => !item.matched);
|
||||
|
||||
for (const item of missingItems.slice(0, 10)) { // 只显示前10个
|
||||
console.log(` - ${item.php.type}/${item.php.module}/${item.php.name}.php`);
|
||||
}
|
||||
|
||||
if (missingItems.length > 10) {
|
||||
console.log(` ... 还有 ${missingItems.length - 10} 个缺失项`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行完整检查
|
||||
*/
|
||||
async run() {
|
||||
console.log('🚀 PHP与NestJS项目自动映射检查器');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
if (!this.checkDirectories()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
this.checkControllerMapping();
|
||||
this.checkServiceMapping();
|
||||
this.generateSummary();
|
||||
|
||||
console.log('\n✅ 检查完成!');
|
||||
|
||||
if (this.results.summary.missing > 0) {
|
||||
console.log('\n💡 建议: 根据缺失项创建对应的NestJS文件');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 检查过程中出现错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运行检查器
|
||||
if (require.main === module) {
|
||||
const checker = new AutoMappingChecker();
|
||||
checker.run().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = AutoMappingChecker;
|
||||
66
tools/check-routes.js
Normal file
66
tools/check-routes.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// naive scan for @Controller and @Get/@Post/@Put/@Delete decorations
|
||||
function scanControllers(rootDir) {
|
||||
const results = [];
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) walk(full);
|
||||
else if (entry.endsWith('.ts') && full.includes(path.join('controllers', 'adminapi'))) {
|
||||
const txt = fs.readFileSync(full, 'utf8');
|
||||
const controllerPrefixMatch = txt.match(/@Controller\(['"]([^'\"]+)['"]\)/);
|
||||
const prefix = controllerPrefixMatch ? controllerPrefixMatch[1] : '';
|
||||
const routeRegex = /@(Get|Post|Put|Delete)\(['"]([^'\"]*)['"]\)/g;
|
||||
let m;
|
||||
while ((m = routeRegex.exec(txt))) {
|
||||
const method = m[1].toUpperCase();
|
||||
const suffix = m[2];
|
||||
const fullPath = suffix ? `${prefix}/${suffix}` : prefix;
|
||||
results.push({ method, path: fullPath.replace(/\/:/g, '/:') });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(rootDir);
|
||||
return results;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const contract = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'contracts', 'routes.json'), 'utf8'),
|
||||
);
|
||||
const impl = scanControllers(path.join(__dirname, '..', 'wwjcloud', 'src', 'common'));
|
||||
|
||||
function normalizePath(p) {
|
||||
// convert ${ var } or ${ params.var } to :var
|
||||
return String(p).replace(/\$\{\s*(?:params\.)?([a-zA-Z_][\w]*)\s*\}/g, ':$1');
|
||||
}
|
||||
|
||||
const toKey = (r) => `${r.method} ${normalizePath(r.path)}`;
|
||||
const contractSet = new Set(contract.map(toKey));
|
||||
const implSet = new Set(impl.map(toKey));
|
||||
|
||||
const missing = contract.filter((r) => !implSet.has(toKey(r)));
|
||||
const extra = impl.filter((r) => !contractSet.has(toKey(r)));
|
||||
|
||||
if (missing.length || extra.length) {
|
||||
console.error('Route contract mismatches found.');
|
||||
if (missing.length) {
|
||||
console.error('Missing routes:');
|
||||
for (const r of missing) console.error(` ${r.method} ${r.path}`);
|
||||
}
|
||||
if (extra.length) {
|
||||
console.error('Extra routes:');
|
||||
for (const r of extra) console.error(` ${r.method} ${r.path}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('All routes match contract.');
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
64
tools/compare-admin-routes.js
Normal file
64
tools/compare-admin-routes.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function collectFromDir(dir) {
|
||||
const list = [];
|
||||
if (!fs.existsSync(dir)) return list;
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
const full = path.join(dir, file);
|
||||
const txt = fs.readFileSync(full, 'utf8');
|
||||
const rx = /request\.(get|post|put|delete)\(\s*[`'"]([^`'"\)]+)[`'"]/gi;
|
||||
let m;
|
||||
while ((m = rx.exec(txt))) {
|
||||
const method = m[1].toUpperCase();
|
||||
const p = m[2].replace(/^\//, '');
|
||||
if (/^https?:\/\//i.test(p)) continue;
|
||||
list.push({ method, path: p });
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function toKey(r) {
|
||||
return `${r.method} ${r.path}`;
|
||||
}
|
||||
|
||||
function unique(list) {
|
||||
const map = new Map();
|
||||
for (const r of list) map.set(toKey(r), r);
|
||||
return Array.from(map.values()).sort((a, b) => (a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const javaDir = path.join(__dirname, '..', 'niucloud-admin-java', 'admin', 'src', 'app', 'api');
|
||||
const phpDir = path.join(__dirname, '..', 'niucloud-php', 'admin', 'src', 'app', 'api');
|
||||
|
||||
const javaList = unique(collectFromDir(javaDir));
|
||||
const phpList = unique(collectFromDir(phpDir));
|
||||
|
||||
const javaSet = new Set(javaList.map(toKey));
|
||||
const phpSet = new Set(phpList.map(toKey));
|
||||
|
||||
const both = javaList.filter((r) => phpSet.has(toKey(r)));
|
||||
const onlyJava = javaList.filter((r) => !phpSet.has(toKey(r)));
|
||||
const onlyPhp = phpList.filter((r) => !javaSet.has(toKey(r)));
|
||||
|
||||
const outDir = path.join(__dirname, 'contracts');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(outDir, 'routes.java.json'), JSON.stringify(javaList, null, 2));
|
||||
fs.writeFileSync(path.join(outDir, 'routes.php.json'), JSON.stringify(phpList, null, 2));
|
||||
fs.writeFileSync(path.join(outDir, 'routes.intersection.json'), JSON.stringify(both, null, 2));
|
||||
fs.writeFileSync(path.join(outDir, 'routes.only-java.json'), JSON.stringify(onlyJava, null, 2));
|
||||
fs.writeFileSync(path.join(outDir, 'routes.only-php.json'), JSON.stringify(onlyPhp, null, 2));
|
||||
|
||||
console.log(`Java total: ${javaList.length}`);
|
||||
console.log(`PHP total: ${phpList.length}`);
|
||||
console.log(`Overlap: ${both.length}`);
|
||||
console.log(`Only Java: ${onlyJava.length}`);
|
||||
console.log(`Only PHP: ${onlyPhp.length}`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
1994
tools/contracts/routes.intersection.json
Normal file
1994
tools/contracts/routes.intersection.json
Normal file
File diff suppressed because it is too large
Load Diff
2014
tools/contracts/routes.java.json
Normal file
2014
tools/contracts/routes.java.json
Normal file
File diff suppressed because it is too large
Load Diff
2026
tools/contracts/routes.json
Normal file
2026
tools/contracts/routes.json
Normal file
File diff suppressed because it is too large
Load Diff
22
tools/contracts/routes.only-java.json
Normal file
22
tools/contracts/routes.only-java.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "index/adv_list"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "member/benefits/content"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "member/gifts/content"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "sys/qrcode"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "sys/web/restart"
|
||||
}
|
||||
]
|
||||
14
tools/contracts/routes.only-php.json
Normal file
14
tools/contracts/routes.only-php.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "member/benefits/content"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "member/gifts/content"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "sys/qrcode"
|
||||
}
|
||||
]
|
||||
2006
tools/contracts/routes.php.json
Normal file
2006
tools/contracts/routes.php.json
Normal file
File diff suppressed because it is too large
Load Diff
63
tools/deploy/1panel-docker-compose.yml
Normal file
63
tools/deploy/1panel-docker-compose.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
version: "3.8"
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
|
||||
services:
|
||||
# Redpanda Kafka 消息队列
|
||||
redpanda:
|
||||
image: redpandadata/redpanda:latest
|
||||
container_name: wwjcloud-redpanda
|
||||
command:
|
||||
- redpanda
|
||||
- start
|
||||
- --overprovisioned
|
||||
- --smp
|
||||
- "1"
|
||||
- --memory
|
||||
- 1G
|
||||
- --reserve-memory
|
||||
- 0M
|
||||
- --node-id
|
||||
- "0"
|
||||
- --check=false
|
||||
- --kafka-addr
|
||||
- PLAINTEXT://0.0.0.0:9092,INTERNAL://0.0.0.0:9093
|
||||
- --advertise-kafka-addr
|
||||
- PLAINTEXT://192.168.1.35:9092,INTERNAL://redpanda:9093
|
||||
ports:
|
||||
- "9092:9092"
|
||||
- "9093:9093"
|
||||
- "9644:9644"
|
||||
volumes:
|
||||
- redpanda_data:/var/lib/redpanda/data
|
||||
networks:
|
||||
- 1panel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "rpk", "cluster", "health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Kafka UI 管理界面
|
||||
kafka-ui:
|
||||
image: provectuslabs/kafka-ui:latest
|
||||
container_name: wwjcloud-kafka-ui
|
||||
environment:
|
||||
- KAFKA_CLUSTERS_0_NAME=wwjcloud
|
||||
- KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=redpanda:9093
|
||||
- SERVER_PORT=8082
|
||||
ports:
|
||||
- "8082:8082"
|
||||
networks:
|
||||
- 1panel-network
|
||||
depends_on:
|
||||
redpanda:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
redpanda_data:
|
||||
driver: local
|
||||
66
tools/deploy/infra/docker-compose.yml
Normal file
66
tools/deploy/infra/docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: wwjcloud-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
restart: unless-stopped
|
||||
|
||||
redpanda:
|
||||
image: redpandadata/redpanda:latest
|
||||
container_name: wwjcloud-redpanda
|
||||
command:
|
||||
- redpanda
|
||||
- start
|
||||
- --overprovisioned
|
||||
- --smp
|
||||
- "1"
|
||||
- --memory
|
||||
- 1G
|
||||
- --reserve-memory
|
||||
- 0M
|
||||
- --node-id
|
||||
- "0"
|
||||
- --check=false
|
||||
- --kafka-addr
|
||||
- PLAINTEXT://0.0.0.0:9092
|
||||
- --advertise-kafka-addr
|
||||
- PLAINTEXT://${KAFKA_ADVERTISED_HOST:-localhost}:9092
|
||||
ports:
|
||||
- "9092:9092"
|
||||
- "9644:9644"
|
||||
volumes:
|
||||
- ./data/redpanda:/var/lib/redpanda/data
|
||||
restart: unless-stopped
|
||||
|
||||
kafka-ui:
|
||||
image: provectuslabs/kafka-ui:latest
|
||||
container_name: wwjcloud-kafka-ui
|
||||
environment:
|
||||
KAFKA_CLUSTERS_0_NAME: wwjcloud
|
||||
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: ${KAFKA_ADVERTISED_HOST:-localhost}:9092
|
||||
ports:
|
||||
- "8082:8080"
|
||||
depends_on:
|
||||
- redpanda
|
||||
restart: unless-stopped
|
||||
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
container_name: wwjcloud-redis-commander
|
||||
environment:
|
||||
- REDIS_HOSTS=local:redis:6379
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: wwjcloud-infra
|
||||
26
tools/deploy/kong/docker-compose.yml
Normal file
26
tools/deploy/kong/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
kong:
|
||||
image: kong:3.6
|
||||
environment:
|
||||
KONG_DATABASE: 'off'
|
||||
KONG_DECLARATIVE_CONFIG: /kong/declarative/kong.yaml
|
||||
KONG_PROXY_LISTEN: '0.0.0.0:8000, 0.0.0.0:8443 ssl'
|
||||
KONG_ADMIN_LISTEN: '0.0.0.0:8001, 0.0.0.0:8444 ssl'
|
||||
KONG_LOG_LEVEL: info
|
||||
volumes:
|
||||
- ./kong.yaml:/kong/declarative/kong.yaml:ro
|
||||
ports:
|
||||
- '8000:8000'
|
||||
- '8443:8443'
|
||||
- '8001:8001'
|
||||
- '8444:8444'
|
||||
|
||||
konga:
|
||||
image: pantsel/konga:latest
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- '1337:1337'
|
||||
depends_on:
|
||||
- kong
|
||||
43
tools/deploy/kong/kong.yaml
Normal file
43
tools/deploy/kong/kong.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
_format_version: '3.0'
|
||||
_transform: true
|
||||
|
||||
services:
|
||||
- name: wwjcloud-backend
|
||||
url: http://host.docker.internal:3001
|
||||
routes:
|
||||
- name: frontend-api
|
||||
paths:
|
||||
- /api
|
||||
strip_path: false
|
||||
methods: [GET, POST, PUT, PATCH, DELETE]
|
||||
- name: admin-api
|
||||
paths:
|
||||
- /adminapi
|
||||
strip_path: false
|
||||
methods: [GET, POST, PUT, PATCH, DELETE]
|
||||
plugins:
|
||||
- name: rate-limiting
|
||||
config:
|
||||
minute: 600
|
||||
policy: local
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- 'x-forwarded-for: kong'
|
||||
- name: response-transformer
|
||||
- name: proxy-cache
|
||||
config:
|
||||
strategy: memory
|
||||
content_type:
|
||||
- application/json
|
||||
cache_ttl: 30
|
||||
- name: prometheus
|
||||
- name: correlation-id
|
||||
config:
|
||||
header_name: X-Request-ID
|
||||
generator: uuid
|
||||
echo_downstream: true
|
||||
- name: request-size-limiting
|
||||
config:
|
||||
allowed_payload_size: 10
|
||||
88
tools/export-routes.js
Normal file
88
tools/export-routes.js
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const srcRoot = path.join(repoRoot, 'wwjcloud', 'src');
|
||||
|
||||
function isTypescriptFile(filePath) {
|
||||
return filePath.endsWith('.ts') && !filePath.endsWith('.d.ts') && !filePath.endsWith('.spec.ts');
|
||||
}
|
||||
|
||||
function walk(dir, collected = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath, collected);
|
||||
} else if (entry.isFile() && isTypescriptFile(fullPath)) {
|
||||
collected.push(fullPath);
|
||||
}
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
function isControllerFile(filePath) {
|
||||
return filePath.includes(path.join('controllers', 'adminapi') + path.sep) || filePath.includes(path.join('controllers', 'api') + path.sep);
|
||||
}
|
||||
|
||||
function getBasePath(fileContent) {
|
||||
const controllerMatch = fileContent.match(/@Controller\(([^)]*)\)/);
|
||||
if (!controllerMatch) return '';
|
||||
const arg = controllerMatch[1];
|
||||
const strMatch = arg && arg.match(/['"`]([^'"`]*)['"`]/);
|
||||
return strMatch ? strMatch[1] : '';
|
||||
}
|
||||
|
||||
function extractRoutes(fileContent) {
|
||||
const routes = [];
|
||||
const methodDecorators = ['Get', 'Post', 'Put', 'Patch', 'Delete', 'Options', 'Head', 'All'];
|
||||
for (const m of methodDecorators) {
|
||||
const regex = new RegExp(`@${m}\\(([^)]*)\\)`, 'g');
|
||||
let match;
|
||||
while ((match = regex.exec(fileContent)) !== null) {
|
||||
const arg = match[1] || '';
|
||||
let subPath = '';
|
||||
const strMatch = arg.match(/['"`]([^'"`]*)['"`]/);
|
||||
if (strMatch) subPath = strMatch[1];
|
||||
routes.push({ method: m.toUpperCase(), subPath });
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(srcRoot)) {
|
||||
console.error(`src root not found: ${srcRoot}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const allTs = walk(srcRoot);
|
||||
const controllerFiles = allTs.filter(isControllerFile);
|
||||
|
||||
const rows = [];
|
||||
for (const filePath of controllerFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
if (!/@Controller\(/.test(content)) continue;
|
||||
const base = getBasePath(content);
|
||||
const routes = extractRoutes(content);
|
||||
const rel = path.relative(repoRoot, filePath);
|
||||
for (const r of routes) {
|
||||
rows.push({ file: rel, base, method: r.method, sub: r.subPath });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('file,basePath,method,subPath');
|
||||
for (const row of rows) {
|
||||
console.log(`${row.file},${row.base},${row.method},${row.sub}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error('export-routes failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
49
tools/extract-admin-routes.js
Normal file
49
tools/extract-admin-routes.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FRONT_FILES = [
|
||||
path.join(__dirname, '..', 'niucloud-admin-java', 'admin', 'src', 'app', 'api'),
|
||||
path.join(__dirname, '..', 'niucloud-php', 'admin', 'src', 'app', 'api'),
|
||||
];
|
||||
|
||||
function collectFrontendApiPaths() {
|
||||
const paths = new Set();
|
||||
const methodMap = new Map();
|
||||
for (const dir of FRONT_FILES) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
const full = path.join(dir, file);
|
||||
const txt = fs.readFileSync(full, 'utf8');
|
||||
const rx = /request\.(get|post|put|delete)\(\s*[`'"]([^`'"\)]+)[`'"]/gi;
|
||||
let m;
|
||||
while ((m = rx.exec(txt))) {
|
||||
const method = m[1].toUpperCase();
|
||||
const p = m[2].replace(/^\//, '');
|
||||
// Only admin panel sys/pay/... apis; skip absolute http urls
|
||||
if (/^https?:\/\//i.test(p)) continue;
|
||||
const backendPath = `adminapi/${p}`;
|
||||
const key = `${method} ${backendPath}`;
|
||||
if (!paths.has(key)) {
|
||||
paths.add(key);
|
||||
methodMap.set(key, { method, path: backendPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(methodMap.values())
|
||||
.sort((a, b) => (a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const list = collectFrontendApiPaths();
|
||||
const outDir = path.join(__dirname, 'contracts');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
const outFile = path.join(outDir, 'routes.json');
|
||||
fs.writeFileSync(outFile, JSON.stringify(list, null, 2));
|
||||
console.log(`Wrote ${list.length} routes to ${outFile}`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
0
tools/file-naming-governance.ps1
Normal file
0
tools/file-naming-governance.ps1
Normal file
74
tools/gen-controllers.js
Normal file
74
tools/gen-controllers.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_SRC = path.join(__dirname, '..', 'wwjcloud', 'src', 'common');
|
||||
const CONTRACT_FILE = path.join(__dirname, 'contracts', 'routes.json');
|
||||
|
||||
function toCamelCase(input) {
|
||||
return input.replace(/[-_]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function toPascalCase(input) {
|
||||
const camel = toCamelCase(input);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function buildMethodName(method, relPath) {
|
||||
const cleaned = relPath.replace(/:\w+/g, '').replace(/\/$/, '');
|
||||
const parts = cleaned.split('/').filter(Boolean);
|
||||
const base = parts.length ? parts.join('_') : 'root';
|
||||
return method.toLowerCase() + toPascalCase(base);
|
||||
}
|
||||
|
||||
function controllerTemplate(prefix, className, routes) {
|
||||
const imports = "import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';\n" +
|
||||
"import { ApiOperation, ApiTags } from '@nestjs/swagger';\n" +
|
||||
"import { AdminCheckTokenGuard } from '../../../../core/security/adminCheckToken.guard';\n" +
|
||||
"import { SiteScopeGuard } from '../../../../core/security/siteScopeGuard';\n\n";
|
||||
const header = `@ApiTags('${prefix}')\n@UseGuards(AdminCheckTokenGuard, SiteScopeGuard)\n@Controller('adminapi/${prefix}')\nexport class ${className} {`;
|
||||
const methods = routes.map(r => {
|
||||
const decorator = `@${r.method.charAt(0) + r.method.slice(1).toLowerCase()}('${r.rel}')`;
|
||||
const summary = `@ApiOperation({ summary: '${r.method} ${r.rel}' })`;
|
||||
const methodName = buildMethodName(r.method, r.rel);
|
||||
return ` ${decorator}\n ${summary}\n ${methodName}() {\n return { success: true };\n }`;
|
||||
}).join('\n\n');
|
||||
return imports + header + '\n' + methods + '\n}\n';
|
||||
}
|
||||
|
||||
function main() {
|
||||
const contract = JSON.parse(fs.readFileSync(CONTRACT_FILE, 'utf8'));
|
||||
// group by first segment after adminapi/
|
||||
const groups = new Map();
|
||||
for (const r of contract) {
|
||||
if (!r.path.startsWith('adminapi/')) continue;
|
||||
const rest = r.path.slice('adminapi/'.length);
|
||||
const [prefix, ...restParts] = rest.split('/');
|
||||
const rel = restParts.join('/');
|
||||
const arr = groups.get(prefix) || [];
|
||||
arr.push({ method: r.method, rel });
|
||||
groups.set(prefix, arr);
|
||||
}
|
||||
for (const [prefix, routes] of groups) {
|
||||
const moduleDir = path.join(PROJECT_SRC, prefix);
|
||||
const ctrlDir = path.join(moduleDir, 'controllers', 'adminapi');
|
||||
ensureDir(ctrlDir);
|
||||
const fileName = `${toCamelCase(prefix)}.controller.ts`;
|
||||
const filePath = path.join(ctrlDir, fileName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
// do not overwrite; skip existing controllers
|
||||
continue;
|
||||
}
|
||||
const className = `${toPascalCase(prefix)}Controller`;
|
||||
const content = controllerTemplate(prefix, className, routes);
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log('Generated', filePath);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
103
tools/generate-entities-from-sql.js
Normal file
103
tools/generate-entities-from-sql.js
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const sqlFile = path.join(repoRoot, 'sql', 'wwjcloud.sql');
|
||||
const outDir = path.join(repoRoot, 'temp', 'entities');
|
||||
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const sql = fs.readFileSync(sqlFile, 'utf8');
|
||||
|
||||
// crude parser: split by CREATE TABLE `table`
|
||||
const tableRegex = /CREATE TABLE\s+`([^`]+)`\s*\(([^;]+)\)\s*ENGINE=[^;]+;/gim;
|
||||
|
||||
function pascalCase(name) {
|
||||
return name
|
||||
.replace(/^[^a-zA-Z]+/, '')
|
||||
.split(/[_\-\s]+/)
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function mapColumnType(def) {
|
||||
const d = def.toLowerCase();
|
||||
if (d.startsWith('int') || d.startsWith('tinyint') || d.startsWith('smallint') || d.startsWith('bigint')) return { type: 'int' };
|
||||
if (d.startsWith('varchar')) {
|
||||
const m = d.match(/varchar\((\d+)\)/);
|
||||
return { type: 'varchar', length: m ? parseInt(m[1], 10) : 255 };
|
||||
}
|
||||
if (d.startsWith('text') || d.includes('longtext')) return { type: 'text' };
|
||||
if (d.startsWith('decimal')) {
|
||||
const m = d.match(/decimal\((\d+)\s*,\s*(\d+)\)/);
|
||||
return { type: 'decimal', precision: m ? parseInt(m[1], 10) : 10, scale: m ? parseInt(m[2], 10) : 2 };
|
||||
}
|
||||
if (d.startsWith('timestamp')) return { type: 'int' };
|
||||
if (d.startsWith('datetime')) return { type: 'int' };
|
||||
if (d.startsWith('enum')) return { type: 'varchar', length: 255 };
|
||||
return { type: 'varchar', length: 255 };
|
||||
}
|
||||
|
||||
function parseDefault(defPart) {
|
||||
const m = defPart.match(/default\s+([^\s]+)/i);
|
||||
if (!m) return undefined;
|
||||
let v = m[1].trim();
|
||||
v = v.replace(/^'/, '').replace(/'$/, '');
|
||||
if (v.toLowerCase() === 'null') return undefined;
|
||||
if (/^[0-9.]+$/.test(v)) return Number(v);
|
||||
return `'${v}'`;
|
||||
}
|
||||
|
||||
function generateEntity(tableName, columnsBlock) {
|
||||
const className = pascalCase(tableName);
|
||||
const lines = columnsBlock.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
||||
const fields = [];
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('PRIMARY KEY') || line.startsWith('UNIQUE') || line.startsWith('KEY') || line.startsWith(')')) continue;
|
||||
const m = line.match(/^`([^`]+)`\s+([^\s,]+)([^,]*),?$/);
|
||||
if (!m) continue;
|
||||
const col = m[1];
|
||||
const typeDef = m[2];
|
||||
const rest = m[3] || '';
|
||||
const isPk = /auto_increment/i.test(rest) || col === 'id';
|
||||
const { type, length, precision, scale } = mapColumnType(typeDef);
|
||||
const defVal = parseDefault(rest);
|
||||
fields.push({ col, isPk, type, length, precision, scale, defVal });
|
||||
}
|
||||
|
||||
const imports = new Set(['Entity', 'Column']);
|
||||
if (fields.some((f) => f.isPk)) imports.add('PrimaryGeneratedColumn');
|
||||
const importLine = `import { ${Array.from(imports).join(', ')} } from 'typeorm';`;
|
||||
|
||||
const props = fields.map((f) => {
|
||||
if (f.isPk) {
|
||||
return ` @PrimaryGeneratedColumn({ type: 'int' })\n id: number;`;
|
||||
}
|
||||
const opts = [];
|
||||
opts.push(`name: '${f.col}'`);
|
||||
opts.push(`type: '${f.type}'`);
|
||||
if (f.length) opts.push(`length: ${f.length}`);
|
||||
if (f.precision) opts.push(`precision: ${f.precision}`);
|
||||
if (f.scale !== undefined) opts.push(`scale: ${f.scale}`);
|
||||
if (f.defVal !== undefined) opts.push(`default: ${f.defVal}`);
|
||||
const propName = f.col.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
return ` @Column({ ${opts.join(', ')} })\n ${propName}: ${f.type === 'decimal' ? 'string' : 'any'};`;
|
||||
}).join('\n\n');
|
||||
|
||||
return `${importLine}\n\n@Entity('${tableName}')\nexport class ${className} {\n${props}\n}\n`;
|
||||
}
|
||||
|
||||
let match;
|
||||
let count = 0;
|
||||
while ((match = tableRegex.exec(sql)) !== null) {
|
||||
const table = match[1];
|
||||
const body = match[2];
|
||||
const ts = generateEntity(table, body);
|
||||
const outFile = path.join(outDir, `${table}.ts`);
|
||||
fs.writeFileSync(outFile, ts, 'utf8');
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log(`Generated ${count} entities into ${path.relative(repoRoot, outDir)}`);
|
||||
261
tools/migration-completeness-report.md
Normal file
261
tools/migration-completeness-report.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# PHP迁移完整性检查报告
|
||||
|
||||
生成时间: 2025-09-16T06:14:25.046Z
|
||||
|
||||
## 📊 总体统计
|
||||
|
||||
- **PHP模块总数**: 25
|
||||
- **NestJS模块总数**: 48
|
||||
- **迁移完整性**: 18%
|
||||
- **缺失模块数**: 0
|
||||
- **缺失控制器数**: 110
|
||||
- **缺失方法数**: 7
|
||||
|
||||
## ❌ 缺失模块列表
|
||||
|
||||
✅ 所有模块已迁移
|
||||
|
||||
## ❌ 缺失控制器列表
|
||||
|
||||
- **addon/adminapi**: Addon (20 个方法)
|
||||
- **addon/adminapi**: AddonDevelop (9 个方法)
|
||||
- **addon/adminapi**: App (1 个方法)
|
||||
- **addon/adminapi**: Backup (9 个方法)
|
||||
- **addon/adminapi**: Upgrade (9 个方法)
|
||||
- **addon/api**: Addon (1 个方法)
|
||||
- **aliapp/adminapi**: Config (3 个方法)
|
||||
- **applet/adminapi**: SiteVersion (4 个方法)
|
||||
- **applet/adminapi**: Version (7 个方法)
|
||||
- **applet/adminapi**: VersionDownload (1 个方法)
|
||||
- **channel/adminapi**: H5 (2 个方法)
|
||||
- **channel/adminapi**: Pc (2 个方法)
|
||||
- **dict/adminapi**: Dict (8 个方法)
|
||||
- **diy/adminapi**: Config (3 个方法)
|
||||
- **diy/adminapi**: Diy (23 个方法)
|
||||
- **diy/adminapi**: DiyForm (24 个方法)
|
||||
- **diy/adminapi**: DiyRoute (8 个方法)
|
||||
- **diy/api**: Diy (4 个方法)
|
||||
- **diy/api**: DiyForm (6 个方法)
|
||||
- **generator/adminapi**: Generator (12 个方法)
|
||||
- **home/adminapi**: Site (6 个方法)
|
||||
- **login/adminapi**: Captcha (3 个方法)
|
||||
- **login/adminapi**: Config (2 个方法)
|
||||
- **login/adminapi**: Login (3 个方法)
|
||||
- **login/api**: Config (1 个方法)
|
||||
- **login/api**: Login (6 个方法)
|
||||
- **login/api**: Register (2 个方法)
|
||||
- **member/adminapi**: Account (13 个方法)
|
||||
- **member/adminapi**: Address (4 个方法)
|
||||
- **member/adminapi**: CashOut (10 个方法)
|
||||
- **member/adminapi**: Config (10 个方法)
|
||||
- **member/adminapi**: Member (20 个方法)
|
||||
- **member/adminapi**: MemberLabel (6 个方法)
|
||||
- **member/adminapi**: MemberLevel (6 个方法)
|
||||
- **member/adminapi**: MemberSign (4 个方法)
|
||||
- **member/api**: Account (8 个方法)
|
||||
- **member/api**: Address (5 个方法)
|
||||
- **member/api**: CashOutAccount (6 个方法)
|
||||
- **member/api**: Level (1 个方法)
|
||||
- **member/api**: Member (8 个方法)
|
||||
- **member/api**: MemberCashOut (7 个方法)
|
||||
- **member/api**: MemberSign (6 个方法)
|
||||
- **niucloud/adminapi**: Cloud (8 个方法)
|
||||
- **niucloud/adminapi**: Module (6 个方法)
|
||||
- **notice/adminapi**: NiuSms (28 个方法)
|
||||
- **notice/adminapi**: Notice (7 个方法)
|
||||
- **notice/adminapi**: NoticeLog (2 个方法)
|
||||
- **notice/adminapi**: SmsLog (2 个方法)
|
||||
- **pay/adminapi**: Pay (8 个方法)
|
||||
- **pay/adminapi**: PayChannel (6 个方法)
|
||||
- **pay/adminapi**: PayRefund (5 个方法)
|
||||
- **pay/adminapi**: Transfer (3 个方法)
|
||||
- **pay/api**: Pay (6 个方法)
|
||||
- **pay/api**: Transfer (1 个方法)
|
||||
- **poster/adminapi**: Poster (1 个方法)
|
||||
- **poster/api**: Poster (1 个方法)
|
||||
- **site/adminapi**: Site (17 个方法)
|
||||
- **site/adminapi**: SiteAccount (4 个方法)
|
||||
- **site/adminapi**: SiteGroup (7 个方法)
|
||||
- **site/adminapi**: User (8 个方法)
|
||||
- **site/adminapi**: UserLog (3 个方法)
|
||||
- **stat/adminapi**: SiteStat (1 个方法)
|
||||
- **stat/adminapi**: Stat (1 个方法)
|
||||
- **sys/adminapi**: Agreement (3 个方法)
|
||||
- **sys/adminapi**: App (1 个方法)
|
||||
- **sys/adminapi**: Area (5 个方法)
|
||||
- **sys/adminapi**: Attachment (9 个方法)
|
||||
- **sys/adminapi**: Channel (1 个方法)
|
||||
- **sys/adminapi**: Common (2 个方法)
|
||||
- **sys/adminapi**: Config (14 个方法)
|
||||
- **sys/adminapi**: Export (6 个方法)
|
||||
- **sys/adminapi**: Menu (11 个方法)
|
||||
- **sys/adminapi**: Poster (12 个方法)
|
||||
- **sys/adminapi**: Printer (18 个方法)
|
||||
- **sys/adminapi**: Role (7 个方法)
|
||||
- **sys/adminapi**: Schedule (11 个方法)
|
||||
- **sys/adminapi**: ScheduleLog (3 个方法)
|
||||
- **sys/adminapi**: System (9 个方法)
|
||||
- **sys/adminapi**: Ueditor (2 个方法)
|
||||
- **sys/api**: Area (4 个方法)
|
||||
- **sys/api**: Config (7 个方法)
|
||||
- **sys/api**: Index (2 个方法)
|
||||
- **sys/api**: Scan (1 个方法)
|
||||
- **sys/api**: Task (2 个方法)
|
||||
- **sys/api**: Verify (6 个方法)
|
||||
- **upload/adminapi**: Storage (3 个方法)
|
||||
- **upload/adminapi**: Upload (5 个方法)
|
||||
- **upload/api**: Upload (4 个方法)
|
||||
- **user/adminapi**: User (13 个方法)
|
||||
- **verify/adminapi**: Verifier (7 个方法)
|
||||
- **verify/adminapi**: Verify (2 个方法)
|
||||
- **weapp/adminapi**: Config (5 个方法)
|
||||
- **weapp/adminapi**: Delivery (1 个方法)
|
||||
- **weapp/adminapi**: Package (2 个方法)
|
||||
- **weapp/adminapi**: Template (2 个方法)
|
||||
- **weapp/adminapi**: Version (6 个方法)
|
||||
- **weapp/api**: Serve (1 个方法)
|
||||
- **weapp/api**: Weapp (6 个方法)
|
||||
- **wechat/adminapi**: Config (3 个方法)
|
||||
- **wechat/adminapi**: Media (4 个方法)
|
||||
- **wechat/adminapi**: Menu (2 个方法)
|
||||
- **wechat/adminapi**: Reply (9 个方法)
|
||||
- **wechat/adminapi**: Template (2 个方法)
|
||||
- **wechat/api**: Serve (1 个方法)
|
||||
- **wechat/api**: Wechat (10 个方法)
|
||||
- **wxoplatform/adminapi**: Config (3 个方法)
|
||||
- **wxoplatform/adminapi**: Oplatform (3 个方法)
|
||||
- **wxoplatform/adminapi**: Server (2 个方法)
|
||||
- **wxoplatform/adminapi**: WeappVersion (7 个方法)
|
||||
- **agreement/api**: Agreement (1 个方法)
|
||||
|
||||
## ❌ 缺失方法列表
|
||||
|
||||
- **auth/Auth**: authMenuList()
|
||||
- **auth/Auth**: getAuthAddonList()
|
||||
- **auth/Auth**: get()
|
||||
- **auth/Auth**: modify()
|
||||
- **auth/Auth**: edit()
|
||||
- **auth/Auth**: site()
|
||||
- **auth/Auth**: getShowMenuList()
|
||||
|
||||
## ➕ 额外模块列表
|
||||
|
||||
- captcha
|
||||
- cash_out
|
||||
- common
|
||||
- diy_form
|
||||
- diy_form_export
|
||||
- http
|
||||
- install
|
||||
- job
|
||||
- member_export
|
||||
- Menu
|
||||
- notice_template
|
||||
- paytype
|
||||
- printer
|
||||
- qrcode
|
||||
- queue
|
||||
- Resetpassword
|
||||
- scan
|
||||
- schedule
|
||||
- system
|
||||
- transfer
|
||||
- upgrade
|
||||
- WorkerCommand
|
||||
- workerman
|
||||
|
||||
## 🎯 改进建议
|
||||
|
||||
- 需要创建 110 个缺失的控制器
|
||||
- 需要实现 7 个缺失的方法
|
||||
- 迁移完整性较低,建议优先完成核心模块的迁移
|
||||
- 发现 23 个额外模块,请确认是否为新增功能
|
||||
|
||||
## 📋 详细模块对比
|
||||
|
||||
### PHP项目模块结构
|
||||
- **addon**: 5 个管理端控制器, 1 个前台控制器
|
||||
- **aliapp**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **applet**: 3 个管理端控制器, 0 个前台控制器
|
||||
- **auth**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **channel**: 2 个管理端控制器, 0 个前台控制器
|
||||
- **dict**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **diy**: 4 个管理端控制器, 2 个前台控制器
|
||||
- **generator**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **home**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **login**: 3 个管理端控制器, 3 个前台控制器
|
||||
- **member**: 8 个管理端控制器, 7 个前台控制器
|
||||
- **niucloud**: 2 个管理端控制器, 0 个前台控制器
|
||||
- **notice**: 4 个管理端控制器, 0 个前台控制器
|
||||
- **pay**: 4 个管理端控制器, 2 个前台控制器
|
||||
- **poster**: 1 个管理端控制器, 1 个前台控制器
|
||||
- **site**: 5 个管理端控制器, 0 个前台控制器
|
||||
- **stat**: 2 个管理端控制器, 0 个前台控制器
|
||||
- **sys**: 16 个管理端控制器, 6 个前台控制器
|
||||
- **upload**: 2 个管理端控制器, 1 个前台控制器
|
||||
- **user**: 1 个管理端控制器, 0 个前台控制器
|
||||
- **verify**: 2 个管理端控制器, 0 个前台控制器
|
||||
- **weapp**: 5 个管理端控制器, 2 个前台控制器
|
||||
- **wechat**: 5 个管理端控制器, 2 个前台控制器
|
||||
- **wxoplatform**: 4 个管理端控制器, 0 个前台控制器
|
||||
- **agreement**: 0 个管理端控制器, 1 个前台控制器
|
||||
|
||||
### NestJS项目模块结构
|
||||
- **addon**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **agreement**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **aliapp**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **applet**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **auth**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **captcha**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **cash_out**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **channel**: 0 个控制器, 0 个服务, 4 个实体
|
||||
- **common**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **dict**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **diy**: 0 个控制器, 0 个服务, 9 个实体
|
||||
- **diy_form**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **diy_form_export**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **generator**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **home**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **http**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **install**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **job**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **login**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **member**: 0 个控制器, 0 个服务, 11 个实体
|
||||
- **member_export**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **Menu**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **niucloud**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **notice**: 0 个控制器, 0 个服务, 3 个实体
|
||||
- **notice_template**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **pay**: 0 个控制器, 0 个服务, 4 个实体
|
||||
- **paytype**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **poster**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **printer**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **qrcode**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **queue**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **Resetpassword**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **scan**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **schedule**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **site**: 0 个控制器, 0 个服务, 7 个实体
|
||||
- **stat**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **sys**: 0 个控制器, 0 个服务, 26 个实体
|
||||
- **system**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **transfer**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **upgrade**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **upload**: 0 个控制器, 3 个服务, 1 个实体
|
||||
- **user**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **verify**: 0 个控制器, 0 个服务, 1 个实体
|
||||
- **weapp**: 0 个控制器, 0 个服务, 2 个实体
|
||||
- **wechat**: 0 个控制器, 0 个服务, 5 个实体
|
||||
- **WorkerCommand**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **workerman**: 1 个控制器, 1 个服务, 1 个实体
|
||||
- **wxoplatform**: 0 个控制器, 0 个服务, 2 个实体
|
||||
|
||||
## 🔧 下一步行动计划
|
||||
|
||||
1. **优先级1**: 完成缺失的核心模块迁移
|
||||
2. **优先级2**: 补全缺失的控制器和方法
|
||||
3. **优先级3**: 验证业务逻辑一致性
|
||||
4. **优先级4**: 完善测试覆盖率
|
||||
|
||||
---
|
||||
*报告由 PHP迁移完整性检查器 自动生成*
|
||||
97
tools/scan-guards.js
Normal file
97
tools/scan-guards.js
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const srcRoot = path.join(repoRoot, 'wwjcloud', 'src');
|
||||
|
||||
function isTypescriptFile(filePath) {
|
||||
return filePath.endsWith('.ts') && !filePath.endsWith('.d.ts') && !filePath.endsWith('.spec.ts');
|
||||
}
|
||||
|
||||
function walk(dir, collected = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath, collected);
|
||||
} else if (entry.isFile() && isTypescriptFile(fullPath)) {
|
||||
collected.push(fullPath);
|
||||
}
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
function isAdminApiControllerFile(filePath) {
|
||||
return filePath.includes(path.join('controllers', 'adminapi') + path.sep);
|
||||
}
|
||||
|
||||
function extractControllerInfo(fileContent) {
|
||||
const controllerMatch = fileContent.match(/@Controller\(([^)]*)\)/);
|
||||
const basePathLiteral = controllerMatch ? controllerMatch[1] : '';
|
||||
let basePath = '';
|
||||
if (basePathLiteral) {
|
||||
const strMatch = basePathLiteral.match(/['"`]([^'"`]*)['"`]/);
|
||||
basePath = strMatch ? strMatch[1] : '';
|
||||
}
|
||||
|
||||
const classDeclIdx = fileContent.indexOf('export class');
|
||||
const header = classDeclIdx > -1 ? fileContent.slice(0, classDeclIdx) : fileContent;
|
||||
const guardsSection = header;
|
||||
const hasUseGuards = /@UseGuards\(([^)]*)\)/.test(guardsSection);
|
||||
let guards = [];
|
||||
if (hasUseGuards) {
|
||||
const m = guardsSection.match(/@UseGuards\(([^)]*)\)/);
|
||||
if (m) {
|
||||
guards = m[1].split(',').map(s => s.trim());
|
||||
}
|
||||
}
|
||||
const hasJwt = guards.some(g => /JwtAuthGuard/.test(g));
|
||||
const hasRoles = guards.some(g => /RolesGuard/.test(g));
|
||||
|
||||
return { basePath, hasJwt, hasRoles };
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(srcRoot)) {
|
||||
console.error(`src root not found: ${srcRoot}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const allTsFiles = walk(srcRoot);
|
||||
const adminControllers = allTsFiles.filter(isAdminApiControllerFile);
|
||||
|
||||
const problems = [];
|
||||
for (const filePath of adminControllers) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
if (!/@Controller\(/.test(content)) continue;
|
||||
const info = extractControllerInfo(content);
|
||||
const rel = path.relative(repoRoot, filePath);
|
||||
|
||||
const missing = [];
|
||||
if (!info.hasJwt) missing.push('JwtAuthGuard');
|
||||
if (!info.hasRoles) missing.push('RolesGuard');
|
||||
if (missing.length > 0) {
|
||||
problems.push({ file: rel, basePath: info.basePath || '', missing });
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length === 0) {
|
||||
console.log('OK: All adminapi controllers have class-level JwtAuthGuard and RolesGuard.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('file,basePath,missingGuards');
|
||||
for (const p of problems) {
|
||||
console.log(`${p.file},${p.basePath},${p.missing.join('|')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error('scan-guards failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
342
tools/structure-validator.js
Normal file
342
tools/structure-validator.js
Normal file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* NestJS项目结构验证器
|
||||
* 检查项目目录结构、分层规范、命名规范等
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class StructureValidator {
|
||||
constructor() {
|
||||
this.projectRoot = process.cwd();
|
||||
this.srcRoot = path.join(this.projectRoot, 'wwjcloud', 'src');
|
||||
this.commonRoot = path.join(this.srcRoot, 'common');
|
||||
this.issues = [];
|
||||
this.stats = {
|
||||
modules: 0,
|
||||
controllers: 0,
|
||||
services: 0,
|
||||
entities: 0,
|
||||
dtos: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加问题记录
|
||||
*/
|
||||
addIssue(type, message, path = '') {
|
||||
this.issues.push({
|
||||
type,
|
||||
message,
|
||||
path,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查基础目录结构
|
||||
*/
|
||||
checkBaseStructure() {
|
||||
console.log('🏗️ 检查基础目录结构...');
|
||||
|
||||
const requiredDirs = [
|
||||
'wwjcloud/src',
|
||||
'wwjcloud/src/common',
|
||||
'wwjcloud/src/config',
|
||||
'wwjcloud/src/core',
|
||||
'wwjcloud/src/vendor'
|
||||
];
|
||||
|
||||
for (const dir of requiredDirs) {
|
||||
const fullPath = path.join(this.projectRoot, dir);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
this.addIssue('structure', `缺少必需目录: ${dir}`, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模块结构
|
||||
*/
|
||||
checkModuleStructure() {
|
||||
console.log('📦 检查模块结构...');
|
||||
|
||||
if (!fs.existsSync(this.commonRoot)) {
|
||||
this.addIssue('structure', 'common目录不存在', this.commonRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
const modules = fs.readdirSync(this.commonRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => entry.name);
|
||||
|
||||
this.stats.modules = modules.length;
|
||||
|
||||
for (const moduleName of modules) {
|
||||
this.validateModule(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单个模块
|
||||
*/
|
||||
validateModule(moduleName) {
|
||||
const modulePath = path.join(this.commonRoot, moduleName);
|
||||
const moduleFile = path.join(modulePath, `${moduleName}.module.ts`);
|
||||
|
||||
// 检查模块文件
|
||||
if (!fs.existsSync(moduleFile)) {
|
||||
this.addIssue('module', `缺少模块文件: ${moduleName}.module.ts`, moduleFile);
|
||||
}
|
||||
|
||||
// 检查标准目录结构
|
||||
const expectedDirs = ['controllers', 'services', 'entities', 'dto'];
|
||||
const optionalDirs = ['guards', 'decorators', 'interfaces', 'enums'];
|
||||
|
||||
for (const dir of expectedDirs) {
|
||||
const dirPath = path.join(modulePath, dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
this.addIssue('structure', `模块 ${moduleName} 缺少 ${dir} 目录`, dirPath);
|
||||
} else {
|
||||
this.validateModuleDirectory(moduleName, dir, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查控制器分层
|
||||
this.checkControllerLayers(moduleName, modulePath);
|
||||
|
||||
// 检查服务分层
|
||||
this.checkServiceLayers(moduleName, modulePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模块目录
|
||||
*/
|
||||
validateModuleDirectory(moduleName, dirType, dirPath) {
|
||||
const files = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
if (file.isFile() && file.name.endsWith('.ts')) {
|
||||
this.validateFileName(moduleName, dirType, file.name, path.join(dirPath, file.name));
|
||||
|
||||
// 统计文件数量
|
||||
if (dirType === 'controllers') this.stats.controllers++;
|
||||
else if (dirType === 'services') this.stats.services++;
|
||||
else if (dirType === 'entities') this.stats.entities++;
|
||||
else if (dirType === 'dto') this.stats.dtos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件命名
|
||||
*/
|
||||
validateFileName(moduleName, dirType, fileName, filePath) {
|
||||
const expectedPatterns = {
|
||||
controllers: /^[a-z][a-zA-Z0-9]*\.controller\.ts$/,
|
||||
services: /^[a-z][a-zA-Z0-9]*\.service\.ts$/,
|
||||
entities: /^[a-z][a-zA-Z0-9]*\.entity\.ts$/,
|
||||
dto: /^[A-Z][a-zA-Z0-9]*Dto\.ts$/
|
||||
};
|
||||
|
||||
const pattern = expectedPatterns[dirType];
|
||||
if (pattern && !pattern.test(fileName)) {
|
||||
this.addIssue('naming',
|
||||
`文件命名不符合规范: ${fileName} (应符合 ${pattern})`,
|
||||
filePath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查控制器分层
|
||||
*/
|
||||
checkControllerLayers(moduleName, modulePath) {
|
||||
const controllersPath = path.join(modulePath, 'controllers');
|
||||
if (!fs.existsSync(controllersPath)) return;
|
||||
|
||||
const expectedLayers = ['adminapi', 'api'];
|
||||
let hasLayers = false;
|
||||
|
||||
for (const layer of expectedLayers) {
|
||||
const layerPath = path.join(controllersPath, layer);
|
||||
if (fs.existsSync(layerPath)) {
|
||||
hasLayers = true;
|
||||
this.validateLayerFiles(moduleName, 'controllers', layer, layerPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有直接在controllers目录下的文件
|
||||
const directFiles = fs.readdirSync(controllersPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.controller.ts'));
|
||||
|
||||
if (directFiles.length > 0 && hasLayers) {
|
||||
this.addIssue('structure',
|
||||
`模块 ${moduleName} 的控制器既有分层又有直接文件,建议统一结构`,
|
||||
controllersPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务分层
|
||||
*/
|
||||
checkServiceLayers(moduleName, modulePath) {
|
||||
const servicesPath = path.join(modulePath, 'services');
|
||||
if (!fs.existsSync(servicesPath)) return;
|
||||
|
||||
const expectedLayers = ['admin', 'api', 'core'];
|
||||
|
||||
for (const layer of expectedLayers) {
|
||||
const layerPath = path.join(servicesPath, layer);
|
||||
if (fs.existsSync(layerPath)) {
|
||||
this.validateLayerFiles(moduleName, 'services', layer, layerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分层文件
|
||||
*/
|
||||
validateLayerFiles(moduleName, dirType, layer, layerPath) {
|
||||
const files = fs.readdirSync(layerPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.ts'));
|
||||
|
||||
if (files.length === 0) {
|
||||
this.addIssue('structure',
|
||||
`模块 ${moduleName} 的 ${dirType}/${layer} 目录为空`,
|
||||
layerPath
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
this.validateFileName(moduleName, dirType, file.name, path.join(layerPath, file.name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查依赖关系
|
||||
*/
|
||||
checkDependencies() {
|
||||
console.log('🔗 检查依赖关系...');
|
||||
|
||||
// 这里可以添加更复杂的依赖关系检查
|
||||
// 例如检查循环依赖、不当的跨层依赖等
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查代码质量
|
||||
*/
|
||||
checkCodeQuality() {
|
||||
console.log('✨ 检查代码质量...');
|
||||
|
||||
// 检查是否有空的类或方法
|
||||
// 检查是否有TODO注释
|
||||
// 检查是否有硬编码值等
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成报告
|
||||
*/
|
||||
generateReport() {
|
||||
console.log('\n📊 验证报告');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
// 统计信息
|
||||
console.log('📈 项目统计:');
|
||||
console.log(` 模块数量: ${this.stats.modules}`);
|
||||
console.log(` 控制器数量: ${this.stats.controllers}`);
|
||||
console.log(` 服务数量: ${this.stats.services}`);
|
||||
console.log(` 实体数量: ${this.stats.entities}`);
|
||||
console.log(` DTO数量: ${this.stats.dtos}`);
|
||||
|
||||
// 问题分类统计
|
||||
const issuesByType = this.issues.reduce((acc, issue) => {
|
||||
acc[issue.type] = (acc[issue.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log('\n🚨 问题统计:');
|
||||
if (Object.keys(issuesByType).length === 0) {
|
||||
console.log(' ✅ 未发现问题');
|
||||
} else {
|
||||
for (const [type, count] of Object.entries(issuesByType)) {
|
||||
console.log(` ${type}: ${count} 个问题`);
|
||||
}
|
||||
}
|
||||
|
||||
// 详细问题列表
|
||||
if (this.issues.length > 0) {
|
||||
console.log('\n📋 详细问题列表:');
|
||||
|
||||
const groupedIssues = this.issues.reduce((acc, issue) => {
|
||||
if (!acc[issue.type]) acc[issue.type] = [];
|
||||
acc[issue.type].push(issue);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const [type, issues] of Object.entries(groupedIssues)) {
|
||||
console.log(`\n${type.toUpperCase()} 问题:`);
|
||||
for (const issue of issues) {
|
||||
console.log(` ❌ ${issue.message}`);
|
||||
if (issue.path) {
|
||||
console.log(` 路径: ${issue.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 建议
|
||||
console.log('\n💡 改进建议:');
|
||||
if (this.issues.length === 0) {
|
||||
console.log(' 🎉 项目结构良好,继续保持!');
|
||||
} else {
|
||||
console.log(' 1. 优先解决结构性问题');
|
||||
console.log(' 2. 统一命名规范');
|
||||
console.log(' 3. 完善缺失的文件和目录');
|
||||
console.log(' 4. 定期运行此工具进行检查');
|
||||
}
|
||||
|
||||
return this.issues.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行验证
|
||||
*/
|
||||
async run() {
|
||||
console.log('🔍 NestJS项目结构验证器');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
try {
|
||||
this.checkBaseStructure();
|
||||
this.checkModuleStructure();
|
||||
this.checkDependencies();
|
||||
this.checkCodeQuality();
|
||||
|
||||
const isValid = this.generateReport();
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
if (isValid) {
|
||||
console.log('✅ 验证通过!项目结构符合规范。');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('❌ 验证失败!发现结构问题,请查看上述报告。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 验证过程中出现错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运行验证器
|
||||
if (require.main === module) {
|
||||
const validator = new StructureValidator();
|
||||
validator.run().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = StructureValidator;
|
||||
Reference in New Issue
Block a user