modify: task 集成数据库操作

This commit is contained in:
2026-04-15 16:05:31 +08:00
parent 84b122e93a
commit 69d1c5fc67
39 changed files with 1374 additions and 70 deletions
+208
View File
@@ -18,6 +18,7 @@
"@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1",
"typescript": "~6.0.0",
"vite": "^8.0.3",
@@ -28,6 +29,24 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.9.3",
"resolved": "https://registry.npmmirror.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",
"integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -561,6 +580,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmmirror.com/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
@@ -879,6 +905,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
@@ -1266,6 +1299,13 @@
"node": ">=14"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/ast-kit": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
@@ -1370,6 +1410,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001788",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
@@ -1406,6 +1459,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
@@ -1617,6 +1680,21 @@
}
}
},
"node_modules/fs-extra": {
"version": "11.3.4",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz",
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1642,6 +1720,35 @@
"node": ">=6.9.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/handlebars": {
"version": "4.7.9",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.9.tgz",
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -1728,6 +1835,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
@@ -1762,6 +1882,19 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
@@ -2090,6 +2223,16 @@
"node": ">= 0.10.0"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
@@ -2166,6 +2309,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz",
@@ -2236,6 +2386,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-typescript-codegen": {
"version": "0.29.0",
"resolved": "https://registry.npmmirror.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
"integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.4",
"camelcase": "^6.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8"
},
"bin": {
"openapi": "bin/index.js"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2542,6 +2709,16 @@
"node": ">=18"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2626,6 +2803,20 @@
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"license": "MIT"
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
@@ -2633,6 +2824,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unplugin": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
@@ -3048,6 +3249,13 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/wsl-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
+3 -1
View File
@@ -9,7 +9,8 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"format": "prettier --write --experimental-cli src/"
"format": "prettier --write --experimental-cli src/",
"openapi": "npx openapi-typescript-codegen --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/"
},
"dependencies": {
"pinia": "^3.0.4",
@@ -22,6 +23,7 @@
"@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1",
"typescript": "~6.0.0",
"vite": "^8.0.3",
+26
View File
@@ -1,6 +1,25 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { DefaultService } from './api/generated'
const result = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)
onMounted(async () => {
try {
const data = await DefaultService.rootGet()
result.value = data
console.log('API 请求成功:', data)
} catch (error: any) {
error.value = error.message || '请求失败'
} finally {
loading.value = false
}
})
</script>
<template>
@@ -10,6 +29,13 @@ import HelloWorld from './components/HelloWorld.vue'
<div class="wrapper">
<HelloWorld msg="You did it!" />
<!-- 显示加载状态或错误信息 -->
<div v-if="loading">加载中...</div>
<div v-else-if="error" style="color: red;">{{ error }}</div>
<div v-else>
<p>请求结果: {{ result }}</p>
</div>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
@@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};
@@ -0,0 +1,131 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}
@@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
VERSION: 'chongming',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};
@@ -0,0 +1,322 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json']
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};
@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { RefreshTokenResponse } from './models/RefreshTokenResponse';
export type { TaskRequest } from './models/TaskRequest';
export type { TodoCreate } from './models/TodoCreate';
export type { TodoRead } from './models/TodoRead';
export type { TodoUpdate } from './models/TodoUpdate';
export type { TokenResponse } from './models/TokenResponse';
export type { UserCreate } from './models/UserCreate';
export type { UserLogin } from './models/UserLogin';
export type { UserRead } from './models/UserRead';
export type { ValidationError } from './models/ValidationError';
export { AuthenticationService } from './services/AuthenticationService';
export { DefaultService } from './services/DefaultService';
export { TodoService } from './services/TodoService';
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ValidationError } from './ValidationError';
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 刷新 Token 响应模型
*/
export type RefreshTokenResponse = {
access_token: string;
token_type?: string;
};
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TaskRequest = {
task_name: string;
interval: number;
};
@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoCreate = {
title: string;
description?: (string | null);
completed?: boolean;
};
@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoRead = {
title: string;
description?: (string | null);
completed?: boolean;
id: number;
};
@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoUpdate = {
title?: (string | null);
description?: (string | null);
completed?: (boolean | null);
};
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Token 响应模型
*/
export type TokenResponse = {
access_token: string;
refresh_token: string;
token_type?: string;
expires_in: number;
refresh_expires_in: number;
};
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 用户创建模型
*/
export type UserCreate = {
email: string;
username?: (string | null);
full_name?: (string | null);
is_active?: boolean;
is_superuser?: boolean;
created_at?: (string | null);
password: string;
};
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserLogin = {
email: string;
password: string;
};
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 用户读取模型
*/
export type UserRead = {
email: string;
username?: (string | null);
full_name?: (string | null);
is_active?: boolean;
is_superuser?: boolean;
created_at?: (string | null);
id: number;
};
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ValidationError = {
loc: Array<(string | number)>;
msg: string;
type: string;
input?: any;
ctx?: Record<string, any>;
};
@@ -0,0 +1,109 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RefreshTokenResponse } from '../models/RefreshTokenResponse';
import type { TokenResponse } from '../models/TokenResponse';
import type { UserCreate } from '../models/UserCreate';
import type { UserLogin } from '../models/UserLogin';
import type { UserRead } from '../models/UserRead';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class AuthenticationService {
/**
* Register
* @param requestBody
* @returns UserRead Successful Response
* @throws ApiError
*/
public static registerApiV1AuthRegisterPost(
requestBody: UserCreate,
): CancelablePromise<UserRead> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/register',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Login
* @param requestBody
* @returns TokenResponse Successful Response
* @throws ApiError
*/
public static loginApiV1AuthLoginPost(
requestBody: UserLogin,
): CancelablePromise<TokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/login',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* 刷新访问令牌
* @returns RefreshTokenResponse Successful Response
* @throws ApiError
*/
public static refreshTokenApiV1AuthRefreshPost(): CancelablePromise<RefreshTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/refresh',
});
}
/**
* Logout
* @returns any Successful Response
* @throws ApiError
*/
public static logoutApiV1AuthLogoutPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/logout',
});
}
/**
* 登出所有设备
* @returns any Successful Response
* @throws ApiError
*/
public static logoutAllApiV1AuthLogoutAllPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/logout-all',
});
}
/**
* 获取用户会话
* 获取用户的所有活跃会话
* @returns any Successful Response
* @throws ApiError
*/
public static getSessionsApiV1AuthSessionsGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/auth/sessions',
});
}
/**
* 获取当前用户信息
* 获取当前登录用户的信息
* @returns UserRead Successful Response
* @throws ApiError
*/
public static getCurrentUserInfoApiV1AuthMeGet(): CancelablePromise<UserRead> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/auth/me',
});
}
}
@@ -0,0 +1,42 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TaskRequest } from '../models/TaskRequest';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class DefaultService {
/**
* 根路径
* 根路径,返回应用信息
* @returns any Successful Response
* @throws ApiError
*/
public static rootGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/',
});
}
/**
* Add Task
* 添加定时任务接口
* @param requestBody
* @returns any Successful Response
* @throws ApiError
*/
public static addTaskSchedulePost(
requestBody: TaskRequest,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/schedule',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}
@@ -0,0 +1,118 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TodoCreate } from '../models/TodoCreate';
import type { TodoRead } from '../models/TodoRead';
import type { TodoUpdate } from '../models/TodoUpdate';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class TodoService {
/**
* Create Todo
* @param requestBody
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static createTodoApiV1TodosPost(
requestBody: TodoCreate,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/todos/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Read Todos
* @param offset
* @param limit
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static readTodosApiV1TodosGet(
offset?: number,
limit: number = 100,
): CancelablePromise<Array<TodoRead>> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/todos/',
query: {
'offset': offset,
'limit': limit,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Read Todo
* @param todoId
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static readTodoApiV1TodosTodoIdGet(
todoId: number,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Todo
* @param todoId
* @param requestBody
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static updateTodoApiV1TodosTodoIdPatch(
todoId: number,
requestBody: TodoUpdate,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Todo
* @param todoId
* @returns any Successful Response
* @throws ApiError
*/
public static deleteTodoApiV1TodosTodoIdDelete(
todoId: number,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
errors: {
422: `Validation Error`,
},
});
}
}
+5
View File
@@ -5,7 +5,12 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { OpenAPI } from "./api/generated"
/**
* 打包时注释该项
*/
OpenAPI.BASE = "http://localhost:8000"
const app = createApp(App)
app.use(createPinia())
+4 -1
View File
@@ -10,7 +10,10 @@ export default defineConfig({
vue(),
vueDevTools(),
],
base: '/stats/',
/**
* 打包开启此配置项
*/
base: '/static/',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
+21 -22
View File
@@ -1,7 +1,7 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Depends
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, text
@@ -12,10 +12,9 @@ from pydantic import BaseModel
from .core.config import get_config
from .core.cache import get_cache
from .core.logger import get_logger
from .core.scheduler import get_task_service, get_task_service_instance, TaskService
from .core.static_files import static_files_handler
from .core.scheduler import get_task_service_instance
from .core.static_files import SVFSStaticFiles
from .api import api_router
from .task.execute_background import execute_background_task
from .task import init_tasks_callback
@@ -45,12 +44,6 @@ async def lifespan(app: FastAPI):
if database_config is None:
raise ValueError("配置不存在")
task_service = get_task_service_instance()
await task_service.start(init_tasks_callback)
app.state.task_service = task_service
# 启动时:创建引擎和会话工厂,初始化数据库表
engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker(
@@ -77,6 +70,10 @@ async def lifespan(app: FastAPI):
if "already exists" in str(e):
raise
task_service = get_task_service_instance(async_session_maker)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
yield # 应用运行期间
# 关闭时:释放引擎
@@ -114,21 +111,23 @@ async def root():
}
@app.post("/schedule")
async def add_task(
request: TaskRequest, task_service: TaskService = Depends(get_task_service)
):
"""添加定时任务接口"""
job = await task_service.add_interval_job(
execute_background_task,
seconds=request.interval,
args=[request.task_name],
job_id=request.task_name,
)
return {"status": "success", "job_id": job.id}
# @app.post("/schedule")
# async def add_task(
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
# ):
# """添加定时任务接口"""
# job = await task_service.add_interval_job(
# execute_background_task,
# seconds=request.interval,
# args=[request.task_name],
# job_id=request.task_name,
# )
# return {"status": "success", "job_id": job.id}
if os.path.exists("static.svfs"):
logger.info("静态文件已存在,将使用静态文件服务")
static_files_handler = SVFSStaticFiles(directory="/")
app.mount("/static", static_files_handler, name="static")
+3 -3
View File
@@ -3,7 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import TokenData
from ...core.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
from ...core.security import verify_password
from ...service.user import UserService
from ...service.auth import get_auth_service
@@ -38,7 +38,7 @@ async def register(
return user
@router.post("/login")
@router.post("/login", response_model=TokenResponse)
async def login(
user_data: UserLogin,
request: Request,
@@ -55,7 +55,7 @@ async def login(
return await auth_service.create_tokens(user, request, device_id=None)
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌")
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
+7
View File
@@ -366,6 +366,13 @@ class TokenResponse(BaseModel):
refresh_expires_in: int
class RefreshTokenResponse(BaseModel):
"""刷新 Token 响应模型"""
access_token: str
token_type: str = "bearer"
# Token 数据模型
class TokenData(BaseModel):
"""Token 数据模型"""
+12 -5
View File
@@ -48,7 +48,8 @@ def acquire_scheduler_lock():
class TaskService:
"""任务调度服务"""
def __init__(self):
def __init__(self, async_session_maker=None):
self.async_session_maker = async_session_maker
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get(
"job_store_path", "scheduler_jobs.db"
@@ -172,15 +173,21 @@ class TaskService:
async def add_date_job(
self,
func: Callable,
run_date: datetime,
run_date: Optional[datetime] = None,
job_id: Optional[str] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加一次性任务"""
trigger = DateTrigger(run_date=run_date)
job = self.scheduler.add_job(
func, trigger=trigger, id=job_id, args=args or [], kwargs=kwargs or {}
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加一次性任务: {job.id}")
return job
@@ -270,10 +277,10 @@ class TaskService:
_task_service: Optional[TaskService] = None
def get_task_service_instance():
def get_task_service_instance(async_session_maker=None):
global _task_service
if _task_service is None:
_task_service = TaskService()
_task_service = TaskService(async_session_maker)
return _task_service
+8 -16
View File
@@ -1,4 +1,3 @@
import os
from fastapi import HTTPException
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
@@ -7,12 +6,8 @@ from sqlite_vfs.core import SQLiteVFS
class SVFSStaticFiles(StaticFiles):
def __init__(self, directory, vfs_db_path="static.svfs"):
if not os.path.exists(vfs_db_path):
raise RuntimeError(
f"虚拟文件系统数据库 {vfs_db_path} 不存在,请先运行打包脚本"
)
self.vfs_db_path = vfs_db_path
self.vfs = SQLiteVFS(vfs_db_path, compress=False)
vfs_db_path = vfs_db_path
self._vfs = SQLiteVFS(vfs_db_path, compress=False)
super().__init__(directory=directory)
async def get_response(self, path: str, scope):
@@ -20,7 +15,7 @@ class SVFSStaticFiles(StaticFiles):
normalized_path = "/" + path.replace("\\", "/")
# 获取文件信息
file_info = self.vfs.get_file_info(normalized_path)
file_info = self._vfs.get_file_info(normalized_path)
if file_info is None:
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
@@ -37,22 +32,22 @@ class SVFSStaticFiles(StaticFiles):
".json",
]
):
index_info = self.vfs.get_file_info("/index.html")
index_info = self._vfs.get_file_info("/index.html")
if index_info:
content = self.vfs.read_file("/index.html")
content = self._vfs.read_file("/index.html")
return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="File not found")
# 如果是目录,也返回 index.html(某些情况)
if file_info["is_directory"]:
index_info = self.vfs.get_file_info(f"{normalized_path}/index.html")
index_info = self._vfs.get_file_info(f"{normalized_path}/index.html")
if index_info:
content = self.vfs.read_file(f"{normalized_path}/index.html")
content = self._vfs.read_file(f"{normalized_path}/index.html")
return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="Directory index not found")
# 读取文件内容
content = self.vfs.read_file(normalized_path)
content = self._vfs.read_file(normalized_path)
content_type = file_info.get("content_type") or "application/octet-stream"
# 简单根据扩展名补充 MIME 类型(可选)
if normalized_path.endswith(".js"):
@@ -65,6 +60,3 @@ class SVFSStaticFiles(StaticFiles):
content_type = "image/x-icon"
return Response(content=content, media_type=content_type)
static_files_handler = SVFSStaticFiles(directory="/")
+15
View File
@@ -0,0 +1,15 @@
from .user import User
from .role import Role
from .permission import Permission
from .relationship_table import UserRole, RolePermission
from .todo import Todo
__all__ = [
"User",
"Role",
"Permission",
"UserRole",
"RolePermission",
"Todo",
]
+24
View File
@@ -0,0 +1,24 @@
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from .relationship_table import RolePermission
if TYPE_CHECKING:
from .role import Role
# ---------- 权限表 ----------
class Permission(SQLModel, table=True):
"""权限项,如 user:create, order:delete"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # 权限标识符
description: Optional[str] = None
resource: str = Field(index=True) # 资源,如 user, order, product
action: str # 操作,如 create, read, update, delete
# 多对多关联
roles: List["Role"] = Relationship(
back_populates="permissions", link_model=RolePermission
)
@@ -0,0 +1,17 @@
from uuid import UUID
from sqlmodel import SQLModel, Field
# ---------- 关联表 ----------
class UserRole(SQLModel, table=True):
"""用户角色关联表"""
user_id: UUID = Field(foreign_key="user.id", primary_key=True)
role_id: int = Field(foreign_key="role.id", primary_key=True)
class RolePermission(SQLModel, table=True):
"""角色权限关联表"""
role_id: int = Field(foreign_key="role.id", primary_key=True)
permission_id: int = Field(foreign_key="permission.id", primary_key=True)
+21
View File
@@ -0,0 +1,21 @@
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from .relationship_table import UserRole, RolePermission
if TYPE_CHECKING:
from .user import User
from .permission import Permission
# ---------- 角色表 ----------
class Role(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # admin, manager, operator, viewer
description: Optional[str] = None
is_only_user: bool = Field(default=False)
users: List["User"] = Relationship(back_populates="roles", link_model=UserRole)
permissions: List["Permission"] = Relationship(
back_populates="roles", link_model=RolePermission
)
+11 -4
View File
@@ -1,8 +1,14 @@
from typing import Optional
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field
from sqlmodel import SQLModel, Field, Relationship
from pydantic import BaseModel
from .relationship_table import UserRole
from uuid import uuid4, UUID
if TYPE_CHECKING:
from .role import Role
class UserBase(SQLModel):
email: str = Field(unique=True, index=True)
@@ -16,8 +22,9 @@ class UserBase(SQLModel):
class User(UserBase, table=True):
"""用户模型"""
id: Optional[int] = Field(default=None, primary_key=True)
id: UUID = Field(default_factory=uuid4, primary_key=True)
hashed_password: str
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRole)
class UserCreate(UserBase):
@@ -29,7 +36,7 @@ class UserCreate(UserBase):
class UserRead(UserBase):
"""用户读取模型"""
id: int
id: UUID
class UserLogin(BaseModel):
+5 -12
View File
@@ -1,20 +1,13 @@
from typing import Callable, Optional
from ..core.scheduler import TaskService
from .execute_background import execute_background_task
task_dict = {"execute_background_task": execute_background_task}
def get_task(task_name: str) -> Optional[Callable]:
return task_dict.get(task_name, None)
from .execute_background import dev_init_admin
async def init_tasks_callback(task_service: TaskService):
await task_service.add_interval_job(
execute_background_task,
seconds=1,
args=["task_name"],
await task_service.add_date_job(
dev_init_admin,
run_date=None,
args=[],
job_id="task_name",
)
+12 -5
View File
@@ -1,8 +1,15 @@
from ..core.scheduler import logger
from ..service.user import UserService
from ..core.scheduler import get_task_service_instance
async def execute_background_task(task_name: str):
from datetime import datetime
"""模拟后台任务执行"""
logger.info(f"Executing task: {task_name} at {datetime.utcnow()}")
async def dev_init_admin():
task_service = get_task_service_instance()
if task_service.async_session_maker is None:
return
async with task_service.async_session_maker() as session:
admin_user = await UserService.get_user_by_email("admin", session)
if admin_user: # type: ignore
logger.info("管理员用户已存在") # type: ignore
return
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
+9
View File
@@ -157,6 +157,8 @@ async def init_database():
from ..app.core.config import get_config
from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ..app.service.user import UserService
database = get_config()["database"]
database_type = database["type"]
@@ -167,6 +169,9 @@ async def init_database():
engine = create_async_engine(
"sqlite+aiosqlite:///./build/database.db", **database_config
)
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with engine.begin() as conn:
if database_type == "sqlite":
@@ -176,9 +181,13 @@ async def init_database():
await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
)
except OperationalError as e:
if "already exists" in str(e):
raise
# 添加管理员用户
async with async_session_maker() as session:
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
async def main():