初始化应用

This commit is contained in:
2025-06-20 02:33:53 +08:00
parent e56eaab69a
commit f756082399
31 changed files with 9369 additions and 782 deletions

13
backend/.cnb.yml Normal file
View File

@@ -0,0 +1,13 @@
# .cnb.yml
$:
vscode:
- docker:
image: docker.cnb.cool/kevisual/dev-env:latest
services:
- vscode
- docker
imports: https://cnb.cool/kevisual/env/-/blob/main/env.yml
# 开发环境启动后会执行的任务
stages:
- name: pnpm install
script: pnpm install

70
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,70 @@
node_modules
# mac
.DS_Store
.env*
!.env*example
dist
build
logs
.turbo
pack-dist
# astro
.astro
# next
.next
# nuxt
.nuxt
# vercel
.vercel
# vuepress
.vuepress/dist
# coverage
coverage/
# typescript
*.tsbuildinfo
# debug logs
*.log
*.tmp
# vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# idea
.idea
# system
Thumbs.db
ehthumbs.db
Desktop.ini
# temp files
*.tmp
*.temp
# local development
*.local
public/r
.pnpm-store
storage/
pages

3
backend/.npmrc Normal file
View File

@@ -0,0 +1,3 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
ignore-workspace-root-check=true

25
backend/bun.config.mjs Normal file
View File

@@ -0,0 +1,25 @@
// @ts-check
import { resolvePath } from '@kevisual/use-config/env';
import { execSync } from 'node:child_process';
const entry = 'src/index.ts';
const naming = 'app';
const external = ['sequelize', 'pg', 'sqlite3', 'minio', '@kevisual/router', 'pm2'];
/**
* @type {import('bun').BuildConfig}
*/
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: `${naming}.js`,
},
external: external,
env: 'KEVISUAL_*',
});
// const cmd = `dts -i src/index.ts -o app.d.ts`;
const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
execSync(cmd, { stdio: 'inherit' });

22
backend/kevisual.json Normal file
View File

@@ -0,0 +1,22 @@
{
"sync": {
".gitignore": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/gitignore/node.txt"
},
".npmrc": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/npm/.npmrc"
},
"tsconfig.json": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/ts/backend.json"
},
"bun.config.mjs": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/bun/bun.config.mjs"
},
".cnb.yml": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/cnb/dev.yml"
},
"package.json": {
"url": "https://kevisual.xiongxiao.me/root/ai/code/config/npm/back-01-base/package.json"
}
}
}

64
backend/package.json Normal file
View File

@@ -0,0 +1,64 @@
{
"name": "ticket-services",
"version": "0.0.1",
"description": "",
"main": "index.js",
"basename": "/root/ticket-services",
"app": {
"key": "ticket-services",
"entry": "dist/app.js",
"type": "system-app"
},
"files": [
"dist"
],
"scripts": {
"dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/dev.ts ",
"build": "rimraf dist && bun run bun.config.mjs",
"test": "tsx test/**/*.ts",
"clean": "rm -rf dist",
"pub": "npm run build && envision pack -p -u",
"cmd": "tsx cmd/index.ts "
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"types": "types/index.d.ts",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@kevisual/code-center-module": "0.0.23",
"@kevisual/router": "0.0.23",
"@kevisual/use-config": "^1.0.19",
"cookie": "^1.0.2",
"dayjs": "^1.11.13",
"formidable": "^3.5.4",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@kevisual/context": "^0.0.3",
"@kevisual/local-proxy": "^0.0.6",
"@kevisual/types": "^0.0.10",
"@kevisual/use-config": "^1.0.19",
"@types/bun": "^1.2.16",
"@types/crypto-js": "^4.2.2",
"@types/formidable": "^3.4.5",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.0.3",
"commander": "^14.0.0",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"inquire": "^0.4.8",
"ioredis": "^5.6.1",
"nodemon": "^3.1.10",
"pg": "^8.16.1",
"rimraf": "^6.0.1",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7",
"tape": "^5.9.0",
"typescript": "^5.8.3"
},
"packageManager": "pnpm@10.12.1"
}

3572
backend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- sqlite3

7
backend/src/app.ts Normal file
View File

@@ -0,0 +1,7 @@
import { App } from '@kevisual/router';
import { useContextKey } from '@kevisual/context';
export const app = useContextKey<App>('app', () => {
return new App();
});

1
backend/src/dev.ts Normal file
View File

@@ -0,0 +1 @@
import './index.ts';

15
backend/src/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
initProxy({
pagesDir: './pages',
watch: true,
home: '/root/tickets',
});
import { app } from './app.ts';
import './routes/ticket/list.ts';
app.listen(3004, () => {
console.log('Server is running on http://localhost:3004');
});
app.onServerRequest(proxyRoute);

View File

@@ -0,0 +1,9 @@
import { Sequelize } from 'sequelize';
import path from 'node:path';
const sqlitePath = path.join(process.cwd(), 'storage', 'ticket', 'db.sqlite');
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: sqlitePath,
logging: false,
});

View File

@@ -0,0 +1,126 @@
import { app } from '@/app.ts';
// import mockTickets from './mock/data.ts';
import { TicketModel } from './model.ts';
import { Sequelize, Op, WhereOptions } from 'sequelize';
// 定义搜索参数类型
type SearchTicketsParams = {
current?: number;
pageSize?: number;
search?: string;
status?: string;
type?: string;
sort?: string;
order?: 'asc' | 'desc';
uid?: string;
startDate?: string;
endDate?: string;
};
app
.route({
path: 'ticket',
key: 'list',
})
.define(async (ctx) => {
const searchForm = ctx.query?.data || {} as SearchTicketsParams;
const {
current = 1,
pageSize = 10,
search,
status,
type,
sort = 'createdAt',
order = 'desc',
uid,
startDate,
endDate
} = searchForm;
// 构建查询条件
let where: any = {};
// 如果提供了搜索关键词,同时搜索标题和描述
if (search) {
where[Op.or] = [
{ title: { [Op.like]: `%${search}%` } },
{ description: { [Op.like]: `%${search}%` } }
];
}
// 添加其他过滤条件
if (status) where.status = status;
if (type) where.type = type;
if (uid) where.uid = uid;
// 日期范围过滤
if (startDate || endDate) {
const dateFilter: any = {};
if (startDate) dateFilter[Op.gte] = new Date(startDate);
if (endDate) dateFilter[Op.lte] = new Date(endDate);
where.createdAt = dateFilter;
}
// 验证排序字段是否有效防止SQL注入
const validSortFields = ['id', 'title', 'status', 'type', 'price', 'createdAt', 'updatedAt'];
const sortField = validSortFields.includes(sort) ? sort : 'createdAt';
const sortOrder = order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
const { rows, count } = await TicketModel.findAndCountAll({
where,
offset: (current - 1) * pageSize,
limit: pageSize,
order: [[sortField, sortOrder]],
});
ctx.body = {
list: rows,
pagination: {
total: count,
current: Number(current),
pageSize: Number(pageSize),
totalPages: Math.ceil(count / Number(pageSize)),
},
};
})
.addTo(app);
app
.route({
path: 'ticket',
key: 'update',
})
.define(async (ctx) => {
const data = ctx.query?.data || {};
const { id, ...updateData } = data;
if (!id) {
ctx.throw(400, 'ID is required for update');
}
const ticket = await TicketModel.findByPk(id);
if (!ticket) {
ctx.throw(404, 'Ticket not found');
}
await ticket.update(updateData);
ctx.body = ticket;
})
.addTo(app);
app
.route({
path: 'ticket',
key: 'delete',
})
.define(async (ctx) => {
const data = ctx.query?.data || {};
const { id } = data;
if (!id) {
ctx.throw(400, 'ID is required for deletion');
}
const ticket = await TicketModel.findByPk(id);
if (!ticket) {
ctx.throw(404, 'Ticket not found');
}
await ticket.destroy();
ctx.body = { message: 'Ticket deleted successfully' };
})
.addTo(app);

View File

@@ -0,0 +1,57 @@
const ticketStatus = ['rule', 'people'] as const;
export type Ticket = {
id: string;
type: string;
title: string;
description: string;
status: (typeof ticketStatus)[number];
price: string;
createdAt: string;
updatedAt: string;
uid: string;
};
// 票务类型列表
const ticketTypes = ['演唱会', '话剧', '电影', '展览', '体育赛事'];
// 生成随机日期字符串过去30天内
const generateRandomDate = () => {
const now = new Date();
const pastDays = Math.floor(Math.random() * 30);
const date = new Date(now.getTime() - pastDays * 24 * 60 * 60 * 1000);
return date.toISOString();
};
// 生成随机价格100-2000元
const generateRandomPrice = () => {
return `¥${Math.floor(Math.random() * 1900 + 100)}`;
};
// 生成随机ID
const generateRandomId = () => {
return Math.random().toString(36).substring(2, 10);
};
// 生成20条模拟数据
export const mockTickets: Ticket[] = Array.from({ length: 20 }, (_, index) => {
const createdAt = generateRandomDate();
const updatedAt = new Date(new Date(createdAt).getTime() + Math.random() * 24 * 60 * 60 * 1000).toISOString();
const type = ticketTypes[Math.floor(Math.random() * ticketTypes.length)];
const status = ticketStatus[Math.floor(Math.random() * ticketStatus.length)];
return {
id: `ticket-${generateRandomId()}`,
type,
title: `${type}票务-${index + 1}`,
description: `这是一张${type}的门票,编号为${index + 1},提供优质观演体验。`,
status,
price: generateRandomPrice(),
createdAt,
updatedAt,
uid: `user-${generateRandomId()}`,
};
});
// 导出默认数据
export default mockTickets;

View File

@@ -0,0 +1,80 @@
import { DataTypes, Model, UUIDV4 } from 'sequelize';
import { sequelize } from '@/modules/sequelize.ts';
// 定义票据状态类型
const ticketStatus = ['rule', 'people'] as const;
export type Ticket = {
id: string;
type: string;
title: string;
description: string;
status: (typeof ticketStatus)[number];
price: string;
createdAt: string;
updatedAt: string;
uid: string;
};
// 创建Ticket模型类
export class TicketModel extends Model<Ticket> implements Ticket {
declare id: string;
declare type: string;
declare title: string;
declare description: string;
declare status: (typeof ticketStatus)[number];
declare price: string;
declare createdAt: string;
declare updatedAt: string;
declare uid: string;
}
// 初始化模型
TicketModel.init(
{
id: {
type: DataTypes.UUID,
defaultValue: UUIDV4,
primaryKey: true,
},
type: {
type: DataTypes.STRING,
allowNull: false,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'rule',
},
price: {
type: DataTypes.STRING,
allowNull: false,
},
uid: {
type: DataTypes.STRING,
allowNull: false,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
sequelize,
tableName: 'tickets',
timestamps: true,
},
);
await TicketModel.sync({ alter: true })

View File

@@ -0,0 +1,18 @@
import { mockTickets } from './../routes/ticket/mock/data.ts';
import { TicketModel } from '@/routes/ticket/model.ts';
export const main = async () => {
// Clear existing data
// await TicketModel.destroy({ where: {}, truncate: true });
// Insert mock tickets
for (const ticket of mockTickets) {
delete ticket.id; // Remove id to let Sequelize generate it
await TicketModel.create(ticket);
}
console.log('Mock tickets inserted successfully.');
};
main()

18
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "@kevisual/types/json/backend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*",
],
}