generated from template/vite-react-template
初始化应用
This commit is contained in:
13
backend/.cnb.yml
Normal file
13
backend/.cnb.yml
Normal 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
70
backend/.gitignore
vendored
Normal 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
3
backend/.npmrc
Normal 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
25
backend/bun.config.mjs
Normal 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
22
backend/kevisual.json
Normal 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
64
backend/package.json
Normal 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
3572
backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
backend/pnpm-workspace.yaml
Normal file
2
backend/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- sqlite3
|
||||
7
backend/src/app.ts
Normal file
7
backend/src/app.ts
Normal 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
1
backend/src/dev.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './index.ts';
|
||||
15
backend/src/index.ts
Normal file
15
backend/src/index.ts
Normal 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);
|
||||
9
backend/src/modules/sequelize.ts
Normal file
9
backend/src/modules/sequelize.ts
Normal 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,
|
||||
});
|
||||
126
backend/src/routes/ticket/list.ts
Normal file
126
backend/src/routes/ticket/list.ts
Normal 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);
|
||||
57
backend/src/routes/ticket/mock/data.ts
Normal file
57
backend/src/routes/ticket/mock/data.ts
Normal 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;
|
||||
80
backend/src/routes/ticket/model.ts
Normal file
80
backend/src/routes/ticket/model.ts
Normal 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 })
|
||||
18
backend/src/test/insert-demos.ts
Normal file
18
backend/src/test/insert-demos.ts
Normal 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
18
backend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/backend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@kevisual"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user