create-skweb 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-check.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +65 -0
- package/CHANGELOG.md +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +199 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +115 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/rollup.config.mjs +71 -0
- package/src/cli.ts +118 -0
- package/src/index.ts +122 -0
- package/templates/cjs/ecosystem.config.js +32 -0
- package/templates/cjs/eslint.config.js +25 -0
- package/templates/cjs/package.json +46 -0
- package/templates/cjs/src/app.js +91 -0
- package/templates/cjs/src/commands/migrate.js +35 -0
- package/templates/cjs/src/commands/seed.js +58 -0
- package/templates/cjs/src/controllers/hello.js +33 -0
- package/templates/cjs/src/controllers/user.js +101 -0
- package/templates/cjs/src/cronActions/cleanup.js +12 -0
- package/templates/cjs/src/cronWorker.js +85 -0
- package/templates/cjs/src/index.js +7 -0
- package/templates/cjs/src/models/SysUser.js +38 -0
- package/templates/cjs/src/seeds/SysUser.json +1 -0
- package/templates/cjs/src/services/database.js +15 -0
- package/templates/cjs/src/utils/config.js +65 -0
- package/templates/cjs/src/utils/logger.js +53 -0
- package/templates/cjs/test/index.js +13 -0
- package/templates/mjs/ecosystem.config.js +32 -0
- package/templates/mjs/eslint.config.js +25 -0
- package/templates/mjs/package.json +46 -0
- package/templates/mjs/src/app.js +97 -0
- package/templates/mjs/src/commands/migrate.js +38 -0
- package/templates/mjs/src/commands/seed.js +63 -0
- package/templates/mjs/src/controllers/hello.js +33 -0
- package/templates/mjs/src/controllers/user.js +101 -0
- package/templates/mjs/src/cronActions/cleanup.js +9 -0
- package/templates/mjs/src/cronWorker.js +91 -0
- package/templates/mjs/src/index.js +8 -0
- package/templates/mjs/src/models/SysUser.js +40 -0
- package/templates/mjs/src/seeds/SysUser.json +1 -0
- package/templates/mjs/src/services/database.js +16 -0
- package/templates/mjs/src/utils/config.js +63 -0
- package/templates/mjs/src/utils/logger.js +52 -0
- package/templates/mjs/test/index.mjs +13 -0
- package/templates/ts/ecosystem.config.js +36 -0
- package/templates/ts/eslint.config.js +17 -0
- package/templates/ts/package.json +57 -0
- package/templates/ts/rollup.config.mjs +39 -0
- package/templates/ts/src/app.ts +96 -0
- package/templates/ts/src/commands/migrate.ts +38 -0
- package/templates/ts/src/commands/seed.ts +63 -0
- package/templates/ts/src/controllers/hello.ts +33 -0
- package/templates/ts/src/controllers/user.ts +101 -0
- package/templates/ts/src/cronActions/cleanup.ts +10 -0
- package/templates/ts/src/cronWorker.ts +90 -0
- package/templates/ts/src/index.ts +8 -0
- package/templates/ts/src/models/SysUser.ts +49 -0
- package/templates/ts/src/seeds/SysUser.json +1 -0
- package/templates/ts/src/services/database.ts +16 -0
- package/templates/ts/src/utils/config.ts +68 -0
- package/templates/ts/src/utils/logger.ts +63 -0
- package/templates/ts/test/index.mjs +13 -0
- package/templates/ts/tsconfig.json +20 -0
- package/test/index.mjs +137 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import express from 'skweb-express'
|
|
6
|
+
import expressLogger from 'skweb-express-logger'
|
|
7
|
+
import { WebFramework } from 'skweb-framework'
|
|
8
|
+
import logger from './utils/logger.js'
|
|
9
|
+
import { checkRequiredEnv } from './utils/config.js'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = path.dirname(__filename)
|
|
13
|
+
|
|
14
|
+
async function bootstrap(): Promise<void> {
|
|
15
|
+
logger.info('[app] Initializing web process...')
|
|
16
|
+
|
|
17
|
+
if (!checkRequiredEnv()) {
|
|
18
|
+
logger.error('[app] Configuration validation failed, exiting...')
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const framework = new WebFramework({
|
|
23
|
+
db: {
|
|
24
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
25
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
26
|
+
username: process.env.DB_USER || 'root',
|
|
27
|
+
password: process.env.DB_PASS || '',
|
|
28
|
+
database: process.env.DB_NAME || 'test-project',
|
|
29
|
+
dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
|
|
30
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
31
|
+
underscored: true,
|
|
32
|
+
logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
|
|
33
|
+
},
|
|
34
|
+
redis: {
|
|
35
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
36
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
37
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await framework.runPluginBeforeSetup()
|
|
42
|
+
await framework.setupModels(path.join(__dirname, './models'))
|
|
43
|
+
await framework.loadPluginModels()
|
|
44
|
+
await framework.runPluginAfterSetup()
|
|
45
|
+
|
|
46
|
+
const app = framework.createApp() as express.Express & Record<string, any>
|
|
47
|
+
|
|
48
|
+
app.disable('x-powered-by')
|
|
49
|
+
app.disable('etag')
|
|
50
|
+
|
|
51
|
+
app.set('trust proxy', true)
|
|
52
|
+
|
|
53
|
+
app.set('wrap response', true)
|
|
54
|
+
app.setOkCode(0)
|
|
55
|
+
app.setOkMessage('success')
|
|
56
|
+
|
|
57
|
+
app.setJWT('default', {
|
|
58
|
+
secret: process.env.JWT_SECRET || 'please-change-this-in-production',
|
|
59
|
+
algorithms: ['HS256'],
|
|
60
|
+
sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
|
61
|
+
requestProperty: 'resolvedToken'
|
|
62
|
+
})
|
|
63
|
+
app.setJwtHintHeader('JwtHint')
|
|
64
|
+
|
|
65
|
+
app.use(expressLogger())
|
|
66
|
+
app.use(express.static(path.join(__dirname, '..', 'public')))
|
|
67
|
+
|
|
68
|
+
await framework.loadControllers(path.join(__dirname, './controllers'))
|
|
69
|
+
await framework.loadPluginControllers()
|
|
70
|
+
|
|
71
|
+
framework.start(Number(process.env.PORT) || 3000)
|
|
72
|
+
|
|
73
|
+
logger.info('[app] Initialization complete (web process)')
|
|
74
|
+
|
|
75
|
+
process.on('SIGINT', async () => {
|
|
76
|
+
logger.info('[app] Received SIGINT, shutting down gracefully...')
|
|
77
|
+
await framework.stop()
|
|
78
|
+
process.exit(0)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
process.on('SIGTERM', async () => {
|
|
82
|
+
logger.info('[app] Received SIGTERM, shutting down gracefully...')
|
|
83
|
+
await framework.stop()
|
|
84
|
+
process.exit(0)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
89
|
+
if (isMainModule) {
|
|
90
|
+
bootstrap().catch((err) => {
|
|
91
|
+
logger.error('[app] Fatal error:', err)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { bootstrap }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { Database } from 'skweb-sequelize'
|
|
6
|
+
import logger from '../utils/logger.js'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
|
|
11
|
+
async function runMigrate(): Promise<void> {
|
|
12
|
+
logger.info('[migrate] Running database migrations...')
|
|
13
|
+
|
|
14
|
+
const db = new Database({
|
|
15
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
16
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
17
|
+
username: process.env.DB_USER || 'root',
|
|
18
|
+
password: process.env.DB_PASS || '',
|
|
19
|
+
database: process.env.DB_NAME || 'test-project',
|
|
20
|
+
dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
|
|
21
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
22
|
+
underscored: true,
|
|
23
|
+
logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await db.loadModelsDir(path.join(__dirname, '..', 'models'))
|
|
27
|
+
db.associateAllModels()
|
|
28
|
+
await db.sequelize.sync({ alter: true })
|
|
29
|
+
logger.info('[migrate] Database schema synced')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
33
|
+
if (isMainModule) {
|
|
34
|
+
runMigrate().catch((err) => {
|
|
35
|
+
logger.error('[migrate] Fatal error:', err)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { Database } from 'skweb-sequelize'
|
|
6
|
+
import logger from '../utils/logger.js'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
|
|
11
|
+
async function runSeed(): Promise<void> {
|
|
12
|
+
logger.info('[seed] Initializing database with seed data...')
|
|
13
|
+
|
|
14
|
+
const db = new Database({
|
|
15
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
16
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
17
|
+
username: process.env.DB_USER || 'root',
|
|
18
|
+
password: process.env.DB_PASS || '',
|
|
19
|
+
database: process.env.DB_NAME || 'test-project',
|
|
20
|
+
dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
|
|
21
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
22
|
+
underscored: true,
|
|
23
|
+
logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await db.loadModelsDir(path.join(__dirname, '..', 'models'))
|
|
27
|
+
db.associateAllModels()
|
|
28
|
+
|
|
29
|
+
const seedsDir = path.join(__dirname, '..', 'seeds')
|
|
30
|
+
const fs = await import('fs/promises')
|
|
31
|
+
let files: string[]
|
|
32
|
+
try {
|
|
33
|
+
files = await fs.readdir(seedsDir)
|
|
34
|
+
} catch {
|
|
35
|
+
logger.warn('[seed] No seeds directory found')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (!file.endsWith('.json')) continue
|
|
41
|
+
const modelName = path.basename(file, '.json')
|
|
42
|
+
const Model = db.sequelize.models[modelName]
|
|
43
|
+
if (!Model) {
|
|
44
|
+
logger.warn(`[seed] Model ${modelName} not found`)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
const data = JSON.parse(await fs.readFile(path.join(seedsDir, file), 'utf-8'))
|
|
48
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
49
|
+
await Model.bulkCreate(data, { ignoreDuplicates: true })
|
|
50
|
+
logger.info(`[seed] Seeded ${modelName} with ${data.length} records`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.info('[seed] Database initialization complete')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
58
|
+
if (isMainModule) {
|
|
59
|
+
runSeed().catch((err) => {
|
|
60
|
+
logger.error('[seed] Fatal error:', err)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
method: 'get',
|
|
4
|
+
path: '/',
|
|
5
|
+
async handler() {
|
|
6
|
+
return {
|
|
7
|
+
message: 'Welcome to SKWeb!',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
timestamp: new Date().toISOString()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
method: 'get',
|
|
15
|
+
path: '/health',
|
|
16
|
+
async handler() {
|
|
17
|
+
return {
|
|
18
|
+
status: 'ok',
|
|
19
|
+
timestamp: new Date().toISOString()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
method: 'post',
|
|
25
|
+
path: '/echo',
|
|
26
|
+
async handler(req: any) {
|
|
27
|
+
return {
|
|
28
|
+
message: 'Echo received',
|
|
29
|
+
data: req.body
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import createHttpError from 'http-errors'
|
|
2
|
+
import { db } from '../services/database.js'
|
|
3
|
+
|
|
4
|
+
const { SysUser } = db.sequelize.models
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{
|
|
8
|
+
method: 'get',
|
|
9
|
+
path: '/api/users',
|
|
10
|
+
async handler(req: any) {
|
|
11
|
+
const { page = 1, limit = 10 } = req.query
|
|
12
|
+
const users = await SysUser.findAndCountAll({
|
|
13
|
+
offset: (Number(page) - 1) * Number(limit),
|
|
14
|
+
limit: Number(limit)
|
|
15
|
+
})
|
|
16
|
+
return {
|
|
17
|
+
list: users.rows,
|
|
18
|
+
total: users.count
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
method: 'get',
|
|
24
|
+
path: '/api/users/:id',
|
|
25
|
+
paramsSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: ['id'],
|
|
28
|
+
properties: { id: { type: 'integer', minimum: 1 } }
|
|
29
|
+
},
|
|
30
|
+
async handler(req: any) {
|
|
31
|
+
const { id } = req.validatedParams
|
|
32
|
+
const user = await SysUser.findByPk(id)
|
|
33
|
+
if (!user) {
|
|
34
|
+
throw new createHttpError.NotFound('User not found')
|
|
35
|
+
}
|
|
36
|
+
return user
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
method: 'post',
|
|
41
|
+
path: '/api/users',
|
|
42
|
+
bodySchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
required: ['username', 'password'],
|
|
45
|
+
properties: {
|
|
46
|
+
username: { type: 'string', minLength: 3 },
|
|
47
|
+
password: { type: 'string', minLength: 6 },
|
|
48
|
+
email: { type: 'string', format: 'email' },
|
|
49
|
+
phone: { type: 'string' }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async handler(req: any) {
|
|
53
|
+
return await SysUser.create(req.validatedBody)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
method: 'put',
|
|
58
|
+
path: '/api/users/:id',
|
|
59
|
+
paramsSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['id'],
|
|
62
|
+
properties: { id: { type: 'integer', minimum: 1 } }
|
|
63
|
+
},
|
|
64
|
+
bodySchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
password: { type: 'string', minLength: 6 },
|
|
68
|
+
email: { type: 'string', format: 'email' },
|
|
69
|
+
phone: { type: 'string' },
|
|
70
|
+
status: { type: 'integer' }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async handler(req: any) {
|
|
74
|
+
const { id } = req.validatedParams
|
|
75
|
+
const user = await SysUser.findByPk(id)
|
|
76
|
+
if (!user) {
|
|
77
|
+
throw new createHttpError.NotFound('User not found')
|
|
78
|
+
}
|
|
79
|
+
await user.update(req.validatedBody)
|
|
80
|
+
return user
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
method: 'delete',
|
|
85
|
+
path: '/api/users/:id',
|
|
86
|
+
paramsSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
required: ['id'],
|
|
89
|
+
properties: { id: { type: 'integer', minimum: 1 } }
|
|
90
|
+
},
|
|
91
|
+
async handler(req: any) {
|
|
92
|
+
const { id } = req.validatedParams
|
|
93
|
+
const user = await SysUser.findByPk(id)
|
|
94
|
+
if (!user) {
|
|
95
|
+
throw new createHttpError.NotFound('User not found')
|
|
96
|
+
}
|
|
97
|
+
await user.destroy()
|
|
98
|
+
return { message: 'User deleted' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import logger from '../utils/logger.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 清理任务示例 - 每天凌晨3点清理过期数据
|
|
5
|
+
*/
|
|
6
|
+
export default async function cleanup(): Promise<void> {
|
|
7
|
+
logger.info(`[cron] Running cleanup task at ${new Date().toISOString()}`)
|
|
8
|
+
// 业务逻辑:清理 7 天前的临时文件、过期 token 等
|
|
9
|
+
logger.info('[cron] Cleanup task completed')
|
|
10
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { CronFramework } from 'skweb-framework'
|
|
6
|
+
import logger from './utils/logger.js'
|
|
7
|
+
import { checkRequiredEnv } from './utils/config.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
async function startCronWorker(): Promise<void> {
|
|
13
|
+
logger.info('[cron-worker] Starting cron worker process...')
|
|
14
|
+
|
|
15
|
+
if (!checkRequiredEnv()) {
|
|
16
|
+
logger.error('[cron-worker] Configuration validation failed, exiting...')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const framework = new CronFramework({
|
|
21
|
+
db: {
|
|
22
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
23
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
24
|
+
username: process.env.DB_USER || 'root',
|
|
25
|
+
password: process.env.DB_PASS || '',
|
|
26
|
+
database: process.env.DB_NAME || 'test-project',
|
|
27
|
+
dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
|
|
28
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
29
|
+
underscored: true,
|
|
30
|
+
logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
|
|
31
|
+
},
|
|
32
|
+
redis: {
|
|
33
|
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
34
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
35
|
+
password: process.env.REDIS_PASSWORD || undefined
|
|
36
|
+
},
|
|
37
|
+
channel: 'cron:task:cmd',
|
|
38
|
+
logger,
|
|
39
|
+
onSaveTask: async (taskData: { taskId: string; cache: unknown; data: unknown }) => {
|
|
40
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask as any
|
|
41
|
+
if (SysCronTask) {
|
|
42
|
+
await SysCronTask.update(
|
|
43
|
+
{ cache: taskData.cache, data: taskData.data },
|
|
44
|
+
{ where: { id: taskData.taskId } }
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
loadTasks: async () => {
|
|
49
|
+
const SysCronTask = framework.db?.sequelize?.models.SysCronTask as any
|
|
50
|
+
if (!SysCronTask) return []
|
|
51
|
+
return (await SysCronTask.findAll()).map((t: any) => ({
|
|
52
|
+
id: String(t.id),
|
|
53
|
+
cronTime: t.cronTime,
|
|
54
|
+
method: t.method
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await framework.runPluginBeforeSetup()
|
|
60
|
+
await framework.setupModels(path.join(__dirname, './models'))
|
|
61
|
+
await framework.loadPluginModels()
|
|
62
|
+
await framework.runPluginAfterSetup()
|
|
63
|
+
|
|
64
|
+
await framework.loadCronActions(path.join(__dirname, './cronActions'))
|
|
65
|
+
await framework.startWorker()
|
|
66
|
+
framework.start()
|
|
67
|
+
logger.info('[cron-worker] Cron worker started')
|
|
68
|
+
|
|
69
|
+
process.on('SIGINT', async () => {
|
|
70
|
+
logger.info('[cron-worker] Received SIGINT, shutting down...')
|
|
71
|
+
await framework.stop()
|
|
72
|
+
process.exit(0)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
process.on('SIGTERM', async () => {
|
|
76
|
+
logger.info('[cron-worker] Received SIGTERM, shutting down...')
|
|
77
|
+
await framework.stop()
|
|
78
|
+
process.exit(0)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`
|
|
83
|
+
if (isMainModule) {
|
|
84
|
+
startCronWorker().catch((err) => {
|
|
85
|
+
logger.error('[cron-worker] Fatal error:', err)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { startCronWorker }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Model, DataTypes, type Sequelize, type ModelStatic } from 'sequelize'
|
|
2
|
+
|
|
3
|
+
export default (sequelize: Sequelize): ModelStatic<Model> => {
|
|
4
|
+
class SysUser extends Model {
|
|
5
|
+
declare id: number
|
|
6
|
+
declare username: string
|
|
7
|
+
declare password: string
|
|
8
|
+
declare email: string
|
|
9
|
+
declare phone: string
|
|
10
|
+
declare status: number
|
|
11
|
+
declare createdAt: Date
|
|
12
|
+
declare updatedAt: Date
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SysUser.init({
|
|
16
|
+
id: {
|
|
17
|
+
type: DataTypes.INTEGER,
|
|
18
|
+
primaryKey: true,
|
|
19
|
+
autoIncrement: true
|
|
20
|
+
},
|
|
21
|
+
username: {
|
|
22
|
+
type: DataTypes.STRING(50),
|
|
23
|
+
allowNull: false,
|
|
24
|
+
unique: true
|
|
25
|
+
},
|
|
26
|
+
password: {
|
|
27
|
+
type: DataTypes.STRING(255),
|
|
28
|
+
allowNull: false
|
|
29
|
+
},
|
|
30
|
+
email: {
|
|
31
|
+
type: DataTypes.STRING(100)
|
|
32
|
+
},
|
|
33
|
+
phone: {
|
|
34
|
+
type: DataTypes.STRING(20)
|
|
35
|
+
},
|
|
36
|
+
status: {
|
|
37
|
+
type: DataTypes.TINYINT,
|
|
38
|
+
defaultValue: 1
|
|
39
|
+
}
|
|
40
|
+
}, {
|
|
41
|
+
sequelize,
|
|
42
|
+
modelName: 'SysUser',
|
|
43
|
+
tableName: 'sys_users',
|
|
44
|
+
timestamps: true,
|
|
45
|
+
underscored: true
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return SysUser
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"id": 1, "username": "admin", "password": "$2b$10$example", "email": "admin@example.com", "phone": "13800138000", "status": 1}]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Database } from 'skweb-sequelize'
|
|
2
|
+
|
|
3
|
+
const db = new Database({
|
|
4
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
5
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
6
|
+
username: process.env.DB_USER || 'root',
|
|
7
|
+
password: process.env.DB_PASS || '',
|
|
8
|
+
database: process.env.DB_NAME || '{{name}}',
|
|
9
|
+
dialect: (process.env.DB_DIALECT as any) || 'mysql',
|
|
10
|
+
charset: process.env.DB_CHARSET || 'utf8mb4',
|
|
11
|
+
underscored: true,
|
|
12
|
+
logging: process.env.DEBUG_DB === 'true'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export { db }
|
|
16
|
+
export default db
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logger from './logger.js'
|
|
2
|
+
|
|
3
|
+
interface ConfigValidationResult {
|
|
4
|
+
isValid: boolean
|
|
5
|
+
errors: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validateConfig(): ConfigValidationResult {
|
|
9
|
+
const errors: string[] = []
|
|
10
|
+
|
|
11
|
+
if (!process.env.DB_HOST) {
|
|
12
|
+
logger.warn('[config] DB_HOST not set, using default: 127.0.0.1')
|
|
13
|
+
}
|
|
14
|
+
if (!process.env.DB_NAME) {
|
|
15
|
+
logger.warn('[config] DB_NAME not set, using default: test-project')
|
|
16
|
+
}
|
|
17
|
+
if (!process.env.DB_USER) {
|
|
18
|
+
logger.warn('[config] DB_USER not set, using default: root')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (process.env.JWT_SECRET === 'please-change-this-in-production') {
|
|
22
|
+
logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!process.env.REDIS_HOST) {
|
|
26
|
+
logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const port = Number(process.env.PORT)
|
|
30
|
+
if (port && (port < 1 || port > 65535)) {
|
|
31
|
+
errors.push('PORT must be between 1 and 65535')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dbPort = Number(process.env.DB_PORT)
|
|
35
|
+
if (dbPort && (dbPort < 1 || dbPort > 65535)) {
|
|
36
|
+
errors.push('DB_PORT must be between 1 and 65535')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const redisPort = Number(process.env.REDIS_PORT)
|
|
40
|
+
if (redisPort && (redisPort < 1 || redisPort > 65535)) {
|
|
41
|
+
errors.push('REDIS_PORT must be between 1 and 65535')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
isValid: errors.length === 0,
|
|
46
|
+
errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function checkRequiredEnv(): boolean {
|
|
51
|
+
const result = validateConfig()
|
|
52
|
+
|
|
53
|
+
if (result.errors.length > 0) {
|
|
54
|
+
logger.error('[config] Configuration validation failed:')
|
|
55
|
+
result.errors.forEach((err) => logger.error(` - ${err}`))
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.env.NODE_ENV === 'production') {
|
|
60
|
+
if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'please-change-this-in-production') {
|
|
61
|
+
logger.error('[config] JWT_SECRET must be set in production!')
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.info('[config] Configuration validation passed')
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Winston 日志配置
|
|
3
|
+
*
|
|
4
|
+
* 支持通过环境变量控制:
|
|
5
|
+
* - LOG_LEVEL: 日志级别 (debug/info/warn/error),默认 info
|
|
6
|
+
* - LOG_DIR: 日志文件目录,默认 ./logs
|
|
7
|
+
* - LOG_CONSOLE: 是否同时输出到控制台 (true/false),默认 true
|
|
8
|
+
*/
|
|
9
|
+
import winston from 'winston'
|
|
10
|
+
import path from 'path'
|
|
11
|
+
import fs from 'fs'
|
|
12
|
+
|
|
13
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || 'info'
|
|
14
|
+
const LOG_DIR = process.env.LOG_DIR || path.join(process.cwd(), 'logs')
|
|
15
|
+
const LOG_CONSOLE = process.env.LOG_CONSOLE !== 'false'
|
|
16
|
+
|
|
17
|
+
// 确保日志目录存在
|
|
18
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
19
|
+
fs.mkdirSync(LOG_DIR, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { combine, timestamp, printf, colorize, errors, splat, json } = winston.format
|
|
23
|
+
|
|
24
|
+
// 控制台输出格式:带颜色 + 易读
|
|
25
|
+
const consoleFormat = printf(({ level, message, timestamp: ts, stack }) => {
|
|
26
|
+
return `${ts} [${level}] ${stack || message}`
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// 文件输出格式:JSON 结构(便于日志分析系统采集)
|
|
30
|
+
const fileFormat = combine(timestamp(), errors({ stack: true }), splat(), json())
|
|
31
|
+
|
|
32
|
+
export const logger = winston.createLogger({
|
|
33
|
+
level: LOG_LEVEL,
|
|
34
|
+
format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), errors({ stack: true }), splat()),
|
|
35
|
+
transports: [
|
|
36
|
+
// 错误日志单独输出
|
|
37
|
+
new winston.transports.File({
|
|
38
|
+
filename: path.join(LOG_DIR, 'error.log'),
|
|
39
|
+
level: 'error',
|
|
40
|
+
format: fileFormat,
|
|
41
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
42
|
+
maxFiles: 10
|
|
43
|
+
}),
|
|
44
|
+
// 所有日志
|
|
45
|
+
new winston.transports.File({
|
|
46
|
+
filename: path.join(LOG_DIR, 'combined.log'),
|
|
47
|
+
format: fileFormat,
|
|
48
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
49
|
+
maxFiles: 10
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 生产环境:输出到控制台
|
|
55
|
+
if (LOG_CONSOLE) {
|
|
56
|
+
logger.add(
|
|
57
|
+
new winston.transports.Console({
|
|
58
|
+
format: combine(colorize(), consoleFormat)
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default logger
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect } from 'chai'
|
|
2
|
+
|
|
3
|
+
describe('SKWeb App Tests', function() {
|
|
4
|
+
describe('Basic Tests', function() {
|
|
5
|
+
it('should pass basic test', function() {
|
|
6
|
+
expect(true).to.equal(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should have correct package structure', function() {
|
|
10
|
+
expect('SKWeb').to.be.a('string')
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "Node",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"rootDir": "src",
|
|
15
|
+
"outDir": "dist",
|
|
16
|
+
"baseUrl": "."
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
20
|
+
}
|