feat: 完成sys模块迁移,对齐PHP/Java框架

- 重构sys模块架构,严格按admin/api/core分层
- 对齐所有sys实体与数据库表结构
- 实现完整的adminapi控制器,匹配PHP/Java契约
- 修复依赖注入问题,确保服务正确注册
- 添加自动迁移工具和契约验证
- 完善多租户支持和审计功能
- 统一命名规范,与PHP业务逻辑保持一致
This commit is contained in:
万物街
2025-09-21 21:29:28 +08:00
parent 2e361795d9
commit 127a4db1e3
839 changed files with 24932 additions and 57988 deletions

129
tools/README.md Normal file
View 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或联系开发团队

View 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
View 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();

View 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();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2026
tools/contracts/routes.json Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"method": "GET",
"path": "member/benefits/content"
},
{
"method": "GET",
"path": "member/gifts/content"
},
{
"method": "GET",
"path": "sys/qrcode"
}
]

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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
View 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);
}
}

View 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();

View File

74
tools/gen-controllers.js Normal file
View 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();

View 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)}`);

View 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
View 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);
}
}

View 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;