This commit is contained in:
xiaopeng 2025-01-26 12:37:27 +08:00
parent 657f009408
commit f004db2058
8 changed files with 403 additions and 25 deletions

286
.cursorrules Normal file
View File

@ -0,0 +1,286 @@
# 项目背景
- 数据库MySQL、Sqlite、Postgres、Typeorm
- 语言TypeScript、JavaScript、CommonJS
- 框架Koa.js、midway.js、cool-admin-midway
- 项目版本8.x
# 目录
项目目录:
├── .vscode(代码片段,根据关键字可以快速地生成代码)
├── public(静态资源文件如js、css或者上传的文件)
├── src
│ └── comm(通用库)
│ └── modules(项目模块)
│ └── config
│ │ └── config.default.ts(默认配置,不区分环境,都生效)
│ │ └── config.local.ts(本地开发配置对应npm run dev)
│ │ └── config.prod.ts(生产环境配置对应npm run start)
│ └── configuration.ts(midway的配置文件)
│ └── welcome.ts(环境的controller)
│ └── interface.ts(类型声明)
├── package.json(依赖管理,项目信息)
├── bootstrap.js(生产环境启动入口文件可借助pm2等工具多进程启动)
└── ...
模块目录
├── modules
│ └── base(基础的权限管理系统)
│ │ └── controller(api接口)
│ │ └── dto(参数校验)
│ │ └── entity(实体类)
│ │ └── middleware(中间件)
│ │ └── schedule(定时任务)
│ │ └── service(服务,写业务逻辑)
│ │ └── config.ts(必须,模块的配置)
│ │ └── db.json(可选,初始化该模块的数据)
│ │ └── menu.json(可选,初始化该模块的菜单)
# 核心
- Controller
```ts
import { Get, Provide } from "@midwayjs/core";
import { CoolController, BaseController } from "@cool-midway/core";
import { BaseSysUserEntity } from "../../../base/entity/sys/user";
import { DemoAppGoodsEntity } from "../../entity/goods";
/**
* 商品
*/
@Provide()
@CoolController({
// 添加通用CRUD接口
api: ["add", "delete", "update", "info", "list", "page"],
// 设置表实体
entity: DemoAppGoodsEntity,
// 向表插入当前登录用户ID
insertParam: (ctx) => {
return {
// 获得当前登录的后台用户ID需要请求头传Authorization参数
userId: ctx.admin.userId,
};
},
// 操作crud之前做的事情 @cool-midway/core@3.2.14 新增
before: (ctx) => {
// 将前端的数据转JSON格式存数据库
const { data } = ctx.request.body;
ctx.request.body.data = JSON.stringify(data);
},
// info接口忽略价格字段
infoIgnoreProperty: ["a.price"],
// 分页查询配置
pageQueryOp: {
// 让title字段支持模糊查询
keyWordLikeFields: ["a.title"],
// 让type字段支持筛选请求筛选字段与表字段一致是情况
fieldEq: ["a.type"],
// 多表关联,请求筛选字段与表字段不一致的情况
fieldEq: [{ column: "a.id", requestParam: "id" }],
// 指定返回字段,注意多表查询这个是必要的,否则会出现重复字段的问题
select: ["a.*", "b.name", "a.name AS userName"],
// 4.x置为过时 改用 join 关联表用户表
leftJoin: [
{
entity: BaseSysUserEntity,
alias: "b",
condition: "a.userId = b.id",
},
],
// 4.x新增
join: [
{
entity: BaseSysUserEntity,
alias: "b",
condition: "a.userId = b.id",
type: "innerJoin",
},
],
// 4.x 新增 追加其他条件
extend: async (find: SelectQueryBuilder<DemoGoodsEntity>) => {
find.groupBy("a.id");
},
// 增加其他条件
where: async (ctx) => {
// 获取body参数
const { a } = ctx.request.body;
return [
// 价格大于90
["a.price > :price", { price: 90.0 }],
// 满足条件才会执行
["a.price > :price", { price: 90.0 }, "条件"],
// 多个条件一起
[
"(a.price = :price or a.userId = :userId)",
{ price: 90.0, userId: ctx.admin.userId },
],
];
},
// 可选添加排序默认按createTime Desc排序
addOrderBy: {
price: "desc",
},
},
})
export class DemoAppGoodsController extends BaseController {
/**
* 其他接口
*/
@Get("/other")
async other() {
return this.ok("hello, cool-admin!!!");
}
}
```
注意:
- CoolController的entityalias 为 "a";
- 如果是多表查询,必须设置 select 参数,否则会出现重复字段的错误,因为每个表都继承了 BaseEntity至少都有 id、createTime、updateTime 三个相同的字段;
- keyWordLikeFields、fieldEq等配置哪个字段都需要有对应的别名
- Entity
```ts
// BaseEntity的路径是固定不能修改
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
/**
* demo模块-用户信息
*/
// 表名必须包含模块固定格式模块_
@Entity('demo_user_info')
// DemoUserInfoEntity是模块+表名+Entity
export class DemoUserInfoEntity extends BaseEntity {
@Index()
@Column({ comment: '手机号', length: 11 })
phone: string;
@Index({ unique: true })
@Column({ comment: '身份证', length: 50 })
idCard: string;
// 生日只需要精确到哪一天所以type:'date',如果需要精确到时分秒,应为'datetime'
@Column({ comment: '生日', type: 'date' })
birthday: Date;
@Column({ comment: '状态', dict: ['禁用', '启用'], default: 1 })
status: number;
@Column({ comment: '分类', dict: ['普通', '会员', '超级会员'], default: 0, type: 'tinyint' })
type: number;
// 由于labels的类型是一个数组所以Column中的type类型必须得是'json'
@Column({ comment: '标签', nullable: true, type: 'json' })
labels: string[];
@Column({
comment: '余额',
type: 'decimal',
precision: 12,
scale: 2,
})
balance: number;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '简介', type: 'text', nullable: true })
summary: string;
@Column({ comment: '省', length: 50 })
province: string;
@Column({ comment: '市', length: 50 })
city: string;
@Column({ comment: '区', length: 50 })
district: string;
@Column({ comment: '详细地址', length: 255 })
address: string;
}
```
注意:
- 禁止使用外键如ManyToOne、JoinColumn等
- comment需要简短如班级表的名称不要叫班级名称直接叫名称
- dict如果遇到可选项如状态、类型等需要配置
- BaseEntity的路径是固定不能修改
- Service
```ts
import { Init, Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
/**
* 描述
*/
@Provide()
export class XxxService extends BaseService {
@InjectEntityModel(实体)
xxxEntity: Repository<实体>;
// 获得ctx
@Inject()
ctx;
@Init()
async init() {
await super.init();
this.setEntity(this.xxxEntity);
}
/**
* 其它方法
*/
async xxx() {}
}
```
- 自动路由
规则: /controller 文件夹下的文件夹名或者文件名/模块文件夹名/方法名
// 模块目录
├── modules
│ └── demo(模块名)
│ │ └── controller(api接口)
│ │ │ └── app(参数校验)
│ │ │ │ └── goods.ts(商品的controller)
│ │ │ └── pay.ts(支付的controller)
│ │ └── config.ts(必须,模块的配置)
│ │ └── init.sql(可选初始化该模块的sql)
生成的路由前缀为: /pay/demo/xxx(具体的方法)与/app/demo/goods/xxx(具体的方法)
- config.ts
```ts
import { ModuleConfig } from '@cool-midway/core';
/**
* 模块配置
*/
export default () => {
return {
// 模块名称
name: 'xxx',
// 模块描述
description: 'xxx',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};
```
# 其它
- 根据需要进行必要的关联表查询;
- 禁止出现import 但是没有使用的情况;
- 所有代码需有类级注释;

View File

@ -19,10 +19,10 @@
"@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.20.0",
"@midwayjs/validate": "^3.20.0",
"@midwayjs/view-ejs": "^3.20.0",
"adm-zip": "^0.5.16",
"axios": "^1.7.9",
"cron": "^3.5.0",
"deasync": "^0.1.30",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
@ -75,11 +75,10 @@
],
"assets": [
"public/**/*",
"typings/**/*",
"node_modules/sqlite3/build/Release/node_sqlite3.node"
"typings/**/*"
],
"targets": [
"node20-win-x64"
"node20-macos-arm64"
],
"outputPath": "build"
},

35
pnpm-lock.yaml generated
View File

@ -53,9 +53,6 @@ importers:
'@midwayjs/validate':
specifier: ^3.20.0
version: 3.20.0
'@midwayjs/view-ejs':
specifier: ^3.20.0
version: 3.20.0
adm-zip:
specifier: ^0.5.16
version: 0.5.16
@ -65,6 +62,9 @@ importers:
cron:
specifier: ^3.5.0
version: 3.5.0
deasync:
specifier: ^0.1.30
version: 0.1.30
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
@ -740,14 +740,6 @@ packages:
resolution: {integrity: sha512-LCMna/wAz4LDRKyMQh8Uoh+2W4qhdNJ4frAX5gzrxIUbCN5uTbBpmqqYSLA/j24ItSye4htSdQ7r+H9WM+LUIw==}
engines: {node: '>=12'}
'@midwayjs/view-ejs@3.20.0':
resolution: {integrity: sha512-HyFAeE6UqmmY7mWDeMsTKXYOARBIAL4Ce9l28hjJudJxTokf514l9OeRpPyWCb6hwMJf/AKgqyYZBcGFLiXhNA==}
engines: {node: '>=12'}
'@midwayjs/view@3.20.0':
resolution: {integrity: sha512-rb8hrrfjnA0t7HFoiVb9JqCKzB/OULDbVRBy2gDy2EcnN1da7RtX7m7VU8hCAkqC7ClGDOOhghaYigmp/iVn6A==}
engines: {node: '>=12'}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
cpu: [arm64]
@ -1646,6 +1638,10 @@ packages:
dayjs@1.8.36:
resolution: {integrity: sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==}
deasync@0.1.30:
resolution: {integrity: sha512-OaAjvEQuQ9tJsKG4oHO9nV1UHTwb2Qc2+fadB0VeVtD0Z9wiG1XPGLJ4W3aLhAoQSYTaLROFRbd5X20Dkzf7MQ==}
engines: {node: '>=0.11.0'}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@ -3401,6 +3397,9 @@ packages:
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
node-addon-api@1.7.2:
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
@ -5432,13 +5431,6 @@ snapshots:
'@midwayjs/i18n': 3.20.0
joi: 17.13.3
'@midwayjs/view-ejs@3.20.0':
dependencies:
'@midwayjs/view': 3.20.0
ejs: 3.1.10
'@midwayjs/view@3.20.0': {}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
optional: true
@ -6511,6 +6503,11 @@ snapshots:
dayjs@1.8.36: {}
deasync@0.1.30:
dependencies:
bindings: 1.5.0
node-addon-api: 1.7.2
debug@3.2.7:
dependencies:
ms: 2.1.3
@ -8507,6 +8504,8 @@ snapshots:
node-abort-controller@3.1.1: {}
node-addon-api@1.7.2: {}
node-addon-api@7.1.1: {}
node-fetch@2.7.0(encoding@0.1.13):

49
src/comm/port.ts Normal file
View File

@ -0,0 +1,49 @@
const net = require('net');
const deasync = require('deasync');
/**
*
* @param {number} port -
* @returns {boolean} -
*/
function isPortAvailableSync(port) {
let available = false;
let checked = false; // 新增标志变量,表示检查是否完成
const server = net.createServer();
server.once('error', err => {
if (err.code === 'EADDRINUSE') {
checked = true; // 标记检查完成
server.close();
}
});
server.once('listening', () => {
available = true; // 标记端口可用
checked = true; // 标记检查完成
server.close();
});
server.listen(port);
// 阻塞直到检查完成checked === true
deasync.loopWhile(() => !checked);
return available;
}
/**
*
* @param {number} startPort -
* @returns {number} -
*/
export function checkPort(startPort: number) {
let port = startPort;
while (port <= 65535) {
if (isPortAvailableSync(port)) {
console.log(`Valid port: ${port}`);
return port;
}
port++;
}
throw new Error('No available port found');
}

View File

@ -3,6 +3,7 @@ import { MidwayConfig } from '@midwayjs/core';
import { CoolCacheStore } from '@cool-midway/core';
import * as path from 'path';
import { pCachePath, pUploadPath } from '../comm/path';
import { checkPort } from '../comm/port';
// redis缓存
// import { redisStore } from 'cache-manager-ioredis-yet';
@ -11,7 +12,7 @@ export default {
// 确保每个项目唯一,项目首次启动会自动生成
keys: '576848ea-bb0c-4c0c-ac95-c8602ef908b5',
koa: {
port: 8001,
port: checkPort(8001),
},
// 开启异步上下文管理
asyncContextManager: {

View File

@ -17,7 +17,7 @@ export default {
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true,
// 打印日志
logging: true,
logging: false,
// 实体路径
entities: ['**/modules/*/entity'],
// 订阅者

View File

@ -16,7 +16,7 @@ export default {
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true,
// 打印日志
logging: true,
logging: false,
// 实体路径
entities,
// 订阅者

View File

@ -0,0 +1,44 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { App, ILogger, IMidwayApplication, Inject } from '@midwayjs/core';
/**
*
*/
@CoolEvent()
export class BaseAppEvent {
@App()
app: IMidwayApplication;
@Inject()
logger: ILogger;
@Event('onServerReady')
async onServerReady() {
if (!process['pkg']) return;
const port = this.app.getConfig('koa.port') || 8001;
this.logger.info(`Server is running at http://127.0.0.1:${port}`);
const url = `http://127.0.0.1:${port}`;
// 使用 child_process 打开浏览器
const { exec } = require('child_process');
let command;
switch (process.platform) {
case 'darwin': // macOS
command = `open ${url}`;
break;
case 'win32': // Windows
command = `start ${url}`;
break;
default: // Linux
command = `xdg-open ${url}`;
break;
}
exec(command, (error: any) => {
if (!error) {
this.logger.info(`Application has opened in browser at ${url}`);
}
});
}
}