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", "@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"vite": "^8.0.3", "vite": "^8.0.3",
@@ -28,6 +29,24 @@
"node": "^20.19.0 || >=22.12.0" "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": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -561,6 +580,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
@@ -879,6 +905,13 @@
"tslib": "^2.4.0" "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": { "node_modules/@types/node": {
"version": "24.12.2", "version": "24.12.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
@@ -1266,6 +1299,13 @@
"node": ">=14" "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": { "node_modules/ast-kit": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz", "resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
@@ -1370,6 +1410,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001788", "version": "1.0.30001788",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
@@ -1406,6 +1459,16 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/confbox": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1642,6 +1720,35 @@
"node": ">=6.9.0" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -1728,6 +1835,19 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
@@ -1762,6 +1882,19 @@
"node": ">=6" "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": { "node_modules/kolorist": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
@@ -2090,6 +2223,16 @@
"node": ">= 0.10.0" "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": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", "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": "^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": { "node_modules/node-releases": {
"version": "2.0.37", "version": "2.0.37",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz",
@@ -2236,6 +2386,23 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2542,6 +2709,16 @@
"node": ">=18" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "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==", "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"license": "MIT" "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": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
@@ -2633,6 +2824,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unplugin": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
@@ -3048,6 +3249,13 @@
"node": "^18.17.0 || >=20.5.0" "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": { "node_modules/wsl-utils": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
+3 -1
View File
@@ -9,7 +9,8 @@
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --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": { "dependencies": {
"pinia": "^3.0.4", "pinia": "^3.0.4",
@@ -22,6 +23,7 @@
"@vitejs/plugin-vue": "^6.0.5", "@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"vite": "^8.0.3", "vite": "^8.0.3",
+26
View File
@@ -1,6 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue' 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> </script>
<template> <template>
@@ -10,6 +29,13 @@ import HelloWorld from './components/HelloWorld.vue'
<div class="wrapper"> <div class="wrapper">
<HelloWorld msg="You did it!" /> <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> <nav>
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</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 App from './App.vue'
import router from './router' import router from './router'
import { OpenAPI } from "./api/generated"
/**
* 打包时注释该项
*/
OpenAPI.BASE = "http://localhost:8000"
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
+4 -1
View File
@@ -10,7 +10,10 @@ export default defineConfig({
vue(), vue(),
vueDevTools(), vueDevTools(),
], ],
base: '/stats/', /**
* 打包开启此配置项
*/
base: '/static/',
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
+21 -22
View File
@@ -1,7 +1,7 @@
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Depends from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, text from sqlmodel import SQLModel, text
@@ -12,10 +12,9 @@ from pydantic import BaseModel
from .core.config import get_config from .core.config import get_config
from .core.cache import get_cache from .core.cache import get_cache
from .core.logger import get_logger from .core.logger import get_logger
from .core.scheduler import get_task_service, get_task_service_instance, TaskService from .core.scheduler import get_task_service_instance
from .core.static_files import static_files_handler from .core.static_files import SVFSStaticFiles
from .api import api_router from .api import api_router
from .task.execute_background import execute_background_task
from .task import init_tasks_callback from .task import init_tasks_callback
@@ -45,12 +44,6 @@ async def lifespan(app: FastAPI):
if database_config is None: if database_config is None:
raise ValueError("配置不存在") 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) engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker( async_session_maker = async_sessionmaker(
@@ -77,6 +70,10 @@ async def lifespan(app: FastAPI):
if "already exists" in str(e): if "already exists" in str(e):
raise raise
task_service = get_task_service_instance(async_session_maker)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
yield # 应用运行期间 yield # 应用运行期间
# 关闭时:释放引擎 # 关闭时:释放引擎
@@ -114,21 +111,23 @@ async def root():
} }
@app.post("/schedule") # @app.post("/schedule")
async def add_task( # async def add_task(
request: TaskRequest, task_service: TaskService = Depends(get_task_service) # request: TaskRequest, task_service: TaskService = Depends(get_task_service)
): # ):
"""添加定时任务接口""" # """添加定时任务接口"""
job = await task_service.add_interval_job( # job = await task_service.add_interval_job(
execute_background_task, # execute_background_task,
seconds=request.interval, # seconds=request.interval,
args=[request.task_name], # args=[request.task_name],
job_id=request.task_name, # job_id=request.task_name,
) # )
return {"status": "success", "job_id": job.id} # return {"status": "success", "job_id": job.id}
if os.path.exists("static.svfs"): if os.path.exists("static.svfs"):
logger.info("静态文件已存在,将使用静态文件服务")
static_files_handler = SVFSStaticFiles(directory="/")
app.mount("/static", static_files_handler, name="static") 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 sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session 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 ...core.security import verify_password
from ...service.user import UserService from ...service.user import UserService
from ...service.auth import get_auth_service from ...service.auth import get_auth_service
@@ -38,7 +38,7 @@ async def register(
return user return user
@router.post("/login") @router.post("/login", response_model=TokenResponse)
async def login( async def login(
user_data: UserLogin, user_data: UserLogin,
request: Request, request: Request,
@@ -55,7 +55,7 @@ async def login(
return await auth_service.create_tokens(user, request, device_id=None) 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( async def refresh_token(
request: Request, request: Request,
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme), credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
+7
View File
@@ -366,6 +366,13 @@ class TokenResponse(BaseModel):
refresh_expires_in: int refresh_expires_in: int
class RefreshTokenResponse(BaseModel):
"""刷新 Token 响应模型"""
access_token: str
token_type: str = "bearer"
# Token 数据模型 # Token 数据模型
class TokenData(BaseModel): class TokenData(BaseModel):
"""Token 数据模型""" """Token 数据模型"""
+12 -5
View File
@@ -48,7 +48,8 @@ def acquire_scheduler_lock():
class TaskService: class TaskService:
"""任务调度服务""" """任务调度服务"""
def __init__(self): def __init__(self, async_session_maker=None):
self.async_session_maker = async_session_maker
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开) # 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get( self.job_store_db_path = config.get("scheduler", {}).get(
"job_store_path", "scheduler_jobs.db" "job_store_path", "scheduler_jobs.db"
@@ -172,15 +173,21 @@ class TaskService:
async def add_date_job( async def add_date_job(
self, self,
func: Callable, func: Callable,
run_date: datetime, run_date: Optional[datetime] = None,
job_id: Optional[str] = None, job_id: Optional[str] = None,
args: Optional[List] = None, args: Optional[List] = None,
kwargs: Optional[Dict] = None, kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job: ) -> Job:
"""添加一次性任务""" """添加一次性任务"""
trigger = DateTrigger(run_date=run_date) trigger = DateTrigger(run_date=run_date)
job = self.scheduler.add_job( 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}") logger.info(f"添加一次性任务: {job.id}")
return job return job
@@ -270,10 +277,10 @@ class TaskService:
_task_service: Optional[TaskService] = None _task_service: Optional[TaskService] = None
def get_task_service_instance(): def get_task_service_instance(async_session_maker=None):
global _task_service global _task_service
if _task_service is None: if _task_service is None:
_task_service = TaskService() _task_service = TaskService(async_session_maker)
return _task_service return _task_service
+8 -16
View File
@@ -1,4 +1,3 @@
import os
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.responses import Response from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -7,12 +6,8 @@ from sqlite_vfs.core import SQLiteVFS
class SVFSStaticFiles(StaticFiles): class SVFSStaticFiles(StaticFiles):
def __init__(self, directory, vfs_db_path="static.svfs"): def __init__(self, directory, vfs_db_path="static.svfs"):
if not os.path.exists(vfs_db_path): vfs_db_path = vfs_db_path
raise RuntimeError( self._vfs = SQLiteVFS(vfs_db_path, compress=False)
f"虚拟文件系统数据库 {vfs_db_path} 不存在,请先运行打包脚本"
)
self.vfs_db_path = vfs_db_path
self.vfs = SQLiteVFS(vfs_db_path, compress=False)
super().__init__(directory=directory) super().__init__(directory=directory)
async def get_response(self, path: str, scope): async def get_response(self, path: str, scope):
@@ -20,7 +15,7 @@ class SVFSStaticFiles(StaticFiles):
normalized_path = "/" + path.replace("\\", "/") 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: if file_info is None:
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html # SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
@@ -37,22 +32,22 @@ class SVFSStaticFiles(StaticFiles):
".json", ".json",
] ]
): ):
index_info = self.vfs.get_file_info("/index.html") index_info = self._vfs.get_file_info("/index.html")
if index_info: 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") return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(status_code=404, detail="File not found")
# 如果是目录,也返回 index.html(某些情况) # 如果是目录,也返回 index.html(某些情况)
if file_info["is_directory"]: 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: 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") return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="Directory index not found") 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" content_type = file_info.get("content_type") or "application/octet-stream"
# 简单根据扩展名补充 MIME 类型(可选) # 简单根据扩展名补充 MIME 类型(可选)
if normalized_path.endswith(".js"): if normalized_path.endswith(".js"):
@@ -65,6 +60,3 @@ class SVFSStaticFiles(StaticFiles):
content_type = "image/x-icon" content_type = "image/x-icon"
return Response(content=content, media_type=content_type) 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 pydantic import BaseModel
from .relationship_table import UserRole
from uuid import uuid4, UUID
if TYPE_CHECKING:
from .role import Role
class UserBase(SQLModel): class UserBase(SQLModel):
email: str = Field(unique=True, index=True) email: str = Field(unique=True, index=True)
@@ -16,8 +22,9 @@ class UserBase(SQLModel):
class User(UserBase, table=True): 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 hashed_password: str
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRole)
class UserCreate(UserBase): class UserCreate(UserBase):
@@ -29,7 +36,7 @@ class UserCreate(UserBase):
class UserRead(UserBase): class UserRead(UserBase):
"""用户读取模型""" """用户读取模型"""
id: int id: UUID
class UserLogin(BaseModel): class UserLogin(BaseModel):
+5 -12
View File
@@ -1,20 +1,13 @@
from typing import Callable, Optional from typing import Callable, Optional
from ..core.scheduler import TaskService from ..core.scheduler import TaskService
from .execute_background import execute_background_task from .execute_background import dev_init_admin
task_dict = {"execute_background_task": execute_background_task}
def get_task(task_name: str) -> Optional[Callable]:
return task_dict.get(task_name, None)
async def init_tasks_callback(task_service: TaskService): async def init_tasks_callback(task_service: TaskService):
await task_service.add_interval_job( await task_service.add_date_job(
execute_background_task, dev_init_admin,
seconds=1, run_date=None,
args=["task_name"], args=[],
job_id="task_name", job_id="task_name",
) )
+12 -5
View File
@@ -1,8 +1,15 @@
from ..core.scheduler import logger 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): async def dev_init_admin():
from datetime import datetime task_service = get_task_service_instance()
if task_service.async_session_maker is None:
"""模拟后台任务执行""" return
logger.info(f"Executing task: {task_name} at {datetime.utcnow()}") 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 ..app.core.config import get_config
from sqlmodel import SQLModel, text from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ..app.service.user import UserService
database = get_config()["database"] database = get_config()["database"]
database_type = database["type"] database_type = database["type"]
@@ -167,6 +169,9 @@ async def init_database():
engine = create_async_engine( engine = create_async_engine(
"sqlite+aiosqlite:///./build/database.db", **database_config "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: async with engine.begin() as conn:
if database_type == "sqlite": if database_type == "sqlite":
@@ -176,9 +181,13 @@ async def init_database():
await conn.run_sync( await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True) lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
) )
except OperationalError as e: except OperationalError as e:
if "already exists" in str(e): if "already exists" in str(e):
raise raise
# 添加管理员用户
async with async_session_maker() as session:
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
async def main(): async def main():