init
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM oven/bun:1 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装依赖(缓存层)
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
RUN bun install --production
|
||||||
|
|
||||||
|
# 复制源码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建(Bun 支持直接运行 TS,无需编译)
|
||||||
|
# 如果你希望预编译,可加 RUN bun build ...
|
||||||
|
|
||||||
|
# 创建非 root 用户(安全最佳实践)
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nextjs -u 1001 -G nodejs
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["bun", "run", "src/index.ts"]
|
||||||
87
bun.lock
Normal file
87
bun.lock
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "saas-admin-api",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"elysia": "^1.0.0",
|
||||||
|
"mysql2": "^3.10.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"bun-types": "^1.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.1.1.tgz", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||||
|
|
||||||
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.41.tgz", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
|
||||||
|
|
||||||
|
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||||
|
|
||||||
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@20.19.25", "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="],
|
||||||
|
|
||||||
|
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.3", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.3.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"denque": ["denque@2.1.0", "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@16.6.1", "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
|
"elysia": ["elysia@1.4.16", "https://registry.npmmirror.com/elysia/-/elysia-1.4.16.tgz", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="],
|
||||||
|
|
||||||
|
"exact-mirror": ["exact-mirror@0.2.3", "https://registry.npmmirror.com/exact-mirror/-/exact-mirror-0.2.3.tgz", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="],
|
||||||
|
|
||||||
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
|
"file-type": ["file-type@21.1.1", "https://registry.npmmirror.com/file-type/-/file-type-21.1.1.tgz", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="],
|
||||||
|
|
||||||
|
"generate-function": ["generate-function@2.3.1", "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.0", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"is-property": ["is-property@1.0.2", "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
|
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@7.18.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
||||||
|
|
||||||
|
"lru.min": ["lru.min@1.1.3", "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.3.tgz", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="],
|
||||||
|
|
||||||
|
"memoirist": ["memoirist@0.4.0", "https://registry.npmmirror.com/memoirist/-/memoirist-0.4.0.tgz", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"mysql2": ["mysql2@3.15.3", "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="],
|
||||||
|
|
||||||
|
"named-placeholders": ["named-placeholders@1.1.3", "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
||||||
|
|
||||||
|
"openapi-types": ["openapi-types@12.1.3", "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"seq-queue": ["seq-queue@0.0.5", "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
||||||
|
|
||||||
|
"sqlstring": ["sqlstring@2.3.3", "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||||
|
|
||||||
|
"strtok3": ["strtok3@10.3.4", "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.4.tgz", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||||
|
|
||||||
|
"token-types": ["token-types@6.1.1", "https://registry.npmmirror.com/token-types/-/token-types-6.1.1.tgz", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
|
||||||
|
|
||||||
|
"uint8array-extras": ["uint8array-extras@1.5.0", "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# mysql:
|
||||||
|
# image: mysql:8.0
|
||||||
|
# container_name: saas-mysql
|
||||||
|
# restart: always
|
||||||
|
# environment:
|
||||||
|
# MYSQL_ROOT_PASSWORD: root_secure_password
|
||||||
|
# MYSQL_DATABASE: lucky_sass
|
||||||
|
# MYSQL_USER: saas_admin
|
||||||
|
# MYSQL_PASSWORD: saas_secure_password
|
||||||
|
# ports:
|
||||||
|
# - "3306:3306"
|
||||||
|
# volumes:
|
||||||
|
# - ./init_v2.0.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
# - mysql_data:/var/lib/mysql
|
||||||
|
# command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||||
|
|
||||||
|
saas-admin-api:
|
||||||
|
build: .
|
||||||
|
container_name: saas-admin-api
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
DB_HOST: mysql
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_USER: saas_admin
|
||||||
|
DB_PASSWORD: saas_secure_password
|
||||||
|
DB_NAME: lucky_sass
|
||||||
|
ADMIN_API_KEY: sk-admin-xxxxxxxxxxxxxxxx
|
||||||
|
PORT: 8080
|
||||||
|
depends_on:
|
||||||
|
# - mysql
|
||||||
|
# 等待 MySQL 就绪(可选:使用 wait-for-it.sh 更健壮)
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "saas-admin-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "SaaS 平台数据查询 API,供 Dify Agent 调用,支持自然语言转结构化查询",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"start": "bun run src/index.ts",
|
||||||
|
"build": "echo \"Bun runs TypeScript directly — no build needed.\"",
|
||||||
|
"test": "echo \"Add tests with @types/bun or Jest later\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"elysia": "^1.0.0",
|
||||||
|
"mysql2": "^3.10.0",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"bun-types": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.0.0"
|
||||||
|
},
|
||||||
|
"keywords": ["bun", "elysia", "mysql", "saas", "dify", "natural-language-query"],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
102
saas-admin-api.postman_collection.json
Normal file
102
saas-admin-api.postman_collection.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"name": "SaaS Admin API - query_platform_data",
|
||||||
|
"_postman_id": "unique-id-here",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "商家 GMV(昨天)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"商家 TechStore 昨天的 GMV 是多少?\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "订单状态",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"订单 L20251127001 的状态是什么?\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "用户信息",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"用户 12345 的信息?\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "商品销量",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"商品 iPhone 16 卖了多少件?\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "未发货订单",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"列出所有未发货订单\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "系统公告",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{ "key": "Authorization", "value": "Bearer sk-admin-xxxxxxxxxxxxxxxx" },
|
||||||
|
{ "key": "Content-Type", "value": "application/json" }
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question\": \"最近的系统公告有哪些?\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{base_url}}/admin/query"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{ "key": "base_url", "value": "http://localhost:8080" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
src/db.ts
Normal file
22
src/db.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// src/db.ts
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
// 创建连接池(推荐用于生产)
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 'saas_db',
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0,
|
||||||
|
timezone: '+08:00', // 根据你的时区调整
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Connected to MySQL');
|
||||||
|
|
||||||
|
export default pool;
|
||||||
12
src/index.ts
Normal file
12
src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/index.ts
|
||||||
|
import { Elysia } from 'elysia';
|
||||||
|
import adminRoutes from './routes/admin';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
config();
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(adminRoutes)
|
||||||
|
.get('/', () => 'SaaS Admin API - Ready for Dify Agent')
|
||||||
|
.listen(process.env.PORT || 8080);
|
||||||
|
|
||||||
|
console.log(`🦊 Elysia running on http://localhost:${app.server?.port}`);
|
||||||
273
src/queryEngine.ts
Normal file
273
src/queryEngine.ts
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// src/queryEngine.ts
|
||||||
|
import pool from './db';
|
||||||
|
|
||||||
|
type QueryContext = {
|
||||||
|
current_tenant_id?: string;
|
||||||
|
operator_role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function executeNaturalLanguageQuery(
|
||||||
|
question: string,
|
||||||
|
context: QueryContext = {}
|
||||||
|
) {
|
||||||
|
const q = question.toLowerCase().trim();
|
||||||
|
|
||||||
|
// 工具函数:安全转义 LIKE
|
||||||
|
const escapeLike = (str: string) => str.replace(/[%_]/g, '\\$&');
|
||||||
|
|
||||||
|
// 场景 1: 商家 GMV(支持时间范围)
|
||||||
|
if ((q.includes('gmv') || q.includes('销售额')) && q.includes('商家')) {
|
||||||
|
const nameMatch = q.match(/商家\s*["']?([a-zA-Z0-9\u4e00-\u9fa5]+)["']?/);
|
||||||
|
const merchantName = nameMatch ? nameMatch[1] : null;
|
||||||
|
if (!merchantName) throw new Error('请指定商家名称');
|
||||||
|
|
||||||
|
let timeFilter = '';
|
||||||
|
let params: any[] = [`%${escapeLike(merchantName)}%`];
|
||||||
|
|
||||||
|
if (q.includes('昨天')) {
|
||||||
|
timeFilter = 'AND DATE(FROM_UNIXTIME(o.create_time)) = CURDATE() - INTERVAL 1 DAY';
|
||||||
|
} else if (q.includes('上周')) {
|
||||||
|
timeFilter = 'AND YEARWEEK(FROM_UNIXTIME(o.create_time), 1) = YEARWEEK(CURDATE() - INTERVAL 1 WEEK, 1)';
|
||||||
|
} else if (q.includes('上月')) {
|
||||||
|
timeFilter = 'AND DATE_FORMAT(FROM_UNIXTIME(o.create_time), "%Y-%m") = DATE_FORMAT(CURDATE() - INTERVAL 1 MONTH, "%Y-%m")';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
m.merch_name AS merchant,
|
||||||
|
COALESCE(SUM(o.order_money), 0) AS gmv,
|
||||||
|
COUNT(o.order_id) AS order_count
|
||||||
|
FROM lucky_merch m
|
||||||
|
LEFT JOIN lucky_order o ON m.merch_id = o.merch_id
|
||||||
|
WHERE m.merch_name LIKE ? ${timeFilter}
|
||||||
|
GROUP BY m.merch_id, m.merch_name
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, params);
|
||||||
|
return (rows as any[])[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 2: 订单状态
|
||||||
|
if (q.includes('订单') && (q.includes('状态') || q.includes('详情'))) {
|
||||||
|
const idMatch = q.match(/订单\s*["']?([a-zA-Z0-9]+)["']?/);
|
||||||
|
const orderId = idMatch ? idMatch[1] : null;
|
||||||
|
if (!orderId) throw new Error('请提供订单编号或ID');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
order_no,
|
||||||
|
order_status_name AS status,
|
||||||
|
pay_status,
|
||||||
|
delivery_status,
|
||||||
|
order_money,
|
||||||
|
name AS buyer_name,
|
||||||
|
mobile,
|
||||||
|
create_time
|
||||||
|
FROM lucky_order
|
||||||
|
WHERE order_no = ? OR order_id = ?
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [orderId, isNaN(Number(orderId)) ? null : Number(orderId)]);
|
||||||
|
return (rows as any[])[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 3: 用户信息
|
||||||
|
if (q.includes('用户') && (q.includes('信息') || q.includes('详情') || q.includes('资料'))) {
|
||||||
|
const idMatch = q.match(/用户\s*["']?(\d+)["']?/);
|
||||||
|
const userId = idMatch && idMatch[1] !== undefined ? parseInt(idMatch[1]) : null;
|
||||||
|
if (!userId) throw new Error('请提供用户ID');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT member_id, nickname, mobile, email
|
||||||
|
FROM lucky_member
|
||||||
|
WHERE member_id = ?
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [userId]);
|
||||||
|
return (rows as any[])[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 4: 商品销量
|
||||||
|
if (q.includes('商品') && q.includes('销量')) {
|
||||||
|
const nameMatch = q.match(/商品\s*["']?([a-zA-Z0-9\u4e00-\u9fa5\s]+)["']?/);
|
||||||
|
const goodsName = nameMatch && nameMatch[1] ? nameMatch[1].trim() : null;
|
||||||
|
if (!goodsName) throw new Error('请指定商品名称');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
g.goods_name,
|
||||||
|
SUM(og.num) AS total_sold
|
||||||
|
FROM lucky_goods g
|
||||||
|
JOIN lucky_order_goods og ON g.goods_id = og.goods_id
|
||||||
|
WHERE g.goods_name LIKE ?
|
||||||
|
GROUP BY g.goods_id, g.goods_name
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [`%${escapeLike(goodsName)}%`]);
|
||||||
|
return Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 5: 退款率(按商家)
|
||||||
|
if (q.includes('退款率') && q.includes('商家')) {
|
||||||
|
const nameMatch = q.match(/商家\s*["']?([a-zA-Z0-9\u4e00-\u9fa5]+)["']?/);
|
||||||
|
const merchantName = nameMatch ? nameMatch[1] : null;
|
||||||
|
if (!merchantName) throw new Error('请指定商家名称');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
m.merch_name,
|
||||||
|
COUNT(o.order_id) AS total_orders,
|
||||||
|
SUM(CASE WHEN o.refund_status > 0 THEN 1 ELSE 0 END) AS refunded_orders,
|
||||||
|
ROUND(
|
||||||
|
SUM(CASE WHEN o.refund_status > 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(o.order_id),
|
||||||
|
2
|
||||||
|
) AS refund_rate_percent
|
||||||
|
FROM lucky_merch m
|
||||||
|
JOIN lucky_order o ON m.merch_id = o.merch_id
|
||||||
|
WHERE m.merch_name LIKE ?
|
||||||
|
GROUP BY m.merch_id, m.merch_name
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [`%${escapeLike(merchantName)}%`]);
|
||||||
|
return (rows as any)[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 6: 未发货订单
|
||||||
|
if (q.includes('未发货') && q.includes('订单')) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
order_no,
|
||||||
|
name AS buyer,
|
||||||
|
order_money,
|
||||||
|
create_time
|
||||||
|
FROM lucky_order
|
||||||
|
WHERE delivery_status = 0 -- 假设 0=未发货
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
LIMIT 20
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 7: 正常营业商户
|
||||||
|
if (q.includes('商户') && (q.includes('列表') || q.includes('所有'))) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
merch_id,
|
||||||
|
merch_name,
|
||||||
|
status,
|
||||||
|
balance,
|
||||||
|
create_time
|
||||||
|
FROM lucky_merch
|
||||||
|
WHERE status = 1 -- 1=正常
|
||||||
|
ORDER BY sort ASC
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 8: 商户提现记录
|
||||||
|
if (q.includes('提现') && q.includes('商户')) {
|
||||||
|
const nameMatch = q.match(/商户\s*["']?([a-zA-Z0-9\u4e00-\u9fa5]+)["']?/);
|
||||||
|
const merchantName = nameMatch ? nameMatch[1] : null;
|
||||||
|
if (!merchantName) throw new Error('请指定商户名称');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
withdraw_no,
|
||||||
|
apply_money,
|
||||||
|
money AS arrived_amount,
|
||||||
|
status_name,
|
||||||
|
apply_time
|
||||||
|
FROM lucky_merch_withdraw w
|
||||||
|
JOIN lucky_merch m ON w.merch_id = m.merch_id
|
||||||
|
WHERE m.merch_name LIKE ?
|
||||||
|
ORDER BY apply_time DESC
|
||||||
|
LIMIT 10
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [`%${escapeLike(merchantName)}%`]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 9: 用户充值记录
|
||||||
|
if (q.includes('充值') && q.includes('用户')) {
|
||||||
|
const idMatch = q.match(/用户\s*["']?(\d+)["']?/);
|
||||||
|
const userId = idMatch && idMatch[1] ? parseInt(idMatch[1]) : null;
|
||||||
|
if (!userId) throw new Error('请提供用户ID');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
order_no,
|
||||||
|
price AS amount,
|
||||||
|
status,
|
||||||
|
create_time,
|
||||||
|
pay_time
|
||||||
|
FROM lucky_recharge_card_order
|
||||||
|
WHERE member_id = ?
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [userId]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 10: 邀请奖励
|
||||||
|
if (q.includes('邀请') && q.includes('奖励')) {
|
||||||
|
const idMatch = q.match(/用户\s*["']?(\d+)["']?/);
|
||||||
|
const userId = idMatch && idMatch[1] ? parseInt(idMatch[1]) : null;
|
||||||
|
if (!userId) throw new Error('请提供用户ID');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
recommend_name AS activity,
|
||||||
|
point,
|
||||||
|
balance,
|
||||||
|
create_time
|
||||||
|
FROM lucky_member_recommend_award
|
||||||
|
WHERE member_id = ?
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql, [userId]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 11: 系统公告
|
||||||
|
if (q.includes('公告') || q.includes('通知')) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
create_time,
|
||||||
|
is_top
|
||||||
|
FROM lucky_notice
|
||||||
|
ORDER BY is_top DESC, create_time DESC
|
||||||
|
LIMIT 5
|
||||||
|
`;
|
||||||
|
const [rows] = await pool.execute(sql);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 12: 店铺笔记
|
||||||
|
if (q.includes('笔记') || q.includes('文章')) {
|
||||||
|
const titleMatch = q.match(/笔记\s*["']?([a-zA-Z0-9\u4e00-\u9fa5\s]+)["']?/);
|
||||||
|
const noteTitle = titleMatch && titleMatch[1] ? titleMatch[1].trim() : null;
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT note_title, note_abstract, release_time, read_num
|
||||||
|
FROM lucky_notes
|
||||||
|
WHERE status = 1 -- 已发布
|
||||||
|
`;
|
||||||
|
let params: any[] = [];
|
||||||
|
|
||||||
|
if (noteTitle) {
|
||||||
|
sql += ' AND note_title LIKE ?';
|
||||||
|
params.push(`%${escapeLike(noteTitle)}%`);
|
||||||
|
}
|
||||||
|
sql += ' ORDER BY release_time DESC LIMIT 5';
|
||||||
|
|
||||||
|
const [rows] = await pool.execute(sql, params);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认兜底
|
||||||
|
throw new Error(`
|
||||||
|
无法理解该问题。支持的查询包括:
|
||||||
|
- 商家 GMV(如“TechStore 昨天的 GMV”)
|
||||||
|
- 订单状态(如“订单 L20251127001 状态”)
|
||||||
|
- 用户/商品/提现/充值/公告等信息
|
||||||
|
`.trim());
|
||||||
|
}
|
||||||
46
src/routes/admin.ts
Normal file
46
src/routes/admin.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// src/routes/admin.ts
|
||||||
|
import { Elysia } from 'elysia';
|
||||||
|
import { verifyApiKey, sanitizeResult } from '../security';
|
||||||
|
import { executeNaturalLanguageQuery } from '../queryEngine';
|
||||||
|
|
||||||
|
const adminRoutes = new Elysia({ prefix: '/admin' })
|
||||||
|
.use(verifyApiKey)
|
||||||
|
.post('/query', async ({ body }) => {
|
||||||
|
if (!body || typeof body !== 'object') {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { question, context } = body as { question: string; context?: object };
|
||||||
|
if (typeof question !== 'string' || !question.trim()) {
|
||||||
|
return new Response(JSON.stringify({ error: "Missing or invalid 'question'" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = await executeNaturalLanguageQuery(question, context || {});
|
||||||
|
const sanitized = sanitizeResult(result);
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: sanitized,
|
||||||
|
metadata: {
|
||||||
|
executed_query_type: 'natural_language_fallback',
|
||||||
|
data_source: 'direct_sql',
|
||||||
|
processing_time_ms: Math.round(endTime - startTime),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message }),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default adminRoutes;
|
||||||
83
src/security.ts
Normal file
83
src/security.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// src/security.ts
|
||||||
|
import { Elysia, Context } from 'elysia';
|
||||||
|
|
||||||
|
// 脱敏规则配置
|
||||||
|
const MASK_RULES: Record<string, (value: string) => string> = {
|
||||||
|
// 手机号:138****1234
|
||||||
|
mobile: (v) => v.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
|
||||||
|
phone: (v) => v.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
|
||||||
|
|
||||||
|
// 邮箱:ab***@example.com
|
||||||
|
email: (v) => v.replace(/(.{2}).+(@.*)/, '$1***$2'),
|
||||||
|
|
||||||
|
// 身份证:110***********1234
|
||||||
|
id_card: (v) => v.length > 8
|
||||||
|
? v.substring(0, 3) + '*'.repeat(v.length - 7) + v.substring(v.length - 4)
|
||||||
|
: v,
|
||||||
|
|
||||||
|
// 银行卡:6222 **** **** 1234
|
||||||
|
bank_card: (v) => v.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2'),
|
||||||
|
|
||||||
|
// 用户名:如果长度>4,则中间用*
|
||||||
|
nickname: (v) => v.length > 4
|
||||||
|
? v.substring(0, 2) + '*'.repeat(v.length - 4) + v.substring(v.length - 2)
|
||||||
|
: v,
|
||||||
|
|
||||||
|
// 默认规则
|
||||||
|
default: (v) => v.length > 6
|
||||||
|
? v.substring(0, 2) + '*'.repeat(v.length - 4) + v.substring(v.length - 2)
|
||||||
|
: v
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否为敏感字段(支持模糊匹配)
|
||||||
|
const isSensitiveField = (key: string): boolean => {
|
||||||
|
const sensitiveKeys = ['mobile', 'phone', 'email', 'id_card', 'bank_card', 'nickname', 'realname', 'address'];
|
||||||
|
return sensitiveKeys.some(term => key.toLowerCase().includes(term));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取脱敏函数
|
||||||
|
const getMaskFn = (key: string): ((v: string) => string) => {
|
||||||
|
for (const [term, fn] of Object.entries(MASK_RULES)) {
|
||||||
|
if (key.toLowerCase().includes(term)) {
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MASK_RULES.default || ((v: string) => v);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归脱敏
|
||||||
|
export const sanitizeResult = (data: any): any => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(sanitizeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'object' && data !== null) {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (value == null) {
|
||||||
|
result[key] = value;
|
||||||
|
} else if (isSensitiveField(key)) {
|
||||||
|
result[key] = typeof value === 'string' ? getMaskFn(key)(value) : value;
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
result[key] = sanitizeResult(value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// API Key 验证(修复:使用 Elysia 插件类型)
|
||||||
|
export const verifyApiKey = (app: Elysia) => app.onBeforeHandle((ctx) => {
|
||||||
|
const authHeader = ctx.request.headers.get('authorization');
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
if (token !== process.env.ADMIN_API_KEY) {
|
||||||
|
return new Response('Forbidden', { status: 403 });
|
||||||
|
}
|
||||||
|
});
|
||||||
45
tsconfig.json
Normal file
45
tsconfig.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* 基础选项 */
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
/* 类型检查增强 */
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
/* 输出与源码 */
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
/* Bun & ESM 支持 */
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"types": ["bun-types", "@types/node"],
|
||||||
|
|
||||||
|
/* 路径别名(可选) */
|
||||||
|
// "baseUrl": "./src",
|
||||||
|
// "paths": {
|
||||||
|
// "@/*": ["./*"]
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"coverage"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user