commit 28ab17324d2578ab907ac252780db17a64616c65 Author: Rogee Date: Mon Dec 15 17:55:32 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5737fdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +bin/* +vendor/ +__debug_bin* +build/* +.vscode +.idea +tmp/ +.gocache/ +.gotmp/ +docker-compose.yml +sqlite.db +go.work +go.work.sum +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +node_modules/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bbecac5 --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +SHELL := /bin/sh + +BACKEND_DIR := backend +FRONTEND_ADMIN_DIR := frontend/admin +FRONTEND_USER_DIR := frontend/user +FRONTEND_SUPERADMIN_DIR := frontend/superadmin + +ATOMCTL ?= atomctl + +TENANT_CODE ?= demo +TENANT_NAME ?= Demo Tenant +TENANT_UUID ?= $(shell python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null || uuidgen 2>/dev/null || echo "00000000-0000-0000-0000-000000000000") + +.PHONY: help +help: + @printf "%s\n" "Targets:" + @printf "%s\n" " make migrate Run backend migrations (ARGS=up)" + @printf "%s\n" " make gen-route atomctl gen route (backend)" + @printf "%s\n" " make gen-provider atomctl gen provider (backend)" + @printf "%s\n" " make build-frontend Build admin+user frontends (requires npm)" + @printf "%s\n" " make serve Run backend server (serves built dist)" + @printf "%s\n" " make preview build + serve" + @printf "%s\n" "" + @printf "%s\n" "Preview URLs (after make preview):" + @printf "%s\n" " User : http://localhost:8080/t/$(TENANT_CODE)/" + @printf "%s\n" " Admin: http://localhost:8080/t/$(TENANT_CODE)/admin/" + +.PHONY: migrate +migrate: + @cd $(BACKEND_DIR) && $(ATOMCTL) migrate $(ARGS) + +.PHONY: model +model: + @cd $(BACKEND_DIR) && $(ATOMCTL) gen model + +.PHONY: gen-route +gen-route: + @cd $(BACKEND_DIR) && $(ATOMCTL) gen route --path . + +.PHONY: gen-provider +gen-provider: + @cd $(BACKEND_DIR) && $(ATOMCTL) gen provider + +.PHONY: build-admin +build-admin: + @cd $(FRONTEND_ADMIN_DIR) && npm install + @cd $(FRONTEND_ADMIN_DIR) && npm run build + +.PHONY: build-user +build-user: + @cd $(FRONTEND_USER_DIR) && npm install + @cd $(FRONTEND_USER_DIR) && npm run build + +.PHONY: build-superadmin +build-superadmin: + @cd $(FRONTEND_SUPERADMIN_DIR) && npm install + @cd $(FRONTEND_SUPERADMIN_DIR) && npm run build + +.PHONY: build-frontend +build-frontend: build-admin build-user build-superadmin + +.PHONY: serve +serve: + @cd $(BACKEND_DIR) && mkdir -p .gocache .gotmp + @cd $(BACKEND_DIR) && env GOCACHE=$$PWD/.gocache GOTMPDIR=$$PWD/.gotmp go run . serve + +.PHONY: preview +preview: build-frontend serve diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b58bc1 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# QuyUn v2(多租户版本) + +实现约定以 `specs/` 为准: + +- 路由前缀:`/t/:tenant_code/...`(API:`/t/:tenant_code/v1`;Admin:`/t/:tenant_code/admin`;User:`/t/:tenant_code/`) +- 数据库:见 `specs/DB.sql`(后端迁移:`backend/database/migrations/00001_init_multi_tenant.sql`) + +## 开发(后端) + +在 `backend/`: + +- 启动依赖:`docker compose up -d postgres redis` +- 运行迁移:`env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go run . migrate up` +- 启动服务:`env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go run . serve` + +> 注意:当前 API 仅做了路由骨架(大部分返回 501),用于前后端联调与路由/租户前缀验证。 + +## 开发(前端) + +- Admin:`frontend/admin/` +- User:`frontend/user/` +- Super Admin:`frontend/superadmin/`(访问:`/super/`;API:`/super/v1`) + - 角色类型管理:`/super/v1/roles` + - 租户统计:`/super/v1/statistics`、`/super/v1/tenants` + +两端均会从 `location.pathname` 推导 `tenant_code`,并设置: + +- Admin router base:`/t//admin/` +- User router base:`/t//` +- API base:`/t//v1` diff --git a/atomctl.md b/atomctl.md new file mode 100644 index 0000000..a8e0866 --- /dev/null +++ b/atomctl.md @@ -0,0 +1,346 @@ +# atomctl v2 命令行工具 + +Atom 应用脚手架与代码生成工具。基于 cobra 构建,集成新项目/模块脚手架、HTTP 路由与 Provider 生成、数据库迁移与模型生成、Swagger 文档、Proto 生成与代码格式化等能力。 + +## 安装与使用 + +- 安装已发布版本:`go install go.ipao.vip/atomctl/v2@latest` +- 本地构建:`go build -o atomctl .` +- 开发运行:`go run . --help` +- 运行测试:`go test ./...` + +运行 `atomctl --help` 查看总览。 + +## 顶级命令 + +- `new`:项目脚手架与代码模板初始化 +- `gen`:代码生成(route/provider/model/enum/service) +- `migrate`:数据库迁移(pressly/goose) +- `swag`:Swagger 文档生成与格式化 +- `fmt`:Go 代码格式化(自动安装/调用 gofumpt) +- `buf`:Proto 代码生成(自动安装 buf 并执行 generate) + +--- + +## new 脚手架 + +创建项目/组件初始文件。该命令组带有以下持久化参数(所有子命令通用): + +- `--force, -f`:覆盖已存在文件/目录 +- `--dry-run`:仅预览生成动作,不写入文件 +- `--dir`:输出基目录(默认 `.`) + +子命令: + +### new project (别名:p) + +- 功能:基于内置模板创建新项目,或在已有 go.mod 的项目内进行就地初始化 +- 参数: + - `[module]`:可选。在空目录中创建项目时必填(例如 `github.com/acme/myapp`);在已有项目内可省略 +- 选项: + - `--force`:当目标目录存在时强制覆盖 +- 行为说明: + - 识别模板中的隐藏文件占位(模板以 `-` 开头的文件名会渲染为 `.` 前缀,例如 `-.gitignore` -> `.gitignore`) + - 支持 `.tpl` 渲染与 `.raw` 直写;也支持文件内通过 `atomctl:mode=tpl|raw` 指示 + - 在当前模块内执行时会跳过覆盖已有的 `go.mod` + - 生成后提示后续步骤(`cd` 与 `go mod tidy`) + +示例: + +``` +atomctl new project github.com/acme/demo +atomctl new -f --dir ./playground project github.com/acme/demo +atomctl new project # 在已有 go.mod 的项目中就地初始化 +``` + +### new provider + +- 功能:创建 Provider 脚手架到 `providers/` +- 参数: + - ``:可选。省略名称时会列出可用的内置预置 providers 供选择 +- 选项:继承 `--dry-run`、`--dir` +- 行为: + - 若 `` 与内置 `templates/providers/` 目录同名,则渲染该目录 + - 否则回退渲染 `templates/providers/default` 到 `providers/` + +示例: + +``` +atomctl new provider # 列出可用预置 providers +atomctl new provider redis # 渲染 providers/redis 预置 +atomctl new provider email # 未命中预置,回退 default 渲染到 providers/email +atomctl new --dry-run --dir ./demo provider cache +``` + +### new event (别名:e) + +- 功能:生成事件发布者与订阅者模板 +- 参数: + - ``:必填,事件名(驼峰自动转换为 snake 作为 topic) +- 选项: + - `--only`:仅生成某一侧,`publisher` 或 `subscriber` + - 继承 `--dry-run`、`--dir` +- 行为说明: + - 生成 publisher 到 `app/events/publishers/.go` + - 生成 subscriber 到 `app/events/subscribers/.go` + - 追加常量到 `app/events/topics.go`:`const Topic{Name} = "event:"`(避免重复) + +示例: + +``` +atomctl new event UserCreated +atomctl new event UserCreated --only=publisher +``` + +### new job + +- 功能:生成任务模板文件;支持可选的定时任务模板 +- 参数: + - ``:必填,任务名 +- 选项: + - 继承 `--dry-run`、`--dir` + - `--cron`:除生成 `app/jobs/.go` 外,额外生成 `app/jobs/cron_.go` + +示例: + +``` +atomctl new job SendDailyReport # 生成 app/jobs/send_daily_report.go +atomctl new job SendDailyReport --cron # 同时生成 cron 版本 app/jobs/cron_send_daily_report.go +``` + +> 说明:代码中已存在 `new module` 实现,但当前未注册到 `new` 子命令中(处于弃用状态)。 + +--- + +## gen 代码生成 + +命令组持久化参数: + +- `-c, --config`:数据库配置文件路径(默认 `config.toml`),用于 `gen model` +- 执行完成后自动调用 `atomctl fmt` 格式化生成代码 + +子命令: + +### gen model (别名:m) + +- 功能:连接 PostgreSQL,基于配置生成 GORM 模型与相关文件(委托 `go.ipao.vip/gen`)到 `./database`,并读取 `./database/.transform.yaml` 做类型/命名转换 +- 配置文件 `[Database]` 字段: + - `Username`、`Password`、`Database`、`Host`、`Port`、`Schema`、`SslMode`、`TimeZone` +- 示例 `config.toml`: + +```toml +[Database] +Host = "127.0.0.1" +Port = 5432 +Username = "postgres" +Password = "secret" +Database = "app" +Schema = "public" +SslMode = "disable" +TimeZone = "Asia/Shanghai" +``` + +示例: + +``` +atomctl gen -c config.toml model +``` + +### gen route + +- 功能:扫描项目 `app/http` 目录的控制器,解析注释生成 `routes.gen.go` +- 参数: + - `--path`:扫描根目录(默认当前工作目录);也可作为位置参数传入项目根路径 +- 注释规则: + - `@Router []` 指定路由与方法,如 `@Router /users/:id [get]` + - `@Bind () key() [model()]` + - position 枚举:`path|query|body|header|cookie|local|file` + - model() 定义形式: + 1) `model()` 默认字段 `id`,类型 `int` + 2) `model(id)` 指定字段 `id`,类型默认 `int` + 3) `model(id:int)` 指定字段 `id`,类型 `int` + - 示例: + - `@Bind id path key(id)`(PathParam 绑定) + - `@Bind page query key(page)`(QueryParam 绑定) + - `@Bind auth header key(Authorization)` + - `@Bind file file key(upload)`(文件) + - `@Bind user path key(id) model(id:int)`(从路径 id 加载模型,按 id:int 查询) + +示例: + +```go +// UserController +// @Router /users/:id [get] +// @Bind id path key(id) model(id:int) +func (c *UserController) Show(ctx context.Context, id int) (*User, error) { /* ... */ } +``` + +执行: + +``` +atomctl gen route --path . +``` + +> 注意:生成完成后会自动触发 `gen provider` 以补全依赖注入。 + +#### Bind 参数类型与位置支持 + +- 标量类型(scalar):`string`、`bool`、`int|int8|int16|int32|int64`、`uint|uint8|uint16|uint32|uint64`、`float32|float64` +- 位置与类型支持: + - `path`:支持标量;也支持通过 `model()` 从路径值加载模型记录(推荐标量或 model 查找)。 + - 示例:`@Bind id path key(id)`、`@Bind user path key(id) model(id:int)` + - `query`:支持标量或结构体(结构体将按字段名/标签聚合解析查询参数)。 + - 示例:`@Bind page query key(page)`、`@Bind filter query key(q)` 或 `@Bind opts query key(_)` + - `header`:支持标量或结构体(按字段名/标签解析请求头)。 + - 示例:`@Bind auth header key(Authorization)` + - `cookie`:支持标量;`string` 有快捷写法(`CookieParam`)。 + - 示例:`@Bind sid cookie key(session_id)` + - `body`:支持结构体或标量(通常用于 JSON 体,推荐结构体)。 + - 示例:`@Bind input body key(_)` + - `file`:固定类型为 `multipart.FileHeader`,用于文件上传。 + - 示例:`@Bind file file key(upload)` + - `local`:任意类型(从上下文本地存取,而非请求来源)。 + - 示例:`@Bind user local key(currentUser)` + + +### gen provider (别名:p) + +- 功能:扫描 Go 源码查找带 `@provider` 注释的结构体,生成 `provider.gen.go` 实现依赖注入与分组注册 +- 注释语法:`@provider():[except|only] [returnType] [group]` + - `mode`:`grpc|event|job|cronjob|model`(留空为默认) + - `:only`:仅注入标注了结构体字段 tag `inject:"true"` 的依赖 + - `:except`:注入全部非标量字段,除非字段 tag `inject:"false"` + - `returnType`:Provide 返回类型(如 `contracts.Initial`) + - `group`:归属分组(如 `atom.GroupInitial`) +- 行为特性: + - 自动分析字段类型与 import,忽略标量类型 + - `grpc`:注入 `providers/grpc.Grpc`,设置 `GrpcRegisterFunc` + - `event`:注入 `providers/event.PubSub` + - `job|cronjob`:注入 `providers/job.Job` 并引入 `github.com/riverqueue/river` + - `model`:标记需要 `Prepare` 钩子 + +示例: + +```go +// @provider(job):only contracts.Initial atom.GroupInitial +type UserJob struct { + // 仅在 only 模式下注入 + Repo *repo.UserRepo `inject:"true"` + Log *log.Logger `inject:"true"` +} +``` + +执行:`atomctl gen provider`(支持在任意子目录执行,自动写入对应包下的 `provider.gen.go`) + +### gen enum (别名:e) + +- 功能:在工程内扫描包含 `ENUM(...)` 且带 `swagger:enum` 的类型定义文件,为每个文件生成 `*.gen.go` +- 选项: + - `-f, --flag`:生成 `flag.Value` 支持(默认 true) + - `-m, --marshal`:生成 JSON 编解码支持 + - `-s, --sql`:生成 `database/sql` 相关驱动与 Null 支持(默认 true) + +示例:`atomctl gen enum -m -s` + +### gen service + +- 功能:根据 `app/services` 下的服务文件聚合生成 `services.gen.go` +- 选项: + - `--path`:服务目录(默认 `./app/services`) + +示例:`atomctl gen service --path ./app/services` + +--- + +## migrate 数据库迁移(goose) + +- 用法:`atomctl migrate [action] [args...]` +- 支持 action:`up|up-by-one|up-to|create|down|down-to|fix|redo|reset|status|version` +- 选项: + - `-c, --config`:数据库配置文件(默认 `config.toml`,读取 `[Database]`) + - `--dir`:迁移目录(默认 `database/migrations`) + - `--table`:迁移版本表名(默认 `migrations`) +- 说明: + - 执行 `create` 时若未指定脚本类型,会自动追加 `sql` 类型 + - 连接配置同上 `gen model` 的 `[Database]` 字段 + +示例: + +``` +atomctl migrate status +atomctl migrate up +atomctl migrate create add_users_table +``` + +--- + +## swag 文档 + +### swag init (别名:i) + +- 功能:生成 Swagger 文档(go/json/yaml) +- 选项: + - `--dir`:项目根目录(默认 `.`) + - `--out`:输出目录(默认 `docs`) + - `--main`:主入口文件(默认 `main.go`) + +示例:`atomctl swag init --dir . --out docs --main cmd/server/main.go` + +### swag fmt (别名:f) + +- 功能:格式化接口注释与路由 +- 选项: + - `--dir`:扫描目录(默认 `./app/http`) + - `--main`:主入口文件(默认 `main.go`) + +示例:`atomctl swag fmt --dir ./app/http --main main.go` + +--- + +## fmt 代码格式化 + +- 功能:调用 `gofumpt -extra` 格式化全项目;若未安装将自动 `go install mvdan.cc/gofumpt@latest` +- 选项: + - `--check`:仅检查不写入,输出未格式化文件列表 + - `--path`:格式化范围(默认 `.`) + +示例: + +``` +atomctl fmt # 写入格式化 +atomctl fmt --check --path ./pkg +``` + +--- + +## buf Proto 生成 + +- 功能:检查/安装 `buf` 并执行 `buf generate`(在指定目录) +- 选项: + - `--dir`:执行目录(默认 `.`) + - `--dry-run`:仅预览不执行 +- 说明:若找不到 `buf.yaml` 会给出提示但仍尝试生成 + +示例:`atomctl buf --dir ./proto` + +--- + +## 配置与约定 + +- Go 版本:1.23+ +- 数据库配置段落:`[Database]`(被 `gen model` 与 `migrate` 读取) +- 代码生成默认位置: + - 路由:`app/http/**/routes.gen.go` + - Provider:每个包内的 `provider.gen.go` + - 模型:`./database`(含 `.transform.yaml`) + - Enum:原文件同目录生成 `*.gen.go` + - Services:`app/services/services.gen.go` + +--- + +## 常见问题 + +- 路由未生成?确保控制器方法注释包含 `@Router`,且形参通过 `@Bind` 标注位置/键名。 +- Provider 未注入期望字段?检查 `@provider:only|:except` 与字段 tag `inject:"true|false"` 的组合是否正确。 +- 执行 `fmt` 报 gofumpt 不存在?工具会自动安装,如仍失败请检查网络与 `GOBIN`/`PATH`。 +- `buf generate` 找不到命令?工具会自动安装 buf,若仍失败请手动安装或检查网络代理。 diff --git a/backend/.air.toml b/backend/.air.toml new file mode 100644 index 0000000..f97236a --- /dev/null +++ b/backend/.air.toml @@ -0,0 +1,40 @@ +# .air.toml - Air 热重载配置文件 + +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "toml"] + kill_delay = "0s" + log = "build-errors.log" + send_interrupt = false + stop_on_root = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..90df546 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..00c3ede --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,58 @@ +# 应用配置 +APP_MODE=development +APP_BASE_URI=http://localhost:8080 + +# HTTP 服务配置 +HTTP_PORT=8080 +HTTP_HOST=0.0.0.0 + +# 数据库配置 +DB_HOST=localhost +DB_PORT=5432 +DB_NAME={{.ProjectName}} +DB_USER=postgres +DB_PASSWORD=password +DB_SSL_MODE=disable +DB_MAX_CONNECTIONS=25 +DB_MAX_IDLE_CONNECTIONS=5 +DB_CONNECTION_LIFETIME=5m + +# Redis 配置 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# JWT 配置 +JWT_SECRET_KEY=your-secret-key-here +JWT_EXPIRES_TIME=168h + +# HashIDs 配置 +HASHIDS_SALT=your-salt-here + +# 日志配置 +LOG_LEVEL=info +LOG_FORMAT=json + +# 文件上传配置 +UPLOAD_MAX_SIZE=10MB +UPLOAD_PATH=./uploads + +# 邮件配置 +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= + +# 第三方服务配置 +REDIS_URL=redis://localhost:6379/0 +DATABASE_URL=postgres://postgres:password@localhost:5432/{{.ProjectName}}?sslmode=disable + +# 开发配置 +ENABLE_SWAGGER=true +ENABLE_CORS=true +DEBUG_MODE=true + +# 监控配置 +ENABLE_METRICS=false +METRICS_PORT=9090 \ No newline at end of file diff --git a/backend/.gitea/workflows/build.yml b/backend/.gitea/workflows/build.yml new file mode 100644 index 0000000..6784f72 --- /dev/null +++ b/backend/.gitea/workflows/build.yml @@ -0,0 +1,42 @@ +name: Build Application +run-name: ${{ gitea.actor }} Build Application +on: [push] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies and build frontend + run: | + cd frontend + npm config set registry https://npm.hub.ipao.vip + npm install + npm run build + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.22" + + - name: Build Go application + run: | + cd backend + mkdir -p build + go env -w GOPROXY=https://go.hub.ipao.vip,direct + go env -w GONOPROXY='git.ipao.vip' + go env -w GONOSUMDB='git.ipao.vip' + go mod tidy + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/app . + + - name: Build final Docker image + run: | + docker login -u ${{ secrets.DOCKER_AF_USERNAME }} -p ${{ secrets.DOCKER_AF_PASSWORD }} docker-af.hub.ipao.vip + docker build --push -t docker-af.hub.ipao.vip/rogeecn/test:latest . diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ae3e86d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,29 @@ +bin/* +vendor/ +__debug_bin* +backend +build/* +.vscode +.idea +tmp/ +.gocache/ +.gotmp/ +docker-compose.yml +sqlite.db +go.work +go.work.sum +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 0000000..19d4f68 --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,294 @@ +# golangci-lint 配置文件 +# https://golangci-lint.run/usage/configuration/ + +# 运行时配置 +run: + # 默认并行处理器数量 + default-concurrency: 4 + + # 超时时间 + timeout: 5m + + # 退出代码 + issues-exit-code: 1 + + # 测试包含的文件 + tests: true + + # 是否跳过文件 + skip-files: + - "_test\\.go$" + - ".*\\.gen\\.go$" + - ".*\\.pb\\.go$" + + # 是否跳过目录 + skip-dirs: + - "vendor" + - "node_modules" + - ".git" + - "build" + - "dist" + +# 输出配置 +output: + # 输出格式 + format: colored-line-number + + # 打印已使用的 linter + print-issued-lines: true + + # 打印 linter 名称 + print-linter-name: true + + # 唯一性检查 + uniq-by-line: true + +# linter 启用配置 +linters-settings: + # 错误检查 + errcheck: + # 检查类型断言 + check-type-assertions: true + # 检查赋值 + check-blank: true + + # 代码复杂度 + gocyclo: + # 最小复杂度 + min-complexity: 15 + + # 函数参数和返回值 + gocognit: + # 最小认知复杂度 + min-complexity: 20 + + # 函数长度 + funlen: + # 最大行数 + lines: 60 + # 最大语句数 + statements: 40 + + # 代码行长度 + lll: + # 最大行长度 + line-length: 120 + + # 导入顺序 + importas: + # 别名规则 + no-unaliased: true + alias: + - pkg: "github.com/sirupsen/logrus" + alias: "logrus" + - pkg: "github.com/stretchr/testify/assert" + alias: "assert" + - pkg: "github.com/stretchr/testify/suite" + alias: "suite" + + # 重复导入 + dupl: + # 重复代码块的最小 token 数 + threshold: 100 + + # 空值检查 + nilerr: + # 检查返回 nil 的函数 + check-type-assertions: true + check-blank: true + + # 代码格式化 + gofmt: + # 格式化简化 + simplify: true + + # 导入检查 + goimports: + # 本地前缀 + local-prefixes: "{{.ModuleName}}" + + # 静态检查 + staticcheck: + # 检查版本 + go_version: "1.22" + + # 结构体标签 + structtag: + # 检查标签 + required: [] + # 是否允许空标签 + allow-omit-latest: true + + # 未使用的变量 + unused: + # 检查字段 + check-exported-fields: true + + # 变量命名 + varnamelen: + # 最小变量名长度 + min-name-length: 2 + # 检查参数 + check-parameters: true + # 检查返回值 + check-return: true + # 检查接收器 + check-receiver: true + # 检查变量 + check-variable: true + # 忽略名称 + ignore-names: + - "ok" + - "err" + - "T" + - "i" + - "n" + - "v" + # 忽略类型 + ignore-type-assert-ok: true + ignore-map-index-ok: true + ignore-chan-recv-ok: true + ignore-decls: + - "T any" + - "w http.ResponseWriter" + - "r *http.Request" + +# 启用的 linter +linters: + enable: + # 错误检查 + - errcheck + - errorlint + - goerr113 + + # 代码复杂度 + - gocyclo + - gocognit + - funlen + + # 代码风格 + - gofmt + - goimports + - lll + - misspell + - whitespace + + # 导入检查 + - importas + - dupl + + # 静态检查 + - staticcheck + - unused + - typecheck + - ineffassign + - bodyclose + - contextcheck + - nilerr + + # 测试检查 + - tparallel + - testpackage + - thelper + + # 性能检查 + - prealloc + - unconvert + + # 安全检查 + - gosec + - noctx + - rowserrcheck + + # 代码质量 + - revive + - varnamelen + - exportloopref + - forcetypeassert + - govet + - paralleltest + - nlreturn + - wastedassign + - wrapcheck + +# 禁用的 linter +linters-disable: + - deadcode # 被 unused 替代 + - varcheck # 被 unused 替代 + - structcheck # 被 unused 替代 + - interfacer # 已弃用 + - maligned # 已弃用 + - scopelint # 已弃用 + +# 问题配置 +issues: + # 排除规则 + exclude-rules: + # 排除测试文件的某些规则 + - path: _test\.go + linters: + - funlen + - gocyclo + - dupl + - gochecknoglobals + - gochecknoinits + + # 排除生成的文件 + - path: \.gen\.go$ + linters: + - lll + - funlen + - gocyclo + + # 排除错误处理中的简单错误检查 + - path: .* + text: "Error return value of `.*` is not checked" + + # 排除特定的 golangci-lint 注释 + - path: .* + text: "// nolint:.*" + + # 排除 context.Context 的未使用检查 + - path: .* + text: "context.Context should be the first parameter of a function" + + # 排除某些性能优化建议 + - path: .* + text: "predeclared" + + # 排除某些重复代码检查 + - path: .* + linters: + - dupl + text: "is duplicate of" + + # 最大问题数 + max-issues-per-linter: 50 + + # 最大相同问题数 + max-same-issues: 3 + +# 严重性配置 +severity: + # 默认严重性 + default-severity: error + + # 规则严重性 + rules: + - linters: + - dupl + - gosec + severity: warning + + - linters: + - misspell + - whitespace + severity: info + +# 性能配置 +performance: + # 是否使用内存缓存 + use-memory-cache: true + + # 缓存超时时间 + cache-timeout: 5m \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..255ddd4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,82 @@ +# 多阶段构建 Dockerfile +# 阶段 1: 构建应用 +FROM golang:1.22-alpine AS builder + +# 安装构建依赖 +RUN apk add --no-cache git ca-certificates tzdata + +# 设置工作目录 +WORKDIR /app + +# 复制 go mod 文件 +COPY go.mod go.sum ./ + +# 设置 Go 代理 +ENV GOPROXY=https://goproxy.cn,direct +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN go build -a -installsuffix cgo -ldflags="-w -s" -o main . + +# 阶段 2: 构建前端(如果有) +# 如果有前端构建,取消下面的注释 +# FROM node:18-alpine AS frontend-builder +# WORKDIR /app +# COPY frontend/package*.json ./ +# RUN npm ci --only=production +# COPY frontend/ . +# RUN npm run build + +# 阶段 3: 运行时镜像 +FROM alpine:3.20 AS runtime + +# 安装运行时依赖 +RUN apk add --no-cache ca-certificates tzdata curl + +# 设置时区 +RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone && \ + apk del tzdata + +# 创建非 root 用户 +RUN addgroup -g 1000 appgroup && \ + adduser -u 1000 -G appgroup -s /bin/sh -D appuser + +# 创建必要的目录 +RUN mkdir -p /app/config /app/logs /app/uploads && \ + chown -R appuser:appgroup /app + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制应用 +COPY --from=builder /app/main . +COPY --chown=appuser:appgroup config.toml ./config/ + +# 如果有前端构建,取消下面的注释 +# COPY --from=frontend-builder /app/dist ./dist + +# 创建空目录供应用使用 +RUN mkdir -p /app/logs /app/uploads && \ + chown -R appuser:appgroup /app + +# 切换到非 root 用户 +USER appuser + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# 暴露端口 +EXPOSE 8080 + +# 启动应用 +CMD ["./main", "serve"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..4140057 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,32 @@ +# 开发环境 Dockerfile +FROM golang:1.22-alpine AS builder + +# 安装必要的工具 +RUN apk add --no-cache git ca-certificates tzdata + +# 设置工作目录 +WORKDIR /app + +# 复制 go mod 文件 +COPY go.mod go.sum ./ + +# 设置 Go 代理 +RUN go env -w GOPROXY=https://goproxy.cn,direct + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 设置时区 +ENV TZ=Asia/Shanghai + +# 安装 air 用于热重载 +RUN go install github.com/air-verse/air@latest + +# 暴露端口 +EXPOSE 8080 9090 + +# 启动命令 +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..1adb571 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,64 @@ +buildAt=`date +%Y/%m/%d-%H:%M:%S` +gitHash=`(git log -1 --pretty=format:%H 2>/dev/null || echo "no-commit")` +version=`(git describe --tags --exact-match HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null | grep -v HEAD 2>/dev/null || echo "dev")` +# 修改为项目特定的变量路径 +flags="-X 'quyun/v2/pkg/utils.Version=${version}' -X 'quyun/v2/pkg/utils.BuildAt=${buildAt}' -X 'quyun/v2/pkg/utils.GitHash=${gitHash}'" +release_flags="-w -s ${flags}" + +GOPATH:=$(shell go env GOPATH) + +.PHONY: tidy +tidy: + @go mod tidy + +.PHONY: release +release: + @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=${flags} -o bin/release/v2 . + @cp config.toml bin/release/ + +.PHONY: build +build: + @go build -ldflags=${flags} -o bin/v2 . + +.PHONY: run +run: build + @./bin/v2 + +.PHONY: test +test: + @go test -v ./tests/... -cover + +.PHONY: info +info: + @echo "Build Information:" + @echo "==================" + @echo "Build Time: $(buildAt)" + @echo "Git Hash: $(gitHash)" + @echo "Version: $(version)" + +.PHONY: lint +lint: + @golangci-lint run + +.PHONY: tools +tools: + go install github.com/air-verse/air@latest + go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest + go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + go install github.com/bufbuild/buf/cmd/buf@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go get -u go.ipao.vip/atom + go get -u google.golang.org/genproto + go get -u github.com/gofiber/fiber/v3 +.PHONY: init +init: tools + @atomctl swag init + @atomctl gen enum + @atomctl gen route + @atomctl gen service + @buf generate + @go mod tidy + @go get -u + @go mod tidy \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7e1274b --- /dev/null +++ b/backend/README.md @@ -0,0 +1,114 @@ +## 路由生成(gen route) + +通过在控制器方法上编写注释,解析器从 Go AST 中读取注释、方法签名与参数列表,自动生成路由注册与参数绑定代码。 + +- 核心标签:`@Router` 定义路径与方法;`@Bind` 定义方法参数的来源与键名。 +- 生成行为:输出 `router.(path, FuncN(...))` 或 `DataFuncN(...)` 包装调用,并自动汇聚所需 imports 与控制器注入字段。 + +### 快速开始 + +``` +atomctl gen route [path] +``` + +- 生成文件:当前包目录下 `routes.gen.go` +- 分组与排序:按控制器分组,导入、方法、路由项稳定排序,便于审阅 diff。 + +### 注释语法 + +- `@Router []` + - 示例:`@Router /users/:id [get]` + +- `@Bind [key()] [model(|[:])]` + - `paramName` 与方法参数名一致(大小写敏感) + - `position`:`path`、`query`、`body`、`header`、`cookie`、`local`、`file` + - 可选: + - `key()` 覆盖默认键名; + - `model()` 详见“模型绑定”。 + +### 参数绑定规则(按 position) + +- query:标量用 `QueryParam[T]("key")`,非标量用 `Query[T]("key")` +- path:标量用 `PathParam[T]("key")`,非标量用 `Path[T]("key")` + - 若使用 `model()`(仅在 path 有效),会按字段值查询并绑定为 `T`,详见下文 +- header:`Header[T]("key")` +- body:`Body[T]("key")` +- cookie:`string` 用 `CookieParam("key")`,其他用 `Cookie[T]("key")` +- file:`File[multipart.FileHeader]("key")` +- local:`Local[T]("key")` + +说明: + +- 标量类型集合:`string`、`int`、`int32`、`int64`、`float32`、`float64`、`bool` +- `key` 默认等于 `paramName`;设置 `key(...)` 后以其为准 +- `file` 使用固定类型 `multipart.FileHeader` + +### 类型与指针处理 + +- 支持 `T`、`*T`、`pkg.T`、`*pkg.T`;会正确收集选择子表达式对应 import +- 忽略结尾为 `Context` 或 `Ctx` 的参数(框架上下文) +- 指针处理:除 `local` 外会去掉前导 `*` 作为泛型实参;`local` 保留指针(便于写回) + +### 解析与匹配 + +- 先收集注释中的多条 `@Bind`,再按“方法参数列表顺序”匹配并输出绑定器,确保调用顺序与方法签名一致 +- 未在方法参数中的 `@Bind` 会被忽略;缺失 `@Router` 或方法无注释将跳过该方法 +- import 自动收集去重;控制器注入字段名为类型名的小驼峰形式,例如 `userController *UserController` + +### 返回值与包装函数 + +- 返回值个数 > 1:使用 `DataFuncN` +- 否则使用 `FuncN` +- `N` 为参与绑定的参数个数 + +### 模型绑定(path + model) + +当 `@Bind ... model(...)` 配合 `position=path` 使用时,将根据路径参数值查询模型并绑定为方法参数类型的实例(`T` 来自方法参数)。 + +- 语法: + - 仅字段:`model(id)`(推荐) + - 指定字段与类型:`model(id:int)`、`model(code:string)`(用于非字符串路径参数) + - 指定类型与字段:`model(pkg.Type:field)` 或 `model(pkg.Type)`(字段缺省为 `id`) +- 行为: + - 生成的绑定器会按给定字段构造查询条件并返回首条记录 + - 自动注入 import:`field "go.ipao.vip/gen/field"`,用于构造字段条件表达式 + +示例: + +```go +// @Router /users/:id [get] +// @Bind user path key(id) model(id) +func (uc *UserController) Show(ctx context.Context, user *models.User) (*UserDTO, error) +``` + +### 完整示例 + +注释与方法签名: + +```go +// @Router /users/:id [get] +// @Bind user path key(id) model(id) +// @Bind fields query +// @Bind token header key(Authorization) +// @Bind sess cookie key(session_id) +// @Bind cfg local +func (uc *UserController) GetUser(ctx context.Context, user *models.User, fields []string, token string, sess string, cfg *AppConfig) (*User, error) +``` + +生成的路由注册(示意): + +```go +router.Get("/users/:id", DataFunc4( + r.userController.GetUser, + PathModel[models.User]("id", "id"), + Query[[]string]("fields"), + Header[string]("Authorization"), + CookieParam("session_id"), +)) +``` + +### 错误与限制 + +- 无效的 `@Router` 语法会报错;无效的 `position` 会在解析阶段触发错误 +- `file` 仅支持单文件头;`model()` 仅在 `position=path` 时参与代码生成 +- 请确保路由段变量名与 `key(...)` 保持一致 diff --git a/backend/app/commands/event/event.go b/backend/app/commands/event/event.go new file mode 100644 index 0000000..4325d0e --- /dev/null +++ b/backend/app/commands/event/event.go @@ -0,0 +1,58 @@ +package event + +import ( + "context" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "quyun/v2/app/commands" + "quyun/v2/app/events/subscribers" + "quyun/v2/providers/app" + "quyun/v2/providers/event" + "quyun/v2/providers/postgres" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + postgres.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("event"), + atom.Short("start event processor"), + atom.RunE(Serve), + atom.Providers( + defaultProviders(). + With( + subscribers.Provide, + ), + ), + ) +} + +type Service struct { + dig.In + + App *app.Config + PubSub *event.PubSub + Initials []contracts.Initial `group:"initials"` +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(ctx context.Context, svc Service) error { + log.SetFormatter(&log.JSONFormatter{}) + + if svc.App.IsDevMode() { + log.SetLevel(log.DebugLevel) + } + + return svc.PubSub.Serve(ctx) + }) +} diff --git a/backend/app/commands/grpc/grpc.go b/backend/app/commands/grpc/grpc.go new file mode 100644 index 0000000..43c45a9 --- /dev/null +++ b/backend/app/commands/grpc/grpc.go @@ -0,0 +1,57 @@ +package grpc + +import ( + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "quyun/v2/app/commands" + "quyun/v2/app/grpc/users" + "quyun/v2/providers/app" + "quyun/v2/providers/grpc" + "quyun/v2/providers/postgres" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + postgres.DefaultProvider(), + grpc.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("grpc"), + atom.Short("run grpc server"), + atom.RunE(Serve), + atom.Providers( + defaultProviders(). + With( + users.Provide, + ), + ), + ) +} + +type Service struct { + dig.In + + App *app.Config + Grpc *grpc.Grpc + Initials []contracts.Initial `group:"initials"` +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(svc Service) error { + log.SetFormatter(&log.JSONFormatter{}) + + if svc.App.IsDevMode() { + log.SetLevel(log.DebugLevel) + } + + return svc.Grpc.Serve() + }) +} diff --git a/backend/app/commands/http/http.go b/backend/app/commands/http/http.go new file mode 100644 index 0000000..a0b33c4 --- /dev/null +++ b/backend/app/commands/http/http.go @@ -0,0 +1,100 @@ +package http + +import ( + "context" + "database/sql" + "sort" + "strings" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "quyun/v2/app/commands" + "quyun/v2/app/errorx" + "quyun/v2/app/http/api" + "quyun/v2/app/http/super" + "quyun/v2/app/http/web" + "quyun/v2/app/jobs" + "quyun/v2/app/tenancy" + _ "quyun/v2/docs" + "quyun/v2/providers/app" + "quyun/v2/providers/http" + "quyun/v2/providers/http/swagger" + "quyun/v2/providers/job" + "quyun/v2/providers/jwt" + "quyun/v2/providers/postgres" + + "github.com/gofiber/fiber/v3/middleware/favicon" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + http.DefaultProvider(), + postgres.DefaultProvider(), + jwt.DefaultProvider(), + job.DefaultProvider(), + {Provider: api.Provide}, + {Provider: super.Provide}, + {Provider: web.Provide}, + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("serve"), + atom.Short("run http server"), + atom.RunE(Serve), + atom.Providers( + defaultProviders(). + With( + jobs.Provide, + ), + ), + ) +} + +type Service struct { + dig.In + + App *app.Config + Job *job.Job + Http *http.Service + DB *sql.DB + Initials []contracts.Initial `group:"initials"` + Routes []contracts.HttpRoute `group:"routes"` +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(ctx context.Context, svc Service) error { + log.SetFormatter(&log.JSONFormatter{}) + + if svc.App.Mode == app.AppModeDevelopment { + log.SetLevel(log.DebugLevel) + + svc.Http.Engine.Get("/swagger/*", swagger.HandlerDefault) + } + svc.Http.Engine.Use(errorx.Middleware) + svc.Http.Engine.Use(favicon.New(favicon.Config{ + Data: []byte{}, + })) + + rootGroup := svc.Http.Engine.Group("") + tenantGroup := svc.Http.Engine.Group("/t/:tenant_code", tenancy.Middleware(svc.DB)) + + sort.SliceStable(svc.Routes, func(i, j int) bool { + return svc.Routes[i].Name() < svc.Routes[j].Name() + }) + for _, route := range svc.Routes { + if strings.HasPrefix(route.Name(), "super") { + route.Register(rootGroup) + continue + } + route.Register(tenantGroup) + } + + return svc.Http.Serve(ctx) + }) +} diff --git a/backend/app/commands/migrate/20140202000000_river_queue.go b/backend/app/commands/migrate/20140202000000_river_queue.go new file mode 100644 index 0000000..40e2255 --- /dev/null +++ b/backend/app/commands/migrate/20140202000000_river_queue.go @@ -0,0 +1,35 @@ +package migrate + +import ( + "context" + "database/sql" + + "github.com/pkg/errors" + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(RiverQueueUp, RiverQueueDown) +} + +func RiverQueueUp(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil) + if err != nil { + return errors.Wrap(err, "river migrate up failed") + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{TargetVersion: -1}) + return err +} + +func RiverQueueDown(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil) + if err != nil { + return errors.Wrap(err, "river migrate down failed") + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{TargetVersion: -1}) + return err +} diff --git a/backend/app/commands/migrate/migrate.go b/backend/app/commands/migrate/migrate.go new file mode 100644 index 0000000..9dd44e3 --- /dev/null +++ b/backend/app/commands/migrate/migrate.go @@ -0,0 +1,89 @@ +package migrate + +import ( + "context" + "database/sql" + + "quyun/v2/app/commands" + "quyun/v2/database" + "quyun/v2/providers/postgres" + + "github.com/pressly/goose/v3" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.uber.org/dig" + + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + postgres.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("migrate"), + atom.Short("run migrations"), + atom.RunE(Serve), + atom.Providers(defaultProviders()), + atom.Example("migrate [up|up-by-one|up-to|create|down|down-to|fix|redo|reset|status|version]"), + ) +} + +type Service struct { + dig.In + + DB *sql.DB +} + +// migrate +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(ctx context.Context, svc Service) error { + if len(args) == 0 { + args = append(args, "up") + } + + if args[0] == "create" { + return nil + } + + action, args := args[0], args[1:] + log.Infof("migration action: %s args: %+v", action, args) + + goose.SetBaseFS(database.MigrationFS) + goose.SetTableName("migrations") + goose.AddNamedMigrationNoTxContext("0001_river_job.go", RiverUp, RiverDown) + + return goose.RunContext(context.Background(), action, svc.DB, "migrations", args...) + }) +} + +func RiverUp(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil) + if err != nil { + return err + } + + // Migrate up. An empty MigrateOpts will migrate all the way up, but + // best practice is to specify a specific target version. + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{}) + return err +} + +func RiverDown(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil) + if err != nil { + return err + } + + // TargetVersion -1 removes River's schema completely. + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: -1, + }) + return err +} diff --git a/backend/app/commands/queue/error.go b/backend/app/commands/queue/error.go new file mode 100644 index 0000000..3300b00 --- /dev/null +++ b/backend/app/commands/queue/error.go @@ -0,0 +1,24 @@ +package queue + +import ( + "context" + + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" + log "github.com/sirupsen/logrus" +) + +type CustomErrorHandler struct{} + +func (*CustomErrorHandler) HandleError(ctx context.Context, job *rivertype.JobRow, err error) *river.ErrorHandlerResult { + log.Infof("Job errored with: %s\n", err) + return nil +} + +func (*CustomErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *river.ErrorHandlerResult { + log.Infof("Job panicked with: %v\n", panicVal) + log.Infof("Stack trace: %s\n", trace) + return &river.ErrorHandlerResult{ + SetCancelled: true, + } +} diff --git a/backend/app/commands/queue/river.go b/backend/app/commands/queue/river.go new file mode 100644 index 0000000..2365127 --- /dev/null +++ b/backend/app/commands/queue/river.go @@ -0,0 +1,67 @@ +package queue + +import ( + "context" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + + "quyun/v2/app/commands" + "quyun/v2/app/jobs" + "quyun/v2/providers/app" + "quyun/v2/providers/job" + "quyun/v2/providers/postgres" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + postgres.DefaultProvider(), + job.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("queue"), + atom.Short("start queue processor"), + atom.RunE(Serve), + atom.Providers( + defaultProviders(). + With( + jobs.Provide, + ), + ), + ) +} + +type Service struct { + dig.In + + App *app.Config + Job *job.Job + Initials []contracts.Initial `group:"initials"` + CronJobs []contracts.CronJob `group:"cron_jobs"` +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(ctx context.Context, svc Service) error { + log.SetFormatter(&log.JSONFormatter{}) + + if svc.App.IsDevMode() { + log.SetLevel(log.DebugLevel) + } + + if err := svc.Job.Start(ctx); err != nil { + return err + } + defer svc.Job.Close() + + <-ctx.Done() + return nil + }) +} diff --git a/backend/app/commands/service.go b/backend/app/commands/service.go new file mode 100644 index 0000000..e1cc4af --- /dev/null +++ b/backend/app/commands/service.go @@ -0,0 +1,14 @@ +package commands + +import ( + "go.ipao.vip/atom/container" + "quyun/v2/providers/app" + "quyun/v2/providers/event" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{ + app.DefaultProvider(), + event.DefaultProvider(), + }, providers...) +} diff --git a/backend/app/commands/testx/testing.go b/backend/app/commands/testx/testing.go new file mode 100644 index 0000000..42a97e4 --- /dev/null +++ b/backend/app/commands/testx/testing.go @@ -0,0 +1,39 @@ +package testx + +import ( + "testing" + + "quyun/v2/database" + "quyun/v2/providers/job" + "quyun/v2/providers/jwt" + "quyun/v2/providers/postgres" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + + "github.com/rogeecn/fabfile" + . "github.com/smartystreets/goconvey/convey" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{ + postgres.DefaultProvider(), + jwt.DefaultProvider(), + job.DefaultProvider(), + database.DefaultProvider(), + }, providers...) +} + +func Serve(providers container.Providers, t *testing.T, invoke any) { + Convey("tests boot up", t, func() { + file := fabfile.MustFind("config.toml") + + // localEnv := os.Getenv("ENV_LOCAL") + // if localEnv != "" { + // file = fabfile.MustFind("config." + localEnv + ".toml") + // } + + So(atom.LoadProviders(file, providers), ShouldBeNil) + So(container.Container.Invoke(invoke), ShouldBeNil) + }) +} diff --git a/backend/app/console/.gitkeep b/backend/app/console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/errorx/app_error.go b/backend/app/errorx/app_error.go new file mode 100644 index 0000000..ed70ea7 --- /dev/null +++ b/backend/app/errorx/app_error.go @@ -0,0 +1,65 @@ +package errorx + +import ( + "fmt" + "runtime" +) + +// AppError 应用错误结构 +type AppError struct { + Code ErrorCode `json:"code"` + Message string `json:"message"` + StatusCode int `json:"-"` + Data any `json:"data,omitempty"` + ID string `json:"id,omitempty"` + + // 调试信息 + originalErr error + file string + params []any + sql string +} + +// Error 实现 error 接口 +func (e *AppError) Error() string { + return fmt.Sprintf("[%d] %s", e.Code, e.Message) +} + +// Unwrap 允许通过 errors.Unwrap 遍历到原始错误 +func (e *AppError) Unwrap() error { return e.originalErr } + +// WithData 添加数据 +func (e *AppError) WithData(data any) *AppError { + e.Data = data + return e +} + +// WithMsg 设置消息 +func (e *AppError) WithMsg(msg string) *AppError { + e.Message = msg + return e +} + +// WithSQL 记录SQL信息 +func (e *AppError) WithSQL(sql string) *AppError { + e.sql = sql + return e +} + +// WithParams 记录参数信息,并自动获取调用位置 +func (e *AppError) WithParams(params ...any) *AppError { + e.params = params + if _, file, line, ok := runtime.Caller(1); ok { + e.file = fmt.Sprintf("%s:%d", file, line) + } + return e +} + +// NewError 创建应用错误 +func NewError(code ErrorCode, statusCode int, message string) *AppError { + return &AppError{ + Code: code, + Message: message, + StatusCode: statusCode, + } +} diff --git a/backend/app/errorx/codes.go b/backend/app/errorx/codes.go new file mode 100644 index 0000000..a319573 --- /dev/null +++ b/backend/app/errorx/codes.go @@ -0,0 +1,90 @@ +package errorx + +// ErrorCode 错误码类型 +type ErrorCode int + +const ( + // 1000-1099: 数据相关错误 + CodeRecordNotFound ErrorCode = 1001 + CodeRecordDuplicated ErrorCode = 1002 + CodeDataCorrupted ErrorCode = 1003 + CodeDataTooLarge ErrorCode = 1004 + CodeDataValidationFail ErrorCode = 1005 + CodeConstraintViolated ErrorCode = 1006 + CodeDataExpired ErrorCode = 1007 + CodeDataLocked ErrorCode = 1008 + + // 1100-1199: 请求相关错误 + CodeBadRequest ErrorCode = 1101 + CodeMissingParameter ErrorCode = 1102 + CodeInvalidParameter ErrorCode = 1103 + CodeParameterTooLong ErrorCode = 1104 + CodeParameterTooShort ErrorCode = 1105 + CodeInvalidFormat ErrorCode = 1106 + CodeUnsupportedMethod ErrorCode = 1107 + CodeRequestTooLarge ErrorCode = 1108 + CodeInvalidJSON ErrorCode = 1109 + CodeInvalidXML ErrorCode = 1110 + + // 1200-1299: 认证授权错误 + CodeUnauthorized ErrorCode = 1201 + CodeForbidden ErrorCode = 1202 + CodeTokenExpired ErrorCode = 1203 + CodeTokenInvalid ErrorCode = 1204 + CodeTokenMissing ErrorCode = 1205 + CodePermissionDenied ErrorCode = 1206 + CodeAccountDisabled ErrorCode = 1207 + CodeAccountLocked ErrorCode = 1208 + CodeInvalidCredentials ErrorCode = 1209 + CodeSessionExpired ErrorCode = 1210 + + // 1300-1399: 业务逻辑错误 + CodeBusinessLogic ErrorCode = 1301 + CodeWorkflowError ErrorCode = 1302 + CodeStatusConflict ErrorCode = 1303 + CodeOperationFailed ErrorCode = 1304 + CodeResourceConflict ErrorCode = 1305 + CodePreconditionFailed ErrorCode = 1306 + CodeQuotaExceeded ErrorCode = 1307 + CodeResourceExhausted ErrorCode = 1308 + + // 1400-1499: 外部服务错误 + CodeExternalService ErrorCode = 1401 + CodeServiceUnavailable ErrorCode = 1402 + CodeServiceTimeout ErrorCode = 1403 + CodeThirdPartyError ErrorCode = 1404 + CodeNetworkError ErrorCode = 1405 + CodeDatabaseError ErrorCode = 1406 + CodeCacheError ErrorCode = 1407 + CodeMessageQueueError ErrorCode = 1408 + + // 1500-1599: 系统错误 + CodeInternalError ErrorCode = 1501 + CodeConfigurationError ErrorCode = 1502 + CodeFileSystemError ErrorCode = 1503 + CodeMemoryError ErrorCode = 1504 + CodeConcurrencyError ErrorCode = 1505 + CodeDeadlockError ErrorCode = 1506 + + // 1600-1699: 限流和频率控制 + CodeRateLimitExceeded ErrorCode = 1601 + CodeTooManyRequests ErrorCode = 1602 + CodeConcurrentLimit ErrorCode = 1603 + CodeAPIQuotaExceeded ErrorCode = 1604 + + // 1700-1799: 文件和上传错误 + CodeFileNotFound ErrorCode = 1701 + CodeFileTooBig ErrorCode = 1702 + CodeInvalidFileType ErrorCode = 1703 + CodeFileCorrupted ErrorCode = 1704 + CodeUploadFailed ErrorCode = 1705 + CodeDownloadFailed ErrorCode = 1706 + CodeFilePermission ErrorCode = 1707 + + // 1800-1899: 加密和安全错误 + CodeEncryptionError ErrorCode = 1801 + CodeDecryptionError ErrorCode = 1802 + CodeSignatureInvalid ErrorCode = 1803 + CodeCertificateInvalid ErrorCode = 1804 + CodeSecurityViolation ErrorCode = 1805 +) diff --git a/backend/app/errorx/handler.go b/backend/app/errorx/handler.go new file mode 100644 index 0000000..adbb2e6 --- /dev/null +++ b/backend/app/errorx/handler.go @@ -0,0 +1,105 @@ +package errorx + +import ( + "errors" + "net/http" + + "github.com/gofiber/fiber/v3" + "gorm.io/gorm" +) + +// ErrorHandler 错误处理器 +type ErrorHandler struct{} + +// NewErrorHandler 创建错误处理器 +func NewErrorHandler() *ErrorHandler { + return &ErrorHandler{} +} + +// Handle 处理错误并返回统一格式 +func (h *ErrorHandler) Handle(err error) *AppError { + if appErr, ok := err.(*AppError); ok { + return appErr + } + + // 处理 Fiber 错误 + if fiberErr, ok := err.(*fiber.Error); ok { + return h.handleFiberError(fiberErr) + } + + // 处理 GORM 错误 + if appErr := h.handleGormError(err); appErr != nil { + return appErr + } + + // 默认内部错误 + return &AppError{ + Code: ErrInternalError.Code, + Message: err.Error(), + StatusCode: http.StatusInternalServerError, + originalErr: err, + } +} + +// handleFiberError 处理 Fiber 错误 +func (h *ErrorHandler) handleFiberError(fiberErr *fiber.Error) *AppError { + var appErr *AppError + + switch fiberErr.Code { + case http.StatusBadRequest: + appErr = ErrBadRequest + case http.StatusUnauthorized: + appErr = ErrUnauthorized + case http.StatusForbidden: + appErr = ErrForbidden + case http.StatusNotFound: + appErr = ErrRecordNotFound + case http.StatusMethodNotAllowed: + appErr = ErrUnsupportedMethod + case http.StatusRequestEntityTooLarge: + appErr = ErrRequestTooLarge + case http.StatusTooManyRequests: + appErr = ErrTooManyRequests + default: + appErr = ErrInternalError + } + + return &AppError{ + Code: appErr.Code, + Message: fiberErr.Message, + StatusCode: fiberErr.Code, + originalErr: fiberErr, + } +} + +// handleGormError 处理 GORM 错误 +func (h *ErrorHandler) handleGormError(err error) *AppError { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &AppError{ + Code: ErrRecordNotFound.Code, + Message: ErrRecordNotFound.Message, + StatusCode: ErrRecordNotFound.StatusCode, + originalErr: err, + } + } + + if errors.Is(err, gorm.ErrDuplicatedKey) { + return &AppError{ + Code: ErrRecordDuplicated.Code, + Message: ErrRecordDuplicated.Message, + StatusCode: ErrRecordDuplicated.StatusCode, + originalErr: err, + } + } + + if errors.Is(err, gorm.ErrInvalidTransaction) { + return &AppError{ + Code: ErrConcurrencyError.Code, + Message: "事务无效", + StatusCode: ErrConcurrencyError.StatusCode, + originalErr: err, + } + } + + return nil +} diff --git a/backend/app/errorx/middleware.go b/backend/app/errorx/middleware.go new file mode 100644 index 0000000..9fd1beb --- /dev/null +++ b/backend/app/errorx/middleware.go @@ -0,0 +1,38 @@ +package errorx + +import "github.com/gofiber/fiber/v3" + +// 全局实例 +var DefaultSender = NewResponseSender() + +// Middleware 错误处理中间件 +func Middleware(c fiber.Ctx) error { + err := c.Next() + if err != nil { + return DefaultSender.SendError(c, err) + } + return nil +} + +// 便捷函数 +func Wrap(err error) *AppError { + if err == nil { + return nil + } + + if appErr, ok := err.(*AppError); ok { + return &AppError{ + Code: appErr.Code, + Message: appErr.Message, + StatusCode: appErr.StatusCode, + Data: appErr.Data, + originalErr: appErr, + } + } + + return DefaultSender.handler.Handle(err) +} + +func SendError(ctx fiber.Ctx, err error) error { + return DefaultSender.SendError(ctx, err) +} diff --git a/backend/app/errorx/predefined.go b/backend/app/errorx/predefined.go new file mode 100644 index 0000000..0f7970e --- /dev/null +++ b/backend/app/errorx/predefined.go @@ -0,0 +1,105 @@ +package errorx + +import "net/http" + +// 预定义错误 - 数据相关 +var ( + ErrRecordNotFound = NewError(CodeRecordNotFound, http.StatusNotFound, "记录不存在") + ErrRecordDuplicated = NewError(CodeRecordDuplicated, http.StatusConflict, "记录重复") + ErrDataCorrupted = NewError(CodeDataCorrupted, http.StatusBadRequest, "数据损坏") + ErrDataTooLarge = NewError(CodeDataTooLarge, http.StatusRequestEntityTooLarge, "数据过大") + ErrDataValidationFail = NewError(CodeDataValidationFail, http.StatusBadRequest, "数据验证失败") + ErrConstraintViolated = NewError(CodeConstraintViolated, http.StatusConflict, "约束违规") + ErrDataExpired = NewError(CodeDataExpired, http.StatusGone, "数据已过期") + ErrDataLocked = NewError(CodeDataLocked, http.StatusLocked, "数据已锁定") +) + +// 预定义错误 - 请求相关 +var ( + ErrBadRequest = NewError(CodeBadRequest, http.StatusBadRequest, "请求错误") + ErrMissingParameter = NewError(CodeMissingParameter, http.StatusBadRequest, "缺少必需参数") + ErrInvalidParameter = NewError(CodeInvalidParameter, http.StatusBadRequest, "参数无效") + ErrParameterTooLong = NewError(CodeParameterTooLong, http.StatusBadRequest, "参数过长") + ErrParameterTooShort = NewError(CodeParameterTooShort, http.StatusBadRequest, "参数过短") + ErrInvalidFormat = NewError(CodeInvalidFormat, http.StatusBadRequest, "格式无效") + ErrUnsupportedMethod = NewError(CodeUnsupportedMethod, http.StatusMethodNotAllowed, "不支持的请求方法") + ErrRequestTooLarge = NewError(CodeRequestTooLarge, http.StatusRequestEntityTooLarge, "请求体过大") + ErrInvalidJSON = NewError(CodeInvalidJSON, http.StatusBadRequest, "JSON格式错误") + ErrInvalidXML = NewError(CodeInvalidXML, http.StatusBadRequest, "XML格式错误") +) + +// 预定义错误 - 认证授权 +var ( + ErrUnauthorized = NewError(CodeUnauthorized, http.StatusUnauthorized, "未授权") + ErrForbidden = NewError(CodeForbidden, http.StatusForbidden, "禁止访问") + ErrTokenExpired = NewError(CodeTokenExpired, http.StatusUnauthorized, "Token已过期") + ErrTokenInvalid = NewError(CodeTokenInvalid, http.StatusUnauthorized, "Token无效") + ErrTokenMissing = NewError(CodeTokenMissing, http.StatusUnauthorized, "Token缺失") + ErrPermissionDenied = NewError(CodePermissionDenied, http.StatusForbidden, "权限不足") + ErrAccountDisabled = NewError(CodeAccountDisabled, http.StatusForbidden, "账户已禁用") + ErrAccountLocked = NewError(CodeAccountLocked, http.StatusLocked, "账户已锁定") + ErrInvalidCredentials = NewError(CodeInvalidCredentials, http.StatusUnauthorized, "凭据无效") + ErrSessionExpired = NewError(CodeSessionExpired, http.StatusUnauthorized, "会话已过期") +) + +// 预定义错误 - 业务逻辑 +var ( + ErrBusinessLogic = NewError(CodeBusinessLogic, http.StatusBadRequest, "业务逻辑错误") + ErrWorkflowError = NewError(CodeWorkflowError, http.StatusBadRequest, "工作流错误") + ErrStatusConflict = NewError(CodeStatusConflict, http.StatusConflict, "状态冲突") + ErrOperationFailed = NewError(CodeOperationFailed, http.StatusInternalServerError, "操作失败") + ErrResourceConflict = NewError(CodeResourceConflict, http.StatusConflict, "资源冲突") + ErrPreconditionFailed = NewError(CodePreconditionFailed, http.StatusPreconditionFailed, "前置条件失败") + ErrQuotaExceeded = NewError(CodeQuotaExceeded, http.StatusForbidden, "配额超限") + ErrResourceExhausted = NewError(CodeResourceExhausted, http.StatusTooManyRequests, "资源耗尽") +) + +// 预定义错误 - 外部服务 +var ( + ErrExternalService = NewError(CodeExternalService, http.StatusBadGateway, "外部服务错误") + ErrServiceUnavailable = NewError(CodeServiceUnavailable, http.StatusServiceUnavailable, "服务不可用") + ErrServiceTimeout = NewError(CodeServiceTimeout, http.StatusRequestTimeout, "服务超时") + ErrThirdPartyError = NewError(CodeThirdPartyError, http.StatusBadGateway, "第三方服务错误") + ErrNetworkError = NewError(CodeNetworkError, http.StatusBadGateway, "网络错误") + ErrDatabaseError = NewError(CodeDatabaseError, http.StatusInternalServerError, "数据库错误") + ErrCacheError = NewError(CodeCacheError, http.StatusInternalServerError, "缓存错误") + ErrMessageQueueError = NewError(CodeMessageQueueError, http.StatusInternalServerError, "消息队列错误") +) + +// 预定义错误 - 系统错误 +var ( + ErrInternalError = NewError(CodeInternalError, http.StatusInternalServerError, "内部错误") + ErrConfigurationError = NewError(CodeConfigurationError, http.StatusInternalServerError, "配置错误") + ErrFileSystemError = NewError(CodeFileSystemError, http.StatusInternalServerError, "文件系统错误") + ErrMemoryError = NewError(CodeMemoryError, http.StatusInternalServerError, "内存错误") + ErrConcurrencyError = NewError(CodeConcurrencyError, http.StatusInternalServerError, "并发错误") + ErrDeadlockError = NewError(CodeDeadlockError, http.StatusInternalServerError, "死锁错误") +) + +// 预定义错误 - 限流 +var ( + ErrRateLimitExceeded = NewError(CodeRateLimitExceeded, http.StatusTooManyRequests, "请求频率超限") + ErrTooManyRequests = NewError(CodeTooManyRequests, http.StatusTooManyRequests, "请求过多") + ErrConcurrentLimit = NewError(CodeConcurrentLimit, http.StatusTooManyRequests, "并发数超限") + ErrAPIQuotaExceeded = NewError(CodeAPIQuotaExceeded, http.StatusTooManyRequests, "API配额超限") +) + +// 预定义错误 - 文件处理 +var ( + ErrFileNotFound = NewError(CodeFileNotFound, http.StatusNotFound, "文件不存在") + ErrFileTooBig = NewError(CodeFileTooBig, http.StatusRequestEntityTooLarge, "文件过大") + ErrInvalidFileType = NewError(CodeInvalidFileType, http.StatusBadRequest, "文件类型无效") + ErrFileCorrupted = NewError(CodeFileCorrupted, http.StatusBadRequest, "文件损坏") + ErrUploadFailed = NewError(CodeUploadFailed, http.StatusInternalServerError, "上传失败") + ErrDownloadFailed = NewError(CodeDownloadFailed, http.StatusInternalServerError, "下载失败") + ErrFilePermission = NewError(CodeFilePermission, http.StatusForbidden, "文件权限不足") +) + +// 预定义错误 - 安全相关 +var ( + ErrEncryptionError = NewError(CodeEncryptionError, http.StatusInternalServerError, "加密错误") + ErrDecryptionError = NewError(CodeDecryptionError, http.StatusInternalServerError, "解密错误") + ErrSignatureInvalid = NewError(CodeSignatureInvalid, http.StatusUnauthorized, "签名无效") + ErrCertificateInvalid = NewError(CodeCertificateInvalid, http.StatusUnauthorized, "证书无效") + ErrSecurityViolation = NewError(CodeSecurityViolation, http.StatusForbidden, "安全违规") +) diff --git a/backend/app/errorx/response.go b/backend/app/errorx/response.go new file mode 100644 index 0000000..d31c60d --- /dev/null +++ b/backend/app/errorx/response.go @@ -0,0 +1,127 @@ +package errorx + +import ( + "errors" + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/binder" + "github.com/gofiber/utils/v2" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// ResponseSender 响应发送器 +type ResponseSender struct { + handler *ErrorHandler +} + +// NewResponseSender 创建响应发送器 +func NewResponseSender() *ResponseSender { + return &ResponseSender{ + handler: NewErrorHandler(), + } +} + +// SendError 发送错误响应 +func (s *ResponseSender) SendError(ctx fiber.Ctx, err error) error { + appErr := s.handler.Handle(err) + + // 记录错误日志 + s.logError(appErr) + + // 根据 Content-Type 返回不同格式 + return s.sendResponse(ctx, appErr) +} + +// logError 记录错误日志 +func (s *ResponseSender) logError(appErr *AppError) { + // 确保每个错误实例都有唯一ID,便于日志关联 + if appErr.ID == "" { + appErr.ID = uuid.NewString() + } + + // 构造详细的错误级联链路(包含类型、状态、定位等) + chain := make([]map[string]any, 0, 4) + var e error = appErr + for e != nil { + entry := map[string]any{ + "type": fmt.Sprintf("%T", e), + "error": e.Error(), + } + switch v := e.(type) { + case *AppError: + entry["code"] = v.Code + entry["statusCode"] = v.StatusCode + if v.file != "" { + entry["file"] = v.file + } + if len(v.params) > 0 { + entry["params"] = v.params + } + if v.sql != "" { + entry["sql"] = v.sql + } + if v.ID != "" { + entry["id"] = v.ID + } + case *fiber.Error: + entry["statusCode"] = v.Code + entry["message"] = v.Message + } + + // GORM 常见错误归类标记 + if errors.Is(e, gorm.ErrRecordNotFound) { + entry["gorm"] = "record_not_found" + } else if errors.Is(e, gorm.ErrDuplicatedKey) { + entry["gorm"] = "duplicated_key" + } else if errors.Is(e, gorm.ErrInvalidTransaction) { + entry["gorm"] = "invalid_transaction" + } + + chain = append(chain, entry) + e = errors.Unwrap(e) + } + + root := chain[len(chain)-1]["error"] + + logEntry := log.WithFields(log.Fields{ + "id": appErr.ID, + "code": appErr.Code, + "statusCode": appErr.StatusCode, + "file": appErr.file, + "sql": appErr.sql, + "params": appErr.params, + "error_chain": chain, + "root_error": root, + }) + + if appErr.originalErr != nil { + logEntry = logEntry.WithError(appErr.originalErr) + } + + // 根据错误级别记录不同级别的日志 + if appErr.StatusCode >= 500 { + logEntry.Error("系统错误: ", appErr.Message) + } else if appErr.StatusCode >= 400 { + logEntry.Warn("客户端错误: ", appErr.Message) + } else { + logEntry.Info("应用错误: ", appErr.Message) + } +} + +// sendResponse 发送响应 +func (s *ResponseSender) sendResponse(ctx fiber.Ctx, appErr *AppError) error { + contentType := utils.ToLower(utils.UnsafeString(ctx.Request().Header.ContentType())) + contentType = binder.FilterFlags(utils.ParseVendorSpecificContentType(contentType)) + + switch contentType { + case fiber.MIMETextXML, fiber.MIMEApplicationXML: + return ctx.Status(appErr.StatusCode).XML(appErr) + case fiber.MIMETextHTML, fiber.MIMETextPlain: + return ctx.Status(appErr.StatusCode).SendString(appErr.Message) + default: + return ctx.Status(appErr.StatusCode).JSON(appErr) + } +} diff --git a/backend/app/events/publishers/user_register.go b/backend/app/events/publishers/user_register.go new file mode 100644 index 0000000..af1a671 --- /dev/null +++ b/backend/app/events/publishers/user_register.go @@ -0,0 +1,26 @@ +package publishers + +import ( + "encoding/json" + + "quyun/v2/app/events" + "quyun/v2/providers/event" + + "go.ipao.vip/atom/contracts" +) + +var _ contracts.EventPublisher = (*UserRegister)(nil) + +type UserRegister struct { + event.DefaultChannel + + ID int64 `json:"id"` +} + +func (e *UserRegister) Marshal() ([]byte, error) { + return json.Marshal(e) +} + +func (e *UserRegister) Topic() string { + return events.TopicUserRegister +} diff --git a/backend/app/events/subscribers/provider.gen.go b/backend/app/events/subscribers/provider.gen.go new file mode 100755 index 0000000..e66ab95 --- /dev/null +++ b/backend/app/events/subscribers/provider.gen.go @@ -0,0 +1,27 @@ +package subscribers + +import ( + "quyun/v2/providers/event" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + __event *event.PubSub, + ) (contracts.Initial, error) { + obj := &UserRegister{} + if err := obj.Prepare(); err != nil { + return nil, err + } + __event.Handle("handler:UserRegister", obj) + + return obj, nil + }, atom.GroupInitial); err != nil { + return err + } + return nil +} diff --git a/backend/app/events/subscribers/user_register.go b/backend/app/events/subscribers/user_register.go new file mode 100644 index 0000000..f529a46 --- /dev/null +++ b/backend/app/events/subscribers/user_register.go @@ -0,0 +1,45 @@ +package subscribers + +import ( + "encoding/json" + + "quyun/v2/app/events" + "quyun/v2/app/events/publishers" + "quyun/v2/providers/event" + + "github.com/ThreeDotsLabs/watermill/message" + "github.com/sirupsen/logrus" + "go.ipao.vip/atom/contracts" +) + +var _ contracts.EventHandler = (*UserRegister)(nil) + +// @provider(event) +type UserRegister struct { + event.DefaultChannel + event.DefaultPublishTo + + log *logrus.Entry `inject:"false"` +} + +func (e *UserRegister) Prepare() error { + e.log = logrus.WithField("module", "events.subscribers.user_register") + return nil +} + +// Topic implements contracts.EventHandler. +func (e *UserRegister) Topic() string { + return events.TopicUserRegister +} + +// Handler implements contracts.EventHandler. +func (e *UserRegister) Handler(msg *message.Message) ([]*message.Message, error) { + var payload publishers.UserRegister + err := json.Unmarshal(msg.Payload, &payload) + if err != nil { + return nil, err + } + e.log.Infof("received event %s", msg.Payload) + + return nil, nil +} diff --git a/backend/app/events/subscribers/utils.go b/backend/app/events/subscribers/utils.go new file mode 100644 index 0000000..45419ef --- /dev/null +++ b/backend/app/events/subscribers/utils.go @@ -0,0 +1,24 @@ +package subscribers + +import ( + "encoding/json" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" +) + +func toMessage(event any) (*message.Message, error) { + b, err := json.Marshal(event) + if err != nil { + return nil, err + } + return message.NewMessage(watermill.NewUUID(), b), nil +} + +func toMessageList(event any) ([]*message.Message, error) { + m, err := toMessage(event) + if err != nil { + return nil, err + } + return []*message.Message{m}, nil +} diff --git a/backend/app/events/topics.go b/backend/app/events/topics.go new file mode 100644 index 0000000..e2b777a --- /dev/null +++ b/backend/app/events/topics.go @@ -0,0 +1,6 @@ +package events + +const ( + TopicProcessed = "event:processed" + TopicUserRegister = "event:user_register" +) diff --git a/backend/app/grpc/users/handler.go b/backend/app/grpc/users/handler.go new file mode 100644 index 0000000..e8764a1 --- /dev/null +++ b/backend/app/grpc/users/handler.go @@ -0,0 +1,26 @@ +package users + +import ( + "context" + + userv1 "quyun/v2/pkg/proto/user/v1" +) + +// @provider(grpc) userv1.RegisterUserServiceServer +type Users struct { + userv1.UnimplementedUserServiceServer +} + +func (u *Users) ListUsers(ctx context.Context, in *userv1.ListUsersRequest) (*userv1.ListUsersResponse, error) { + // userv1.UserServiceServer + return &userv1.ListUsersResponse{}, nil +} + +// GetUser implements userv1.UserServiceServer +func (u *Users) GetUser(ctx context.Context, in *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { + return &userv1.GetUserResponse{ + User: &userv1.User{ + Id: in.Id, + }, + }, nil +} diff --git a/backend/app/grpc/users/provider.gen.go b/backend/app/grpc/users/provider.gen.go new file mode 100755 index 0000000..d708c0f --- /dev/null +++ b/backend/app/grpc/users/provider.gen.go @@ -0,0 +1,25 @@ +package users + +import ( + userv1 "quyun/v2/pkg/proto/user/v1" + "quyun/v2/providers/grpc" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + __grpc *grpc.Grpc, + ) (contracts.Initial, error) { + obj := &Users{} + userv1.RegisterUserServiceServer(__grpc.Server, obj) + + return obj, nil + }, atom.GroupInitial); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/api/controller.go b/backend/app/http/api/controller.go new file mode 100644 index 0000000..ca9f224 --- /dev/null +++ b/backend/app/http/api/controller.go @@ -0,0 +1,207 @@ +package api + +import ( + "strconv" + "strings" + + "quyun/v2/app/errorx" + "quyun/v2/app/tenancy" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type ApiController struct{} + +type PostsQuery struct { + Page int `query:"page"` + Limit int `query:"limit"` + Keyword string `query:"keyword"` +} + +type UpdateUsernameReq struct { + Username string `json:"username"` +} + +// WeChatOAuthStart +// +// @Router /v1/auth/wechat [get] +func (a *ApiController) WeChatOAuthStart(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat oauth: /auth/wechat"}) +} + +// WeChatOAuthCallback +// +// @Router /v1/auth/login [get] +func (a *ApiController) WeChatOAuthCallback(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat oauth: /auth/login"}) +} + +// ListPosts +// +// @Router /v1/posts [get] +// @Bind query query +func (a *ApiController) ListPosts(ctx fiber.Ctx, query *PostsQuery) error { + page := 1 + limit := 10 + if query != nil { + if query.Page > 0 { + page = query.Page + } + if query.Limit > 0 { + limit = query.Limit + } + } + return ctx.JSON(fiber.Map{ + "items": []any{}, + "total": 0, + "page": page, + "limit": limit, + }) +} + +// ShowPost +// +// @Router /v1/posts/:id/show [get] +// @Bind id path +func (a *ApiController) ShowPost(ctx fiber.Ctx, id int) error { + if id <= 0 { + return errorx.ErrInvalidParameter.WithMsg("invalid id") + } + return fiber.ErrNotFound +} + +// PlayPost +// +// @Router /v1/posts/:id/play [get] +// @Bind id path +func (a *ApiController) PlayPost(ctx fiber.Ctx, id int) error { + if id <= 0 { + return errorx.ErrInvalidParameter.WithMsg("invalid id") + } + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "post play"}) +} + +// MinePosts +// +// @Router /v1/posts/mine [get] +// @Bind query query +func (a *ApiController) MinePosts(ctx fiber.Ctx, query *PostsQuery) error { + page := 1 + limit := 10 + if query != nil { + if query.Page > 0 { + page = query.Page + } + if query.Limit > 0 { + limit = query.Limit + } + } + return ctx.JSON(fiber.Map{ + "items": []any{}, + "total": 0, + "page": page, + "limit": limit, + }) +} + +// BuyPost +// +// @Router /v1/posts/:id/buy [post] +// @Bind id path +func (a *ApiController) BuyPost(ctx fiber.Ctx, id int) error { + if id <= 0 { + return errorx.ErrInvalidParameter.WithMsg("invalid id") + } + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "post buy"}) +} + +// UserProfile +// +// @Router /v1/users/profile [get] +func (a *ApiController) UserProfile(ctx fiber.Ctx) error { + tenantCode := ctx.Locals(tenancy.LocalTenantCode) + return ctx.JSON(fiber.Map{ + "id": 0, + "created_at": "1970-01-01T00:00:00Z", + "username": "", + "avatar": "", + "balance": 0, + "tenant": tenantCode, + }) +} + +// UpdateUsername +// +// @Router /v1/users/username [put] +// @Bind req body +func (a *ApiController) UpdateUsername(ctx fiber.Ctx, req *UpdateUsernameReq) error { + if req == nil { + return errorx.ErrInvalidJSON + } + name := strings.TrimSpace(req.Username) + if name == "" { + return errorx.ErrDataValidationFail.WithMsg("username required") + } + if len([]rune(name)) > 12 { + return errorx.ErrDataValidationFail.WithMsg("username too long") + } + return ctx.JSON(fiber.Map{"ok": true}) +} + +// WechatJSSDK +// +// @Router /v1/wechats/js-sdk [get] +func (a *ApiController) WechatJSSDK(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat js-sdk"}) +} + +// AdminAuth (stub) +// +// @Router /v1/admin/auth [post] +func (a *ApiController) AdminAuth(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin auth"}) +} + +// AdminStatistics (stub) +// +// @Router /v1/admin/statistics [get] +func (a *ApiController) AdminStatistics(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin statistics"}) +} + +// AdminOrders (stub) +// +// @Router /v1/admin/orders [get] +func (a *ApiController) AdminOrders(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin orders list"}) +} + +// AdminOrderRefund (stub) +// +// @Router /v1/admin/orders/:id/refund [post] +// @Bind id path +func (a *ApiController) AdminOrderRefund(ctx fiber.Ctx, id int) error { + if id <= 0 { + return errorx.ErrInvalidParameter.WithMsg("invalid id") + } + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin orders refund"}) +} + +// AdminMedias (stub) +// +// @Router /v1/admin/medias [get] +func (a *ApiController) AdminMedias(ctx fiber.Ctx) error { + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin medias list"}) +} + +// AdminMediaShow (stub) +// +// @Router /v1/admin/medias/:id [get] +func (a *ApiController) AdminMediaShow(ctx fiber.Ctx) error { + _, err := strconv.ParseInt(ctx.Params("id"), 10, 64) + if err != nil { + return errorx.ErrInvalidParameter.WithMsg("invalid id") + } + return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin medias show"}) +} diff --git a/backend/app/http/api/provider.gen.go b/backend/app/http/api/provider.gen.go new file mode 100755 index 0000000..20e718a --- /dev/null +++ b/backend/app/http/api/provider.gen.go @@ -0,0 +1,33 @@ +package api + +import ( + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func() (*ApiController, error) { + obj := &ApiController{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + apiController *ApiController, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + apiController: apiController, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/api/routes.gen.go b/backend/app/http/api/routes.gen.go new file mode 100644 index 0000000..d791eaf --- /dev/null +++ b/backend/app/http/api/routes.gen.go @@ -0,0 +1,114 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package api provides HTTP route definitions and registration +// for the quyun/v2 application. +package api + +import ( + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the api module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + // Controller instances + apiController *ApiController +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.api") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "api" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: ApiController + r.log.Debugf("Registering route: Get /v1/admin/medias -> apiController.AdminMedias") + router.Get("/v1/admin/medias", Func0( + r.apiController.AdminMedias, + )) + r.log.Debugf("Registering route: Get /v1/admin/medias/:id -> apiController.AdminMediaShow") + router.Get("/v1/admin/medias/:id", Func0( + r.apiController.AdminMediaShow, + )) + r.log.Debugf("Registering route: Get /v1/admin/orders -> apiController.AdminOrders") + router.Get("/v1/admin/orders", Func0( + r.apiController.AdminOrders, + )) + r.log.Debugf("Registering route: Get /v1/admin/statistics -> apiController.AdminStatistics") + router.Get("/v1/admin/statistics", Func0( + r.apiController.AdminStatistics, + )) + r.log.Debugf("Registering route: Get /v1/auth/login -> apiController.WeChatOAuthCallback") + router.Get("/v1/auth/login", Func0( + r.apiController.WeChatOAuthCallback, + )) + r.log.Debugf("Registering route: Get /v1/auth/wechat -> apiController.WeChatOAuthStart") + router.Get("/v1/auth/wechat", Func0( + r.apiController.WeChatOAuthStart, + )) + r.log.Debugf("Registering route: Get /v1/posts -> apiController.ListPosts") + router.Get("/v1/posts", Func1( + r.apiController.ListPosts, + Query[PostsQuery]("query"), + )) + r.log.Debugf("Registering route: Get /v1/posts/:id/play -> apiController.PlayPost") + router.Get("/v1/posts/:id/play", Func1( + r.apiController.PlayPost, + PathParam[int]("id"), + )) + r.log.Debugf("Registering route: Get /v1/posts/:id/show -> apiController.ShowPost") + router.Get("/v1/posts/:id/show", Func1( + r.apiController.ShowPost, + PathParam[int]("id"), + )) + r.log.Debugf("Registering route: Get /v1/posts/mine -> apiController.MinePosts") + router.Get("/v1/posts/mine", Func1( + r.apiController.MinePosts, + Query[PostsQuery]("query"), + )) + r.log.Debugf("Registering route: Get /v1/users/profile -> apiController.UserProfile") + router.Get("/v1/users/profile", Func0( + r.apiController.UserProfile, + )) + r.log.Debugf("Registering route: Get /v1/wechats/js-sdk -> apiController.WechatJSSDK") + router.Get("/v1/wechats/js-sdk", Func0( + r.apiController.WechatJSSDK, + )) + r.log.Debugf("Registering route: Post /v1/admin/auth -> apiController.AdminAuth") + router.Post("/v1/admin/auth", Func0( + r.apiController.AdminAuth, + )) + r.log.Debugf("Registering route: Post /v1/admin/orders/:id/refund -> apiController.AdminOrderRefund") + router.Post("/v1/admin/orders/:id/refund", Func1( + r.apiController.AdminOrderRefund, + PathParam[int]("id"), + )) + r.log.Debugf("Registering route: Post /v1/posts/:id/buy -> apiController.BuyPost") + router.Post("/v1/posts/:id/buy", Func1( + r.apiController.BuyPost, + PathParam[int]("id"), + )) + r.log.Debugf("Registering route: Put /v1/users/username -> apiController.UpdateUsername") + router.Put("/v1/users/username", Func1( + r.apiController.UpdateUsername, + Body[UpdateUsernameReq]("req"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend/app/http/super/auth.go b/backend/app/http/super/auth.go new file mode 100644 index 0000000..5aeb7ae --- /dev/null +++ b/backend/app/http/super/auth.go @@ -0,0 +1,17 @@ +package super + +import ( + "quyun/v2/providers/app" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type authController struct { + app *app.Config +} + +func (s *authController) auth(ctx fiber.Ctx) error { + // user,err:= + return nil +} diff --git a/backend/app/http/super/controller.go b/backend/app/http/super/controller.go new file mode 100644 index 0000000..de01cd7 --- /dev/null +++ b/backend/app/http/super/controller.go @@ -0,0 +1,345 @@ +package super + +import ( + "database/sql" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "quyun/v2/app/errorx" + "quyun/v2/providers/app" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type SuperController struct { + DB *sql.DB + App *app.Config +} + +func (s *SuperController) auth(ctx fiber.Ctx) error { + token := "" + if s.App != nil && s.App.Super != nil { + token = strings.TrimSpace(s.App.Super.Token) + } + + if token == "" { + if s.App != nil && s.App.IsDevMode() { + return nil + } + return errorx.ErrUnauthorized.WithMsg("missing super admin token") + } + + auth := strings.TrimSpace(ctx.Get(fiber.HeaderAuthorization)) + const prefix = "Bearer " + if !strings.HasPrefix(auth, prefix) { + return errorx.ErrUnauthorized.WithMsg("invalid authorization") + } + if strings.TrimSpace(strings.TrimPrefix(auth, prefix)) != token { + return errorx.ErrUnauthorized.WithMsg("invalid token") + } + return nil +} + +type SuperStatisticsResp struct { + TenantsTotal int64 `json:"tenants_total"` + TenantsEnabled int64 `json:"tenants_enabled"` + TenantAdminsTotal int64 `json:"tenant_admins_total"` + TenantAdminsExpired int64 `json:"tenant_admins_expired"` +} + +// Statistics +// +// @Router /super/v1/statistics [get] +func (s *SuperController) Statistics(ctx fiber.Ctx) error { + if err := s.auth(ctx); err != nil { + return err + } + + var out SuperStatisticsResp + + if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants`).Scan(&out.TenantsTotal); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants WHERE status = 0`).Scan(&out.TenantsEnabled); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin'`).Scan(&out.TenantAdminsTotal); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin' AND expires_at IS NOT NULL AND expires_at <= now()`).Scan(&out.TenantAdminsExpired); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + + return ctx.JSON(out) +} + +type TenantsQuery struct { + Page int `query:"page"` + Limit int `query:"limit"` + Keyword string `query:"keyword"` +} + +type SuperTenantRow struct { + ID int64 `json:"id"` + TenantCode string `json:"tenant_code"` + TenantUUID string `json:"tenant_uuid"` + Name string `json:"name"` + Status int16 `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + TenantAdminCount int64 `json:"tenant_admin_count"` + TenantAdminExpireAt *time.Time `json:"tenant_admin_expire_at,omitempty"` +} + +// Tenants +// +// @Router /super/v1/tenants [get] +// @Bind query query +func (s *SuperController) Tenants(ctx fiber.Ctx, query *TenantsQuery) error { + if err := s.auth(ctx); err != nil { + return err + } + + page := 1 + limit := 20 + keyword := "" + if query != nil { + if query.Page > 0 { + page = query.Page + } + if query.Limit > 0 { + limit = query.Limit + } + keyword = strings.TrimSpace(query.Keyword) + } + if limit > 200 { + limit = 200 + } + offset := (page - 1) * limit + + where := "1=1" + args := []any{} + if keyword != "" { + where += " AND (lower(t.tenant_code) LIKE $1 OR lower(t.name) LIKE $1)" + args = append(args, "%"+strings.ToLower(keyword)+"%") + } + + countSQL := "SELECT count(*) FROM tenants t WHERE " + where + var total int64 + if err := s.DB.QueryRowContext(ctx.Context(), countSQL, args...).Scan(&total); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + + args = append(args, limit, offset) + limitArg := "$" + strconv.Itoa(len(args)-1) + offsetArg := "$" + strconv.Itoa(len(args)) + + sqlStr := ` +SELECT + t.id, t.tenant_code, t.tenant_uuid, t.name, t.status, t.created_at, t.updated_at, + COALESCE(a.admin_count, 0) AS admin_count, + a.max_expires_at +FROM tenants t +LEFT JOIN ( + SELECT tenant_id, + count(*) AS admin_count, + max(expires_at) AS max_expires_at + FROM user_roles + WHERE role_code = 'tenant_admin' AND tenant_id IS NOT NULL + GROUP BY tenant_id +) a ON a.tenant_id = t.id +WHERE ` + where + ` +ORDER BY t.id DESC +LIMIT ` + limitArg + ` OFFSET ` + offsetArg + + rows, err := s.DB.QueryContext(ctx.Context(), sqlStr, args...) + if err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + defer rows.Close() + + items := make([]SuperTenantRow, 0, limit) + for rows.Next() { + var ( + it SuperTenantRow + uuidStr string + ) + if err := rows.Scan( + &it.ID, + &it.TenantCode, + &uuidStr, + &it.Name, + &it.Status, + &it.CreatedAt, + &it.UpdatedAt, + &it.TenantAdminCount, + &it.TenantAdminExpireAt, + ); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + it.TenantUUID = uuidStr + items = append(items, it) + } + if err := rows.Err(); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + + return ctx.JSON(fiber.Map{ + "items": items, + "total": total, + "page": page, + "limit": limit, + }) +} + +type RoleRow struct { + Code string `json:"code"` + Name string `json:"name"` + Status int16 `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Roles +// +// @Router /super/v1/roles [get] +func (s *SuperController) Roles(ctx fiber.Ctx) error { + if err := s.auth(ctx); err != nil { + return err + } + + rows, err := s.DB.QueryContext(ctx.Context(), `SELECT code, name, status, updated_at FROM roles ORDER BY code`) + if err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + defer rows.Close() + + items := make([]RoleRow, 0, 8) + for rows.Next() { + var it RoleRow + if err := rows.Scan(&it.Code, &it.Name, &it.Status, &it.UpdatedAt); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + items = append(items, it) + } + if err := rows.Err(); err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + + return ctx.JSON(fiber.Map{"items": items}) +} + +type UpdateRoleReq struct { + Name *string `json:"name"` + Status *int16 `json:"status"` +} + +// UpdateRole +// +// @Router /super/v1/roles/:code [put] +// @Bind code path +// @Bind req body +func (s *SuperController) UpdateRole(ctx fiber.Ctx, code string, req *UpdateRoleReq) error { + if err := s.auth(ctx); err != nil { + return err + } + + code = strings.TrimSpace(code) + if code == "" { + return errorx.ErrInvalidParameter.WithMsg("missing code") + } + if req == nil { + return errorx.ErrInvalidJSON + } + + switch code { + case "user", "tenant_admin", "super_admin": + default: + return errorx.ErrInvalidParameter.WithMsg("unknown role code") + } + + set := make([]string, 0, 2) + args := make([]any, 0, 3) + i := 1 + + if req.Name != nil { + set = append(set, "name = $"+strconv.Itoa(i)) + args = append(args, strings.TrimSpace(*req.Name)) + i++ + } + if req.Status != nil { + set = append(set, "status = $"+strconv.Itoa(i)) + args = append(args, *req.Status) + i++ + } + if len(set) == 0 { + return ctx.JSON(fiber.Map{"ok": true}) + } + + set = append(set, "updated_at = now()") + args = append(args, code) + + sqlStr := "UPDATE roles SET " + strings.Join(set, ", ") + " WHERE code = $" + strconv.Itoa(i) + res, err := s.DB.ExecContext(ctx.Context(), sqlStr, args...) + if err != nil { + return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + aff, _ := res.RowsAffected() + if aff == 0 { + return fiber.ErrNotFound + } + return ctx.JSON(fiber.Map{"ok": true}) +} + +func resolveDistDir(primary, fallback string) string { + if st, err := os.Stat(primary); err == nil && st.IsDir() { + return primary + } + return fallback +} + +func sendIndex(ctx fiber.Ctx, distDir string) error { + indexPath := filepath.Join(distDir, "index.html") + if st, err := os.Stat(indexPath); err == nil && !st.IsDir() { + return ctx.SendFile(indexPath) + } + return fiber.ErrNotFound +} + +func sendAssetOrIndex(ctx fiber.Ctx, distDir, rel string) error { + rel = filepath.Clean(strings.TrimSpace(rel)) + if rel == "." || rel == "/" { + rel = "" + } + if strings.HasPrefix(rel, "..") { + return fiber.ErrBadRequest + } + + if rel != "" { + assetPath := filepath.Join(distDir, rel) + if st, err := os.Stat(assetPath); err == nil && !st.IsDir() { + return ctx.SendFile(assetPath) + } + } + + return sendIndex(ctx, distDir) +} + +// SuperIndex +// +// @Router /super [get] +func (s *SuperController) SuperIndex(ctx fiber.Ctx) error { + dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist") + return sendIndex(ctx, dist) +} + +// SuperWildcard +// +// @Router /super/* [get] +func (s *SuperController) SuperWildcard(ctx fiber.Ctx) error { + dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist") + return sendAssetOrIndex(ctx, dist, ctx.Params("*")) +} diff --git a/backend/app/http/super/provider.gen.go b/backend/app/http/super/provider.gen.go new file mode 100755 index 0000000..957d649 --- /dev/null +++ b/backend/app/http/super/provider.gen.go @@ -0,0 +1,54 @@ +package super + +import ( + "database/sql" + + "quyun/v2/providers/app" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + app *app.Config, + ) (*authController, error) { + obj := &authController{ + app: app, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + App *app.Config, + DB *sql.DB, + ) (*SuperController, error) { + obj := &SuperController{ + App: App, + DB: DB, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + superController *SuperController, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + superController: superController, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/super/routes.gen.go b/backend/app/http/super/routes.gen.go new file mode 100644 index 0000000..b59254b --- /dev/null +++ b/backend/app/http/super/routes.gen.go @@ -0,0 +1,70 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package super provides HTTP route definitions and registration +// for the quyun/v2 application. +package super + +import ( + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the super module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + // Controller instances + superController *SuperController +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.super") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "super" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: SuperController + r.log.Debugf("Registering route: Get /super -> superController.SuperIndex") + router.Get("/super", Func0( + r.superController.SuperIndex, + )) + r.log.Debugf("Registering route: Get /super/* -> superController.SuperWildcard") + router.Get("/super/*", Func0( + r.superController.SuperWildcard, + )) + r.log.Debugf("Registering route: Get /super/v1/roles -> superController.Roles") + router.Get("/super/v1/roles", Func0( + r.superController.Roles, + )) + r.log.Debugf("Registering route: Get /super/v1/statistics -> superController.Statistics") + router.Get("/super/v1/statistics", Func0( + r.superController.Statistics, + )) + r.log.Debugf("Registering route: Get /super/v1/tenants -> superController.Tenants") + router.Get("/super/v1/tenants", Func1( + r.superController.Tenants, + Query[TenantsQuery]("query"), + )) + r.log.Debugf("Registering route: Put /super/v1/roles/:code -> superController.UpdateRole") + router.Put("/super/v1/roles/:code", Func2( + r.superController.UpdateRole, + PathParam[string]("code"), + Body[UpdateRoleReq]("req"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend/app/http/v1/demo.go b/backend/app/http/v1/demo.go new file mode 100644 index 0000000..a176b60 --- /dev/null +++ b/backend/app/http/v1/demo.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "mime/multipart" + + "quyun/v2/app/errorx" + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/providers/jwt" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type demo struct{} + +type FooUploadReq struct { + Folder string `json:"folder" form:"folder"` // 上传到指定文件夹 +} + +type FooQuery struct { + Search string `query:"search"` // 搜索关键词 +} + +type FooHeader struct { + ContentType string `header:"Content-Type"` // 内容类型 +} +type Filter struct { + Name string `query:"name"` // 名称 + Age int `query:"age"` // 年龄 +} + +type ResponseItem struct{} + +// Foo +// +// @Summary Test +// @Description Test +// @Tags Test +// @Accept json +// @Produce json +// +// @Param id path int true "ID" +// @Param query query Filter true "Filter" +// @Param pager query requests.Pagination true "Pager" +// @Success 200 {object} requests.Pager{list=ResponseItem} "成功" +// +// @Router /v1/medias/:id [post] +// @Bind query query +// @Bind pager query +// @Bind header header +// @Bind id path +// @Bind req body +// @Bind file file +// @Bind claim local +func (d *demo) Foo( + ctx fiber.Ctx, + id int, + pager *requests.Pagination, + query *FooQuery, + header *FooHeader, + claim *jwt.Claims, + file *multipart.FileHeader, + req *FooUploadReq, +) error { + _, err := services.Test.Test(ctx) + if err != nil { + // 示例:在控制器层自定义错误消息/附加数据 + appErr := errorx.Wrap(err). + WithMsg("获取测试失败"). + WithData(fiber.Map{"route": "/v1/test"}). + WithParams("handler", "Test.Hello") + return appErr + } + return nil +} diff --git a/backend/app/http/v1/provider.gen.go b/backend/app/http/v1/provider.gen.go new file mode 100755 index 0000000..1504210 --- /dev/null +++ b/backend/app/http/v1/provider.gen.go @@ -0,0 +1,33 @@ +package v1 + +import ( + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func() (*demo, error) { + obj := &demo{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + demo *demo, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + demo: demo, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go new file mode 100644 index 0000000..c6e3825 --- /dev/null +++ b/backend/app/http/v1/routes.gen.go @@ -0,0 +1,57 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package v1 provides HTTP route definitions and registration +// for the quyun/v2 application. +package v1 + +import ( + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" + "mime/multipart" + "quyun/v2/app/requests" + "quyun/v2/providers/jwt" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the v1 module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + // Controller instances + demo *demo +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.v1") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "v1" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: demo + r.log.Debugf("Registering route: Post /v1/medias/:id -> demo.Foo") + router.Post("/v1/medias/:id", Func7( + r.demo.Foo, + PathParam[int]("id"), + Query[requests.Pagination]("pager"), + Query[FooQuery]("query"), + Header[FooHeader]("header"), + Local[*jwt.Claims]("claim"), + File[multipart.FileHeader]("file"), + Body[FooUploadReq]("req"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend/app/http/web/controller.go b/backend/app/http/web/controller.go new file mode 100644 index 0000000..2f2c76b --- /dev/null +++ b/backend/app/http/web/controller.go @@ -0,0 +1,78 @@ +package web + +import ( + "os" + "path/filepath" + "strings" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type WebController struct{} + +func resolveDistDir(primary, fallback string) string { + if st, err := os.Stat(primary); err == nil && st.IsDir() { + return primary + } + return fallback +} + +func sendIndex(ctx fiber.Ctx, distDir string) error { + indexPath := filepath.Join(distDir, "index.html") + if st, err := os.Stat(indexPath); err == nil && !st.IsDir() { + return ctx.SendFile(indexPath) + } + return fiber.ErrNotFound +} + +func sendAssetOrIndex(ctx fiber.Ctx, distDir, rel string) error { + rel = filepath.Clean(strings.TrimSpace(rel)) + if rel == "." || rel == "/" { + rel = "" + } + if strings.HasPrefix(rel, "..") { + return fiber.ErrBadRequest + } + + if rel != "" { + assetPath := filepath.Join(distDir, rel) + if st, err := os.Stat(assetPath); err == nil && !st.IsDir() { + return ctx.SendFile(assetPath) + } + } + + return sendIndex(ctx, distDir) +} + +// AdminIndex +// +// @Router /admin [get] +func (w *WebController) AdminIndex(ctx fiber.Ctx) error { + adminDist := resolveDistDir("frontend/admin/dist", "../frontend/admin/dist") + return sendIndex(ctx, adminDist) +} + +// AdminWildcard +// +// @Router /admin/* [get] +func (w *WebController) AdminWildcard(ctx fiber.Ctx) error { + adminDist := resolveDistDir("frontend/admin/dist", "../frontend/admin/dist") + return sendAssetOrIndex(ctx, adminDist, ctx.Params("*")) +} + +// UserIndex +// +// @Router / [get] +func (w *WebController) UserIndex(ctx fiber.Ctx) error { + userDist := resolveDistDir("frontend/user/dist", "../frontend/user/dist") + return sendIndex(ctx, userDist) +} + +// UserWildcard +// +// @Router /* [get] +func (w *WebController) UserWildcard(ctx fiber.Ctx) error { + userDist := resolveDistDir("frontend/user/dist", "../frontend/user/dist") + return sendAssetOrIndex(ctx, userDist, ctx.Params("*")) +} diff --git a/backend/app/http/web/provider.gen.go b/backend/app/http/web/provider.gen.go new file mode 100755 index 0000000..32f254b --- /dev/null +++ b/backend/app/http/web/provider.gen.go @@ -0,0 +1,33 @@ +package web + +import ( + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func() (*WebController, error) { + obj := &WebController{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + webController *WebController, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + webController: webController, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/web/routes.gen.go b/backend/app/http/web/routes.gen.go new file mode 100644 index 0000000..64ddb79 --- /dev/null +++ b/backend/app/http/web/routes.gen.go @@ -0,0 +1,59 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package web provides HTTP route definitions and registration +// for the quyun/v2 application. +package web + +import ( + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the web module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + // Controller instances + webController *WebController +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.web") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "web" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: WebController + r.log.Debugf("Registering route: Get / -> webController.UserIndex") + router.Get("/", Func0( + r.webController.UserIndex, + )) + r.log.Debugf("Registering route: Get /* -> webController.UserWildcard") + router.Get("/*", Func0( + r.webController.UserWildcard, + )) + r.log.Debugf("Registering route: Get /admin -> webController.AdminIndex") + router.Get("/admin", Func0( + r.webController.AdminIndex, + )) + r.log.Debugf("Registering route: Get /admin/* -> webController.AdminWildcard") + router.Get("/admin/*", Func0( + r.webController.AdminWildcard, + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend/app/jobs/demo_cron.go b/backend/app/jobs/demo_cron.go new file mode 100644 index 0000000..88960d4 --- /dev/null +++ b/backend/app/jobs/demo_cron.go @@ -0,0 +1,36 @@ +package jobs + +import ( + "time" + + . "github.com/riverqueue/river" + "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" +) + +var _ contracts.CronJob = (*DemoCronJob)(nil) + +// @provider(cronjob) +type DemoCronJob struct { + log *logrus.Entry `inject:"false"` +} + +// Prepare implements contracts.CronJob. +func (DemoCronJob) Prepare() error { + return nil +} + +// JobArgs implements contracts.CronJob. +func (DemoCronJob) Args() []contracts.CronJobArg { + return []contracts.CronJobArg{ + { + Arg: DemoJob{ + Strings: []string{"a", "b", "c", "d"}, + }, + + PeriodicInterval: PeriodicInterval(time.Second * 10), + RunOnStart: false, + }, + } +} diff --git a/backend/app/jobs/demo_job.go b/backend/app/jobs/demo_job.go new file mode 100644 index 0000000..e36dab8 --- /dev/null +++ b/backend/app/jobs/demo_job.go @@ -0,0 +1,53 @@ +package jobs + +import ( + "context" + "sort" + "time" + + . "github.com/riverqueue/river" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" + _ "go.ipao.vip/atom/contracts" +) + +var _ contracts.JobArgs = DemoJob{} + +type DemoJob struct { + Strings []string `json:"strings"` +} + +func (s DemoJob) InsertOpts() InsertOpts { + return InsertOpts{ + Queue: QueueDefault, + Priority: PriorityDefault, + } +} + +func (DemoJob) Kind() string { return "demo_job" } +func (a DemoJob) UniqueID() string { return a.Kind() } + +var _ Worker[DemoJob] = (*DemoJobWorker)(nil) + +// @provider(job) +type DemoJobWorker struct { + WorkerDefaults[DemoJob] +} + +func (w *DemoJobWorker) NextRetry(job *Job[DemoJob]) time.Time { + return time.Now().Add(30 * time.Second) +} + +func (w *DemoJobWorker) Work(ctx context.Context, job *Job[DemoJob]) error { + logger := log.WithField("job", job.Args.Kind()) + + logger.Infof("[START] %s args: %v", job.Args.Kind(), job.Args.Strings) + defer logger.Infof("[END] %s", job.Args.Kind()) + + // modify below + sort.Strings(job.Args.Strings) + logger.Infof("[%s] Sorted strings: %v\n", time.Now().Format(time.TimeOnly), job.Args.Strings) + + return nil +} diff --git a/backend/app/jobs/demo_job_test.go b/backend/app/jobs/demo_job_test.go new file mode 100644 index 0000000..893ef88 --- /dev/null +++ b/backend/app/jobs/demo_job_test.go @@ -0,0 +1,56 @@ +//go:build legacytests +// +build legacytests + +package jobs + +import ( + "context" + "testing" + + "quyun/v2/app/commands/testx" + "quyun/v2/app/services" + + . "github.com/riverqueue/river" + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type DemoJobSuiteInjectParams struct { + dig.In + + Initials []contracts.Initial `group:"initials"` // nolint:structcheck +} + +type DemoJobSuite struct { + suite.Suite + + DemoJobSuiteInjectParams +} + +func Test_DemoJob(t *testing.T) { + providers := testx.Default().With(Provide, services.Provide) + + testx.Serve(providers, t, func(p DemoJobSuiteInjectParams) { + suite.Run(t, &DemoJobSuite{DemoJobSuiteInjectParams: p}) + }) +} + +func (t *DemoJobSuite) Test_Work() { + Convey("test_work", t.T(), func() { + Convey("step 1", func() { + job := &Job[DemoJob]{ + Args: DemoJob{ + Strings: []string{"a", "b", "c"}, + }, + } + + worker := &DemoJobWorker{} + + err := worker.Work(context.Background(), job) + So(err, ShouldBeNil) + }) + }) +} diff --git a/backend/app/jobs/provider.gen.go b/backend/app/jobs/provider.gen.go new file mode 100755 index 0000000..95331e5 --- /dev/null +++ b/backend/app/jobs/provider.gen.go @@ -0,0 +1,41 @@ +package jobs + +import ( + "quyun/v2/providers/job" + + "github.com/riverqueue/river" + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + __job *job.Job, + ) (contracts.Initial, error) { + obj := &DemoCronJob{} + if err := obj.Prepare(); err != nil { + return nil, err + } + + container.Later(func() error { return __job.AddPeriodicJobs(obj) }) + + return obj, nil + }, atom.GroupInitial); err != nil { + return err + } + if err := container.Container.Provide(func( + __job *job.Job, + ) (contracts.Initial, error) { + obj := &DemoJobWorker{} + if err := river.AddWorkerSafely(__job.Workers, obj); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupInitial); err != nil { + return err + } + return nil +} diff --git a/backend/app/middlewares/mid_debug.go b/backend/app/middlewares/mid_debug.go new file mode 100644 index 0000000..ecb33af --- /dev/null +++ b/backend/app/middlewares/mid_debug.go @@ -0,0 +1,9 @@ +package middlewares + +import ( + "github.com/gofiber/fiber/v3" +) + +func (f *Middlewares) DebugMode(c fiber.Ctx) error { + return c.Next() +} diff --git a/backend/app/middlewares/middlewares.go b/backend/app/middlewares/middlewares.go new file mode 100644 index 0000000..69e0e4c --- /dev/null +++ b/backend/app/middlewares/middlewares.go @@ -0,0 +1,15 @@ +package middlewares + +import ( + log "github.com/sirupsen/logrus" +) + +// @provider +type Middlewares struct { + log *log.Entry `inject:"false"` +} + +func (f *Middlewares) Prepare() error { + f.log = log.WithField("module", "middleware") + return nil +} diff --git a/backend/app/middlewares/provider.gen.go b/backend/app/middlewares/provider.gen.go new file mode 100755 index 0000000..f84d36c --- /dev/null +++ b/backend/app/middlewares/provider.gen.go @@ -0,0 +1,20 @@ +package middlewares + +import ( + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func() (*Middlewares, error) { + obj := &Middlewares{} + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/requests/pagination.go b/backend/app/requests/pagination.go new file mode 100644 index 0000000..e98528d --- /dev/null +++ b/backend/app/requests/pagination.go @@ -0,0 +1,30 @@ +package requests + +import "github.com/samber/lo" + +type Pager struct { + Pagination `json:",inline"` + Total int64 `json:"total"` + Items any `json:"items"` +} + +type Pagination struct { + Page int64 `json:"page" form:"page" query:"page"` + Limit int64 `json:"limit" form:"limit" query:"limit"` +} + +func (filter *Pagination) Offset() int64 { + return (filter.Page - 1) * filter.Limit +} + +func (filter *Pagination) Format() *Pagination { + if filter.Page <= 0 { + filter.Page = 1 + } + + if !lo.Contains([]int64{10, 20, 50, 100}, filter.Limit) { + filter.Limit = 10 + } + + return filter +} diff --git a/backend/app/requests/sort.go b/backend/app/requests/sort.go new file mode 100644 index 0000000..517b419 --- /dev/null +++ b/backend/app/requests/sort.go @@ -0,0 +1,41 @@ +package requests + +import ( + "strings" + + "github.com/samber/lo" +) + +type SortQueryFilter struct { + Asc *string `json:"asc" form:"asc"` + Desc *string `json:"desc" form:"desc"` +} + +func (s *SortQueryFilter) AscFields() []string { + if s.Asc == nil { + return nil + } + return strings.Split(*s.Asc, ",") +} + +func (s *SortQueryFilter) DescFields() []string { + if s.Desc == nil { + return nil + } + return strings.Split(*s.Desc, ",") +} + +func (s *SortQueryFilter) DescID() *SortQueryFilter { + if s.Desc == nil { + s.Desc = lo.ToPtr("id") + } + + items := s.DescFields() + if lo.Contains(items, "id") { + return s + } + + items = append(items, "id") + s.Desc = lo.ToPtr(strings.Join(items, ",")) + return s +} diff --git a/backend/app/services/provider.gen.go b/backend/app/services/provider.gen.go new file mode 100755 index 0000000..6744c96 --- /dev/null +++ b/backend/app/services/provider.gen.go @@ -0,0 +1,45 @@ +package services + +import ( + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" + "gorm.io/gorm" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + db *gorm.DB, + test *test, + user *user, + ) (contracts.Initial, error) { + obj := &services{ + db: db, + test: test, + user: user, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupInitial); err != nil { + return err + } + if err := container.Container.Provide(func() (*test, error) { + obj := &test{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func() (*user, error) { + obj := &user{} + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/services/services.gen.go b/backend/app/services/services.gen.go new file mode 100644 index 0000000..ffa55df --- /dev/null +++ b/backend/app/services/services.gen.go @@ -0,0 +1,31 @@ +package services + +import ( + "gorm.io/gorm" +) + +var _db *gorm.DB + +// exported CamelCase Services +var ( + Test *test + User *user +) + +// @provider(model) +type services struct { + db *gorm.DB + // define Services + test *test + user *user +} + +func (svc *services) Prepare() error { + _db = svc.db + + // set exported Services here + Test = svc.test + User = svc.user + + return nil +} diff --git a/backend/app/services/test.go b/backend/app/services/test.go new file mode 100644 index 0000000..051d196 --- /dev/null +++ b/backend/app/services/test.go @@ -0,0 +1,10 @@ +package services + +import "context" + +// @provider +type test struct{} + +func (t *test) Test(ctx context.Context) (string, error) { + return "Test", nil +} diff --git a/backend/app/services/test_test.go b/backend/app/services/test_test.go new file mode 100644 index 0000000..45de0f7 --- /dev/null +++ b/backend/app/services/test_test.go @@ -0,0 +1,44 @@ +//go:build legacytests +// +build legacytests + +package services + +import ( + "testing" + "time" + + "quyun/v2/app/commands/testx" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type TestSuiteInjectParams struct { + dig.In + + Initials []contracts.Initial `group:"initials"` // nolint:structcheck +} + +type TestSuite struct { + suite.Suite + + TestSuiteInjectParams +} + +func Test_Test(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p TestSuiteInjectParams) { + suite.Run(t, &TestSuite{TestSuiteInjectParams: p}) + }) +} + +func (t *TestSuite) Test_Test() { + Convey("test_work", t.T(), func() { + t.T().Log("start test at", time.Now()) + }) +} diff --git a/backend/app/services/user.go b/backend/app/services/user.go new file mode 100644 index 0000000..d06d7bc --- /dev/null +++ b/backend/app/services/user.go @@ -0,0 +1,33 @@ +package services + +import ( + "context" + + "quyun/v2/database/models" + + "github.com/pkg/errors" +) + +// @provider +type user struct{} + +func (t *user) FindByUsername(ctx context.Context, username string) (*models.User, error) { + tbl, query := models.UserQuery.QueryContext(ctx) + + model, err := query.Where(tbl.Username.Eq(username)).First() + if err != nil { + return nil, errors.Wrapf(err, "FindByusername failed, %s", username) + } + return model, nil +} + +func (t *user) Create(ctx context.Context, user *models.User) (*models.User, error) { + if err := user.EncryptPassword(ctx); err != nil { + return nil, errors.Wrap(err, "encrypt user password failed") + } + + if err := user.Create(ctx); err != nil { + return nil, errors.Wrapf(err, "Create user failed, %s", user.Username) + } + return user, nil +} diff --git a/backend/app/services/user_test.go b/backend/app/services/user_test.go new file mode 100644 index 0000000..a592999 --- /dev/null +++ b/backend/app/services/user_test.go @@ -0,0 +1,96 @@ +package services + +import ( + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/gen/types" + "go.uber.org/dig" +) + +type UserTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` // nolint:structcheck +} + +type UserTestSuite struct { + suite.Suite + + UserTestSuiteInjectParams +} + +func Test_User(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p UserTestSuiteInjectParams) { + suite.Run(t, &UserTestSuite{UserTestSuiteInjectParams: p}) + }) +} + +func (t *UserTestSuite) Test_Create() { + Convey("test user create", t.T(), func() { + database.Truncate(t.T().Context(), t.DB, models.TableNameUser) + + m := &models.User{ + Username: "test-user", + Password: "test-password", + Roles: types.NewArray([]consts.Role{consts.RoleUser}), + Status: consts.UserStatusPendingVerify, + } + + err := m.Create(t.T().Context()) + So(err, ShouldBeNil) + + So(m.ID, ShouldBeGreaterThan, 0) + + same := m.ComparePassword(t.T().Context(), "test-password") + So(same, ShouldBeTrue) + + same = m.ComparePassword(t.T().Context(), "test-password1") + So(same, ShouldBeFalse) + }) +} + +// FindByUsername +func (t *UserTestSuite) Test_FindByUsername() { + Convey("test user FindByUsername", t.T(), func() { + database.Truncate(t.T().Context(), t.DB, models.TableNameUser) + + Convey("user table is empty", func() { + m, err := User.FindByUsername(t.T().Context(), "test-user") + So(err, ShouldNotBeNil) + So(m, ShouldBeNil) + }) + + Convey("insert one record", func() { + username := "test-user" + m := &models.User{ + Username: username, + Password: "test-password", + Roles: types.NewArray([]consts.Role{consts.RoleUser}), + Status: consts.UserStatusPendingVerify, + } + + err := m.Create(t.T().Context()) + So(err, ShouldBeNil) + + Convey("user table is not empty", func() { + m, err := User.FindByUsername(t.T().Context(), username) + So(err, ShouldBeNil) + So(m, ShouldNotBeNil) + }) + }) + }) +} diff --git a/backend/app/tenancy/tenancy.go b/backend/app/tenancy/tenancy.go new file mode 100644 index 0000000..d4171c6 --- /dev/null +++ b/backend/app/tenancy/tenancy.go @@ -0,0 +1,71 @@ +package tenancy + +import ( + "database/sql" + "regexp" + "strings" + + "quyun/v2/app/errorx" + + "github.com/gofiber/fiber/v3" + "github.com/google/uuid" +) + +const ( + LocalTenantCode = "tenant_code" + LocalTenantID = "tenant_id" + LocalTenantUUID = "tenant_uuid" +) + +var tenantCodeRe = regexp.MustCompile(`^[a-z0-9_-]+$`) + +type Tenant struct { + ID int64 + Code string + UUID uuid.UUID +} + +func ResolveTenant(c fiber.Ctx, db *sql.DB) (*Tenant, error) { + raw := strings.TrimSpace(c.Params("tenant_code")) + code := strings.ToLower(raw) + if code == "" || !tenantCodeRe.MatchString(code) { + return nil, errorx.ErrInvalidParameter.WithMsg("invalid tenant_code") + } + + var ( + id int64 + tenantUUID uuid.UUID + status int16 + ) + err := db.QueryRowContext( + c.Context(), + `SELECT id, tenant_uuid, status FROM tenants WHERE lower(tenant_code) = $1 LIMIT 1`, + code, + ).Scan(&id, &tenantUUID, &status) + if err != nil { + if err == sql.ErrNoRows { + return nil, fiber.ErrNotFound + } + return nil, errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error()) + } + + // status: 0 enabled (by default) + if status != 0 { + return nil, fiber.ErrNotFound + } + + return &Tenant{ID: id, Code: code, UUID: tenantUUID}, nil +} + +func Middleware(db *sql.DB) fiber.Handler { + return func(c fiber.Ctx) error { + tenant, err := ResolveTenant(c, db) + if err != nil { + return err + } + c.Locals(LocalTenantCode, tenant.Code) + c.Locals(LocalTenantID, tenant.ID) + c.Locals(LocalTenantUUID, tenant.UUID.String()) + return c.Next() + } +} diff --git a/backend/buf.gen.yaml b/backend/buf.gen.yaml new file mode 100644 index 0000000..9cbda62 --- /dev/null +++ b/backend/buf.gen.yaml @@ -0,0 +1,23 @@ +version: v2 +inputs: + - directory: proto +managed: + enabled: true + override: + - file_option: go_package_prefix + value: quyun/v2/pkg/proto + +plugins: + - local: protoc-gen-go + out: pkg/proto + opt: paths=source_relative + #- local: protoc-gen-grpc-gateway + # out: pkg/proto + # opt: + # - paths=source_relative + # - generate_unbound_methods=true + - local: protoc-gen-go-grpc + out: pkg/proto + opt: paths=source_relative + # - local: protoc-gen-openapiv2 + # out: docs/proto diff --git a/backend/buf.yaml b/backend/buf.yaml new file mode 100644 index 0000000..06039af --- /dev/null +++ b/backend/buf.yaml @@ -0,0 +1,13 @@ +# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE +deps: + - buf.build/googleapis/googleapis + - buf.build/grpc-ecosystem/grpc-gateway diff --git a/backend/config.full.toml b/backend/config.full.toml new file mode 100644 index 0000000..0ea2865 --- /dev/null +++ b/backend/config.full.toml @@ -0,0 +1,239 @@ +# ========================= +# gRPC Server (providers/grpc) +# ========================= +[Grpc] +# 必填 +Port = 9090 # gRPC 监听端口 +# 可选 +# Host = "0.0.0.0" # 监听地址(默认 0.0.0.0) +EnableReflection = true # 开启服务反射(开发/调试友好) +EnableHealth = true # 注册 gRPC health 服务 +ShutdownTimeoutSeconds = 10 # 优雅关停超时,超时后强制 Stop + +# 说明: +# - 统一的拦截器、ServerOption 可通过 providers/grpc/options.go 的 +# UseUnaryInterceptors/UseStreamInterceptors/UseOptions 动态注入。 +# ========================= +# HTTP Server (providers/http) +# ========================= +[Http] +# 必填 +Port = 8080 # HTTP 监听端口 + +# 可选 +# BaseURI = "/api" # 全局前缀 +# StaticRoute = "/static" # 静态路由路径 +# StaticPath = "./public" # 静态文件目录 +[Http.Tls] + +# Cert = "server.crt" +# Key = "server.key" +[Http.Cors] + +# Mode = "enabled" # "enabled"|"disabled"(默认按 Whitelist 推断) +# 白名单项示例(按需追加多条) +# [[Http.Cors.Whitelist]] +# AllowOrigin = "https://example.com" +# AllowHeaders = "Authorization,Content-Type" +# AllowMethods = "GET,POST,PUT,DELETE" +# ExposeHeaders = "X-Request-Id" +# AllowCredentials = true +# ========================= +# Connection Multiplexer (providers/cmux) +# 用于同端口同时暴露 HTTP + gRPC:cmux -> 分发到 Http/Grpc +# ========================= +[Cmux] +# 必填 +Port = 8081 # cmux 监听端口 + +# 可选 +# Host = "0.0.0.0" +# ========================= +# Events / PubSub (providers/event) +# gochannel 为默认内存通道(始终启用) +# 如需 Kafka / Redis Stream / SQL,请按需开启对应小节 +# ========================= +[Events] + +# Kafka(可选) +[Events.Kafka] +# 必填(启用时) +Brokers = ["127.0.0.1:9092"] +# 可选 +ConsumerGroup = "my-group" + +# Redis Stream(可选) +[Events.Redis] +# 必填(启用时) +ConsumerGroup = "my-group" +# 可选 +Streams = ["mystream"] # 订阅的 streams;可在 Handler 侧具体指定 + +# SQL(可选,基于 PostgreSQL) +[Events.Sql] +# 必填(启用时) +ConsumerGroup = "my-group" + +# ========================= +# Job / Queue (providers/job) +# 基于 River(Postgres)队列 +# ========================= +[Job] + +# 可选:每队列并发数(默认 high/default/low 均 10) +#QueueWorkers = { high = 20, default = 10, low = 5 } +# 说明: +# - 需要启用 providers/postgres 以提供数据库连接 +# - 通过 Add/AddWithID 入队,AddPeriodicJob 注册定时任务 +# ========================= +# JWT (providers/jwt) +# ========================= +[JWT] +# 必填 +SigningKey = "your-signing-key" # 密钥 +ExpiresTime = "168h" # 过期时间,形如 "72h", "168h" +# 可选 +Issuer = "my-service" + +# ========================= +# HashIDs (providers/hashids) +# ========================= +[HashIDs] +# 必填 +Salt = "your-salt" + +# 可选 +# Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +# MinLength = 8 +# ========================= +# Redis (providers/redis) +# ========================= +[Redis] +# 必填(若不填 Host/Port,将默认 localhost:6379) +Host = "127.0.0.1" +Port = 6379 + +# 可选 +# Username = "" +# Password = "" +# DB = 0 +# ClientName = "my-service" +# 连接池/重试(可选) +# PoolSize = 50 +# MinIdleConns = 10 +# MaxRetries = 2 +# 超时(秒,可选) +# DialTimeoutSeconds = 5 +# ReadTimeoutSeconds = 3 +# WriteTimeoutSeconds = 3 +# 连接生命周期(秒,可选) +# ConnMaxIdleTimeSeconds = 300 +# ConnMaxLifetimeSeconds = 1800 +# 探活(秒,可选,默认 5) +# PingTimeoutSeconds = 5 +# ========================= +# PostgreSQL / GORM (providers/postgres) +# ========================= +[Database] +# 必填 +Host = "127.0.0.1" +Port = 5432 +Database = "app" + +# 可选(未填 Username 默认 postgres;其它有默认值见代码) +# Username = "postgres" +# Password = "" +# SslMode = "disable" # "disable"|"require"|... +# TimeZone = "Asia/Shanghai" +# Schema = "public" +# Prefix = "" # 表前缀 +# Singular = false # 表名是否使用单数 +# 连接池(可选) +# MaxIdleConns = 10 +# MaxOpenConns = 100 +# ConnMaxLifetimeSeconds = 1800 +# ConnMaxIdleTimeSeconds = 300 +# DSN 增强(可选) +# UseSearchPath = true +# ApplicationName = "my-service" +# ========================= +# HTTP Client (providers/req) +# ========================= +[HttpClient] + +# 可选 +# DevMode = true +# CookieJarFile = "./data/cookies.jar" +# RootCa = ["./ca.crt"] +# UserAgent = "my-service/1.0" +# InsecureSkipVerify = false +# BaseURL = "https://api.example.com" +# ContentType = "application/json" +# Timeout = 10 # 秒 +# CommonHeaders = { X-Request-From = "service" } +# CommonQuery = { locale = "zh-CN" } +[HttpClient.AuthBasic] + +# Username = "" +# Password = "" +# 其它认证 / 代理 / 跳转策略(可选) +# AuthBearerToken = "Bearer " # 或仅 ,内部会设置 Bearer +# ProxyURL = "http://127.0.0.1:7890" +# RedirectPolicy = ["Max:10","SameHost"] +# ========================= +# OpenTracing (Jaeger) (providers/tracing) +# ========================= +[Tracing] +# 必填 +Name = "my-service" +# 可选(Agent / Collector 至少配一个;未配时走默认本地端口) +Reporter_LocalAgentHostPort = "127.0.0.1:6831" +Reporter_CollectorEndpoint = "http://127.0.0.1:14268/api/traces" + +# 行为开关(可选) +# Disabled = false +# Gen128Bit = true +# ZipkinSharedRPCSpan = true +# RPCMetrics = false +# 采样器(可选) +# Sampler_Type = "const" # const|probabilistic|ratelimiting|remote +# Sampler_Param = 1.0 +# Sampler_SamplingServerURL = "" +# Sampler_MaxOperations = 0 +# Sampler_RefreshIntervalSec = 0 +# Reporter(可选) +# Reporter_LogSpans = true +# Reporter_BufferFlushMs = 100 +# Reporter_QueueSize = 1000 +# 进程 Tags(可选) +# [Tracing.Tags] +# version = "1.0.0" +# zone = "az1" +# ========================= +# OpenTelemetry (providers/otel) +# ========================= +[OTEL] +# 必填(建议设置) +ServiceName = "my-service" +# 可选(版本/环境) +Version = "1.0.0" +Env = "dev" +# 导出端点(二选一,若都填优先 HTTP) +# EndpointGRPC = "127.0.0.1:4317" +# EndpointHTTP = "127.0.0.1:4318" +# 认证(可选,支持仅填纯 token,将自动补齐 Bearer) +# Token = "Bearer " +# 安全(可选) +# InsecureGRPC = true +# InsecureHTTP = true +# 采样(可选) +# Sampler = "always" # "always"|"ratio" +# SamplerRatio = 0.1 # 0..1,仅当 Sampler="ratio" 生效 +# 批处理(毫秒,可选) +# BatchTimeoutMs = 5000 +# ExportTimeoutMs = 10000 +# MaxQueueSize = 2048 +# MaxExportBatchSize = 512 +# 指标(毫秒,可选) +# MetricReaderIntervalMs = 10000 # 指标导出周期 +# RuntimeReadMemStatsIntervalMs = 5000 # 运行时指标采集最小周期 diff --git a/backend/config.toml b/backend/config.toml new file mode 100644 index 0000000..20e038f --- /dev/null +++ b/backend/config.toml @@ -0,0 +1,103 @@ +# ========================= +# 应用基础配置 +# ========================= +[App] +# 应用运行模式:development | production | testing +Mode = "development" +# 应用基础URI,用于生成完整URL +BaseURI = "http://localhost:8080" + +[App.Super] +# Super admin API token (optional in development; required in release) +Token = "" + +# ========================= +# HTTP 服务器配置 +# ========================= +[Http] +# HTTP服务监听端口 +Port = 8080 + +# 监听地址(可选,默认 0.0.0.0) +# Host = "0.0.0.0" +# 全局路由前缀(可选) +# BaseURI = "/api/v1" +# ========================= +# 数据库配置 +# ========================= +[Database] +# 数据库主机地址 +Host = "10.1.1.2" +# 数据库端口 +Port = 5433 +# 数据库名称 +Database = "quyun_v2" +# 数据库用户名 +Username = "postgres" +# 数据库密码 +Password = "xixi0202" +# SSL模式:disable | require | verify-ca | verify-full +SslMode = "disable" +# 时区 +TimeZone = "Asia/Shanghai" +# 连接池配置(可选) +MaxIdleConns = 10 +MaxOpenConns = 100 +ConnMaxLifetime = "1800s" +ConnMaxIdleTime = "300s" + +# ========================= +# JWT 认证配置 +# ========================= +[JWT] +# JWT签名密钥(生产环境请使用强密钥) +SigningKey = "your-secret-key-change-in-production" +# Token过期时间,如:72h, 168h, 720h +ExpiresTime = "168h" +# 签发者(可选) +Issuer = "v2" + +# ========================= +# HashIDs 配置 +# ========================= +[HashIDs] +# 盐值(用于ID加密,请使用随机字符串) +Salt = "your-random-salt-here" +# 最小长度(可选) +MinLength = 8 + +# 自定义字符集(可选) +# Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +# ========================= +# Redis 缓存配置 +# ========================= +[Redis] +# Redis主机地址 +Host = "localhost" +# Redis端口 +Port = 6379 +# Redis密码(可选) +Password = "" +# 数据库编号 +DB = 0 +# 连接池配置(可选) +PoolSize = 50 +MinIdleConns = 10 +MaxRetries = 3 +# 超时配置(可选) +DialTimeout = "5s" +ReadTimeout = "3s" +WriteTimeout = "3s" + +# ========================= +# 日志配置 +# ========================= +[Log] +# 日志级别:debug | info | warn | error +Level = "info" +# 日志格式:json | text +Format = "json" +# 输出文件(可选,未配置则输出到控制台) +# Output = "./logs/app.log" +# 是否启用调用者信息(文件名:行号) +EnableCaller = true diff --git a/backend/database/.transform.yaml b/backend/database/.transform.yaml new file mode 100644 index 0000000..054e0f0 --- /dev/null +++ b/backend/database/.transform.yaml @@ -0,0 +1,16 @@ +ignores: + - migrations + - river_client + - river_client_queue + - river_job + - river_leader + - river_migration + - river_queue +imports: + - go.ipao.vip/gen + - quyun/v2/pkg/consts +field_type: + users: + roles: types.Array[consts.Role] + status: consts.UserStatus +field_relate: diff --git a/backend/database/database.go b/backend/database/database.go new file mode 100644 index 0000000..fe6c999 --- /dev/null +++ b/backend/database/database.go @@ -0,0 +1,55 @@ +package database + +import ( + "context" + "database/sql" + "embed" + "fmt" + + "quyun/v2/database/models" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" + "gorm.io/gorm" +) + +//go:embed migrations/* +var MigrationFS embed.FS + +func Truncate(ctx context.Context, db *sql.DB, tableName ...string) error { + for _, name := range tableName { + sql := fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY", name) + if _, err := db.ExecContext(ctx, sql); err != nil { + return err + } + } + return nil +} + +func WrapLike(v string) string { + return "%" + v + "%" +} + +func WrapLikeLeft(v string) string { + return "%" + v +} + +func WrapLikeRight(v string) string { + return "%" + v +} + +func Provide(...opt.Option) error { + return container.Container.Provide(func(db *gorm.DB) contracts.Initial { + models.SetDefault(db) + return models.Q + }, atom.GroupInitial) +} + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{}, + } +} diff --git a/backend/database/migrations/20251215084449_users.sql b/backend/database/migrations/20251215084449_users.sql new file mode 100644 index 0000000..9acfc22 --- /dev/null +++ b/backend/database/migrations/20251215084449_users.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS users( + id bigserial PRIMARY KEY, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW(), + deleted_at timestamptz, + username varchar(255) NOT NULL UNIQUE, + password varchar(255) NOT NULL, + roles text[] NOT NULL DEFAULT ARRAY['user'], + status varchar(50) NOT NULL DEFAULT 'active', + metas jsonb NOT NULL DEFAULT '{}', + verified_at timestamptz +); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS users; + +-- +goose StatementEnd diff --git a/backend/database/models/query.gen.go b/backend/database/models/query.gen.go new file mode 100644 index 0000000..8fcceb0 --- /dev/null +++ b/backend/database/models/query.gen.go @@ -0,0 +1,103 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + "database/sql" + + "gorm.io/gorm" + + "go.ipao.vip/gen" + + "gorm.io/plugin/dbresolver" +) + +var ( + Q = new(Query) + UserQuery *userQuery +) + +func SetDefault(db *gorm.DB, opts ...gen.DOOption) { + *Q = *Use(db, opts...) + UserQuery = &Q.User +} + +func Use(db *gorm.DB, opts ...gen.DOOption) *Query { + return &Query{ + db: db, + User: newUser(db, opts...), + } +} + +type Query struct { + db *gorm.DB + + User userQuery +} + +func (q *Query) Available() bool { return q.db != nil } + +func (q *Query) clone(db *gorm.DB) *Query { + return &Query{ + db: db, + User: q.User.clone(db), + } +} + +func (q *Query) ReadDB() *Query { + return q.ReplaceDB(q.db.Clauses(dbresolver.Read)) +} + +func (q *Query) WriteDB() *Query { + return q.ReplaceDB(q.db.Clauses(dbresolver.Write)) +} + +func (q *Query) ReplaceDB(db *gorm.DB) *Query { + return &Query{ + db: db, + User: q.User.replaceDB(db), + } +} + +type queryCtx struct { + User *userQueryDo +} + +func (q *Query) WithContext(ctx context.Context) *queryCtx { + return &queryCtx{ + User: q.User.WithContext(ctx), + } +} + +func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error { + return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...) +} + +func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx { + tx := q.db.Begin(opts...) + return &QueryTx{Query: q.clone(tx), Error: tx.Error} +} + +type QueryTx struct { + *Query + Error error +} + +func (q *QueryTx) Commit() error { + return q.db.Commit().Error +} + +func (q *QueryTx) Rollback() error { + return q.db.Rollback().Error +} + +func (q *QueryTx) SavePoint(name string) error { + return q.db.SavePoint(name).Error +} + +func (q *QueryTx) RollbackTo(name string) error { + return q.db.RollbackTo(name).Error +} diff --git a/backend/database/models/users.gen.go b/backend/database/models/users.gen.go new file mode 100644 index 0000000..cfacc8b --- /dev/null +++ b/backend/database/models/users.gen.go @@ -0,0 +1,69 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + "time" + + "quyun/v2/pkg/consts" + + "go.ipao.vip/gen" + "go.ipao.vip/gen/types" + "gorm.io/gorm" +) + +const TableNameUser = "users" + +// User mapped from table +type User struct { + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` + Username string `gorm:"column:username;type:character varying(255);not null" json:"username"` + Password string `gorm:"column:password;type:character varying(255);not null" json:"password"` + Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];not null;default:ARRAY['user" json:"roles"` + Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"` + Metas types.JSON `gorm:"column:metas;type:jsonb;not null;default:{}" json:"metas"` + VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"` +} + +// Quick operations without importing query package +// Update applies changed fields to the database using the default DB. +func (m *User) Update(ctx context.Context) (gen.ResultInfo, error) { + return Q.User.WithContext(ctx).Updates(m) +} + +// Save upserts the model using the default DB. +func (m *User) Save(ctx context.Context) error { return Q.User.WithContext(ctx).Save(m) } + +// Create inserts the model using the default DB. +func (m *User) Create(ctx context.Context) error { return Q.User.WithContext(ctx).Create(m) } + +// Delete removes the row represented by the model using the default DB. +func (m *User) Delete(ctx context.Context) (gen.ResultInfo, error) { + return Q.User.WithContext(ctx).Delete(m) +} + +// ForceDelete permanently deletes the row (ignores soft delete) using the default DB. +func (m *User) ForceDelete(ctx context.Context) (gen.ResultInfo, error) { + return Q.User.WithContext(ctx).Unscoped().Delete(m) +} + +// Restore sets deleted_at to NULL for this model's primary key using the default DB. +func (m *User) Restore(ctx context.Context) (gen.ResultInfo, error) { + return Q.User.WithContext(ctx).RestoreByID(m.ID) +} + +// Reload reloads the model from database by its primary key and overwrites current fields. +func (m *User) Reload(ctx context.Context) error { + fresh, err := Q.User.WithContext(ctx).GetByID(m.ID) + if err != nil { + return err + } + *m = *fresh + return nil +} diff --git a/backend/database/models/users.go b/backend/database/models/users.go new file mode 100644 index 0000000..510802d --- /dev/null +++ b/backend/database/models/users.go @@ -0,0 +1,34 @@ +package models + +import ( + "context" + + "quyun/v2/pkg/consts" + + "github.com/samber/lo" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func (m *User) ComparePassword(ctx context.Context, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(m.Password), []byte(password)) + return err == nil +} + +func (m *User) EncryptPassword(ctx context.Context) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(m.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + m.Password = string(bytes) + return nil +} + +func (m *User) HasRole(ctx context.Context, role consts.Role) bool { + return lo.Contains(m.Roles, role) +} + +// BeforeCreate +func (m *User) BeforeCreate(tx *gorm.DB) error { + return m.EncryptPassword(tx.Statement.Context) +} diff --git a/backend/database/models/users.query.gen.go b/backend/database/models/users.query.gen.go new file mode 100644 index 0000000..57c0184 --- /dev/null +++ b/backend/database/models/users.query.gen.go @@ -0,0 +1,508 @@ +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. +// Code generated by go.ipao.vip/gen. DO NOT EDIT. + +package models + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "go.ipao.vip/gen" + "go.ipao.vip/gen/field" + + "gorm.io/plugin/dbresolver" +) + +func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery { + _userQuery := userQuery{} + + _userQuery.userQueryDo.UseDB(db, opts...) + _userQuery.userQueryDo.UseModel(&User{}) + + tableName := _userQuery.userQueryDo.TableName() + _userQuery.ALL = field.NewAsterisk(tableName) + _userQuery.ID = field.NewInt64(tableName, "id") + _userQuery.CreatedAt = field.NewTime(tableName, "created_at") + _userQuery.UpdatedAt = field.NewTime(tableName, "updated_at") + _userQuery.DeletedAt = field.NewField(tableName, "deleted_at") + _userQuery.Username = field.NewString(tableName, "username") + _userQuery.Password = field.NewString(tableName, "password") + _userQuery.Roles = field.NewArray(tableName, "roles") + _userQuery.Status = field.NewField(tableName, "status") + _userQuery.Metas = field.NewJSONB(tableName, "metas") + _userQuery.VerifiedAt = field.NewTime(tableName, "verified_at") + + _userQuery.fillFieldMap() + + return _userQuery +} + +type userQuery struct { + userQueryDo userQueryDo + + ALL field.Asterisk + ID field.Int64 + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + Username field.String + Password field.String + Roles field.Array + Status field.Field + Metas field.JSONB + VerifiedAt field.Time + + fieldMap map[string]field.Expr +} + +func (u userQuery) Table(newTableName string) *userQuery { + u.userQueryDo.UseTable(newTableName) + return u.updateTableName(newTableName) +} + +func (u userQuery) As(alias string) *userQuery { + u.userQueryDo.DO = *(u.userQueryDo.As(alias).(*gen.DO)) + return u.updateTableName(alias) +} + +func (u *userQuery) updateTableName(table string) *userQuery { + u.ALL = field.NewAsterisk(table) + u.ID = field.NewInt64(table, "id") + u.CreatedAt = field.NewTime(table, "created_at") + u.UpdatedAt = field.NewTime(table, "updated_at") + u.DeletedAt = field.NewField(table, "deleted_at") + u.Username = field.NewString(table, "username") + u.Password = field.NewString(table, "password") + u.Roles = field.NewArray(table, "roles") + u.Status = field.NewField(table, "status") + u.Metas = field.NewJSONB(table, "metas") + u.VerifiedAt = field.NewTime(table, "verified_at") + + u.fillFieldMap() + + return u +} + +func (u *userQuery) QueryContext(ctx context.Context) (*userQuery, *userQueryDo) { + return u, u.userQueryDo.WithContext(ctx) +} + +func (u *userQuery) WithContext(ctx context.Context) *userQueryDo { + return u.userQueryDo.WithContext(ctx) +} + +func (u userQuery) TableName() string { return u.userQueryDo.TableName() } + +func (u userQuery) Alias() string { return u.userQueryDo.Alias() } + +func (u userQuery) Columns(cols ...field.Expr) gen.Columns { return u.userQueryDo.Columns(cols...) } + +func (u *userQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := u.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (u *userQuery) fillFieldMap() { + u.fieldMap = make(map[string]field.Expr, 10) + u.fieldMap["id"] = u.ID + u.fieldMap["created_at"] = u.CreatedAt + u.fieldMap["updated_at"] = u.UpdatedAt + u.fieldMap["deleted_at"] = u.DeletedAt + u.fieldMap["username"] = u.Username + u.fieldMap["password"] = u.Password + u.fieldMap["roles"] = u.Roles + u.fieldMap["status"] = u.Status + u.fieldMap["metas"] = u.Metas + u.fieldMap["verified_at"] = u.VerifiedAt +} + +func (u userQuery) clone(db *gorm.DB) userQuery { + u.userQueryDo.ReplaceConnPool(db.Statement.ConnPool) + return u +} + +func (u userQuery) replaceDB(db *gorm.DB) userQuery { + u.userQueryDo.ReplaceDB(db) + return u +} + +type userQueryDo struct{ gen.DO } + +func (u userQueryDo) Debug() *userQueryDo { + return u.withDO(u.DO.Debug()) +} + +func (u userQueryDo) WithContext(ctx context.Context) *userQueryDo { + return u.withDO(u.DO.WithContext(ctx)) +} + +func (u userQueryDo) ReadDB() *userQueryDo { + return u.Clauses(dbresolver.Read) +} + +func (u userQueryDo) WriteDB() *userQueryDo { + return u.Clauses(dbresolver.Write) +} + +func (u userQueryDo) Session(config *gorm.Session) *userQueryDo { + return u.withDO(u.DO.Session(config)) +} + +func (u userQueryDo) Clauses(conds ...clause.Expression) *userQueryDo { + return u.withDO(u.DO.Clauses(conds...)) +} + +func (u userQueryDo) Returning(value interface{}, columns ...string) *userQueryDo { + return u.withDO(u.DO.Returning(value, columns...)) +} + +func (u userQueryDo) Not(conds ...gen.Condition) *userQueryDo { + return u.withDO(u.DO.Not(conds...)) +} + +func (u userQueryDo) Or(conds ...gen.Condition) *userQueryDo { + return u.withDO(u.DO.Or(conds...)) +} + +func (u userQueryDo) Select(conds ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Select(conds...)) +} + +func (u userQueryDo) Where(conds ...gen.Condition) *userQueryDo { + return u.withDO(u.DO.Where(conds...)) +} + +func (u userQueryDo) Order(conds ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Order(conds...)) +} + +func (u userQueryDo) Distinct(cols ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Distinct(cols...)) +} + +func (u userQueryDo) Omit(cols ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Omit(cols...)) +} + +func (u userQueryDo) Join(table schema.Tabler, on ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Join(table, on...)) +} + +func (u userQueryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *userQueryDo { + return u.withDO(u.DO.LeftJoin(table, on...)) +} + +func (u userQueryDo) RightJoin(table schema.Tabler, on ...field.Expr) *userQueryDo { + return u.withDO(u.DO.RightJoin(table, on...)) +} + +func (u userQueryDo) Group(cols ...field.Expr) *userQueryDo { + return u.withDO(u.DO.Group(cols...)) +} + +func (u userQueryDo) Having(conds ...gen.Condition) *userQueryDo { + return u.withDO(u.DO.Having(conds...)) +} + +func (u userQueryDo) Limit(limit int) *userQueryDo { + return u.withDO(u.DO.Limit(limit)) +} + +func (u userQueryDo) Offset(offset int) *userQueryDo { + return u.withDO(u.DO.Offset(offset)) +} + +func (u userQueryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *userQueryDo { + return u.withDO(u.DO.Scopes(funcs...)) +} + +func (u userQueryDo) Unscoped() *userQueryDo { + return u.withDO(u.DO.Unscoped()) +} + +func (u userQueryDo) Create(values ...*User) error { + if len(values) == 0 { + return nil + } + return u.DO.Create(values) +} + +func (u userQueryDo) CreateInBatches(values []*User, batchSize int) error { + return u.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (u userQueryDo) Save(values ...*User) error { + if len(values) == 0 { + return nil + } + return u.DO.Save(values) +} + +func (u userQueryDo) First() (*User, error) { + if result, err := u.DO.First(); err != nil { + return nil, err + } else { + return result.(*User), nil + } +} + +func (u userQueryDo) Take() (*User, error) { + if result, err := u.DO.Take(); err != nil { + return nil, err + } else { + return result.(*User), nil + } +} + +func (u userQueryDo) Last() (*User, error) { + if result, err := u.DO.Last(); err != nil { + return nil, err + } else { + return result.(*User), nil + } +} + +func (u userQueryDo) Find() ([]*User, error) { + result, err := u.DO.Find() + return result.([]*User), err +} + +func (u userQueryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*User, err error) { + buf := make([]*User, 0, batchSize) + err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (u userQueryDo) FindInBatches(result *[]*User, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return u.DO.FindInBatches(result, batchSize, fc) +} + +func (u userQueryDo) Attrs(attrs ...field.AssignExpr) *userQueryDo { + return u.withDO(u.DO.Attrs(attrs...)) +} + +func (u userQueryDo) Assign(attrs ...field.AssignExpr) *userQueryDo { + return u.withDO(u.DO.Assign(attrs...)) +} + +func (u userQueryDo) Joins(fields ...field.RelationField) *userQueryDo { + for _, _f := range fields { + u = *u.withDO(u.DO.Joins(_f)) + } + return &u +} + +func (u userQueryDo) Preload(fields ...field.RelationField) *userQueryDo { + for _, _f := range fields { + u = *u.withDO(u.DO.Preload(_f)) + } + return &u +} + +func (u userQueryDo) FirstOrInit() (*User, error) { + if result, err := u.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*User), nil + } +} + +func (u userQueryDo) FirstOrCreate() (*User, error) { + if result, err := u.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*User), nil + } +} + +func (u userQueryDo) FindByPage(offset int, limit int) (result []*User, count int64, err error) { + result, err = u.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = u.Offset(-1).Limit(-1).Count() + return +} + +func (u userQueryDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = u.Count() + if err != nil { + return + } + + err = u.Offset(offset).Limit(limit).Scan(result) + return +} + +func (u userQueryDo) Scan(result interface{}) (err error) { + return u.DO.Scan(result) +} + +func (u userQueryDo) Delete(models ...*User) (result gen.ResultInfo, err error) { + return u.DO.Delete(models) +} + +// ForceDelete performs a permanent delete (ignores soft-delete) for current scope. +func (u userQueryDo) ForceDelete() (gen.ResultInfo, error) { + return u.Unscoped().Delete() +} + +// Inc increases the given column by step for current scope. +func (u userQueryDo) Inc(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column + step + e := field.NewUnsafeFieldRaw("?+?", column.RawExpr(), step) + return u.DO.UpdateColumn(column, e) +} + +// Dec decreases the given column by step for current scope. +func (u userQueryDo) Dec(column field.Expr, step int64) (gen.ResultInfo, error) { + // column = column - step + e := field.NewUnsafeFieldRaw("?-?", column.RawExpr(), step) + return u.DO.UpdateColumn(column, e) +} + +// Sum returns SUM(column) for current scope. +func (u userQueryDo) Sum(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("SUM(?)", column.RawExpr()) + if err := u.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Avg returns AVG(column) for current scope. +func (u userQueryDo) Avg(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("AVG(?)", column.RawExpr()) + if err := u.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Min returns MIN(column) for current scope. +func (u userQueryDo) Min(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MIN(?)", column.RawExpr()) + if err := u.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// Max returns MAX(column) for current scope. +func (u userQueryDo) Max(column field.Expr) (float64, error) { + var _v float64 + agg := field.NewUnsafeFieldRaw("MAX(?)", column.RawExpr()) + if err := u.Select(agg).Scan(&_v); err != nil { + return 0, err + } + return _v, nil +} + +// PluckMap returns a map[key]value for selected key/value expressions within current scope. +func (u userQueryDo) PluckMap(key, val field.Expr) (map[interface{}]interface{}, error) { + do := u.Select(key, val) + rows, err := do.DO.Rows() + if err != nil { + return nil, err + } + defer rows.Close() + mm := make(map[interface{}]interface{}) + for rows.Next() { + var k interface{} + var v interface{} + if err := rows.Scan(&k, &v); err != nil { + return nil, err + } + mm[k] = v + } + return mm, rows.Err() +} + +// Exists returns true if any record matches the given conditions. +func (u userQueryDo) Exists(conds ...gen.Condition) (bool, error) { + cnt, err := u.Where(conds...).Count() + if err != nil { + return false, err + } + return cnt > 0, nil +} + +// PluckIDs returns all primary key values under current scope. +func (u userQueryDo) PluckIDs() ([]int64, error) { + ids := make([]int64, 0, 16) + pk := field.NewInt64(u.TableName(), "id") + if err := u.DO.Pluck(pk, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// GetByID finds a single record by primary key. +func (u userQueryDo) GetByID(id int64) (*User, error) { + pk := field.NewInt64(u.TableName(), "id") + return u.Where(pk.Eq(id)).First() +} + +// GetByIDs finds records by primary key list. +func (u userQueryDo) GetByIDs(ids ...int64) ([]*User, error) { + if len(ids) == 0 { + return []*User{}, nil + } + pk := field.NewInt64(u.TableName(), "id") + return u.Where(pk.In(ids...)).Find() +} + +// DeleteByID deletes records by primary key. +func (u userQueryDo) DeleteByID(id int64) (gen.ResultInfo, error) { + pk := field.NewInt64(u.TableName(), "id") + return u.Where(pk.Eq(id)).Delete() +} + +// DeleteByIDs deletes records by a list of primary keys. +func (u userQueryDo) DeleteByIDs(ids ...int64) (gen.ResultInfo, error) { + if len(ids) == 0 { + return gen.ResultInfo{RowsAffected: 0, Error: nil}, nil + } + pk := field.NewInt64(u.TableName(), "id") + return u.Where(pk.In(ids...)).Delete() +} + +// RestoreWhere sets deleted_at to NULL for rows matching current scope + conds. +func (u userQueryDo) RestoreWhere(conds ...gen.Condition) (gen.ResultInfo, error) { + col := field.NewField(u.TableName(), "deleted_at") + return u.Unscoped().Where(conds...).UpdateColumn(col, nil) +} + +// RestoreByID sets deleted_at to NULL for the given primary key. +func (u userQueryDo) RestoreByID(id int64) (gen.ResultInfo, error) { + pk := field.NewInt64(u.TableName(), "id") + col := field.NewField(u.TableName(), "deleted_at") + return u.Unscoped().Where(pk.Eq(id)).UpdateColumn(col, nil) +} + +func (u *userQueryDo) withDO(do gen.Dao) *userQueryDo { + u.DO = *do.(*gen.DO) + return u +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..8c35c88 --- /dev/null +++ b/backend/docs/docs.go @@ -0,0 +1,141 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/rogeecn/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "UserName", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/v1/medias/{id}": { + "post": { + "description": "Test", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Test" + ], + "summary": "Test", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "年龄", + "name": "age", + "in": "query" + }, + { + "type": "string", + "description": "名称", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "list": { + "$ref": "#/definitions/v1.ResponseItem" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "requests.Pager": { + "type": "object", + "properties": { + "items": {}, + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "v1.ResponseItem": { + "type": "object" + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "ApiDoc", + Description: "This is a sample server celler server.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/docs/ember.go b/backend/docs/ember.go new file mode 100644 index 0000000..ae898ec --- /dev/null +++ b/backend/docs/ember.go @@ -0,0 +1,10 @@ +package docs + +import ( + _ "embed" + + _ "github.com/rogeecn/swag" +) + +//go:embed swagger.json +var SwaggerSpec string diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json new file mode 100644 index 0000000..c1e57c5 --- /dev/null +++ b/backend/docs/swagger.json @@ -0,0 +1,117 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server celler server.", + "title": "ApiDoc", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "UserName", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/v1/medias/{id}": { + "post": { + "description": "Test", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Test" + ], + "summary": "Test", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "年龄", + "name": "age", + "in": "query" + }, + { + "type": "string", + "description": "名称", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "list": { + "$ref": "#/definitions/v1.ResponseItem" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "requests.Pager": { + "type": "object", + "properties": { + "items": {}, + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "v1.ResponseItem": { + "type": "object" + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml new file mode 100644 index 0000000..f9f7516 --- /dev/null +++ b/backend/docs/swagger.yaml @@ -0,0 +1,75 @@ +basePath: /api/v1 +definitions: + requests.Pager: + properties: + items: {} + limit: + type: integer + page: + type: integer + total: + type: integer + type: object + v1.ResponseItem: + type: object +externalDocs: + description: OpenAPI + url: https://swagger.io/resources/open-api/ +host: localhost:8080 +info: + contact: + email: support@swagger.io + name: UserName + url: http://www.swagger.io/support + description: This is a sample server celler server. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: ApiDoc + version: "1.0" +paths: + /v1/medias/{id}: + post: + consumes: + - application/json + description: Test + parameters: + - description: ID + in: path + name: id + required: true + type: integer + - description: 年龄 + in: query + name: age + type: integer + - description: 名称 + in: query + name: name + type: string + - in: query + name: limit + type: integer + - in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: 成功 + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + list: + $ref: '#/definitions/v1.ResponseItem' + type: object + summary: Test + tags: + - Test +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/backend/fixtures/.gitkeep b/backend/fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..f18ddad --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,134 @@ +module quyun/v2 + +go 1.25.3 + +require ( + github.com/ThreeDotsLabs/watermill v1.5.1 + github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2 + github.com/ThreeDotsLabs/watermill-redisstream v1.4.5 + github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 + github.com/gofiber/fiber/v3 v3.0.0-rc.3 + github.com/gofiber/utils/v2 v2.0.0-rc.4 + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.7.6 + github.com/pkg/errors v0.9.1 + github.com/pressly/goose/v3 v3.26.0 + github.com/redis/go-redis/v9 v9.17.2 + github.com/riverqueue/river v0.28.0 + github.com/riverqueue/river/riverdriver/riverdatabasesql v0.28.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.28.0 + github.com/riverqueue/river/rivertype v0.28.0 + github.com/rogeecn/fabfile v1.7.0 + github.com/rogeecn/swag v1.0.1 + github.com/samber/lo v1.52.0 + github.com/sirupsen/logrus v1.9.3 + github.com/smartystreets/goconvey v1.8.1 + github.com/soheilhy/cmux v0.1.5 + github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.11.1 + github.com/swaggo/files/v2 v2.0.2 + go.ipao.vip/atom v1.2.1 + go.ipao.vip/gen v0.0.0-20250924024520-70c4accdea44 + go.uber.org/dig v1.19.0 + golang.org/x/sync v0.19.0 + google.golang.org/grpc v1.75.1 + google.golang.org/protobuf v1.36.11 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 + gorm.io/plugin/dbresolver v1.6.2 +) + +require ( + github.com/IBM/sarama v1.46.3 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Rican7/retry v0.3.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.2 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gofiber/schema v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/lithammer/shortuuid/v3 v3.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/riverqueue/river/riverdriver v0.28.0 // indirect + github.com/riverqueue/river/rivershared v0.28.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/smarty/assertions v1.15.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.68.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/hints v1.1.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..8ed0c42 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,409 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE= +github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc= +github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= +github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY= +github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4= +github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2 h1:lLmrzZnl8o8U5uLVhMLSFHGSuWLcsqhW1MOtltx2CbQ= +github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk= +github.com/ThreeDotsLabs/watermill-redisstream v1.4.5 h1:SCETqsAYo/CRBb7H3+zWCcSqhMpDrQA4I6dCqC7UPR4= +github.com/ThreeDotsLabs/watermill-redisstream v1.4.5/go.mod h1:Da3wqG1OcvHPODjuJcxSCY1O7D4loIZQpVbZ5u94xRo= +github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 h1:g4uE5Nm3Z6LVB3m+uMgHlN4ne4bDpwf3RJmXYRgMv94= +github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0/go.mod h1:G8/otZYWLTCeYL2Ww3ujQ7gQ/3+jw5Bj0UtyKn7bBjA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84= +github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc= +github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofiber/fiber/v3 v3.0.0-rc.3 h1:h0KXuRHbivSslIpoHD1R/XjUsjcGwt+2vK0avFiYonA= +github.com/gofiber/fiber/v3 v3.0.0-rc.3/go.mod h1:LNBPuS/rGoUFlOyy03fXsWAeWfdGoT1QytwjRVNSVWo= +github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY= +github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= +github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riverqueue/river v0.28.0 h1:j+1vqwRkFzI0kWTbU0p5mH+hX5x8ZJiyVH4p6T1OqLU= +github.com/riverqueue/river v0.28.0/go.mod h1:3oPHvH8cRjpBj391ULViBW+p6gBFRbWCO9RjJfDkb4M= +github.com/riverqueue/river/riverdriver v0.28.0 h1:FvzYl0JjpsxSyMtMRRENneggVdDDm8g69yyFCfDjkt8= +github.com/riverqueue/river/riverdriver v0.28.0/go.mod h1:mprPQKIzMlyrek0+w25K0hvHZilvWBdDRxLvUg6aZcs= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.28.0 h1:d1xLt7fDlCYkobLX9r7Jo86XM53iVlqlAbwXbQMKvKc= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.28.0/go.mod h1:P4sEKDITAxWCJt4NfXgYV+BvYnhccaYGJ0fViEHKdHk= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.28.0 h1:5OTfF344bIVKcpULFJNIqGqFQdqB63u8DOycyzAprww= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.28.0/go.mod h1:ZBXSTqJ8FinI4nf9Bz6KMv561YDWvcqKGc6gWghJcV4= +github.com/riverqueue/river/rivershared v0.28.0 h1:8bJ0SxX95dyjm/H3xYtOXprZgiJHF423msyfWHUNhUg= +github.com/riverqueue/river/rivershared v0.28.0/go.mod h1:6ujXUF1mwCKvgC/OVwRIn8Z3GIuOdjjhiTuRxc4jCb4= +github.com/riverqueue/river/rivertype v0.28.0 h1:JYSpY0DWg34bOKyxB/kWgGXeryjunckYgNNrgKYk4jg= +github.com/riverqueue/river/rivertype v0.28.0/go.mod h1:rWpgI59doOWS6zlVocROcwc00fZ1RbzRwsRTU8CDguw= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogeecn/fabfile v1.7.0 h1:qtwkqaBsJjWrggbvznbd0HGyJ0ebBTOBE893JvD5Tng= +github.com/rogeecn/fabfile v1.7.0/go.mod h1:EPwX7TtVcIWSLJkJAqxSzYjM/aV1Q0wymcaXqnMgzas= +github.com/rogeecn/swag v1.0.1 h1:s1yxLgopqO1m8sqGjVmt6ocMBRubMPIh2JtIPG4xjQE= +github.com/rogeecn/swag v1.0.1/go.mod h1:flG2NXERPxlRl2VdpU2VXTO8iBnQiERyowOXSkZVMOc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok= +github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.ipao.vip/atom v1.2.1 h1:7VlDLSkGNVEZLVM/JVcXXdMTO0+sFsxe1vfIM4Xz8uc= +go.ipao.vip/atom v1.2.1/go.mod h1:woAv+rZf0xd+7mEtKWv4PyazQARFLnrV/qA4qlAK008= +go.ipao.vip/gen v0.0.0-20250924024520-70c4accdea44 h1:i7zFEsfUYRJQo0mXUWI/RoEkgEdTNmLt0Io2rwhqY9E= +go.ipao.vip/gen v0.0.0-20250924024520-70c4accdea44/go.mod h1:ip5X9ioxR9hvM/mrsA77KWXFsrMm5oki5rfY5MSkssM= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.1.6/go.mod h1:W8LmC/6UvVbHKah0+QOC7Ja66EaZXHwUTjgXY8YNWX8= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw= +gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y= +gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc= +gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..ac835d1 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "quyun/v2/app/commands/http" + "quyun/v2/app/commands/migrate" + "quyun/v2/pkg/utils" + + log "github.com/sirupsen/logrus" + "go.ipao.vip/atom" +) + +// @title ApiDoc +// @version 1.0 +// @description This is a sample server celler server. +// @termsOfService http://swagger.io/terms/ +// @contact.name UserName +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8080 +// @BasePath /t/{tenant_code}/v1 +// @securityDefinitions.basic BasicAuth +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ + +func main() { + // 打印构建信息 + utils.PrintBuildInfo("v2") + + opts := []atom.Option{ + atom.Name("v2"), + http.Command(), + migrate.Command(), + } + + if err := atom.Serve(opts...); err != nil { + log.Fatal(err) + } +} diff --git a/backend/main_test.go b/backend/main_test.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/backend/main_test.go @@ -0,0 +1 @@ +package main diff --git a/backend/pkg/consts/consts.gen.go b/backend/pkg/consts/consts.gen.go new file mode 100644 index 0000000..a0935e4 --- /dev/null +++ b/backend/pkg/consts/consts.gen.go @@ -0,0 +1,344 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package consts + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // RoleUser is a Role of type user. + RoleUser Role = "user" + // RoleTenantAdmin is a Role of type tenant_admin. + RoleTenantAdmin Role = "tenant_admin" + // RoleSuperAdmin is a Role of type super_admin. + RoleSuperAdmin Role = "super_admin" +) + +var ErrInvalidRole = fmt.Errorf("not a valid Role, try [%s]", strings.Join(_RoleNames, ", ")) + +var _RoleNames = []string{ + string(RoleUser), + string(RoleTenantAdmin), + string(RoleSuperAdmin), +} + +// RoleNames returns a list of possible string values of Role. +func RoleNames() []string { + tmp := make([]string, len(_RoleNames)) + copy(tmp, _RoleNames) + return tmp +} + +// RoleValues returns a list of the values for Role +func RoleValues() []Role { + return []Role{ + RoleUser, + RoleTenantAdmin, + RoleSuperAdmin, + } +} + +// String implements the Stringer interface. +func (x Role) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x Role) IsValid() bool { + _, err := ParseRole(string(x)) + return err == nil +} + +var _RoleValue = map[string]Role{ + "user": RoleUser, + "tenant_admin": RoleTenantAdmin, + "super_admin": RoleSuperAdmin, +} + +// ParseRole attempts to convert a string to a Role. +func ParseRole(name string) (Role, error) { + if x, ok := _RoleValue[name]; ok { + return x, nil + } + return Role(""), fmt.Errorf("%s is %w", name, ErrInvalidRole) +} + +var errRoleNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *Role) Scan(value interface{}) (err error) { + if value == nil { + *x = Role("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseRole(v) + case []byte: + *x, err = ParseRole(string(v)) + case Role: + *x = v + case *Role: + if v == nil { + return errRoleNilPtr + } + *x = *v + case *string: + if v == nil { + return errRoleNilPtr + } + *x, err = ParseRole(*v) + default: + return errors.New("invalid type for Role") + } + + return +} + +// Value implements the driver Valuer interface. +func (x Role) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *Role) Set(val string) error { + v, err := ParseRole(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *Role) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *Role) Type() string { + return "Role" +} + +type NullRole struct { + Role Role + Valid bool +} + +func NewNullRole(val interface{}) (x NullRole) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullRole) Scan(value interface{}) (err error) { + if value == nil { + x.Role, x.Valid = Role(""), false + return + } + + err = x.Role.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullRole) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.Role), nil +} + +type NullRoleStr struct { + NullRole +} + +func NewNullRoleStr(val interface{}) (x NullRoleStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullRoleStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.Role.String(), nil +} + +const ( + // UserStatusPendingVerify is a UserStatus of type pending_verify. + UserStatusPendingVerify UserStatus = "pending_verify" + // UserStatusVerified is a UserStatus of type verified. + UserStatusVerified UserStatus = "verified" + // UserStatusBanned is a UserStatus of type banned. + UserStatusBanned UserStatus = "banned" +) + +var ErrInvalidUserStatus = fmt.Errorf("not a valid UserStatus, try [%s]", strings.Join(_UserStatusNames, ", ")) + +var _UserStatusNames = []string{ + string(UserStatusPendingVerify), + string(UserStatusVerified), + string(UserStatusBanned), +} + +// UserStatusNames returns a list of possible string values of UserStatus. +func UserStatusNames() []string { + tmp := make([]string, len(_UserStatusNames)) + copy(tmp, _UserStatusNames) + return tmp +} + +// UserStatusValues returns a list of the values for UserStatus +func UserStatusValues() []UserStatus { + return []UserStatus{ + UserStatusPendingVerify, + UserStatusVerified, + UserStatusBanned, + } +} + +// String implements the Stringer interface. +func (x UserStatus) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x UserStatus) IsValid() bool { + _, err := ParseUserStatus(string(x)) + return err == nil +} + +var _UserStatusValue = map[string]UserStatus{ + "pending_verify": UserStatusPendingVerify, + "verified": UserStatusVerified, + "banned": UserStatusBanned, +} + +// ParseUserStatus attempts to convert a string to a UserStatus. +func ParseUserStatus(name string) (UserStatus, error) { + if x, ok := _UserStatusValue[name]; ok { + return x, nil + } + return UserStatus(""), fmt.Errorf("%s is %w", name, ErrInvalidUserStatus) +} + +var errUserStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *UserStatus) Scan(value interface{}) (err error) { + if value == nil { + *x = UserStatus("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseUserStatus(v) + case []byte: + *x, err = ParseUserStatus(string(v)) + case UserStatus: + *x = v + case *UserStatus: + if v == nil { + return errUserStatusNilPtr + } + *x = *v + case *string: + if v == nil { + return errUserStatusNilPtr + } + *x, err = ParseUserStatus(*v) + default: + return errors.New("invalid type for UserStatus") + } + + return +} + +// Value implements the driver Valuer interface. +func (x UserStatus) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *UserStatus) Set(val string) error { + v, err := ParseUserStatus(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *UserStatus) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *UserStatus) Type() string { + return "UserStatus" +} + +type NullUserStatus struct { + UserStatus UserStatus + Valid bool +} + +func NewNullUserStatus(val interface{}) (x NullUserStatus) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullUserStatus) Scan(value interface{}) (err error) { + if value == nil { + x.UserStatus, x.Valid = UserStatus(""), false + return + } + + err = x.UserStatus.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatus) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.UserStatus), nil +} + +type NullUserStatusStr struct { + NullUserStatus +} + +func NewNullUserStatusStr(val interface{}) (x NullUserStatusStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatusStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.UserStatus.String(), nil +} diff --git a/backend/pkg/consts/consts.go b/backend/pkg/consts/consts.go new file mode 100644 index 0000000..6aa34c9 --- /dev/null +++ b/backend/pkg/consts/consts.go @@ -0,0 +1,16 @@ +package consts + +// Format +// +// // swagger:enum CacheKey +// // ENUM( +// // VerifyCode = "code:__CHANNEL__:%s", +// // ) + +// swagger:enum Role +// ENUM( user, tenant_admin, super_admin) +type Role string + +// swagger:enum UserStatus +// ENUM(pending_verify, verified, banned, ) +type UserStatus string diff --git a/backend/pkg/proto/user/v1/user.pb.go b/backend/pkg/proto/user/v1/user.pb.go new file mode 100644 index 0000000..796cb5c --- /dev/null +++ b/backend/pkg/proto/user/v1/user.pb.go @@ -0,0 +1,387 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: user/v1/user.proto + +package userv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// User represents a user entity +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + Phone string `protobuf:"bytes,4,opt,name=phone,proto3" json:"phone,omitempty"` + CreateTime string `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + UpdateTime string `protobuf:"bytes,6,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_user_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetPhone() string { + if x != nil { + return x.Phone + } + return "" +} + +func (x *User) GetCreateTime() string { + if x != nil { + return x.CreateTime + } + return "" +} + +func (x *User) GetUpdateTime() string { + if x != nil { + return x.UpdateTime + } + return "" +} + +type ListUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + PageNumber int32 `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersRequest) Reset() { + *x = ListUsersRequest{} + mi := &file_user_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersRequest) ProtoMessage() {} + +func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. +func (*ListUsersRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *ListUsersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListUsersRequest) GetPageNumber() int32 { + if x != nil { + return x.PageNumber + } + return 0 +} + +type ListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersResponse) Reset() { + *x = ListUsersResponse{} + mi := &file_user_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersResponse) ProtoMessage() {} + +func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. +func (*ListUsersResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{2} +} + +func (x *ListUsersResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +func (x *ListUsersResponse) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +type GetUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserRequest) Reset() { + *x = GetUserRequest{} + mi := &file_user_v1_user_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserRequest) ProtoMessage() {} + +func (x *GetUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. +func (*GetUserRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{3} +} + +func (x *GetUserRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type GetUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserResponse) Reset() { + *x = GetUserResponse{} + mi := &file_user_v1_user_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserResponse) ProtoMessage() {} + +func (x *GetUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead. +func (*GetUserResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{4} +} + +func (x *GetUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_user_v1_user_proto protoreflect.FileDescriptor + +const file_user_v1_user_proto_rawDesc = "" + + "\n" + + "\x12user/v1/user.proto\x12\auser.v1\"\xa0\x01\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x14\n" + + "\x05phone\x18\x04 \x01(\tR\x05phone\x12\x1f\n" + + "\vcreate_time\x18\x05 \x01(\tR\n" + + "createTime\x12\x1f\n" + + "\vupdate_time\x18\x06 \x01(\tR\n" + + "updateTime\"P\n" + + "\x10ListUsersRequest\x12\x1b\n" + + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1f\n" + + "\vpage_number\x18\x02 \x01(\x05R\n" + + "pageNumber\"N\n" + + "\x11ListUsersResponse\x12#\n" + + "\x05users\x18\x01 \x03(\v2\r.user.v1.UserR\x05users\x12\x14\n" + + "\x05total\x18\x02 \x01(\x05R\x05total\" \n" + + "\x0eGetUserRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"4\n" + + "\x0fGetUserResponse\x12!\n" + + "\x04user\x18\x01 \x01(\v2\r.user.v1.UserR\x04user2\x93\x01\n" + + "\vUserService\x12D\n" + + "\tListUsers\x12\x19.user.v1.ListUsersRequest\x1a\x1a.user.v1.ListUsersResponse\"\x00\x12>\n" + + "\aGetUser\x12\x17.user.v1.GetUserRequest\x1a\x18.user.v1.GetUserResponse\"\x00Bx\n" + + "\vcom.user.v1B\tUserProtoP\x01Z!quyun/v2/pkg/proto/user/v1;userv1\xa2\x02\x03UXX\xaa\x02\aUser.V1\xca\x02\aUser\\V1\xe2\x02\x13User\\V1\\GPBMetadata\xea\x02\bUser::V1b\x06proto3" + +var ( + file_user_v1_user_proto_rawDescOnce sync.Once + file_user_v1_user_proto_rawDescData []byte +) + +func file_user_v1_user_proto_rawDescGZIP() []byte { + file_user_v1_user_proto_rawDescOnce.Do(func() { + file_user_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_user_v1_user_proto_rawDesc), len(file_user_v1_user_proto_rawDesc))) + }) + return file_user_v1_user_proto_rawDescData +} + +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_user_v1_user_proto_goTypes = []any{ + (*User)(nil), // 0: user.v1.User + (*ListUsersRequest)(nil), // 1: user.v1.ListUsersRequest + (*ListUsersResponse)(nil), // 2: user.v1.ListUsersResponse + (*GetUserRequest)(nil), // 3: user.v1.GetUserRequest + (*GetUserResponse)(nil), // 4: user.v1.GetUserResponse +} +var file_user_v1_user_proto_depIdxs = []int32{ + 0, // 0: user.v1.ListUsersResponse.users:type_name -> user.v1.User + 0, // 1: user.v1.GetUserResponse.user:type_name -> user.v1.User + 1, // 2: user.v1.UserService.ListUsers:input_type -> user.v1.ListUsersRequest + 3, // 3: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest + 2, // 4: user.v1.UserService.ListUsers:output_type -> user.v1.ListUsersResponse + 4, // 5: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_user_v1_user_proto_init() } +func file_user_v1_user_proto_init() { + if File_user_v1_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_v1_user_proto_rawDesc), len(file_user_v1_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_user_v1_user_proto_goTypes, + DependencyIndexes: file_user_v1_user_proto_depIdxs, + MessageInfos: file_user_v1_user_proto_msgTypes, + }.Build() + File_user_v1_user_proto = out.File + file_user_v1_user_proto_goTypes = nil + file_user_v1_user_proto_depIdxs = nil +} diff --git a/backend/pkg/proto/user/v1/user_grpc.pb.go b/backend/pkg/proto/user/v1/user_grpc.pb.go new file mode 100644 index 0000000..439436c --- /dev/null +++ b/backend/pkg/proto/user/v1/user_grpc.pb.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: user/v1/user.proto + +package userv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + UserService_ListUsers_FullMethodName = "/user.v1.UserService/ListUsers" + UserService_GetUser_FullMethodName = "/user.v1.UserService/GetUser" +) + +// UserServiceClient is the client API for UserService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// UserService provides user-related operations +type UserServiceClient interface { + // ListUsers returns a list of users with pagination + ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) + // GetUser returns detailed information about a specific user + GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) +} + +type userServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { + return &userServiceClient{cc} +} + +func (c *userServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUsersResponse) + err := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetUserResponse) + err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserServiceServer is the server API for UserService service. +// All implementations must embed UnimplementedUserServiceServer +// for forward compatibility. +// +// UserService provides user-related operations +type UserServiceServer interface { + // ListUsers returns a list of users with pagination + ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) + // GetUser returns detailed information about a specific user + GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) + mustEmbedUnimplementedUserServiceServer() +} + +// UnimplementedUserServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUserServiceServer struct{} + +func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} +func (UnimplementedUserServiceServer) testEmbeddedByValue() {} + +// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserServiceServer will +// result in compilation errors. +type UnsafeUserServiceServer interface { + mustEmbedUnimplementedUserServiceServer() +} + +func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { + // If the following call panics, it indicates UnimplementedUserServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UserService_ServiceDesc, srv) +} + +func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "user.v1.UserService", + HandlerType: (*UserServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListUsers", + Handler: _UserService_ListUsers_Handler, + }, + { + MethodName: "GetUser", + Handler: _UserService_GetUser_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "user/v1/user.proto", +} diff --git a/backend/pkg/utils/buffer.go b/backend/pkg/utils/buffer.go new file mode 100644 index 0000000..5746d74 --- /dev/null +++ b/backend/pkg/utils/buffer.go @@ -0,0 +1,26 @@ +package utils + +import ( + "bufio" + "io" +) + +// NewLogBuffer creates a buffer that can be used to capture output stream +// and write to a logger in real time +func NewLogBuffer(output func(string)) io.Writer { + reader, writer := io.Pipe() + + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + output(scanner.Text()) + } + }() + + return writer +} + +// NewCombinedBuffer combines multiple io.Writers +func NewCombinedBuffer(writers ...io.Writer) io.Writer { + return io.MultiWriter(writers...) +} diff --git a/backend/pkg/utils/build_info.go b/backend/pkg/utils/build_info.go new file mode 100644 index 0000000..8dfc5a7 --- /dev/null +++ b/backend/pkg/utils/build_info.go @@ -0,0 +1,44 @@ +package utils + +import "fmt" + +// 构建信息变量,通过 ldflags 在构建时注入 +var ( + // Version 应用版本信息 + Version string + + // BuildAt 构建时间 + BuildAt string + + // GitHash Git 提交哈希 + GitHash string +) + +// GetBuildInfo 获取构建信息 +func GetBuildInfo() map[string]string { + return map[string]string{ + "version": Version, + "buildAt": BuildAt, + "gitHash": GitHash, + } +} + +// PrintBuildInfo 打印构建信息 +func PrintBuildInfo(appName string) { + buildInfo := GetBuildInfo() + + println("========================================") + printf("🚀 %s\n", appName) + println("========================================") + printf("📋 Version: %s\n", buildInfo["version"]) + printf("🕐 Build Time: %s\n", buildInfo["buildAt"]) + printf("🔗 Git Hash: %s\n", buildInfo["gitHash"]) + println("========================================") + println("🌟 Application is starting...") + println() +} + +// 为了避免导入 fmt 包,我们使用内置的 print 和 printf 函数 +func printf(format string, args ...interface{}) { + print(fmt.Sprintf(format, args...)) +} diff --git a/backend/proto/user/v1/user.proto b/backend/proto/user/v1/user.proto new file mode 100644 index 0000000..40be68d --- /dev/null +++ b/backend/proto/user/v1/user.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package user.v1; + +// User represents a user entity +message User { + int64 id = 1; + string username = 2; + string email = 3; + string phone = 4; + string create_time = 5; + string update_time = 6; +} + +message ListUsersRequest { + int32 page_size = 1; + int32 page_number = 2; +} + +message ListUsersResponse { + repeated User users = 1; + int32 total = 2; +} + +message GetUserRequest { + int64 id = 1; +} + +message GetUserResponse { + User user = 1; +} + +// UserService provides user-related operations +service UserService { + // ListUsers returns a list of users with pagination + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {} + + // GetUser returns detailed information about a specific user + rpc GetUser(GetUserRequest) returns (GetUserResponse) {} +} diff --git a/backend/providers/app/app.go b/backend/providers/app/app.go new file mode 100644 index 0000000..d0a566e --- /dev/null +++ b/backend/providers/app/app.go @@ -0,0 +1,18 @@ +package app + +import ( + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*Config, error) { + return &config, nil + }, o.DiOptions()...) +} diff --git a/backend/providers/app/config.gen.go b/backend/providers/app/config.gen.go new file mode 100644 index 0000000..702160e --- /dev/null +++ b/backend/providers/app/config.gen.go @@ -0,0 +1,179 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package app + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // AppModeDevelopment is a AppMode of type development. + AppModeDevelopment AppMode = "development" + // AppModeRelease is a AppMode of type release. + AppModeRelease AppMode = "release" + // AppModeTest is a AppMode of type test. + AppModeTest AppMode = "test" +) + +var ErrInvalidAppMode = fmt.Errorf("not a valid AppMode, try [%s]", strings.Join(_AppModeNames, ", ")) + +var _AppModeNames = []string{ + string(AppModeDevelopment), + string(AppModeRelease), + string(AppModeTest), +} + +// AppModeNames returns a list of possible string values of AppMode. +func AppModeNames() []string { + tmp := make([]string, len(_AppModeNames)) + copy(tmp, _AppModeNames) + return tmp +} + +// AppModeValues returns a list of the values for AppMode +func AppModeValues() []AppMode { + return []AppMode{ + AppModeDevelopment, + AppModeRelease, + AppModeTest, + } +} + +// String implements the Stringer interface. +func (x AppMode) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AppMode) IsValid() bool { + _, err := ParseAppMode(string(x)) + return err == nil +} + +var _AppModeValue = map[string]AppMode{ + "development": AppModeDevelopment, + "release": AppModeRelease, + "test": AppModeTest, +} + +// ParseAppMode attempts to convert a string to a AppMode. +func ParseAppMode(name string) (AppMode, error) { + if x, ok := _AppModeValue[name]; ok { + return x, nil + } + return AppMode(""), fmt.Errorf("%s is %w", name, ErrInvalidAppMode) +} + +var errAppModeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AppMode) Scan(value interface{}) (err error) { + if value == nil { + *x = AppMode("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAppMode(v) + case []byte: + *x, err = ParseAppMode(string(v)) + case AppMode: + *x = v + case *AppMode: + if v == nil { + return errAppModeNilPtr + } + *x = *v + case *string: + if v == nil { + return errAppModeNilPtr + } + *x, err = ParseAppMode(*v) + default: + return errors.New("invalid type for AppMode") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AppMode) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *AppMode) Set(val string) error { + v, err := ParseAppMode(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *AppMode) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *AppMode) Type() string { + return "AppMode" +} + +type NullAppMode struct { + AppMode AppMode + Valid bool +} + +func NewNullAppMode(val interface{}) (x NullAppMode) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullAppMode) Scan(value interface{}) (err error) { + if value == nil { + x.AppMode, x.Valid = AppMode(""), false + return + } + + err = x.AppMode.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullAppMode) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.AppMode), nil +} + +type NullAppModeStr struct { + NullAppMode +} + +func NewNullAppModeStr(val interface{}) (x NullAppModeStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullAppModeStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.AppMode.String(), nil +} diff --git a/backend/providers/app/config.go b/backend/providers/app/config.go new file mode 100644 index 0000000..2b39426 --- /dev/null +++ b/backend/providers/app/config.go @@ -0,0 +1,52 @@ +package app + +import ( + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +const DefaultPrefix = "App" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +// swagger:enum AppMode +// ENUM(development, release, test) +type AppMode string + +type Config struct { + Mode AppMode + Cert *Cert + BaseURI *string + Super *SuperAdmin +} + +func (c *Config) IsDevMode() bool { + return c.Mode == AppModeDevelopment +} + +func (c *Config) IsReleaseMode() bool { + return c.Mode == AppModeRelease +} + +func (c *Config) IsTestMode() bool { + return c.Mode == AppModeTest +} + +type Cert struct { + CA string + Cert string + Key string +} + +type SuperAdmin struct { + // Token is an optional shared token for super-admin API access. + // If empty: development mode allows access; release mode requires token. + Token string +} diff --git a/backend/providers/cmux/config.go b/backend/providers/cmux/config.go new file mode 100644 index 0000000..c88aaba --- /dev/null +++ b/backend/providers/cmux/config.go @@ -0,0 +1,109 @@ +package cmux + +import ( + "fmt" + "net" + "time" + + "quyun/v2/providers/grpc" + "quyun/v2/providers/http" + + log "github.com/sirupsen/logrus" + "github.com/soheilhy/cmux" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + "golang.org/x/sync/errgroup" +) + +const DefaultPrefix = "Cmux" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Host *string + Port uint +} + +func (h *Config) Address() string { + if h.Host == nil { + return fmt.Sprintf(":%d", h.Port) + } + return fmt.Sprintf("%s:%d", *h.Host, h.Port) +} + +type CMux struct { + Http *http.Service + Grpc *grpc.Grpc + Mux cmux.CMux + Base net.Listener +} + +func (c *CMux) Serve() error { + // Protect against slowloris connections when sniffing protocol + // Safe even if SetReadTimeout is a no-op in the cmux version in use + c.Mux.SetReadTimeout(1 * time.Second) + + addr := "" + if c.Base != nil && c.Base.Addr() != nil { + addr = c.Base.Addr().String() + } + log.WithFields(log.Fields{ + "addr": addr, + }).Info("cmux starting") + + // Route classic HTTP/1.x traffic to the HTTP service + httpL := c.Mux.Match(cmux.HTTP1Fast()) + + // Route gRPC (HTTP/2 with content-type application/grpc) to the gRPC service. + // Additionally, send other HTTP/2 traffic to gRPC since Fiber (HTTP) does not serve HTTP/2. + grpcL := c.Mux.Match( + cmux.HTTP2HeaderField("content-type", "application/grpc"), + cmux.HTTP2(), + ) + + var eg errgroup.Group + eg.Go(func() error { + log.WithField("addr", addr).Info("grpc serving via cmux") + err := c.Grpc.ServeWithListener(grpcL) + if err != nil { + log.WithError(err).Error("grpc server exited with error") + } else { + log.Info("grpc server exited") + } + return err + }) + + eg.Go(func() error { + log.WithField("addr", addr).Info("http serving via cmux") + err := c.Http.Listener(httpL) + if err != nil { + log.WithError(err).Error("http server exited with error") + } else { + log.Info("http server exited") + } + return err + }) + + // Run cmux dispatcher; wait for the first error from any goroutine + eg.Go(func() error { + err := c.Mux.Serve() + if err != nil { + log.WithError(err).Error("cmux exited with error") + } else { + log.Info("cmux exited") + } + return err + }) + err := eg.Wait() + if err == nil { + log.Info("cmux and sub-servers exited cleanly") + } + return err +} diff --git a/backend/providers/cmux/provider.go b/backend/providers/cmux/provider.go new file mode 100644 index 0000000..8c10f55 --- /dev/null +++ b/backend/providers/cmux/provider.go @@ -0,0 +1,37 @@ +package cmux + +import ( + "net" + + "quyun/v2/providers/grpc" + "quyun/v2/providers/http" + + "github.com/soheilhy/cmux" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func(http *http.Service, grpc *grpc.Grpc) (*CMux, error) { + l, err := net.Listen("tcp", config.Address()) + if err != nil { + return nil, err + } + + mux := &CMux{ + Http: http, + Grpc: grpc, + Mux: cmux.New(l), + Base: l, + } + // Ensure cmux stops accepting new connections on shutdown + container.AddCloseAble(func() { _ = l.Close() }) + + return mux, nil + }, o.DiOptions()...) +} diff --git a/backend/providers/event/channel.go b/backend/providers/event/channel.go new file mode 100644 index 0000000..f8f25f8 --- /dev/null +++ b/backend/providers/event/channel.go @@ -0,0 +1,30 @@ +package event + +import "go.ipao.vip/atom/contracts" + +const ( + Go contracts.Channel = "go" + Kafka contracts.Channel = "kafka" + Redis contracts.Channel = "redis" + Sql contracts.Channel = "sql" +) + +type DefaultPublishTo struct{} + +func (d *DefaultPublishTo) PublishTo() (contracts.Channel, string) { + return Go, "event:processed" +} + +type DefaultChannel struct{} + +func (d *DefaultChannel) Channel() contracts.Channel { return Go } + +// kafka +type KafkaChannel struct{} + +func (k *KafkaChannel) Channel() contracts.Channel { return Kafka } + +// kafka +type RedisChannel struct{} + +func (k *RedisChannel) Channel() contracts.Channel { return Redis } diff --git a/backend/providers/event/config.go b/backend/providers/event/config.go new file mode 100644 index 0000000..ba5509f --- /dev/null +++ b/backend/providers/event/config.go @@ -0,0 +1,99 @@ +package event + +import ( + "context" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +const DefaultPrefix = "Events" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: ProvideChannel, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Sql *ConfigSql + Kafka *ConfigKafka + Redis *ConfigRedis +} + +type ConfigSql struct { + ConsumerGroup string +} + +type ConfigRedis struct { + ConsumerGroup string + Streams []string +} + +type ConfigKafka struct { + ConsumerGroup string + Brokers []string +} + +type PubSub struct { + Router *message.Router + + publishers map[contracts.Channel]message.Publisher + subscribers map[contracts.Channel]message.Subscriber +} + +func (ps *PubSub) Serve(ctx context.Context) error { + if err := ps.Router.Run(ctx); err != nil { + return err + } + return nil +} + +// publish +func (ps *PubSub) Publish(e contracts.EventPublisher) error { + if e == nil { + return nil + } + + payload, err := e.Marshal() + if err != nil { + return err + } + + msg := message.NewMessage(watermill.NewUUID(), payload) + return ps.getPublisher(e.Channel()).Publish(e.Topic(), msg) +} + +// getPublisher returns the publisher for the specified channel. +func (ps *PubSub) getPublisher(channel contracts.Channel) message.Publisher { + if pub, ok := ps.publishers[channel]; ok { + return pub + } + return ps.publishers[Go] +} + +func (ps *PubSub) getSubscriber(channel contracts.Channel) message.Subscriber { + if sub, ok := ps.subscribers[channel]; ok { + return sub + } + return ps.subscribers[Go] +} + +func (ps *PubSub) Handle(handlerName string, sub contracts.EventHandler) { + publishToCh, publishToTopic := sub.PublishTo() + + ps.Router.AddHandler( + handlerName, + sub.Topic(), + ps.getSubscriber(sub.Channel()), + publishToTopic, + ps.getPublisher(publishToCh), + sub.Handler, + ) +} diff --git a/backend/providers/event/logrus_adapter.go b/backend/providers/event/logrus_adapter.go new file mode 100644 index 0000000..b4cdd41 --- /dev/null +++ b/backend/providers/event/logrus_adapter.go @@ -0,0 +1,60 @@ +package event + +import ( + "github.com/ThreeDotsLabs/watermill" + "github.com/sirupsen/logrus" +) + +// LogrusLoggerAdapter is a watermill logger adapter for logrus. +type LogrusLoggerAdapter struct { + log *logrus.Logger + fields watermill.LogFields +} + +// NewLogrusLogger returns a LogrusLoggerAdapter that sends all logs to +// the passed logrus instance. +func LogrusAdapter() watermill.LoggerAdapter { + return &LogrusLoggerAdapter{log: logrus.StandardLogger()} +} + +// Error logs on level error with err as field and optional fields. +func (l *LogrusLoggerAdapter) Error(msg string, err error, fields watermill.LogFields) { + l.createEntry(fields.Add(watermill.LogFields{"err": err})).Error(msg) +} + +// Info logs on level info with optional fields. +func (l *LogrusLoggerAdapter) Info(msg string, fields watermill.LogFields) { + l.createEntry(fields).Info(msg) +} + +// Debug logs on level debug with optional fields. +func (l *LogrusLoggerAdapter) Debug(msg string, fields watermill.LogFields) { + l.createEntry(fields).Debug(msg) +} + +// Trace logs on level trace with optional fields. +func (l *LogrusLoggerAdapter) Trace(msg string, fields watermill.LogFields) { + l.createEntry(fields).Trace(msg) +} + +// With returns a new LogrusLoggerAdapter that includes fields +// to be re-used between logging statements. +func (l *LogrusLoggerAdapter) With(fields watermill.LogFields) watermill.LoggerAdapter { + return &LogrusLoggerAdapter{ + log: l.log, + fields: l.fields.Add(fields), + } +} + +// createEntry is a helper to add fields to a logrus entry if necessary. +func (l *LogrusLoggerAdapter) createEntry(fields watermill.LogFields) *logrus.Entry { + entry := logrus.NewEntry(l.log) + + allFields := fields.Add(l.fields) + + if len(allFields) > 0 { + entry = entry.WithFields(logrus.Fields(allFields)) + } + + return entry +} diff --git a/backend/providers/event/provider.go b/backend/providers/event/provider.go new file mode 100644 index 0000000..84cd980 --- /dev/null +++ b/backend/providers/event/provider.go @@ -0,0 +1,109 @@ +package event + +import ( + sqlDB "database/sql" + + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" + + "github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka" + "github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream" + "github.com/ThreeDotsLabs/watermill-sql/v3/pkg/sql" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" + "github.com/redis/go-redis/v9" +) + +func ProvideChannel(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*PubSub, error) { + logger := LogrusAdapter() + + publishers := make(map[contracts.Channel]message.Publisher) + subscribers := make(map[contracts.Channel]message.Subscriber) + + // gochannel + client := gochannel.NewGoChannel(gochannel.Config{}, logger) + publishers[Go] = client + subscribers[Go] = client + + // kafka + if config.Kafka != nil { + kafkaPublisher, err := kafka.NewPublisher(kafka.PublisherConfig{ + Brokers: config.Kafka.Brokers, + Marshaler: kafka.DefaultMarshaler{}, + }, logger) + if err != nil { + return nil, err + } + publishers[Kafka] = kafkaPublisher + + kafkaSubscriber, err := kafka.NewSubscriber(kafka.SubscriberConfig{ + Brokers: config.Kafka.Brokers, + Unmarshaler: kafka.DefaultMarshaler{}, + ConsumerGroup: config.Kafka.ConsumerGroup, + }, logger) + if err != nil { + return nil, err + } + subscribers[Kafka] = kafkaSubscriber + } + + // redis + if config.Redis != nil { + var rdb redis.UniversalClient + redisSubscriber, err := redisstream.NewSubscriber(redisstream.SubscriberConfig{ + Client: rdb, + Unmarshaller: redisstream.DefaultMarshallerUnmarshaller{}, + ConsumerGroup: config.Redis.ConsumerGroup, + }, logger) + if err != nil { + return nil, err + } + subscribers[Redis] = redisSubscriber + + redisPublisher, err := redisstream.NewPublisher(redisstream.PublisherConfig{ + Client: rdb, + Marshaller: redisstream.DefaultMarshallerUnmarshaller{}, + }, logger) + if err != nil { + return nil, err + } + publishers[Redis] = redisPublisher + } + + if config.Sql == nil { + var db *sqlDB.DB + sqlPublisher, err := sql.NewPublisher(db, sql.PublisherConfig{ + SchemaAdapter: sql.DefaultPostgreSQLSchema{}, + AutoInitializeSchema: false, + }, logger) + if err != nil { + return nil, err + } + publishers[Sql] = sqlPublisher + + sqlSubscriber, err := sql.NewSubscriber(db, sql.SubscriberConfig{ + SchemaAdapter: sql.DefaultPostgreSQLSchema{}, + ConsumerGroup: config.Sql.ConsumerGroup, + }, logger) + if err != nil { + return nil, err + } + subscribers[Sql] = sqlSubscriber + } + + router, err := message.NewRouter(message.RouterConfig{}, logger) + if err != nil { + return nil, err + } + + return &PubSub{Router: router, publishers: publishers, subscribers: subscribers}, nil + }, o.DiOptions()...) +} diff --git a/backend/providers/grpc/config.go b/backend/providers/grpc/config.go new file mode 100644 index 0000000..b5dcbe2 --- /dev/null +++ b/backend/providers/grpc/config.go @@ -0,0 +1,145 @@ +package grpc + +import ( + "fmt" + "net" + "time" + + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const DefaultPrefix = "Grpc" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Host *string + Port uint + // EnableReflection enables grpc/reflection registration when true + EnableReflection *bool + // EnableHealth enables gRPC health service registration when true + EnableHealth *bool + // ShutdownTimeoutSeconds controls graceful stop timeout; 0 uses default + ShutdownTimeoutSeconds uint +} + +func (h *Config) Address() string { + if h.Port == 0 { + h.Port = 8081 + } + + if h.Host == nil { + return fmt.Sprintf(":%d", h.Port) + } + return fmt.Sprintf("%s:%d", *h.Host, h.Port) +} + +type Grpc struct { + Server *grpc.Server + config *Config + + options []grpc.ServerOption + unaryInterceptors []grpc.UnaryServerInterceptor + streamInterceptors []grpc.StreamServerInterceptor +} + +func (g *Grpc) Init() error { + // merge options and build interceptor chains if provided + var srvOpts []grpc.ServerOption + if len(g.unaryInterceptors) > 0 { + srvOpts = append(srvOpts, grpc.ChainUnaryInterceptor(g.unaryInterceptors...)) + } + if len(g.streamInterceptors) > 0 { + srvOpts = append(srvOpts, grpc.ChainStreamInterceptor(g.streamInterceptors...)) + } + srvOpts = append(srvOpts, g.options...) + + g.Server = grpc.NewServer(srvOpts...) + + // optional reflection and health + if g.config.EnableReflection != nil && *g.config.EnableReflection { + reflection.Register(g.Server) + } + if g.config.EnableHealth != nil && *g.config.EnableHealth { + hs := health.NewServer() + grpc_health_v1.RegisterHealthServer(g.Server, hs) + } + + // graceful stop with timeout fallback to Stop() + container.AddCloseAble(func() { + timeout := g.config.ShutdownTimeoutSeconds + if timeout == 0 { + timeout = 10 + } + done := make(chan struct{}) + go func() { + g.Server.GracefulStop() + close(done) + }() + select { + case <-done: + // graceful stop finished + case <-time.After(time.Duration(timeout) * time.Second): + // timeout, force stop + g.Server.Stop() + } + }) + + return nil +} + +// Serve +func (g *Grpc) Serve() error { + if g.Server == nil { + if err := g.Init(); err != nil { + return err + } + } + + l, err := net.Listen("tcp", g.config.Address()) + if err != nil { + return err + } + + return g.Server.Serve(l) +} + +func (g *Grpc) ServeWithListener(ln net.Listener) error { + return g.Server.Serve(ln) +} + +// UseOptions appends gRPC ServerOptions to be applied when constructing the server. +func (g *Grpc) UseOptions(opts ...grpc.ServerOption) { + g.options = append(g.options, opts...) +} + +// UseUnaryInterceptors appends unary interceptors to be chained. +func (g *Grpc) UseUnaryInterceptors(inters ...grpc.UnaryServerInterceptor) { + g.unaryInterceptors = append(g.unaryInterceptors, inters...) +} + +// UseStreamInterceptors appends stream interceptors to be chained. +func (g *Grpc) UseStreamInterceptors(inters ...grpc.StreamServerInterceptor) { + g.streamInterceptors = append(g.streamInterceptors, inters...) +} + +// Reset clears all configured options and interceptors. +// Useful in tests to ensure isolation. +func (g *Grpc) Reset() { + g.options = nil + g.unaryInterceptors = nil + g.streamInterceptors = nil +} diff --git a/backend/providers/grpc/options.md b/backend/providers/grpc/options.md new file mode 100644 index 0000000..c721bbd --- /dev/null +++ b/backend/providers/grpc/options.md @@ -0,0 +1,513 @@ +# gRPC Server Options & Interceptors Examples + +本文件给出一些可直接拷贝使用的示例,配合本包提供的注册函数: + +- `UseOptions(opts ...grpc.ServerOption)` +- `UseUnaryInterceptors(inters ...grpc.UnaryServerInterceptor)` +- `UseStreamInterceptors(inters ...grpc.StreamServerInterceptor)` + +建议在应用启动或 Provider 初始化阶段调用(在 gRPC 服务构造前)。 + +> 导入建议: +> +> ```go +> import ( +> pgrpc "test/providers/grpc" // 本包 +> grpc "google.golang.org/grpc" // 避免命名冲突 +> ) +> ``` + +## ServerOption 示例 + +最大消息大小限制: + +```go +pgrpc.UseOptions( +grpc.MaxRecvMsgSize(32<<20), // 32 MiB +grpc.MaxSendMsgSize(32<<20), // 32 MiB +) +``` + +限制最大并发流(对 HTTP/2 流并发施加上限): + +```go +pgrpc.UseOptions( +grpc.MaxConcurrentStreams(1024), +) +``` + +Keepalive 参数(需要 keepalive 包): + +```go +import ( +"time" +"google.golang.org/grpc/keepalive" +) + +pgrpc.UseOptions( +grpc.KeepaliveParams(keepalive.ServerParameters{ +MaxConnectionIdle: 5 * time.Minute, +MaxConnectionAge: 30 * time.Minute, +MaxConnectionAgeGrace: 5 * time.Minute, +Time: 2 * time.Minute, // ping 间隔 +Timeout: 20 * time.Second, +}), +grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ +MinTime: 1 * time.Minute, // 客户端 ping 最小间隔 +PermitWithoutStream: true, +}), +) +``` + +## UnaryServerInterceptor 示例 + +简单日志拦截器(logrus): + +```go +import ( +"context" +"time" +log "github.com/sirupsen/logrus" +"google.golang.org/grpc" +) + +func LoggingUnaryInterceptor( +ctx context.Context, +req any, +info *grpc.UnaryServerInfo, +handler grpc.UnaryHandler, +) (any, error) { +start := time.Now() +resp, err := handler(ctx, req) +dur := time.Since(start) +entry := log.WithFields(log.Fields{ +"grpc.method": info.FullMethod, +"grpc.duration_ms": dur.Milliseconds(), +}) +if err != nil { +entry.WithError(err).Warn("grpc unary request failed") +} else { +entry.Info("grpc unary request finished") +} +return resp, err +} + +// 注册 +pgrpc.UseUnaryInterceptors(LoggingUnaryInterceptor) +``` + +恢复拦截器(panic 捕获): + +```go +import ( +"context" +"fmt" +"runtime/debug" +log "github.com/sirupsen/logrus" +"google.golang.org/grpc" +"google.golang.org/grpc/status" +"google.golang.org/grpc/codes" +) + +func RecoveryUnaryInterceptor( +ctx context.Context, +req any, +info *grpc.UnaryServerInfo, +handler grpc.UnaryHandler, +) (any, error) { +defer func() { +if r := recover() ; r != nil { +log.WithField("grpc.method", info.FullMethod).Errorf("panic: %v\n%s", r, debug.Stack()) +} +}() +return handler(ctx, req) +} + +// 或者向客户端返回内部错误: +func RecoveryUnaryInterceptorWithError( +ctx context.Context, +req any, +info *grpc.UnaryServerInfo, +handler grpc.UnaryHandler, +) (any, error) { +defer func() { +if r := recover() ; r != nil { +log.WithField("grpc.method", info.FullMethod).Errorf("panic: %v\n%s", r, debug.Stack()) +} +}() +resp, err := handler(ctx, req) +if rec := recover() ; rec != nil { +return nil, status.Error(codes.Internal, fmt.Sprint(rec)) +} +return resp, err +} + +pgrpc.UseUnaryInterceptors(RecoveryUnaryInterceptor) +``` + +链式调用(与其它拦截器共同使用): + +```go +pgrpc.UseUnaryInterceptors(LoggingUnaryInterceptor, RecoveryUnaryInterceptor) +``` + +## StreamServerInterceptor 示例 + +简单日志拦截器: + +```go +import ( +"time" +log "github.com/sirupsen/logrus" +"google.golang.org/grpc" +) + +func LoggingStreamInterceptor( +srv any, +ss grpc.ServerStream, +info *grpc.StreamServerInfo, +handler grpc.StreamHandler, +) error { +start := time.Now() +err := handler(srv, ss) +dur := time.Since(start) +entry := log.WithFields(log.Fields{ +"grpc.method": info.FullMethod, +"grpc.is_client_stream": info.IsClientStream, +"grpc.is_server_stream": info.IsServerStream, +"grpc.duration_ms": dur.Milliseconds(), +}) +if err != nil { +entry.WithError(err).Warn("grpc stream request failed") +} else { +entry.Info("grpc stream request finished") +} +return err +} + +pgrpc.UseStreamInterceptors(LoggingStreamInterceptor) +``` + +恢复拦截器(panic 捕获): + +```go +import ( +"runtime/debug" +log "github.com/sirupsen/logrus" +"google.golang.org/grpc" +) + +func RecoveryStreamInterceptor( +srv any, +ss grpc.ServerStream, +info *grpc.StreamServerInfo, +handler grpc.StreamHandler, +) (err error) { +defer func() { +if r := recover() ; r != nil { +log.WithField("grpc.method", info.FullMethod).Errorf("panic: %v\n%s", r, debug.Stack()) +} +}() +return handler(srv, ss) +} + +pgrpc.UseStreamInterceptors(RecoveryStreamInterceptor) +``` + +## 组合与测试小贴士 + +- 可以多次调用 `UseOptions/UseUnaryInterceptors/UseStreamInterceptors`,最终会在服务构造时链式生效。 +- 单元测试中如需隔离,建议使用 `pgrpc.Reset()` 清理已注册的选项和拦截器。 +- 若要启用健康检查或反射,请在配置中设置: +- `EnableHealth = true` +- `EnableReflection = true` + +## 更多 ServerOption 示例 + +TLS(服务端或 mTLS): + +```go +import ( +"crypto/tls" +grpcCredentials "google.golang.org/grpc/credentials" +) + +// 使用自定义 tls.Config(可配置 mTLS) +var tlsConfig *tls.Config = &tls.Config{ /* ... */ } +pgrpc.UseOptions( +grpc.Creds(grpcCredentials.NewTLS(tlsConfig)), +) + +// 或者从证书文件加载(仅服务端 TLS) +// pgrpc.UseOptions(grpc.Creds(grpcCredentials.NewServerTLSFromFile(certFile, keyFile))) +``` + +OpenTelemetry 统计/追踪(StatsHandler): + +```go +import ( +otelgrpc "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" +) + +pgrpc.UseOptions( +grpc.StatsHandler(otelgrpc.NewServerHandler()), +) +``` + +流控/缓冲区调优: + +```go +pgrpc.UseOptions( +grpc.InitialWindowSize(1<<20), // 每个流初始窗口(字节) +grpc.InitialConnWindowSize(1<<21), // 连接级窗口 +grpc.ReadBufferSize(64<<10), // 读缓冲 64 KiB +grpc.WriteBufferSize(64<<10), // 写缓冲 64 KiB +) +``` + +连接超时与 Tap Handle(早期拦截): + +```go +import ( +"context" +"time" +"google.golang.org/grpc/tap" +) + +pgrpc.UseOptions( +grpc.ConnectionTimeout(5 * time.Second), +grpc.InTapHandle(func(ctx context.Context, info *tap.Info) (context.Context, error) { +// 在真正的 RPC 处理前进行快速拒绝(如黑名单、IP 检查等) +return ctx, nil +}), +) +``` + +未知服务处理与工作池: + +```go +pgrpc.UseOptions( +grpc.UnknownServiceHandler(func(srv any, stream grpc.ServerStream) error { +// 统一记录未注册方法,或返回自定义错误 +return status.Error(codes.Unimplemented, "unknown service/method") +}), +grpc.NumStreamWorkers(8), // 针对 CPU 密集流处理的工作池 +) +``` + +## 更多 Unary 拦截器示例 + +基于 Metadata 的鉴权: + +```go +import ( +"context" +"strings" +"google.golang.org/grpc/metadata" +"google.golang.org/grpc/status" +"google.golang.org/grpc/codes" +) + +func AuthUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { +md, _ := metadata.FromIncomingContext(ctx) +token := "" +if vals := md.Get("authorization"); len(vals) > 0 { +token = vals[0] +} +if token == "" || !strings.HasPrefix(strings.ToLower(token), "bearer ") { +return nil, status.Error(codes.Unauthenticated, "missing or invalid token") +} +// TODO: 验证 JWT / API-Key +return handler(ctx, req) +} + +pgrpc.UseUnaryInterceptors(AuthUnaryInterceptor) +``` + +方法粒度速率限制(x/time/rate): + +```go +import ( +"context" +"sync" +"golang.org/x/time/rate" +"google.golang.org/grpc/status" +"google.golang.org/grpc/codes" +) + +var ( +rlmu sync.RWMutex +rlm = map[string]*rate.Limiter{} +) + +func limitFor(method string) *rate.Limiter { +rlmu.RLock() ; l := rlm[method]; rlmu.RUnlock() +if l != nil { return l } +rlmu.Lock() ; defer rlmu.Unlock() +if rlm[method] == nil { rlm[method] = rate.NewLimiter(100, 200) } // 100 rps, burst 200 +return rlm[method] +} + +func RateLimitUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { +l := limitFor(info.FullMethod) +if !l.Allow() { +return nil, status.Error(codes.ResourceExhausted, "rate limited") +} +return handler(ctx, req) +} + +pgrpc.UseUnaryInterceptors(RateLimitUnaryInterceptor) +``` + +Request-ID 注入与日志关联: + +```go +import ( +"context" +"github.com/google/uuid" +"google.golang.org/grpc/metadata" +) + +type ctxKey string +const requestIDKey ctxKey = "request_id" + +func RequestIDUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { +md, _ := metadata.FromIncomingContext(ctx) +var rid string +if v := md.Get("x-request-id"); len(v) > 0 { rid = v[0] } +if rid == "" { rid = uuid.New().String() } +ctx = context.WithValue(ctx, requestIDKey, rid) +return handler(ctx, req) +} + +pgrpc.UseUnaryInterceptors(RequestIDUnaryInterceptor) +``` + +无超时/超长请求治理(默认超时/拒绝超长): + +```go +import ( +"context" +"time" +"google.golang.org/grpc/status" +"google.golang.org/grpc/codes" +) + +func DeadlineUnaryInterceptor(max time.Duration) grpc.UnaryServerInterceptor { +return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { +if _, ok := ctx.Deadline() ; !ok { // 未设置超时 +var cancel context.CancelFunc +ctx, cancel = context.WithTimeout(ctx, max) +defer cancel() +} +resp, err := handler(ctx, req) +if err != nil && ctx.Err() == context.DeadlineExceeded { +return nil, status.Error(codes.DeadlineExceeded, "deadline exceeded") +} +return resp, err +} +} + +pgrpc.UseUnaryInterceptors(DeadlineUnaryInterceptor(5*time.Second)) +``` + +## 更多 Stream 拦截器示例 + +基于 Metadata 的鉴权(流): + +```go +import ( +"google.golang.org/grpc/metadata" +"google.golang.org/grpc/status" +"google.golang.org/grpc/codes" +) + +func AuthStreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { +md, _ := metadata.FromIncomingContext(ss.Context()) +if len(md.Get("authorization")) == 0 { +return status.Error(codes.Unauthenticated, "missing token") +} +return handler(srv, ss) +} + +pgrpc.UseStreamInterceptors(AuthStreamInterceptor) +``` + +流级限流(示例:简单 Allow 检查): + +```go +func RateLimitStreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { +l := limitFor(info.FullMethod) +if !l.Allow() { +return status.Error(codes.ResourceExhausted, "rate limited") +} +return handler(srv, ss) +} + +pgrpc.UseStreamInterceptors(RateLimitStreamInterceptor) +``` + +## 压缩与编码 + +注册 gzip 压缩器后,客户端可按需协商使用(新版本通过 encoding 注册): + +```go +import ( +_ "google.golang.org/grpc/encoding/gzip" // 注册 gzip 编解码器 +) + +// 仅需 import 即可,无额外 ServerOption +``` + +## OpenTelemetry 集成(推荐) + +使用 StatsHandler(推荐,不与拦截器同时使用,避免重复埋点): + +```go +import ( +otelgrpc "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" +) + +// 基本接入:使用全局 Tracer/Meter(由 OTEL Provider 初始化) +handler := otelgrpc.NewServerHandler( +otelgrpc.WithTraceEvents(), // 在 span 中记录消息事件 +) +pgrpc.UseOptions(grpc.StatsHandler(handler)) + +// 忽略某些方法(如健康检查),避免噪声: +handler = otelgrpc.NewServerHandler( +otelgrpc.WithFilter(func(ctx context.Context, fullMethod string) bool { +return fullMethod != "/grpc.health.v1.Health/Check" +}), +) +pgrpc.UseOptions(grpc.StatsHandler(handler)) +``` + +使用拦截器版本(如你更偏好 Interceptor 方案;与 StatsHandler 二选一): + +```go +import ( +otelgrpc "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" +) + +pgrpc.UseUnaryInterceptors(otelgrpc.UnaryServerInterceptor()) +pgrpc.UseStreamInterceptors(otelgrpc.StreamServerInterceptor()) +``` + +> 注意:不要同时启用 StatsHandler 和拦截器,否则会重复生成 span/metrics。 + +## OpenTracing(Jaeger)集成 + +当使用 Tracing Provider(Jaeger + OpenTracing)时,可使用 opentracing 的 gRPC 拦截器: + +```go +import ( +opentracing "github.com/opentracing/opentracing-go" +otgrpc "github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc" +) + +pgrpc.UseUnaryInterceptors(otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer())) +pgrpc.UseStreamInterceptors(otgrpc.OpenTracingStreamServerInterceptor(opentracing.GlobalTracer())) +``` + +> 与 OTEL 方案互斥:如果已启用 OTEL,请不要再开启 OpenTracing 拦截器,以免重复埋点。 diff --git a/backend/providers/grpc/provider.go b/backend/providers/grpc/provider.go new file mode 100644 index 0000000..b76ff40 --- /dev/null +++ b/backend/providers/grpc/provider.go @@ -0,0 +1,18 @@ +package grpc + +import ( + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*Grpc, error) { + return &Grpc{config: &config}, nil + }, o.DiOptions()...) +} diff --git a/backend/providers/http/config.go b/backend/providers/http/config.go new file mode 100644 index 0000000..5a7b9a9 --- /dev/null +++ b/backend/providers/http/config.go @@ -0,0 +1,43 @@ +package http + +import ( + "fmt" +) + +const DefaultPrefix = "Http" + +type Config struct { + Host string + Port uint + + StaticPath *string + StaticRoute *string + BaseURI *string + Tls *Tls + Cors *Cors +} + +type Tls struct { + Cert string + Key string +} + +type Cors struct { + Mode string + Whitelist []Whitelist +} + +type Whitelist struct { + AllowOrigin string + AllowHeaders string + AllowMethods string + ExposeHeaders string + AllowCredentials bool +} + +func (h *Config) Address() string { + if h.Host == "" { + return fmt.Sprintf("0.0.0.0:%d", h.Port) + } + return fmt.Sprintf("%s:%d", h.Host, h.Port) +} diff --git a/backend/providers/http/engine.go b/backend/providers/http/engine.go new file mode 100644 index 0000000..c0d0ee5 --- /dev/null +++ b/backend/providers/http/engine.go @@ -0,0 +1,205 @@ +package http + +import ( + "context" + "errors" + "fmt" + "net" + "runtime/debug" + "time" + + log "github.com/sirupsen/logrus" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/compress" + "github.com/gofiber/fiber/v3/middleware/cors" + "github.com/gofiber/fiber/v3/middleware/helmet" + "github.com/gofiber/fiber/v3/middleware/limiter" + "github.com/gofiber/fiber/v3/middleware/logger" + "github.com/gofiber/fiber/v3/middleware/recover" + "github.com/gofiber/fiber/v3/middleware/requestid" + "github.com/samber/lo" +) + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Service struct { + conf *Config + Engine *fiber.App +} + +func (svc *Service) listenerConfig() fiber.ListenConfig { + listenConfig := fiber.ListenConfig{ + EnablePrintRoutes: true, + // DisableStartupMessage: true, + } + + if svc.conf.Tls != nil { + if svc.conf.Tls.Cert == "" || svc.conf.Tls.Key == "" { + panic(errors.New("tls cert and key must be set")) + } + listenConfig.CertFile = svc.conf.Tls.Cert + listenConfig.CertKeyFile = svc.conf.Tls.Key + } + container.AddCloseAble(func() { + svc.Engine.ShutdownWithTimeout(time.Second * 10) + }) + return listenConfig +} + +func (svc *Service) Listener(ln net.Listener) error { + return svc.Engine.Listener(ln, svc.listenerConfig()) +} + +func (svc *Service) Serve(ctx context.Context) error { + // log.WithField("http_address", svc.conf.Address()).Info("http config address") + + ln, err := net.Listen("tcp4", svc.conf.Address()) + if err != nil { + return err + } + + // Run the server in a goroutine so we can listen for context cancellation + serverErr := make(chan error, 1) + go func() { + serverErr <- svc.Engine.Listener(ln, svc.listenerConfig()) + }() + + select { + case <-ctx.Done(): + // Shutdown the server gracefully + if shutdownErr := svc.Engine.Shutdown(); shutdownErr != nil { + return shutdownErr + } + // treat context cancellation as graceful shutdown + return nil + case err := <-serverErr: + return err + } +} + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*Service, error) { + engine := fiber.New(fiber.Config{ + StrictRouting: true, + CaseSensitive: true, + BodyLimit: 10 * 1024 * 1024, // 10 MiB + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + ProxyHeader: fiber.HeaderXForwardedFor, + EnableIPValidation: true, + }) + + // request id first for correlation + engine.Use(requestid.New()) + + // recover with stack + request id + engine.Use(recover.New(recover.Config{ + EnableStackTrace: true, + StackTraceHandler: func(c fiber.Ctx, e any) { + rid := c.Get(fiber.HeaderXRequestID) + log.WithField("request_id", rid).Error(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack())) + }, + })) + + // basic security + compression + engine.Use(helmet.New()) + engine.Use(compress.New(compress.Config{Level: compress.LevelDefault})) + + // optional CORS based on config + if config.Cors != nil { + corsCfg := buildCORSConfig(config.Cors) + if corsCfg != nil { + engine.Use(cors.New(*corsCfg)) + } + } + + // logging with request id and latency + engine.Use(logger.New(logger.Config{ + // requestid middleware stores ctx.Locals("requestid") + Format: `${time} [${ip}] ${method} ${status} ${path} ${latency} rid=${locals:requestid} "${ua}"\n`, + TimeFormat: time.RFC3339, + TimeZone: "Asia/Shanghai", + })) + + // rate limit (enable standard headers; adjust Max via config if needed) + engine.Use(limiter.New(limiter.Config{Max: 0})) + + // static files (Fiber v3 Static helper moved; enable via filesystem middleware later) + // if config.StaticRoute != nil && config.StaticPath != nil { ... } + + // health endpoints + engine.Get("/healthz", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) }) + engine.Get("/readyz", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) }) + + engine.Hooks().OnPostShutdown(func(err error) error { + if err != nil { + log.Error("http server shutdown error: ", err) + } + log.Info("http server has shutdown success") + return nil + }) + + return &Service{ + Engine: engine, + conf: &config, + }, nil + }, o.DiOptions()...) +} + +// buildCORSConfig converts provider Cors config into fiber cors.Config +func buildCORSConfig(c *Cors) *cors.Config { + if c == nil { + return nil + } + if c.Mode == "disabled" { + return nil + } + var ( + origins []string + headers []string + methods []string + exposes []string + allowCreds bool + ) + for _, w := range c.Whitelist { + if w.AllowOrigin != "" { + origins = append(origins, w.AllowOrigin) + } + if w.AllowHeaders != "" { + headers = append(headers, w.AllowHeaders) + } + if w.AllowMethods != "" { + methods = append(methods, w.AllowMethods) + } + if w.ExposeHeaders != "" { + exposes = append(exposes, w.ExposeHeaders) + } + allowCreds = allowCreds || w.AllowCredentials + } + + cfg := cors.Config{ + AllowOrigins: lo.Uniq(origins), + AllowHeaders: lo.Uniq(headers), + AllowMethods: lo.Uniq(methods), + ExposeHeaders: lo.Uniq(exposes), + AllowCredentials: allowCreds, + } + return &cfg +} diff --git a/backend/providers/http/swagger/config.go b/backend/providers/http/swagger/config.go new file mode 100644 index 0000000..4b535a7 --- /dev/null +++ b/backend/providers/http/swagger/config.go @@ -0,0 +1,317 @@ +package swagger + +import ( + "html/template" +) + +// Config stores SwaggerUI configuration variables +type Config struct { + // This parameter can be used to name different swagger document instances. + // default: "" + InstanceName string `json:"-"` + + // Title pointing to title of HTML page. + // default: "Swagger UI" + Title string `json:"-"` + + // URL to fetch external configuration document from. + // default: "" + ConfigURL string `json:"configUrl,omitempty"` + + // The URL pointing to API definition (normally swagger.json or swagger.yaml). + // default: "doc.json" + URL string `json:"url,omitempty"` + + // Enables overriding configuration parameters via URL search params. + // default: false + QueryConfigEnabled bool `json:"queryConfigEnabled,omitempty"` + + // The name of a component available via the plugin system to use as the top-level layout for Swagger UI. + // default: "StandaloneLayout" + Layout string `json:"layout,omitempty"` + + // An array of plugin functions to use in Swagger UI. + // default: [SwaggerUIBundle.plugins.DownloadUrl] + Plugins []template.JS `json:"-"` + + // An array of presets to use in Swagger UI. Usually, you'll want to include ApisPreset if you use this option. + // default: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset] + Presets []template.JS `json:"-"` + + // If set to true, enables deep linking for tags and operations. + // default: true + DeepLinking bool `json:"deepLinking"` + + // Controls the display of operationId in operations list. + // default: false + DisplayOperationId bool `json:"displayOperationId,omitempty"` + + // The default expansion depth for models (set to -1 completely hide the models). + // default: 1 + DefaultModelsExpandDepth int `json:"defaultModelsExpandDepth,omitempty"` + + // The default expansion depth for the model on the model-example section. + // default: 1 + DefaultModelExpandDepth int `json:"defaultModelExpandDepth,omitempty"` + + // Controls how the model is shown when the API is first rendered. + // The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links. + // default: "example" + DefaultModelRendering string `json:"defaultModelRendering,omitempty"` + + // Controls the display of the request duration (in milliseconds) for "Try it out" requests. + // default: false + DisplayRequestDuration bool `json:"displayRequestDuration,omitempty"` + + // Controls the default expansion setting for the operations and tags. + // 'list' (default, expands only the tags), + // 'full' (expands the tags and operations), + // 'none' (expands nothing) + DocExpansion string `json:"docExpansion,omitempty"` + + // If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. + // Can be Boolean to enable or disable, or a string, in which case filtering will be enabled using that string as the filter expression. + // Filtering is case sensitive matching the filter expression anywhere inside the tag. + // default: false + Filter FilterConfig `json:"-"` + + // If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations. + // default: 0 + MaxDisplayedTags int `json:"maxDisplayedTags,omitempty"` + + // Controls the display of vendor extension (x-) fields and values for Operations, Parameters, Responses, and Schema. + // default: false + ShowExtensions bool `json:"showExtensions,omitempty"` + + // Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters. + // default: false + ShowCommonExtensions bool `json:"showCommonExtensions,omitempty"` + + // Apply a sort to the tag list of each API. It can be 'alpha' (sort by paths alphanumerically) or a function (see Array.prototype.sort(). + // to learn how to write a sort function). Two tag name strings are passed to the sorter for each pass. + // default: "" -> Default is the order determined by Swagger UI. + TagsSorter template.JS `json:"-"` + + // Provides a mechanism to be notified when Swagger UI has finished rendering a newly provided definition. + // default: "" -> Function=NOOP + OnComplete template.JS `json:"-"` + + // An object with the activate and theme properties. + SyntaxHighlight *SyntaxHighlightConfig `json:"-"` + + // Controls whether the "Try it out" section should be enabled by default. + // default: false + TryItOutEnabled bool `json:"tryItOutEnabled,omitempty"` + + // Enables the request snippet section. When disabled, the legacy curl snippet will be used. + // default: false + RequestSnippetsEnabled bool `json:"requestSnippetsEnabled,omitempty"` + + // OAuth redirect URL. + // default: "" + OAuth2RedirectUrl string `json:"oauth2RedirectUrl,omitempty"` + + // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + // Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. + // default: "" + RequestInterceptor template.JS `json:"-"` + + // If set, MUST be an array of command line options available to the curl command. This can be set on the mutated request in the requestInterceptor function. + // For example request.curlOptions = ["-g", "--limit-rate 20k"] + // default: nil + RequestCurlOptions []string `json:"request.curlOptions,omitempty"` + + // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + // Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. + // default: "" + ResponseInterceptor template.JS `json:"-"` + + // If set to true, uses the mutated request returned from a requestInterceptor to produce the curl command in the UI, + // otherwise the request before the requestInterceptor was applied is used. + // default: true + ShowMutatedRequest bool `json:"showMutatedRequest"` + + // List of HTTP methods that have the "Try it out" feature enabled. An empty array disables "Try it out" for all operations. + // This does not filter the operations from the display. + // Possible values are ["get", "put", "post", "delete", "options", "head", "patch", "trace"] + // default: nil + SupportedSubmitMethods []string `json:"supportedSubmitMethods,omitempty"` + + // By default, Swagger UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL. + // For example for locally deployed validators (https://github.com/swagger-api/validator-badge). + // Setting it to either none, 127.0.0.1 or localhost will disable validation. + // default: "" + ValidatorUrl string `json:"validatorUrl,omitempty"` + + // If set to true, enables passing credentials, as defined in the Fetch standard, in CORS requests that are sent by the browser. + // Note that Swagger UI cannot currently set cookies cross-domain (see https://github.com/swagger-api/swagger-js/issues/1163). + // as a result, you will have to rely on browser-supplied cookies (which this setting enables sending) that Swagger UI cannot control. + // default: false + WithCredentials bool `json:"withCredentials,omitempty"` + + // Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable. + // default: "" + ModelPropertyMacro template.JS `json:"-"` + + // Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). + // Operation and parameter are objects passed for context, both remain immutable. + // default: "" + ParameterMacro template.JS `json:"-"` + + // If set to true, it persists authorization data and it would not be lost on browser close/refresh. + // default: false + PersistAuthorization bool `json:"persistAuthorization,omitempty"` + + // Configuration information for OAuth2, optional if using OAuth2 + OAuth *OAuthConfig `json:"-"` + + // (authDefinitionKey, username, password) => action + // Programmatically set values for a Basic authorization scheme. + // default: "" + PreauthorizeBasic template.JS `json:"-"` + + // (authDefinitionKey, apiKeyValue) => action + // Programmatically set values for an API key or Bearer authorization scheme. + // In case of OpenAPI 3.0 Bearer scheme, apiKeyValue must contain just the token itself without the Bearer prefix. + // default: "" + PreauthorizeApiKey template.JS `json:"-"` + + // Applies custom CSS styles. + // default: "" + CustomStyle template.CSS `json:"-"` + + // Applies custom JavaScript scripts. + // default "" + CustomScript template.JS `json:"-"` +} + +type FilterConfig struct { + Enabled bool + Expression string +} + +func (fc FilterConfig) Value() interface{} { + if fc.Expression != "" { + return fc.Expression + } + return fc.Enabled +} + +type SyntaxHighlightConfig struct { + // Whether syntax highlighting should be activated or not. + // default: true + Activate bool `json:"activate"` + // Highlight.js syntax coloring theme to use. + // Possible values are ["agate", "arta", "monokai", "nord", "obsidian", "tomorrow-night"] + // default: "agate" + Theme string `json:"theme,omitempty"` +} + +func (shc SyntaxHighlightConfig) Value() interface{} { + if shc.Activate { + return shc + } + return false +} + +type OAuthConfig struct { + // ID of the client sent to the OAuth2 provider. + // default: "" + ClientId string `json:"clientId,omitempty"` + + // Never use this parameter in your production environment. + // It exposes cruicial security information. This feature is intended for dev/test environments only. + // Secret of the client sent to the OAuth2 provider. + // default: "" + ClientSecret string `json:"clientSecret,omitempty"` + + // Application name, displayed in authorization popup. + // default: "" + AppName string `json:"appName,omitempty"` + + // Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl. + // default: "" + Realm string `json:"realm,omitempty"` + + // String array of initially selected oauth scopes + // default: nil + Scopes []string `json:"scopes,omitempty"` + + // Additional query parameters added to authorizationUrl and tokenUrl. + // default: nil + AdditionalQueryStringParams map[string]string `json:"additionalQueryStringParams,omitempty"` + + // Unavailable Only activated for the accessCode flow. + // During the authorization_code request to the tokenUrl, pass the Client Password using the HTTP Basic Authentication scheme + // (Authorization header with Basic base64encode(client_id + client_secret)). + // default: false + UseBasicAuthenticationWithAccessCodeGrant bool `json:"useBasicAuthenticationWithAccessCodeGrant,omitempty"` + + // Only applies to authorizatonCode flows. + // Proof Key for Code Exchange brings enhanced security for OAuth public clients. + // default: false + UsePkceWithAuthorizationCodeGrant bool `json:"usePkceWithAuthorizationCodeGrant,omitempty"` +} + +var ConfigDefault = Config{ + Title: "Swagger UI", + Layout: "StandaloneLayout", + Plugins: []template.JS{ + template.JS("SwaggerUIBundle.plugins.DownloadUrl"), + }, + Presets: []template.JS{ + template.JS("SwaggerUIBundle.presets.apis"), + template.JS("SwaggerUIStandalonePreset"), + }, + DeepLinking: true, + DefaultModelsExpandDepth: 1, + DefaultModelExpandDepth: 1, + DefaultModelRendering: "example", + DocExpansion: "list", + SyntaxHighlight: &SyntaxHighlightConfig{ + Activate: true, + Theme: "agate", + }, + ShowMutatedRequest: true, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + if cfg.Title == "" { + cfg.Title = ConfigDefault.Title + } + + if cfg.Layout == "" { + cfg.Layout = ConfigDefault.Layout + } + + if cfg.DefaultModelRendering == "" { + cfg.DefaultModelRendering = ConfigDefault.DefaultModelRendering + } + + if cfg.DocExpansion == "" { + cfg.DocExpansion = ConfigDefault.DocExpansion + } + + if cfg.Plugins == nil { + cfg.Plugins = ConfigDefault.Plugins + } + + if cfg.Presets == nil { + cfg.Presets = ConfigDefault.Presets + } + + if cfg.SyntaxHighlight == nil { + cfg.SyntaxHighlight = ConfigDefault.SyntaxHighlight + } + + return cfg +} diff --git a/backend/providers/http/swagger/swagger.go b/backend/providers/http/swagger/swagger.go new file mode 100644 index 0000000..0722e61 --- /dev/null +++ b/backend/providers/http/swagger/swagger.go @@ -0,0 +1,103 @@ +package swagger + +import ( + "fmt" + "html/template" + "path" + "strings" + "sync" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" + "github.com/gofiber/utils/v2" + "github.com/rogeecn/swag" + swaggerFiles "github.com/swaggo/files/v2" +) + +const ( + defaultDocURL = "doc.json" + defaultIndex = "index.html" +) + +var HandlerDefault = New() + +// New returns custom handler +func New(config ...Config) fiber.Handler { + cfg := configDefault(config...) + + index, err := template.New("swagger_index.html").Parse(indexTmpl) + if err != nil { + panic(fmt.Errorf("fiber: swagger middleware error -> %w", err)) + } + + var ( + prefix string + once sync.Once + ) + + return func(c fiber.Ctx) error { + // Set prefix + once.Do( + func() { + prefix = strings.ReplaceAll(c.Route().Path, "*", "") + + forwardedPrefix := getForwardedPrefix(c) + if forwardedPrefix != "" { + prefix = forwardedPrefix + prefix + } + + // Set doc url + if len(cfg.URL) == 0 { + cfg.URL = path.Join(prefix, defaultDocURL) + } + }, + ) + + p := c.Path(utils.CopyString(c.Params("*"))) + + switch p { + case defaultIndex: + c.Type("html") + return index.Execute(c, cfg) + case defaultDocURL: + var doc string + if doc, err = swag.ReadDoc(cfg.InstanceName); err != nil { + return err + } + return c.Type("json").SendString(doc) + case "", "/": + return c.Redirect().To(path.Join(prefix, defaultIndex)) + default: + // return fs(c) + return static.New("/swagger", static.Config{ + FS: swaggerFiles.FS, + Browse: true, + })(c) + } + } +} + +func getForwardedPrefix(c fiber.Ctx) string { + header := c.GetReqHeaders()["X-Forwarded-Prefix"] + + if len(header) == 0 { + return "" + } + + prefix := "" + + for _, rawPrefix := range header { + endIndex := len(rawPrefix) + for endIndex > 1 && rawPrefix[endIndex-1] == '/' { + endIndex-- + } + + if endIndex != len(rawPrefix) { + prefix += rawPrefix[:endIndex] + } else { + prefix += rawPrefix + } + } + + return prefix +} diff --git a/backend/providers/http/swagger/template.go b/backend/providers/http/swagger/template.go new file mode 100644 index 0000000..d90607f --- /dev/null +++ b/backend/providers/http/swagger/template.go @@ -0,0 +1,107 @@ +package swagger + +const indexTmpl string = ` + + + + + + {{.Title}} + + + + + {{- if .CustomStyle}} + + {{- end}} + {{- if .CustomScript}} + + {{- end}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +` diff --git a/backend/providers/job/config.go b/backend/providers/job/config.go new file mode 100644 index 0000000..cf54cc8 --- /dev/null +++ b/backend/providers/job/config.go @@ -0,0 +1,67 @@ +package job + +import ( + "github.com/riverqueue/river" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +const DefaultPrefix = "Job" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + // Optional per-queue worker concurrency. If empty, defaults apply. + QueueWorkers QueueWorkersConfig +} + +// QueueWorkers allows configuring worker concurrency per queue. +// Key is the queue name, value is MaxWorkers. If empty, defaults are used. +// Example TOML: +// +// [Job] +// # high=20, default=10, low=5 +// # QueueWorkers = { high = 20, default = 10, low = 5 } +type QueueWorkersConfig map[string]int + +const ( + PriorityDefault = river.PriorityDefault + PriorityLow = 2 + PriorityMiddle = 3 + PriorityHigh = 3 +) + +const ( + QueueHigh = "high" + QueueDefault = river.QueueDefault + QueueLow = "low" +) + +// queueConfig returns a river.QueueConfig map built from QueueWorkers or defaults. +func (c *Config) queueConfig() map[string]river.QueueConfig { + cfg := map[string]river.QueueConfig{} + if c == nil || len(c.QueueWorkers) == 0 { + cfg[QueueHigh] = river.QueueConfig{MaxWorkers: 10} + cfg[QueueDefault] = river.QueueConfig{MaxWorkers: 10} + cfg[QueueLow] = river.QueueConfig{MaxWorkers: 10} + return cfg + } + for name, n := range c.QueueWorkers { + if n <= 0 { + n = 1 + } + cfg[name] = river.QueueConfig{MaxWorkers: n} + } + + if _, ok := cfg[QueueDefault]; !ok { + cfg[QueueDefault] = river.QueueConfig{MaxWorkers: 10} + } + return cfg +} diff --git a/backend/providers/job/provider.go b/backend/providers/job/provider.go new file mode 100644 index 0000000..1c37353 --- /dev/null +++ b/backend/providers/job/provider.go @@ -0,0 +1,207 @@ +package job + +import ( + "context" + "fmt" + "sync" + "time" + + "quyun/v2/providers/postgres" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/pkg/errors" + "github.com/riverqueue/river" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivertype" + "github.com/samber/lo" + log "github.com/sirupsen/logrus" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func(ctx context.Context, dbConf *postgres.Config) (*Job, error) { + workers := river.NewWorkers() + + dbPoolConfig, err := pgxpool.ParseConfig(dbConf.DSN()) + if err != nil { + return nil, err + } + + dbPool, err := pgxpool.NewWithConfig(ctx, dbPoolConfig) + if err != nil { + return nil, err + } + // health check ping with timeout + pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + if err := dbPool.Ping(pingCtx); err != nil { + return nil, fmt.Errorf("job provider: db ping failed: %w", err) + } + container.AddCloseAble(dbPool.Close) + pool := riverpgxv5.New(dbPool) + + queue := &Job{ + Workers: workers, + driver: pool, + ctx: ctx, + conf: &config, + periodicJobs: make(map[string]rivertype.PeriodicJobHandle), + } + container.AddCloseAble(queue.Close) + + return queue, nil + }, o.DiOptions()...) +} + +type Job struct { + ctx context.Context + conf *Config + Workers *river.Workers + driver *riverpgxv5.Driver + + l sync.Mutex + client *river.Client[pgx.Tx] + + periodicJobs map[string]rivertype.PeriodicJobHandle +} + +func (q *Job) Close() { + if q.client == nil { + return + } + + if err := q.client.StopAndCancel(q.ctx); err != nil { + log.Errorf("Failed to stop and cancel client: %s", err) + } + // clear references + q.l.Lock() + q.periodicJobs = map[string]rivertype.PeriodicJobHandle{} + q.l.Unlock() +} + +func (q *Job) Client() (*river.Client[pgx.Tx], error) { + q.l.Lock() + defer q.l.Unlock() + + if q.client == nil { + var err error + q.client, err = river.NewClient(q.driver, &river.Config{ + Workers: q.Workers, + Queues: q.conf.queueConfig(), + }) + if err != nil { + return nil, err + } + } + + return q.client, nil +} + +func (q *Job) Start(ctx context.Context) error { + client, err := q.Client() + if err != nil { + return errors.Wrap(err, "get client failed") + } + + if err := client.Start(ctx); err != nil { + return err + } + defer client.StopAndCancel(ctx) + + <-ctx.Done() + + return nil +} + +func (q *Job) StopAndCancel(ctx context.Context) error { + client, err := q.Client() + if err != nil { + return errors.Wrap(err, "get client failed") + } + + return client.StopAndCancel(ctx) +} + +func (q *Job) AddPeriodicJobs(job contracts.CronJob) error { + for _, job := range job.Args() { + if err := q.AddPeriodicJob(job); err != nil { + return err + } + } + return nil +} + +func (q *Job) AddPeriodicJob(job contracts.CronJobArg) error { + client, err := q.Client() + if err != nil { + return err + } + q.l.Lock() + defer q.l.Unlock() + + q.periodicJobs[job.Arg.UniqueID()] = client.PeriodicJobs().Add(river.NewPeriodicJob( + job.PeriodicInterval, + func() (river.JobArgs, *river.InsertOpts) { + return job.Arg, lo.ToPtr(job.Arg.InsertOpts()) + }, + &river.PeriodicJobOpts{ + RunOnStart: job.RunOnStart, + }, + )) + + return nil +} + +func (q *Job) Cancel(id string) error { + client, err := q.Client() + if err != nil { + return err + } + + q.l.Lock() + defer q.l.Unlock() + + if h, ok := q.periodicJobs[id]; ok { + client.PeriodicJobs().Remove(h) + delete(q.periodicJobs, id) + } + return nil +} + +// CancelContext is like Cancel but allows passing a context. +func (q *Job) CancelContext(ctx context.Context, id string) error { + client, err := q.Client() + if err != nil { + return err + } + q.l.Lock() + defer q.l.Unlock() + if h, ok := q.periodicJobs[id]; ok { + client.PeriodicJobs().Remove(h) + delete(q.periodicJobs, id) + return nil + } + + return nil +} + +func (q *Job) Add(job contracts.JobArgs) error { + client, err := q.Client() + if err != nil { + return err + } + + q.l.Lock() + defer q.l.Unlock() + + _, err = client.Insert(q.ctx, job, lo.ToPtr(job.InsertOpts())) + return err +} diff --git a/backend/providers/jwt/config.go b/backend/providers/jwt/config.go new file mode 100644 index 0000000..dc227d4 --- /dev/null +++ b/backend/providers/jwt/config.go @@ -0,0 +1,35 @@ +package jwt + +import ( + "time" + + log "github.com/sirupsen/logrus" + + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +const DefaultPrefix = "JWT" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + SigningKey string // jwt签名 + ExpiresTime string // 过期时间 + Issuer string // 签发者 +} + +func (c *Config) ExpiresTimeDuration() time.Duration { + d, err := time.ParseDuration(c.ExpiresTime) + if err != nil { + log.Fatal(err) + } + return d +} diff --git a/backend/providers/jwt/jwt.go b/backend/providers/jwt/jwt.go new file mode 100644 index 0000000..dd94465 --- /dev/null +++ b/backend/providers/jwt/jwt.go @@ -0,0 +1,118 @@ +package jwt + +import ( + "errors" + "strings" + "time" + + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + + jwt "github.com/golang-jwt/jwt/v4" + "golang.org/x/sync/singleflight" +) + +const ( + CtxKey = "claims" + HttpHeader = "Authorization" +) + +type BaseClaims struct { + OpenID string `json:"open_id,omitempty"` + Tenant string `json:"tenant,omitempty"` + UserID int64 `json:"user_id,omitempty"` + TenantID int64 `json:"tenant_id,omitempty"` +} + +// Custom claims structure +type Claims struct { + BaseClaims + jwt.RegisteredClaims +} + +const TokenPrefix = "Bearer " + +type JWT struct { + singleflight *singleflight.Group + config *Config + SigningKey []byte +} + +var ( + TokenExpired = errors.New("Token is expired") + TokenNotValidYet = errors.New("Token not active yet") + TokenMalformed = errors.New("That's not even a token") + TokenInvalid = errors.New("Couldn't handle this token:") +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func() (*JWT, error) { + return &JWT{ + singleflight: &singleflight.Group{}, + config: &config, + SigningKey: []byte(config.SigningKey), + }, nil + }, o.DiOptions()...) +} + +func (j *JWT) CreateClaims(baseClaims BaseClaims) *Claims { + ep, _ := time.ParseDuration(j.config.ExpiresTime) + claims := Claims{ + BaseClaims: baseClaims, + RegisteredClaims: jwt.RegisteredClaims{ + NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Second * 10)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: j.config.Issuer, // 签名的发行者 + }, + } + return &claims +} + +// 创建一个token +func (j *JWT) CreateToken(claims *Claims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 +func (j *JWT) CreateTokenByOldToken(oldToken string, claims *Claims) (string, error) { + v, err, _ := j.singleflight.Do("JWT:"+oldToken, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// 解析 token +func (j *JWT) Parse(tokenString string) (*Claims, error) { + tokenString = strings.TrimPrefix(tokenString, TokenPrefix) + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, TokenMalformed + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + // Token is expired + return nil, TokenExpired + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, TokenNotValidYet + } else { + return nil, TokenInvalid + } + } + } + if token != nil { + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, TokenInvalid + } else { + return nil, TokenInvalid + } +} diff --git a/backend/providers/postgres/config.go b/backend/providers/postgres/config.go new file mode 100644 index 0000000..de20ce8 --- /dev/null +++ b/backend/providers/postgres/config.go @@ -0,0 +1,136 @@ +package postgres + +import ( + "fmt" + "strconv" + "time" + + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + "gorm.io/gorm/logger" +) + +const DefaultPrefix = "Database" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Username string + Password string + Database string + Schema string + Host string + Port uint + SslMode string + TimeZone string + Prefix string // 表前缀 + Singular bool // 是否开启全局禁用复数,true表示开启 + MaxIdleConns int // 空闲中的最大连接数 + MaxOpenConns int // 打开到数据库的最大连接数 + // 可选:连接生命周期配置(0 表示不设置) + ConnMaxLifetimeSeconds uint + ConnMaxIdleTimeSeconds uint + + // 可选:GORM 日志与行为配置 + LogLevel string // silent|error|warn|info(默认info) + SlowThresholdMs uint // 慢查询阈值(毫秒)默认200 + ParameterizedQueries bool // 占位符输出,便于日志安全与查询归并 + PrepareStmt bool // 预编译语句缓存 + SkipDefaultTransaction bool // 跳过默认事务 + + // 可选:DSN 增强 + UseSearchPath bool // 在 DSN 中附带 search_path + ApplicationName string // application_name +} + +func (m Config) GormSlowThreshold() time.Duration { + if m.SlowThresholdMs == 0 { + return 200 * time.Millisecond // 默认200ms + } + return time.Duration(m.SlowThresholdMs) * time.Millisecond +} + +func (m Config) GormLogLevel() logger.LogLevel { + switch m.LogLevel { + case "silent": + return logger.Silent + case "error": + return logger.Error + case "warn": + return logger.Warn + case "info", "": + return logger.Info + default: + return logger.Info + } +} + +func (m *Config) checkDefault() { + if m.MaxIdleConns == 0 { + m.MaxIdleConns = 10 + } + + if m.MaxOpenConns == 0 { + m.MaxOpenConns = 100 + } + + if m.Username == "" { + m.Username = "postgres" + } + + if m.SslMode == "" { + m.SslMode = "disable" + } + + if m.TimeZone == "" { + m.TimeZone = "Asia/Shanghai" + } + + if m.Port == 0 { + m.Port = 5432 + } + + if m.Schema == "" { + m.Schema = "public" + } +} + +func (m *Config) EmptyDsn() string { + // 基本 DSN + dsnTpl := "host=%s user=%s password=%s port=%d dbname=%s sslmode=%s TimeZone=%s" + m.checkDefault() + base := fmt.Sprintf(dsnTpl, m.Host, m.Username, m.Password, m.Port, m.Database, m.SslMode, m.TimeZone) + // 附加可选参数 + extras := "" + if m.UseSearchPath && m.Schema != "" { + extras += " search_path=" + m.Schema + } + if m.ApplicationName != "" { + extras += " application_name=" + strconv.Quote(m.ApplicationName) + } + return base + extras +} + +// DSN connection dsn +func (m *Config) DSN() string { + // 基本 DSN + dsnTpl := "host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s" + m.checkDefault() + base := fmt.Sprintf(dsnTpl, m.Host, m.Username, m.Password, m.Database, m.Port, m.SslMode, m.TimeZone) + // 附加可选参数 + extras := "" + if m.UseSearchPath && m.Schema != "" { + extras += " search_path=" + m.Schema + } + if m.ApplicationName != "" { + extras += " application_name=" + strconv.Quote(m.ApplicationName) + } + return base + extras +} diff --git a/backend/providers/postgres/postgres.go b/backend/providers/postgres/postgres.go new file mode 100644 index 0000000..05c8a84 --- /dev/null +++ b/backend/providers/postgres/postgres.go @@ -0,0 +1,91 @@ +package postgres + +import ( + "context" + "database/sql" + "time" + + "github.com/sirupsen/logrus" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var conf Config + if err := o.UnmarshalConfig(&conf); err != nil { + return err + } + + return container.Container.Provide(func() (*gorm.DB, *sql.DB, *Config, error) { + dbConfig := postgres.Config{DSN: conf.DSN()} + + // 安全日志:不打印密码,仅输出关键连接信息 + logrus. + WithFields( + logrus.Fields{ + "host": conf.Host, + "port": conf.Port, + "db": conf.Database, + "schema": conf.Schema, + "ssl": conf.SslMode, + }, + ). + Info("opening PostgreSQL connection") + + // 映射日志等级 + lvl := conf.GormLogLevel() + slow := conf.GormSlowThreshold() + + gormConfig := gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + TablePrefix: conf.Prefix, + SingularTable: conf.Singular, + }, + DisableForeignKeyConstraintWhenMigrating: true, + PrepareStmt: conf.PrepareStmt, + SkipDefaultTransaction: conf.SkipDefaultTransaction, + Logger: logger.New(logrus.StandardLogger(), logger.Config{ + SlowThreshold: slow, + LogLevel: lvl, + IgnoreRecordNotFoundError: true, + Colorful: false, + ParameterizedQueries: conf.ParameterizedQueries, + }), + } + + db, err := gorm.Open(postgres.New(dbConfig), &gormConfig) + if err != nil { + return nil, nil, nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, nil, nil, err + } + sqlDB.SetMaxIdleConns(conf.MaxIdleConns) + sqlDB.SetMaxOpenConns(conf.MaxOpenConns) + if conf.ConnMaxLifetimeSeconds > 0 { + sqlDB.SetConnMaxLifetime(time.Duration(conf.ConnMaxLifetimeSeconds) * time.Second) + } + if conf.ConnMaxIdleTimeSeconds > 0 { + sqlDB.SetConnMaxIdleTime(time.Duration(conf.ConnMaxIdleTimeSeconds) * time.Second) + } + + // Ping 校验 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := sqlDB.PingContext(ctx); err != nil { + return nil, nil, nil, err + } + + // 关闭钩子 + container.AddCloseAble(func() { _ = sqlDB.Close() }) + + return db, sqlDB, &conf, nil + }, o.DiOptions()...) +} diff --git a/backend/tests/README.md b/backend/tests/README.md new file mode 100644 index 0000000..0756793 --- /dev/null +++ b/backend/tests/README.md @@ -0,0 +1,288 @@ +# 测试指南 + +本项目的测试使用 **Convey 框架**,分为三个层次:单元测试、集成测试和端到端测试。 + +## 测试结构 + +``` +tests/ +├── setup_test.go # 测试设置和通用工具 +├── unit/ # 单元测试 +│ ├── config_test.go # 配置测试 +│ └── ... # 其他单元测试 +├── integration/ # 集成测试 +│ ├── database_test.go # 数据库集成测试 +│ └── ... # 其他集成测试 +└── e2e/ # 端到端测试 + ├── api_test.go # API 测试 + └── ... # 其他 E2E 测试 +``` + +## Convey 框架概述 + +Convey 是一个 BDD 风格的 Go 测试框架,提供直观的语法和丰富的断言。 + +### 核心概念 + +- **Convey**: 定义测试上下文,类似于 `Describe` 或 `Context` +- **So**: 断言函数,验证预期结果 +- **Reset**: 清理函数,在每个测试后执行 + +### 基本语法 + +```go +Convey("测试场景描述", t, func() { + Convey("当某个条件发生时", func() { + // 准备测试数据 + result := SomeFunction() + + Convey("那么应该得到预期结果", func() { + So(result, ShouldEqual, "expected") + }) + }) + + Reset(func() { + // 清理测试数据 + }) +}) +``` + +## 运行测试 + +### 运行所有测试 +```bash +go test ./tests/... -v +``` + +### 运行特定类型的测试 +```bash +# 单元测试 +go test ./tests/unit/... -v + +# 集成测试 +go test ./tests/integration/... -v + +# 端到端测试 +go test ./tests/e2e/... -v +``` + +### 运行带覆盖率报告的测试 +```bash +go test ./tests/... -v -coverprofile=coverage.out +go tool cover -html=coverage.out -o coverage.html +``` + +### 运行基准测试 +```bash +go test ./tests/... -bench=. -v +``` + +## 测试环境配置 + +### 单元测试 +- 不需要外部依赖 +- 使用内存数据库或模拟对象 +- 快速执行 + +### 集成测试 +- 需要数据库连接 +- 使用测试数据库 `v2_test` +- 需要启动 Redis 等服务 + +### 端到端测试 +- 需要完整的应用环境 +- 测试真实的 HTTP 请求 +- 可能需要 Docker 环境 + +## Convey 测试最佳实践 + +### 1. 测试结构设计 +- 使用描述性的中文场景描述 +- 遵循 `当...那么...` 的语义结构 +- 嵌套 Convey 块来组织复杂测试逻辑 + +```go +Convey("用户认证测试", t, func() { + var user *User + var token string + + Convey("当用户注册时", func() { + user = &User{Name: "测试用户", Email: "test@example.com"} + err := user.Register() + So(err, ShouldBeNil) + + Convey("那么用户应该被创建", func() { + So(user.ID, ShouldBeGreaterThan, 0) + }) + }) + + Convey("当用户登录时", func() { + token, err := user.Login("password") + So(err, ShouldBeNil) + So(token, ShouldNotBeEmpty) + + Convey("那么应该获得有效的访问令牌", func() { + So(len(token), ShouldBeGreaterThan, 0) + }) + }) +}) +``` + +### 2. 断言使用 +- 使用丰富的 So 断言函数 +- 提供有意义的错误消息 +- 验证所有重要的方面 + +### 3. 数据管理 +- 使用 `Reset` 函数进行清理 +- 每个测试独立准备数据 +- 确保测试间不相互影响 + +### 4. 异步测试 +- 使用适当的超时设置 +- 处理并发测试 +- 使用 channel 进行同步 + +### 5. 错误处理 +- 测试错误情况 +- 验证错误消息 +- 确保错误处理逻辑正确 + +## 常用 Convey 断言 + +### 相等性断言 +```go +So(value, ShouldEqual, expected) +So(value, ShouldNotEqual, expected) +So(value, ShouldResemble, expected) // 深度比较 +So(value, ShouldNotResemble, expected) +``` + +### 类型断言 +```go +So(value, ShouldBeNil) +So(value, ShouldNotBeNil) +So(value, ShouldBeTrue) +So(value, ShouldBeFalse) +So(value, ShouldBeZeroValue) +``` + +### 数值断言 +```go +So(value, ShouldBeGreaterThan, expected) +So(value, ShouldBeLessThan, expected) +So(value, ShouldBeBetween, lower, upper) +``` + +### 集合断言 +```go +So(slice, ShouldHaveLength, expected) +So(slice, ShouldContain, expected) +So(slice, ShouldNotContain, expected) +So(map, ShouldContainKey, key) +``` + +### 字符串断言 +```go +So(str, ShouldContainSubstring, substr) +So(str, ShouldStartWith, prefix) +So(str, ShouldEndWith, suffix) +So(str, ShouldMatch, regexp) +``` + +### 错误断言 +```go +So(err, ShouldBeNil) +So(err, ShouldNotBeNil) +So(err, ShouldError, expectedError) +``` + +## 测试工具 + +- `goconvey/convey` - BDD 测试框架 +- `gomock` - Mock 生成器 +- `httptest` - HTTP 测试 +- `sqlmock` - 数据库 mock +- `testify` - 辅助测试工具(可选) + +## 测试示例 + +### 配置测试示例 +```go +Convey("配置加载测试", t, func() { + var config *Config + + Convey("当从文件加载配置时", func() { + config, err := LoadConfig("config.toml") + So(err, ShouldBeNil) + So(config, ShouldNotBeNil) + + Convey("那么配置应该正确加载", func() { + So(config.App.Mode, ShouldEqual, "development") + So(config.Http.Port, ShouldEqual, 8080) + }) + }) +}) +``` + +### 数据库测试示例 +```go +Convey("数据库操作测试", t, func() { + var db *gorm.DB + + Convey("当连接数据库时", func() { + db = SetupTestDB() + So(db, ShouldNotBeNil) + + Convey("那么应该能够创建记录", func() { + user := User{Name: "测试用户", Email: "test@example.com"} + result := db.Create(&user) + So(result.Error, ShouldBeNil) + So(user.ID, ShouldBeGreaterThan, 0) + }) + }) + + Reset(func() { + if db != nil { + CleanupTestDB(db) + } + }) +}) +``` + +### API 测试示例 +```go +Convey("API 端点测试", t, func() { + var server *httptest.Server + + Convey("当启动测试服务器时", func() { + server = httptest.NewServer(NewApp()) + So(server, ShouldNotBeNil) + + Convey("那么健康检查端点应该正常工作", func() { + resp, err := http.Get(server.URL + "/health") + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + So(result["status"], ShouldEqual, "ok") + }) + }) + + Reset(func() { + if server != nil { + server.Close() + } + }) +}) +``` + +## CI/CD 集成 + +测试会在以下情况下自动运行: +- 代码提交时 +- 创建 Pull Request 时 +- 合并到主分支时 + +测试结果会影响代码合并决策。Convey 的详细输出有助于快速定位问题。 \ No newline at end of file diff --git a/backend/tests/e2e/api_test.go b/backend/tests/e2e/api_test.go new file mode 100644 index 0000000..426b316 --- /dev/null +++ b/backend/tests/e2e/api_test.go @@ -0,0 +1,422 @@ +//go:build legacytests +// +build legacytests + +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + "quyun/v2/app" + "quyun/v2/app/config" +) + +// TestAPIHealth 测试 API 健康检查 +func TestAPIHealth(t *testing.T) { + Convey("API 健康检查测试", t, func() { + var server *httptest.Server + var testConfig *config.Config + + Convey("当启动测试服务器时", func() { + testConfig = &config.Config{ + App: config.AppConfig{ + Mode: "test", + BaseURI: "http://localhost:8080", + }, + Http: config.HttpConfig{ + Port: 8080, + }, + Log: config.LogConfig{ + Level: "debug", + Format: "text", + EnableCaller: true, + }, + } + + app := app.New(testConfig) + server = httptest.NewServer(app) + + Convey("服务器应该成功启动", func() { + So(server, ShouldNotBeNil) + So(server.URL, ShouldNotBeEmpty) + }) + }) + + Convey("当访问健康检查端点时", func() { + resp, err := http.Get(server.URL + "/health") + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + + defer resp.Body.Close() + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + So(err, ShouldBeNil) + + Convey("响应应该包含正确的状态", func() { + So(result["status"], ShouldEqual, "ok") + }) + + Convey("响应应该包含时间戳", func() { + So(result, ShouldContainKey, "timestamp") + }) + + Convey("响应应该是 JSON 格式", func() { + So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8") + }) + }) + + Convey("当访问不存在的端点时", func() { + resp, err := http.Get(server.URL + "/api/nonexistent") + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + + defer resp.Body.Close() + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + So(err, ShouldBeNil) + + Convey("响应应该包含错误信息", func() { + So(result, ShouldContainKey, "error") + }) + }) + + Convey("当测试 CORS 支持", func() { + req, err := http.NewRequest("OPTIONS", server.URL+"/api/test", nil) + So(err, ShouldBeNil) + + req.Header.Set("Origin", "http://localhost:3000") + req.Header.Set("Access-Control-Request-Method", "POST") + req.Header.Set("Access-Control-Request-Headers", "Content-Type,Authorization") + + resp, err := http.DefaultClient.Do(req) + So(err, ShouldBeNil) + defer resp.Body.Close() + + Convey("应该返回正确的 CORS 头", func() { + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(resp.Header.Get("Access-Control-Allow-Origin"), ShouldContainSubstring, "localhost") + So(resp.Header.Get("Access-Control-Allow-Methods"), ShouldContainSubstring, "POST") + }) + }) + + Reset(func() { + if server != nil { + server.Close() + } + }) + }) +} + +// TestAPIPerformance 测试 API 性能 +func TestAPIPerformance(t *testing.T) { + Convey("API 性能测试", t, func() { + var server *httptest.Server + var testConfig *config.Config + + Convey("当准备性能测试时", func() { + testConfig = &config.Config{ + App: config.AppConfig{ + Mode: "test", + BaseURI: "http://localhost:8080", + }, + Http: config.HttpConfig{ + Port: 8080, + }, + Log: config.LogConfig{ + Level: "error", // 减少日志输出以提升性能 + Format: "text", + }, + } + + app := app.New(testConfig) + server = httptest.NewServer(app) + }) + + Convey("当测试响应时间时", func() { + start := time.Now() + resp, err := http.Get(server.URL + "/health") + So(err, ShouldBeNil) + defer resp.Body.Close() + + duration := time.Since(start) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + + Convey("响应时间应该在合理范围内", func() { + So(duration, ShouldBeLessThan, 100*time.Millisecond) + }) + }) + + Convey("当测试并发请求时", func() { + const numRequests = 50 + const maxConcurrency = 10 + const timeout = 5 * time.Second + + var wg sync.WaitGroup + successCount := 0 + errorCount := 0 + var mu sync.Mutex + + // 使用信号量控制并发数 + sem := make(chan struct{}, maxConcurrency) + + start := time.Now() + + for i := 0; i < numRequests; i++ { + wg.Add(1) + go func(requestID int) { + defer wg.Done() + + // 获取信号量 + sem <- struct{}{} + defer func() { <-sem }() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", server.URL+"/health", nil) + if err != nil { + mu.Lock() + errorCount++ + mu.Unlock() + return + } + + client := &http.Client{ + Timeout: timeout, + } + + resp, err := client.Do(req) + if err != nil { + mu.Lock() + errorCount++ + mu.Unlock() + return + } + defer resp.Body.Close() + + mu.Lock() + if resp.StatusCode == http.StatusOK { + successCount++ + } else { + errorCount++ + } + mu.Unlock() + }(i) + } + + wg.Wait() + duration := time.Since(start) + + Convey("所有请求都应该完成", func() { + So(successCount+errorCount, ShouldEqual, numRequests) + }) + + Convey("所有请求都应该成功", func() { + So(errorCount, ShouldEqual, 0) + }) + + Convey("总耗时应该在合理范围内", func() { + So(duration, ShouldBeLessThan, 10*time.Second) + }) + + Convey("并发性能应该良好", func() { + avgTime := duration / numRequests + So(avgTime, ShouldBeLessThan, 200*time.Millisecond) + }) + }) + + Reset(func() { + if server != nil { + server.Close() + } + }) + }) +} + +// TestAPIBehavior 测试 API 行为 +func TestAPIBehavior(t *testing.T) { + Convey("API 行为测试", t, func() { + var server *httptest.Server + var testConfig *config.Config + + Convey("当准备行为测试时", func() { + testConfig = &config.Config{ + App: config.AppConfig{ + Mode: "test", + BaseURI: "http://localhost:8080", + }, + Http: config.HttpConfig{ + Port: 8080, + }, + Log: config.LogConfig{ + Level: "debug", + Format: "text", + EnableCaller: true, + }, + } + + app := app.New(testConfig) + server = httptest.NewServer(app) + }) + + Convey("当测试不同 HTTP 方法时", func() { + testURL := server.URL + "/health" + + Convey("GET 请求应该成功", func() { + resp, err := http.Get(testURL) + So(err, ShouldBeNil) + defer resp.Body.Close() + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("POST 请求应该被处理", func() { + resp, err := http.Post(testURL, "application/json", bytes.NewBuffer([]byte{})) + So(err, ShouldBeNil) + defer resp.Body.Close() + // 健康检查端点通常支持所有方法 + So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) + }) + + Convey("PUT 请求应该被处理", func() { + req, err := http.NewRequest("PUT", testURL, bytes.NewBuffer([]byte{})) + So(err, ShouldBeNil) + resp, err := http.DefaultClient.Do(req) + So(err, ShouldBeNil) + defer resp.Body.Close() + So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) + }) + + Convey("DELETE 请求应该被处理", func() { + req, err := http.NewRequest("DELETE", testURL, nil) + So(err, ShouldBeNil) + resp, err := http.DefaultClient.Do(req) + So(err, ShouldBeNil) + defer resp.Body.Close() + So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) + }) + }) + + Convey("当测试自定义请求头时", func() { + req, err := http.NewRequest("GET", server.URL+"/health", nil) + So(err, ShouldBeNil) + + // 设置各种请求头 + req.Header.Set("User-Agent", "E2E-Test-Agent/1.0") + req.Header.Set("Accept", "application/json") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + req.Header.Set("X-Custom-Header", "test-value") + req.Header.Set("X-Request-ID", "test-request-123") + req.Header.Set("Authorization", "Bearer test-token") + + resp, err := http.DefaultClient.Do(req) + So(err, ShouldBeNil) + defer resp.Body.Close() + + Convey("请求应该成功", func() { + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("响应应该是 JSON 格式", func() { + So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8") + }) + }) + + Convey("当测试错误处理时", func() { + Convey("访问不存在的路径应该返回 404", func() { + resp, err := http.Get(server.URL + "/api/v1/nonexistent") + So(err, ShouldBeNil) + defer resp.Body.Close() + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + }) + + Convey("访问非法路径应该返回 404", func() { + resp, err := http.Get(server.URL + "/../etc/passwd") + So(err, ShouldBeNil) + defer resp.Body.Close() + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + }) + }) + + Reset(func() { + if server != nil { + server.Close() + } + }) + }) +} + +// TestAPIDocumentation 测试 API 文档 +func TestAPIDocumentation(t *testing.T) { + Convey("API 文档测试", t, func() { + var server *httptest.Server + var testConfig *config.Config + + Convey("当准备文档测试时", func() { + testConfig = &config.Config{ + App: config.AppConfig{ + Mode: "test", + BaseURI: "http://localhost:8080", + }, + Http: config.HttpConfig{ + Port: 8080, + }, + Log: config.LogConfig{ + Level: "debug", + Format: "text", + EnableCaller: true, + }, + } + + app := app.New(testConfig) + server = httptest.NewServer(app) + }) + + Convey("当访问 Swagger UI 时", func() { + resp, err := http.Get(server.URL + "/swagger/index.html") + So(err, ShouldBeNil) + defer resp.Body.Close() + + Convey("应该能够访问 Swagger UI", func() { + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("响应应该是 HTML 格式", func() { + contentType := resp.Header.Get("Content-Type") + So(contentType, ShouldContainSubstring, "text/html") + }) + }) + + Convey("当访问 OpenAPI 规范时", func() { + resp, err := http.Get(server.URL + "/swagger/doc.json") + So(err, ShouldBeNil) + defer resp.Body.Close() + + Convey("应该能够访问 OpenAPI 规范", func() { + // 如果存在则返回 200,不存在则返回 404 + So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusNotFound}) + }) + + Convey("如果存在,响应应该是 JSON 格式", func() { + if resp.StatusCode == http.StatusOK { + contentType := resp.Header.Get("Content-Type") + So(contentType, ShouldContainSubstring, "application/json") + } + }) + }) + + Reset(func() { + if server != nil { + server.Close() + } + }) + }) +} diff --git a/backend/tests/integration/database_test.go b/backend/tests/integration/database_test.go new file mode 100644 index 0000000..74e629b --- /dev/null +++ b/backend/tests/integration/database_test.go @@ -0,0 +1,367 @@ +//go:build legacytests +// +build legacytests + +package integration + +import ( + "context" + "database/sql" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "quyun/v2/app/config" + "quyun/v2/app/database" +) + +// TestUser 测试用户模型 +type TestUser struct { + ID int `gorm:"primaryKey"` + Name string `gorm:"size:100;not null"` + Email string `gorm:"size:100;unique;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// TestDatabaseConnection 测试数据库连接 +func TestDatabaseConnection(t *testing.T) { + Convey("数据库连接测试", t, func() { + var db *gorm.DB + var sqlDB *sql.DB + var testConfig *config.Config + var testDBName string + + Convey("当准备测试数据库时", func() { + testDBName = "v2_test_integration" + testConfig = &config.Config{ + Database: config.DatabaseConfig{ + Host: "localhost", + Port: 5432, + Database: testDBName, + Username: "postgres", + Password: "password", + SslMode: "disable", + MaxIdleConns: 5, + MaxOpenConns: 20, + ConnMaxLifetime: 30 * time.Minute, + }, + } + + Convey("应该能够连接到数据库", func() { + dsn := testConfig.Database.GetDSN() + var err error + db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + So(err, ShouldBeNil) + So(db, ShouldNotBeNil) + + sqlDB, err = db.DB() + So(err, ShouldBeNil) + So(sqlDB, ShouldNotBeNil) + + // 设置连接池 + sqlDB.SetMaxIdleConns(testConfig.Database.MaxIdleConns) + sqlDB.SetMaxOpenConns(testConfig.Database.MaxOpenConns) + sqlDB.SetConnMaxLifetime(testConfig.Database.ConnMaxLifetime) + + // 测试连接 + err = sqlDB.Ping() + So(err, ShouldBeNil) + }) + + Convey("应该能够创建测试表", func() { + err := db.Exec(` + CREATE TABLE IF NOT EXISTS integration_test_users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `).Error + So(err, ShouldBeNil) + }) + }) + + Convey("当测试数据库操作时", func() { + Convey("应该能够创建记录", func() { + user := TestUser{ + Name: "Integration Test User", + Email: "integration@example.com", + } + + result := db.Create(&user) + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 1) + So(user.ID, ShouldBeGreaterThan, 0) + }) + + Convey("应该能够查询记录", func() { + // 先插入测试数据 + user := TestUser{ + Name: "Query Test User", + Email: "query@example.com", + } + db.Create(&user) + + // 查询记录 + var result TestUser + err := db.First(&result, "email = ?", "query@example.com").Error + So(err, ShouldBeNil) + So(result.Name, ShouldEqual, "Query Test User") + So(result.Email, ShouldEqual, "query@example.com") + }) + + Convey("应该能够更新记录", func() { + // 先插入测试数据 + user := TestUser{ + Name: "Update Test User", + Email: "update@example.com", + } + db.Create(&user) + + // 更新记录 + result := db.Model(&user).Update("name", "Updated Integration User") + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 1) + + // 验证更新 + var updatedUser TestUser + err := db.First(&updatedUser, user.ID).Error + So(err, ShouldBeNil) + So(updatedUser.Name, ShouldEqual, "Updated Integration User") + }) + + Convey("应该能够删除记录", func() { + // 先插入测试数据 + user := TestUser{ + Name: "Delete Test User", + Email: "delete@example.com", + } + db.Create(&user) + + // 删除记录 + result := db.Delete(&user) + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 1) + + // 验证删除 + var deletedUser TestUser + err := db.First(&deletedUser, user.ID).Error + So(err, ShouldEqual, gorm.ErrRecordNotFound) + }) + }) + + Convey("当测试事务时", func() { + Convey("应该能够执行事务操作", func() { + // 开始事务 + tx := db.Begin() + So(tx, ShouldNotBeNil) + + // 在事务中插入数据 + user := TestUser{ + Name: "Transaction Test User", + Email: "transaction@example.com", + } + result := tx.Create(&user) + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 1) + + // 查询事务中的数据 + var count int64 + tx.Model(&TestUser{}).Count(&count) + So(count, ShouldEqual, 1) + + // 提交事务 + err := tx.Commit().Error + So(err, ShouldBeNil) + + // 验证数据已提交 + db.Model(&TestUser{}).Count(&count) + So(count, ShouldBeGreaterThan, 0) + }) + + Convey("应该能够回滚事务", func() { + // 开始事务 + tx := db.Begin() + + // 在事务中插入数据 + user := TestUser{ + Name: "Rollback Test User", + Email: "rollback@example.com", + } + tx.Create(&user) + + // 回滚事务 + err := tx.Rollback().Error + So(err, ShouldBeNil) + + // 验证数据已回滚 + var count int64 + db.Model(&TestUser{}).Where("email = ?", "rollback@example.com").Count(&count) + So(count, ShouldEqual, 0) + }) + }) + + Convey("当测试批量操作时", func() { + Convey("应该能够批量插入记录", func() { + users := []TestUser{ + {Name: "Batch User 1", Email: "batch1@example.com"}, + {Name: "Batch User 2", Email: "batch2@example.com"}, + {Name: "Batch User 3", Email: "batch3@example.com"}, + } + + result := db.Create(&users) + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 3) + + // 验证批量插入 + var count int64 + db.Model(&TestUser{}).Where("email LIKE ?", "batch%@example.com").Count(&count) + So(count, ShouldEqual, 3) + }) + + Convey("应该能够批量更新记录", func() { + // 先插入测试数据 + users := []TestUser{ + {Name: "Batch Update 1", Email: "batchupdate1@example.com"}, + {Name: "Batch Update 2", Email: "batchupdate2@example.com"}, + } + db.Create(&users) + + // 批量更新 + result := db.Model(&TestUser{}). + Where("email LIKE ?", "batchupdate%@example.com"). + Update("name", "Batch Updated User") + So(result.Error, ShouldBeNil) + So(result.RowsAffected, ShouldEqual, 2) + + // 验证更新 + var updatedCount int64 + db.Model(&TestUser{}). + Where("name = ?", "Batch Updated User"). + Count(&updatedCount) + So(updatedCount, ShouldEqual, 2) + }) + }) + + Convey("当测试查询条件时", func() { + Convey("应该能够使用各种查询条件", func() { + // 插入测试数据 + testUsers := []TestUser{ + {Name: "Alice", Email: "alice@example.com"}, + {Name: "Bob", Email: "bob@example.com"}, + {Name: "Charlie", Email: "charlie@example.com"}, + {Name: "Alice Smith", Email: "alice.smith@example.com"}, + } + db.Create(&testUsers) + + Convey("应该能够使用 LIKE 查询", func() { + var users []TestUser + err := db.Where("name LIKE ?", "Alice%").Find(&users).Error + So(err, ShouldBeNil) + So(len(users), ShouldEqual, 2) + }) + + Convey("应该能够使用 IN 查询", func() { + var users []TestUser + err := db.Where("name IN ?", []string{"Alice", "Bob"}).Find(&users).Error + So(err, ShouldBeNil) + So(len(users), ShouldEqual, 2) + }) + + Convey("应该能够使用 BETWEEN 查询", func() { + var users []TestUser + err := db.Where("id BETWEEN ? AND ?", 1, 3).Find(&users).Error + So(err, ShouldBeNil) + So(len(users), ShouldBeGreaterThan, 0) + }) + + Convey("应该能够使用多条件查询", func() { + var users []TestUser + err := db.Where("name LIKE ? AND email LIKE ?", "%Alice%", "%example.com").Find(&users).Error + So(err, ShouldBeNil) + So(len(users), ShouldEqual, 2) + }) + }) + }) + + Reset(func() { + // 清理测试表 + if db != nil { + db.Exec("DROP TABLE IF EXISTS integration_test_users") + } + // 关闭数据库连接 + if sqlDB != nil { + sqlDB.Close() + } + }) + }) +} + +// TestDatabaseConnectionPool 测试数据库连接池 +func TestDatabaseConnectionPool(t *testing.T) { + Convey("数据库连接池测试", t, func() { + var db *gorm.DB + var sqlDB *sql.DB + + Convey("当配置连接池时", func() { + testConfig := &config.Config{ + Database: config.DatabaseConfig{ + Host: "localhost", + Port: 5432, + Database: "v2_test_pool", + Username: "postgres", + Password: "password", + SslMode: "disable", + MaxIdleConns: 5, + MaxOpenConns: 10, + ConnMaxLifetime: 5 * time.Minute, + }, + } + + dsn := testConfig.Database.GetDSN() + var err error + db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + So(err, ShouldBeNil) + + sqlDB, err = db.DB() + So(err, ShouldBeNil) + + Convey("应该能够设置连接池参数", func() { + sqlDB.SetMaxIdleConns(testConfig.Database.MaxIdleConns) + sqlDB.SetMaxOpenConns(testConfig.Database.MaxOpenConns) + sqlDB.SetConnMaxLifetime(testConfig.Database.ConnMaxLifetime) + + // 验证设置 + stats := sqlDB.Stats() + So(stats.MaxOpenConns, ShouldEqual, testConfig.Database.MaxOpenConns) + So(stats.MaxIdleConns, ShouldEqual, testConfig.Database.MaxIdleConns) + }) + + Convey("应该能够监控连接池状态", func() { + // 获取初始状态 + initialStats := sqlDB.Stats() + So(initialStats.OpenConnections, ShouldEqual, 0) + + // 执行一些查询来创建连接 + for i := 0; i < 3; i++ { + sqlDB.Ping() + } + + // 获取使用后的状态 + afterStats := sqlDB.Stats() + So(afterStats.OpenConnections, ShouldBeGreaterThan, 0) + So(afterStats.InUse, ShouldBeGreaterThan, 0) + }) + }) + + Reset(func() { + // 关闭数据库连接 + if sqlDB != nil { + sqlDB.Close() + } + }) + }) +} diff --git a/backend/tests/setup_test.go b/backend/tests/setup_test.go new file mode 100644 index 0000000..5eb6cbc --- /dev/null +++ b/backend/tests/setup_test.go @@ -0,0 +1,164 @@ +//go:build legacytests +// +build legacytests + +package tests + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +// TestMain 测试入口点 +func TestMain(m *testing.M) { + // 运行测试 + m.Run() +} + +// TestSetup 测试基础设置 +func TestSetup(t *testing.T) { + Convey("测试基础设置", t, func() { + Convey("当初始化测试环境时", func() { + // 初始化测试环境 + testEnv := &TestEnvironment{ + Name: "test-env", + Version: "1.0.0", + } + + Convey("那么测试环境应该被正确创建", func() { + So(testEnv.Name, ShouldEqual, "test-env") + So(testEnv.Version, ShouldEqual, "1.0.0") + }) + }) + }) +} + +// TestEnvironment 测试环境结构 +type TestEnvironment struct { + Name string + Version string + Config map[string]interface{} +} + +// NewTestEnvironment 创建新的测试环境 +func NewTestEnvironment(name string) *TestEnvironment { + return &TestEnvironment{ + Name: name, + Config: make(map[string]interface{}), + } +} + +// WithConfig 设置配置 +func (e *TestEnvironment) WithConfig(key string, value interface{}) *TestEnvironment { + e.Config[key] = value + return e +} + +// GetConfig 获取配置 +func (e *TestEnvironment) GetConfig(key string) interface{} { + return e.Config[key] +} + +// Setup 设置测试环境 +func (e *TestEnvironment) Setup() *TestEnvironment { + // 初始化测试环境 + e.Config["initialized"] = true + return e +} + +// Cleanup 清理测试环境 +func (e *TestEnvironment) Cleanup() { + // 清理测试环境 + e.Config = make(map[string]interface{}) +} + +// TestEnvironmentManagement 测试环境管理 +func TestEnvironmentManagement(t *testing.T) { + Convey("测试环境管理", t, func() { + var env *TestEnvironment + + Convey("当创建新测试环境时", func() { + env = NewTestEnvironment("test-app") + + Convey("那么环境应该有正确的名称", func() { + So(env.Name, ShouldEqual, "test-app") + So(env.Version, ShouldBeEmpty) + So(env.Config, ShouldNotBeNil) + }) + }) + + Convey("当设置配置时", func() { + env.WithConfig("debug", true) + env.WithConfig("port", 8080) + + Convey("那么配置应该被正确设置", func() { + So(env.GetConfig("debug"), ShouldEqual, true) + So(env.GetConfig("port"), ShouldEqual, 8080) + }) + }) + + Convey("当初始化环境时", func() { + env.Setup() + + Convey("那么环境应该被标记为已初始化", func() { + So(env.GetConfig("initialized"), ShouldEqual, true) + }) + }) + + Reset(func() { + if env != nil { + env.Cleanup() + } + }) + }) +} + +// TestConveyBasicUsage 测试 Convey 基础用法 +func TestConveyBasicUsage(t *testing.T) { + Convey("Convey 基础用法测试", t, func() { + Convey("数字操作", func() { + num := 42 + + Convey("应该能够进行基本比较", func() { + So(num, ShouldEqual, 42) + So(num, ShouldBeGreaterThan, 0) + So(num, ShouldBeLessThan, 100) + }) + }) + + Convey("字符串操作", func() { + str := "hello world" + + Convey("应该能够进行字符串比较", func() { + So(str, ShouldEqual, "hello world") + So(str, ShouldContainSubstring, "hello") + So(str, ShouldStartWith, "hello") + So(str, ShouldEndWith, "world") + }) + }) + + Convey("切片操作", func() { + slice := []int{1, 2, 3, 4, 5} + + Convey("应该能够进行切片操作", func() { + So(slice, ShouldHaveLength, 5) + So(slice, ShouldContain, 3) + So(slice, ShouldNotContain, 6) + }) + }) + + Convey("Map 操作", func() { + m := map[string]interface{}{ + "name": "test", + "value": 123, + } + + Convey("应该能够进行 Map 操作", func() { + So(m, ShouldContainKey, "name") + So(m, ShouldContainKey, "value") + So(m["name"], ShouldEqual, "test") + So(m["value"], ShouldEqual, 123) + }) + }) + }) +} diff --git a/backend/tests/unit/config_test.go b/backend/tests/unit/config_test.go new file mode 100644 index 0000000..ced5892 --- /dev/null +++ b/backend/tests/unit/config_test.go @@ -0,0 +1,290 @@ +//go:build legacytests +// +build legacytests + +package unit + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/smartystreets/goconvey/convey" + "quyun/v2/app/config" +) + +// TestConfigLoading 测试配置加载功能 +func TestConfigLoading(t *testing.T) { + Convey("配置加载测试", t, func() { + var testConfig *config.Config + var configPath string + var testDir string + + Convey("当准备测试配置文件时", func() { + originalWd, _ := os.Getwd() + testDir = filepath.Join(originalWd, "..", "..", "fixtures", "test_config") + + Convey("应该创建测试配置目录", func() { + err := os.MkdirAll(testDir, 0o755) + So(err, ShouldBeNil) + }) + + Convey("应该创建测试配置文件", func() { + testConfigContent := `App: + Mode: "test" + BaseURI: "http://localhost:8080" +Http: + Port: 8080 +Database: + Host: "localhost" + Port: 5432 + Database: "test_db" + Username: "test_user" + Password: "test_password" + SslMode: "disable" +Log: + Level: "debug" + Format: "text" + EnableCaller: true` + + configPath = filepath.Join(testDir, "config.toml") + err := os.WriteFile(configPath, []byte(testConfigContent), 0o644) + So(err, ShouldBeNil) + }) + + Convey("应该成功加载配置", func() { + var err error + testConfig, err = config.Load(configPath) + So(err, ShouldBeNil) + So(testConfig, ShouldNotBeNil) + }) + }) + + Convey("验证配置内容", func() { + So(testConfig, ShouldNotBeNil) + + Convey("应用配置应该正确", func() { + So(testConfig.App.Mode, ShouldEqual, "test") + So(testConfig.App.BaseURI, ShouldEqual, "http://localhost:8080") + }) + + Convey("HTTP配置应该正确", func() { + So(testConfig.Http.Port, ShouldEqual, 8080) + }) + + Convey("数据库配置应该正确", func() { + So(testConfig.Database.Host, ShouldEqual, "localhost") + So(testConfig.Database.Port, ShouldEqual, 5432) + So(testConfig.Database.Database, ShouldEqual, "test_db") + So(testConfig.Database.Username, ShouldEqual, "test_user") + So(testConfig.Database.Password, ShouldEqual, "test_password") + So(testConfig.Database.SslMode, ShouldEqual, "disable") + }) + + Convey("日志配置应该正确", func() { + So(testConfig.Log.Level, ShouldEqual, "debug") + So(testConfig.Log.Format, ShouldEqual, "text") + So(testConfig.Log.EnableCaller, ShouldBeTrue) + }) + }) + + Reset(func() { + // 清理测试文件 + if testDir != "" { + os.RemoveAll(testDir) + } + }) + }) +} + +// TestConfigFromEnvironment 测试从环境变量加载配置 +func TestConfigFromEnvironment(t *testing.T) { + Convey("环境变量配置测试", t, func() { + var originalEnvVars map[string]string + + Convey("当设置环境变量时", func() { + // 保存原始环境变量 + originalEnvVars = map[string]string{ + "APP_MODE": os.Getenv("APP_MODE"), + "HTTP_PORT": os.Getenv("HTTP_PORT"), + "DB_HOST": os.Getenv("DB_HOST"), + } + + // 设置测试环境变量 + os.Setenv("APP_MODE", "test") + os.Setenv("HTTP_PORT", "9090") + os.Setenv("DB_HOST", "test-host") + + Convey("环境变量应该被正确设置", func() { + So(os.Getenv("APP_MODE"), ShouldEqual, "test") + So(os.Getenv("HTTP_PORT"), ShouldEqual, "9090") + So(os.Getenv("DB_HOST"), ShouldEqual, "test-host") + }) + }) + + Convey("当从环境变量加载配置时", func() { + originalWd, _ := os.Getwd() + testDir := filepath.Join(originalWd, "..", "..", "fixtures", "test_config_env") + + Convey("应该创建测试配置目录", func() { + err := os.MkdirAll(testDir, 0o755) + So(err, ShouldBeNil) + }) + + Convey("应该创建基础配置文件", func() { + testConfigContent := `App: + Mode: "development" + BaseURI: "http://localhost:3000" +Http: + Port: 3000 +Database: + Host: "localhost" + Port: 5432 + Database: "default_db" + Username: "default_user" + Password: "default_password" + SslMode: "disable"` + + configPath := filepath.Join(testDir, "config.toml") + err := os.WriteFile(configPath, []byte(testConfigContent), 0o644) + So(err, ShouldBeNil) + }) + + Convey("应该成功加载并合并配置", func() { + configPath := filepath.Join(testDir, "config.toml") + loadedConfig, err := config.Load(configPath) + + So(err, ShouldBeNil) + So(loadedConfig, ShouldNotBeNil) + + Convey("环境变量应该覆盖配置文件", func() { + So(loadedConfig.App.Mode, ShouldEqual, "test") + So(loadedConfig.Http.Port, ShouldEqual, 9090) + So(loadedConfig.Database.Host, ShouldEqual, "test-host") + }) + + Convey("配置文件的默认值应该保留", func() { + So(loadedConfig.App.BaseURI, ShouldEqual, "http://localhost:3000") + So(loadedConfig.Database.Database, ShouldEqual, "default_db") + }) + }) + + Reset(func() { + // 清理测试目录 + os.RemoveAll(testDir) + }) + }) + + Reset(func() { + // 恢复原始环境变量 + if originalEnvVars != nil { + for key, value := range originalEnvVars { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } + } + }) + }) +} + +// TestConfigValidation 测试配置验证 +func TestConfigValidation(t *testing.T) { + Convey("配置验证测试", t, func() { + Convey("当配置为空时", func() { + config := &config.Config{} + + Convey("应该检测到缺失的必需配置", func() { + So(config.App.Mode, ShouldBeEmpty) + So(config.Http.Port, ShouldEqual, 0) + So(config.Database.Host, ShouldBeEmpty) + }) + }) + + Convey("当配置端口无效时", func() { + config := &config.Config{ + Http: config.HttpConfig{ + Port: -1, + }, + } + + Convey("应该检测到无效端口", func() { + So(config.Http.Port, ShouldBeLessThan, 0) + }) + }) + + Convey("当配置模式有效时", func() { + validModes := []string{"development", "production", "testing"} + + for _, mode := range validModes { + config := &config.Config{ + App: config.AppConfig{ + Mode: mode, + }, + } + + Convey("模式 "+mode+" 应该是有效的", func() { + So(config.App.Mode, ShouldBeIn, validModes) + }) + } + }) + }) +} + +// TestConfigDefaults 测试配置默认值 +func TestConfigDefaults(t *testing.T) { + Convey("配置默认值测试", t, func() { + Convey("当创建新配置时", func() { + config := &config.Config{} + + Convey("应该有合理的默认值", func() { + // 测试应用的默认值 + So(config.App.Mode, ShouldEqual, "development") + + // 测试HTTP的默认值 + So(config.Http.Port, ShouldEqual, 8080) + + // 测试数据库的默认值 + So(config.Database.Port, ShouldEqual, 5432) + So(config.Database.SslMode, ShouldEqual, "disable") + + // 测试日志的默认值 + So(config.Log.Level, ShouldEqual, "info") + So(config.Log.Format, ShouldEqual, "json") + }) + }) + }) +} + +// TestConfigHelpers 测试配置辅助函数 +func TestConfigHelpers(t *testing.T) { + Convey("配置辅助函数测试", t, func() { + Convey("当使用配置辅助函数时", func() { + config := &config.Config{ + App: config.AppConfig{ + Mode: "production", + BaseURI: "https://api.example.com", + }, + Http: config.HttpConfig{ + Port: 443, + }, + } + + Convey("应该能够获取应用环境", func() { + env := config.App.Mode + So(env, ShouldEqual, "production") + }) + + Convey("应该能够构建完整URL", func() { + fullURL := config.App.BaseURI + "/api/v1/users" + So(fullURL, ShouldEqual, "https://api.example.com/api/v1/users") + }) + + Convey("应该能够判断HTTPS", func() { + isHTTPS := config.Http.Port == 443 + So(isHTTPS, ShouldBeTrue) + }) + }) + }) +} diff --git a/backend/utils/build_info.go b/backend/utils/build_info.go new file mode 100644 index 0000000..8dfc5a7 --- /dev/null +++ b/backend/utils/build_info.go @@ -0,0 +1,44 @@ +package utils + +import "fmt" + +// 构建信息变量,通过 ldflags 在构建时注入 +var ( + // Version 应用版本信息 + Version string + + // BuildAt 构建时间 + BuildAt string + + // GitHash Git 提交哈希 + GitHash string +) + +// GetBuildInfo 获取构建信息 +func GetBuildInfo() map[string]string { + return map[string]string{ + "version": Version, + "buildAt": BuildAt, + "gitHash": GitHash, + } +} + +// PrintBuildInfo 打印构建信息 +func PrintBuildInfo(appName string) { + buildInfo := GetBuildInfo() + + println("========================================") + printf("🚀 %s\n", appName) + println("========================================") + printf("📋 Version: %s\n", buildInfo["version"]) + printf("🕐 Build Time: %s\n", buildInfo["buildAt"]) + printf("🔗 Git Hash: %s\n", buildInfo["gitHash"]) + println("========================================") + println("🌟 Application is starting...") + println() +} + +// 为了避免导入 fmt 包,我们使用内置的 print 和 printf 函数 +func printf(format string, args ...interface{}) { + print(fmt.Sprintf(format, args...)) +} diff --git a/docs/PROJECT_FUNCTIONS_AND_DB_DICTIONARY.md b/docs/PROJECT_FUNCTIONS_AND_DB_DICTIONARY.md new file mode 100644 index 0000000..e35eddf --- /dev/null +++ b/docs/PROJECT_FUNCTIONS_AND_DB_DICTIONARY.md @@ -0,0 +1,523 @@ +# QuyUn 项目功能与数据库字典(用于多租户改造) + +本文档基于当前仓库代码静态分析整理(`frontend/` + `backend/`),目标是帮助你将“单实例/单运营方(单用户后台)”改造为“多租户(多运营方)”。 + +--- + +## 1. 仓库结构与技术栈 + +### 1.1 目录结构 + +- `backend/`:Go 后端(同时包含少量 `package.json` 依赖,但核心为 Go 服务) +- `frontend/admin/`:后台管理端(Vue3 + Vite) +- `frontend/wechat/`:微信 H5 端(Vue3 + Vite) + +### 1.2 关键技术栈 + +**后端** + +- Web 框架:`gofiber/fiber/v3` +- DI / 代码生成:`go.ipao.vip/atom`(路由文件为 `routes.gen.go`) +- 数据库:PostgreSQL(`lib/pq`、`pgx`),迁移:`pressly/goose` +- SQL Builder/ORM:`go-jet/jet`(`backend/database/table/*`、`backend/app/model/*.gen.go`) +- 任务队列:`riverqueue/river`(依赖表:`river_job` 等) +- 对象存储:阿里云 OSS(签名上传、签名下载、删除) +- 微信:网页授权(OAuth)、JS-SDK 签名、支付/退款回调(微信支付 v3) +- 监控链路:OpenTelemetry(存在 provider 但本文档不展开) + +**前端** + +- Vue 3 + Vite +- Admin:PrimeVue + Tailwind(并用少量 DaisyUI) +- WeChat H5:Tailwind + `weixin-js-sdk` + `xgplayer`(视频播放) + +--- + +## 2. 运行时架构与路由总览 + +### 2.1 HTTP 服务入口与静态资源托管 + +后端启动入口:`backend/main.go`,命令为 `serve`(`backend/app/service/http/http.go`)。 + +HTTP 路由前缀固定为 `/v1`: + +- API:统一挂载在 `/v1/*` +- 静态资源: + - 后台管理端:`GET /admin*` → `App.DistAdmin` 指向的 `frontend/admin/dist` + - 微信端:`GET /*` → `App.DistWeChat` 指向的 `frontend/wechat/dist` + +这意味着项目典型部署形态是:**一个后端进程同时提供 API + 两套前端静态文件**。 + +### 2.2 认证/授权(非常影响多租户改造) + +#### 2.2.1 微信端用户认证(Cookie Token + 重定向) + +中间件:`backend/app/middlewares/mid_auth.go` + +- 受保护范围:除以下路径外,均要求登录 + - `/v1/pay/callback/*`(支付回调免登录) + - `/v1/auth/*`(授权流程免登录) + - `/v1/admin/*`(后台 API 走另一套鉴权) +- 机制: + - 读取 Cookie:`token` + - 解析 JWT(`providers/jwt`),拿到 `UserID` + - 查询用户(`users` 表) + - 校验 `users.auth_token.expires_at`(微信 access token 过期则强制重登) + - 未登录时:非 XHR 请求会重定向到 `/v1/auth/wechat?redirect=当前完整URL`;XHR 请求直接返回 401 +- 认证通过后写入:`ctx.Locals("user", *model.Users)` + +> 重要:当前系统的“用户”是**微信 OpenID 用户**,并且 `users.open_id` 在 DB 层为 `UNIQUE`(单租户假设)。 + +#### 2.2.2 后台管理端鉴权(硬编码账号 + 固定 UserID) + +登录接口:`POST /v1/admin/auth`(`backend/app/http/admin/auth.go`) + +- 用户名/密码硬编码在后端:`pl.yang` / `Xixi@0202` +- 登录成功签发 JWT,`UserID` 固定写死为 `-20140202` + +中间件:`backend/app/middlewares/mid_auth_admin.go` + +- 对 `/v1/admin/*` 生效(除 `/v1/admin/auth`) +- 从 Header `Authorization` 或 Query `token` 读取 JWT +- 校验 `jwt.UserID == -20140202`,否则 403 + +> 重要:这套后台体系是“单运营方/单管理员”的实现方式,多租户必须重构为“租户维度的后台账号体系”。 + +### 2.3 API 路由清单(从 `routes.gen.go` 汇总) + +#### 2.3.1 微信端(非 admin)API + +路由文件:`backend/app/http/routes.gen.go` + +- `GET /v1/auth/wechat`:发起微信网页授权(重定向到微信授权页) +- `GET /v1/auth/login`:微信回调,换取 openid + 用户信息,写入 Cookie `token`,再重定向回 `redirect` +- `GET /v1/posts`:曲谱列表(仅已发布),支持 keyword 搜索,返回是否已购买、封面图 OSS 签名 URL +- `GET /v1/posts/:id/show`:曲谱详情(仅已发布),返回购买态、封面图 URL +- `GET /v1/posts/:id/play`:获取可播放视频 URL(未购买返回“短视频/试听”,已购买返回“完整版”) +- `GET /v1/posts/mine`:我的已购曲谱列表 +- `POST /v1/posts/:id/buy`:购买(当前实现主路径为“余额支付”) +- `GET /v1/users/profile`:当前用户资料 + 余额 +- `PUT /v1/users/username`:修改用户名(最多 12 字符) +- `GET /v1/wechats/js-sdk`:获取 JS-SDK 签名配置 +- `POST /v1/pay/callback/:channel`:支付/退款回调入口(免登录),内部将回调入队列异步处理 + +#### 2.3.2 后台管理端(admin)API + +路由文件:`backend/app/http/admin/routes.gen.go` + +- `POST /v1/admin/auth`:后台登录(硬编码账号),返回 Token + +媒体库: + +- `GET /v1/admin/medias`:媒体列表(分页 + keyword) +- `GET /v1/admin/medias/:id`:媒体预览(302 跳转到 OSS 签名 URL) +- `DELETE /v1/admin/medias/:id`:删除 OSS 文件 + DB 记录 + +上传: + +- `GET /v1/admin/uploads/pre-uploaded-check/:md5.:ext?mime=...`:按 md5 判断是否已存在;不存在则返回 OSS 预签名 PUT URL +- `POST /v1/admin/uploads/post-uploaded-action`:上传完成回调:写入 `medias`;若为 `video/mp4` 触发下载/转码类任务 + +曲谱: + +- `GET /v1/admin/posts`:曲谱列表(分页 + keyword),附带销量(购买次数) +- `POST /v1/admin/posts`:创建曲谱(包含封面 head_images 与媒体 assets) +- `PUT /v1/admin/posts/:id`:编辑曲谱 +- `DELETE /v1/admin/posts/:id`:硬删除 +- `GET /v1/admin/posts/:id`:曲谱详情(附带 medias 列表) +- `POST /v1/admin/posts/:id/send-to/:userId`:赠送曲谱(写入 `user_posts`,price=-1) + +用户: + +- `GET /v1/admin/users`:用户列表(分页 + keyword) +- `GET /v1/admin/users/:id`:用户详情 +- `GET /v1/admin/users/:id/articles`:用户已购曲谱(分页) +- `POST /v1/admin/users/:id/balance`:后台给用户充值余额(单位:分) + +订单: + +- `GET /v1/admin/orders`:订单列表(分页 + 按订单号/用户过滤),返回附带 `post_title`、`username` +- `POST /v1/admin/orders/:id/refund`:退款(余额支付直接退余额 + 撤销权限;微信支付走退款 API 并等待回调) + +统计: + +- `GET /v1/admin/statistics`:仪表盘统计(草稿/已发布、媒体数、已完成订单数、用户数、已完成订单金额汇总) + +--- + +## 3. 前端功能梳理 + +### 3.1 Admin(`frontend/admin`) + +**路由(`frontend/admin/src/router.js`,base 为 `/admin/`)** + +- `/`:Dashboard(统计) +- `/medias`:媒体库列表(预览/下载/删除) +- `/medias/uploads`:媒体上传(MD5 去重 + OSS 预签名上传 + 上传后回调) +- `/posts`:曲谱列表(创建/编辑/删除/赠送) +- `/posts/create`:创建曲谱(选择封面图 ≤3、选择媒体资源、设置价格/折扣/状态) +- `/posts/edit/:id`:编辑曲谱 +- `/users`:用户列表(可充值) +- `/users/:id`:用户详情 + 已购列表 +- `/orders`:订单列表 + 退款 +- `/login`:后台登录页 + +**API 调用方式** + +- Axios baseURL:`/v1`(`frontend/admin/src/api/httpClient.js`) +- Token:本地存储 `__token`,请求头写入 `Authorization: Bearer ` + +### 3.2 WeChat H5(`frontend/wechat`) + +**路由(`frontend/wechat/src/router.js`)** + +- `/`:曲谱列表(无限滚动 + 搜索) +- `/posts/:id`:曲谱详情(视频播放 + 购买按钮 + 分享) +- `/purchased`:已购列表(顶部固定播放器 + 列表点播) +- `/profile`:个人信息 + 余额 + +**API 调用方式** + +- Axios baseURL:`/v1`,并且 `withCredentials: true`(携带 Cookie `token`) +- 401 时前端自动跳转 `/v1/auth/wechat?redirect=<当前URL>`(`frontend/wechat/src/api/client.js`) + +**注意:接口路径不一致** + +`frontend/wechat/src/api/userApi.js` 的更新接口调用 `PUT /users/profile`,但后端实际是 `PUT /v1/users/username`。 + +--- + +## 4. 后端业务流程(核心用例) + +### 4.1 微信登录/注册流程 + +1) 前端/中间件发现未登录 → 重定向到 `/v1/auth/wechat?redirect=...` +2) `/v1/auth/wechat` 生成微信授权 URL(回调到 `/v1/auth/login`,并透传 redirect) +3) `/v1/auth/login`: + - `code` 换取 `openid` 与 `access_token` + - 获取“稳定版 token”(stable_token) + - 拉取用户信息(失败则生成随机昵称/头像) + - `users` 表按 `open_id` 查询,不存在则创建;存在则更新用户名/头像/metas/auth_token + - 生成 JWT(claims 里只使用了 `UserID`),写 Cookie:`token` + +### 4.2 曲谱内容模型与呈现 + +曲谱(`posts`)本质是一个“商品/内容条目”,它的媒体资产在 `posts.assets` 里记录(JSON 数组),每个 asset 指向 `medias.id`。 + +- 封面图:`posts.head_images`(媒体 ID 数组)→ 后端转为 OSS 签名 URL 数组返回 +- 播放:`GET /v1/posts/:id/play` + - 若未购买:`preview=true` + - 在 `posts.assets` 中选择 `Type=="video/mp4"` 且 `asset.Metas.Short == preview` 的媒体作为播放源 + - 生成带过期时间的 OSS 签名 URL(预览与正式片过期时间不同) + +### 4.3 购买/订单/权限授予 + +核心表: + +- `orders`:订单记录(支付状态 + 支付/退款回调内容写入 `orders.meta`) +- `user_posts`:用户与曲谱的授予关系(购买、赠送) + +购买接口:`POST /v1/posts/:id/buy` + +- 先检查 `user_posts` 是否已有记录(避免重复购买) +- 创建 `orders`(状态 pending) +- 当前主路径:余额足够则走余额支付,写入 `orders.meta.cost_balance` 并投递 `BalancePayNotify` 任务,随后返回一个特殊响应(`appId: "balance"`) +- 代码中存在“余额不足时走微信 JSAPI 支付”的逻辑,但当前版本在余额不足时提前 `return`,导致后续微信支付逻辑不可达(这属于现状问题,改多租户时建议一并梳理修正)。 + +### 4.4 支付/退款回调(异步任务) + +回调入口:`POST /v1/pay/callback/:channel` + +- `TRANSACTION.SUCCESS` → 入队 `WechatPayNotify` +- `REFUND.*` → 入队 `WechatRefundNotify` + +任务处理(均运行在 River 队列里): + +- `WechatPayNotify`:校验金额、更新订单状态为 completed、扣减余额(若 cost_balance>0)、写入 `user_posts` 授权 +- `BalancePayNotify`:余额支付完成,更新订单状态 completed、扣减余额、写入 `user_posts` +- `WechatRefundNotify`:订单状态按退款状态更新;退款成功则撤销 `user_posts` + +--- + +## 5. 数据库字典(PostgreSQL) + +### 5.1 迁移与生成代码来源 + +- Goose 迁移:`backend/database/migrations/*.sql` +- Jet 表定义(由 DB 反射/生成):`backend/database/table/*.go` +- Model(CRUD + 部分自定义逻辑):`backend/app/model/*` +- 类型映射(jsonb → struct、int2 → enum):`backend/database/transform.yaml` + `backend/database/fields/*` + +### 5.2 业务表一览 + +| 表 | 作用 | +|---|---| +| `users` | 微信用户主体 + 余额 + 微信 token 信息 | +| `posts` | 曲谱/内容商品 | +| `medias` | 媒体资源(OSS 路径、hash 去重、媒体元信息) | +| `user_posts` | 用户-曲谱的授予关系(购买/赠送) | +| `orders` | 订单记录(支付/退款状态与元数据) | + +此外还有 `migrations`(Goose 使用)以及 River 队列表(`river_job` 等,属于基础设施表)。 + +### 5.3 枚举(int2/int)取值定义 + +**posts.status(`fields.PostStatus`)** + +- `0`:draft +- `1`:published + +**users.status(`fields.UserStatus`)** + +- `0`:ok +- `1`:banned +- `2`:blocked + +**orders.status(`fields.OrderStatus`)** + +- `0`:pending +- `1`:paid(当前代码主要使用 `pending/completed/refund_*`,该值可能历史遗留) +- `2`:refund_success +- `3`:refund_closed +- `4`:refund_processing +- `5`:refund_abnormal +- `6`:cancelled +- `7`:completed + +### 5.4 `users` 表 + +迁移:`backend/database/migrations/20250322103119_create_users.sql`、`20250430014015_alter_user.sql`、`20250512113213_alter_user.sql` + +字段(按迁移语义整理): + +- `id int8`:主键(序列起始被设置为 1000) +- `created_at timestamp not null default now()` +- `updated_at timestamp not null default now()` +- `deleted_at timestamp null`:软删除 +- `status int2 not null default 0`:见 `UserStatus` +- `open_id varchar(128) not null unique`:微信 OpenID(单租户假设的关键约束) +- `username varchar(128) not null` +- `avatar text null` +- `metas jsonb not null default '{}'`:用户资料(见下方 JSON 结构) +- `auth_token jsonb not null default '{}'`:微信 token(见下方 JSON 结构) +- `balance int8 not null default 0`:余额(单位:分) + +`users.metas`(`fields.UserMetas`): + +- `city/country/province`:地区 +- `head_image_url`:头像 +- `nickname`:昵称 +- `sex`:性别 +- `privilege[]`:特权列表 + +`users.auth_token`(`fields.UserAuthToken`): + +- `stable_access_token`、`stable_expires_at` +- `access_token`、`expires_at` +- `refresh_token`、`scope`、`is_snapshotuser` + +### 5.5 `medias` 表 + +迁移:`backend/database/migrations/20250321112535_create_medias.sql` + +- `id int8`:主键 +- `created_at timestamp not null default now()` +- `name varchar(255) not null default ''`:原始文件名 +- `mime_type varchar(128) not null default ''` +- `size int8 not null default 0` +- `path varchar(255) not null default ''`:OSS 对象 key(实际业务中通常为 `quyun/.`) +- `metas jsonb not null default '{}'`:媒体元信息 +- `hash varchar(64) not null default ''`:上传 md5(用于去重) + +`medias.metas`(`fields.MediaMetas`): + +- `parent_hash`:关联源文件(例如转码后的视频/封面与原视频关联) +- `short bool`:是否为“试听/预览版” +- `duration int64`:时长(秒) + +### 5.6 `posts` 表 + +迁移:`backend/database/migrations/20250322100215_create_posts.sql` + +- `id int8`:主键 +- `created_at timestamp not null default now()` +- `updated_at timestamp not null default now()` +- `deleted_at timestamp null`:软删除 +- `status int2 not null default 0`:见 `PostStatus` +- `title varchar(128) not null` +- `head_images jsonb not null default '[]'`:封面媒体 ID 数组(int64) +- `description varchar(256) not null`:简介(admin 表单里叫 introduction) +- `content text not null`:正文(微信端详情里展示在 `content`) +- `price int8 not null default 0`:单位:分 +- `discount int2 not null default 100`:折扣百分比(0~100) +- `views int8 not null default 0` +- `likes int8 not null default 0` +- `tags jsonb default '{}'`:标签(代码期望是 `[]string`) +- `assets jsonb default '{}'`:媒体资产(代码期望是 `[]MediaAsset`) + +`posts.assets`(`fields.MediaAsset[]`)元素结构: + +- `type string`:mime_type(例如 `video/mp4`) +- `media int64`:对应 `medias.id` +- `metas MediaMetas`:冗余一份媒体 metas(用于直接判断 short/duration) +- `mark *string`:预留字段(当前业务未见强依赖) + +> 注意:迁移里 `tags/assets` 默认值是 `{}`(object),但代码将其当作数组解析。真实运行依赖于写入时覆盖该字段;多租户改造时建议顺手把默认值修正为 `[]` 并清洗历史数据。 + +### 5.7 `user_posts` 表 + +迁移:`backend/database/migrations/20250322103243_create_user_posts.sql` + +- `id int8`:主键 +- `created_at timestamp not null default now()` +- `updated_at timestamp not null default now()` +- `user_id int8 not null` +- `post_id int8 not null` +- `price int8 not null`:购买/赠送时记录(赠送场景 `price=-1`) + +> 注意:无外键、无唯一约束(同一用户重复 post 的防重由代码实现);多租户/一致性增强时建议加唯一索引与外键。 + +### 5.8 `orders` 表 + +迁移:`backend/database/migrations/20250410130530_create_orders.sql` + +- `id int8`:主键 +- `created_at timestamp not null default now()` +- `updated_at timestamp not null default now()` +- `order_no varchar(64) not null`:系统订单号(当前用时间戳字符串) +- `sub_order_no varchar(64) not null default ''`:子订单号(当前等于 order_no) +- `transaction_id varchar(64) not null default ''`:微信支付交易号 +- `refund_transaction_id varchar(64) not null default ''`:微信退款单号 +- `price int8 not null default 0`:单位:分 +- `discount int2 not null default 100` +- `currency varchar(10) not null default 'CNY'` +- `payment_method varchar(50) not null default 'wechatpay'`:实际可能为 `balance` 或微信回调的 trade_type +- `post_id int8 not null` +- `user_id int8 not null` +- `status int2 not null`:见 `OrderStatus` +- `meta jsonb not null default '{}'` + +`orders.meta`(`fields.OrderMeta`): + +- `pay_notify`:微信支付回调解密内容(结构体较大) +- `refund_resp`:发起退款 API 响应 +- `refund_notify`:微信退款回调解密内容 +- `cost_balance int64`:混合支付时使用余额金额(单位:分) + +--- + +## 6. 单租户假设与“多租户化”切入点清单 + +### 6.1 当前显式/隐式单租户点 + +- DB 约束:`users.open_id UNIQUE`(同一 openid 只能存在一条用户记录) +- Admin 登录:硬编码账号/密码;JWT `UserID` 写死 `-20140202`(没有“租户管理员表”) +- OSS 路径:`providers/ali/oss_client.go` 固定把对象放到 `quyun/` 前缀;上传模块也固定 `UPLOAD_PATH = "quyun"` +- 业务表均缺少租户字段:`posts/medias/orders/user_posts/users` 都没有 `tenant_id` +- 业务查询没有任何“租户过滤”(例如 `admin/posts` 列表直接全表扫描) + +### 6.2 值得注意:JWT Claims 已预留租户字段 + +`backend/providers/jwt/jwt.go` 的 `BaseClaims` 已包含: + +- `tenant string` +- `tenant_id int64` + +但当前业务仅用到 `user_id`,并未实现“解析租户上下文 → 写入 claims → 数据隔离”。 + +--- + +## 7. 多租户改造建议(面向本项目的可落地方案) + +下面给的是“从当前代码出发”的推荐路线,优先保证:改动面可控、隔离强、后续可扩展。 + +### 7.1 明确租户边界与识别方式(建议先定这个) + +对该项目而言,最常见的租户识别方式: + +1) **按域名**(推荐):`tenantA.example.com`、`tenantB.example.com` +2) **按路径前缀**:`/t/:tenant/...`(会影响前端路由与静态文件托管) +3) **按请求头**:`X-Tenant-ID`(适合 B 端 API,但微信 H5 场景常常不便) +4) **按二维码/推广链接参数**:首次进入携带 tenant,再写入 cookie/localStorage(需要防篡改与校验) + +微信场景强依赖“回跳 redirect”,因此建议:**域名识别租户** 或 “二维码/链接识别租户 + 服务端签名 state 防篡改”。 + +### 7.2 数据库层:新增租户表 + 为业务表补 `tenant_id` + +建议新增: + +- `tenants`:`id`、`code`、`name`、`status`、`created_at`、`updated_at`、配置项(可 JSONB) +- `admin_users`(或 `tenant_users`):租户后台账号体系(账号/密码 hash/角色/tenant_id) + +为现有业务表增加: + +- `users.tenant_id` +- `posts.tenant_id` +- `medias.tenant_id` +- `orders.tenant_id` +- `user_posts.tenant_id` + +并建立必要约束/索引(至少): + +- `users`: `UNIQUE(tenant_id, open_id)`(替代当前全局唯一) +- `posts`: `(tenant_id, id)` 常用过滤索引;如有需要加 `(tenant_id, status, deleted_at)` +- `medias`: `UNIQUE(tenant_id, hash)`(上传去重在租户内进行) +- `orders`: `UNIQUE(tenant_id, order_no)`(避免多租户下时间戳订单号冲突) +- `user_posts`: `UNIQUE(tenant_id, user_id, post_id)`(彻底防重) + +### 7.3 应用层:租户上下文注入与强制过滤 + +建议新增一个最先执行的 middleware: + +- 解析租户(host/path/header) +- 写入 `ctx.Locals("tenant", tenant)`(包含 `tenant_id`) +- 后续所有 model 查询都必须带 tenant 条件 + +落点示例(按当前代码组织方式): + +- `middlewares` 新增 `TenantResolve` 并在 `Group("v1").Use(...)` 的最前面插入 +- `Auth` 中间件里查用户时改为:`WHERE tenant_id = ? AND id = ?` +- 微信登录 `GetUserByOpenIDOrCreate` 改为 tenant 维度:`WHERE tenant_id=? AND open_id=?` +- Admin 路由统一加 tenant 过滤(例如 `/admin/posts` 只看当前租户) + +### 7.4 OAuth/支付:租户与微信配置关系 + +必须提前决策:**每个租户是否独立微信 AppID/支付商户**? + +- 若每租户独立:`tenants` 里存微信配置(AppID/AppSecret/MchID/ApiV3Key/...),并按租户动态初始化 client(当前 providers 是“全局单例配置”) +- 若共享同一个公众号/商户:仍然要做数据隔离,但需要在订单号、回调处理里带上 tenant 信息(例如 `order_no` 编码 tenant 前缀,或在 `meta` 中存 tenant_id 并保证可反查) + +### 7.5 OSS:对象 Key 增加租户前缀 + +当前固定前缀 `quyun/`。多租户建议改为: + +- `tenants//quyun/.` 或 `tenants//...` + +并同步改动: + +- 预签名上传:`PreUploadCheck` 返回的 Key +- 上传完成回调写入 `medias.path` +- 后续签名下载/删除统一使用新 path + +### 7.6 队列任务:必须携带 tenant 上下文 + +目前任务参数里不包含 tenant。多租户后建议: + +- Job Args 增加 `TenantID`(或可解析的 `TenantCode`) +- Worker 执行时所有 DB 查询都加 tenant 过滤 +- OSS 操作也按 tenant 前缀 + +否则会出现:回调/任务在多租户环境下“写错库、扣错余额、发错权限”的高危问题。 + +--- + +## 8. 建议你下一步提供的信息(能让多租户设计更准确) + +为了把“多租户模式”落到你期望的形态,建议你确认并告诉我: + +1) 租户识别方式:域名 / 路径前缀 / header / 邀请链接? +2) 微信与支付配置:租户独立还是共享? +3) 租户隔离级别:仅逻辑隔离(tenant_id)还是需要“库/Schema 隔离”? +4) 你期望的后台账号体系:每租户多个管理员?是否需要角色权限? + +确认后我可以按本仓库的结构直接给出:迁移脚本 + middleware + model 查询改造点 + 前端配套改造清单。 diff --git a/frontend/admin/dist/assets/index-CLimnK9W.js b/frontend/admin/dist/assets/index-CLimnK9W.js new file mode 100644 index 0000000..e879004 --- /dev/null +++ b/frontend/admin/dist/assets/index-CLimnK9W.js @@ -0,0 +1,25 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();/** +* @vue/shared v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function cs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const X={},At=[],Be=()=>{},Ar=()=>!1,yn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),fs=e=>e.startsWith("onUpdate:"),fe=Object.assign,us=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},$i=Object.prototype.hasOwnProperty,q=(e,t)=>$i.call(e,t),V=Array.isArray,St=e=>bn(e)==="[object Map]",Sr=e=>bn(e)==="[object Set]",B=e=>typeof e=="function",se=e=>typeof e=="string",ct=e=>typeof e=="symbol",Z=e=>e!==null&&typeof e=="object",Cr=e=>(Z(e)||B(e))&&B(e.then)&&B(e.catch),wr=Object.prototype.toString,bn=e=>wr.call(e),qi=e=>bn(e).slice(8,-1),Or=e=>bn(e)==="[object Object]",as=e=>se(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,jt=cs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),En=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},ki=/-\w/g,Re=En(e=>e.replace(ki,t=>t.slice(1).toUpperCase())),zi=/\B([A-Z])/g,_t=En(e=>e.replace(zi,"-$1").toLowerCase()),xn=En(e=>e.charAt(0).toUpperCase()+e.slice(1)),Tn=En(e=>e?`on${xn(e)}`:""),ot=(e,t)=>!Object.is(e,t),In=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Ji=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Ds;const Rn=()=>Ds||(Ds=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ds(e){if(V(e)){const t={};for(let n=0;n{if(n){const s=n.split(Yi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function hs(e){let t="";if(se(e))t=e;else if(V(e))for(let n=0;n!!(e&&e.__v_isRef===!0),$n=e=>se(e)?e:e==null?"":V(e)||Z(e)&&(e.toString===wr||!B(e.toString))?Ir(e)?$n(e.value):JSON.stringify(e,Nr,2):String(e),Nr=(e,t)=>Ir(t)?Nr(e,t.value):St(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Nn(s,i)+" =>"]=r,n),{})}:Sr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Nn(n))}:ct(t)?Nn(t):Z(t)&&!V(t)&&!Or(t)?String(t):t,Nn=(e,t="")=>{var n;return ct(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let me;class no{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=me,!t&&me&&(this.index=(me.scopes||(me.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(me=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Gt){let t=Gt;for(Gt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Ut;){let t=Ut;for(Ut=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Fr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Hr(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),ms(s),ro(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function qn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Br(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Br(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===zt)||(e.globalVersion=zt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!qn(e))))return;e.flags|=2;const t=e.dep,n=Y,s=Se;Y=e,Se=!0;try{Fr(e);const r=e.fn(e._value);(t.version===0||ot(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{Y=n,Se=s,Hr(e),e.flags&=-3}}function ms(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)ms(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function ro(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Se=!0;const Vr=[];function ke(){Vr.push(Se),Se=!1}function ze(){const e=Vr.pop();Se=e===void 0?!0:e}function Ls(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Y;Y=void 0;try{t()}finally{Y=n}}}let zt=0;class io{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class _s{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Y||!Se||Y===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Y)n=this.activeLink=new io(Y,this),Y.deps?(n.prevDep=Y.depsTail,Y.depsTail.nextDep=n,Y.depsTail=n):Y.deps=Y.depsTail=n,jr(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=Y.depsTail,n.nextDep=void 0,Y.depsTail.nextDep=n,Y.depsTail=n,Y.deps===n&&(Y.deps=s)}return n}trigger(t){this.version++,zt++,this.notify(t)}notify(t){ps();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{gs()}}}function jr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)jr(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const kn=new WeakMap,mt=Symbol(""),zn=Symbol(""),Jt=Symbol("");function oe(e,t,n){if(Se&&Y){let s=kn.get(e);s||kn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new _s),r.map=s,r.key=n),r.track()}}function $e(e,t,n,s,r,i){const o=kn.get(e);if(!o){zt++;return}const l=c=>{c&&c.trigger()};if(ps(),t==="clear")o.forEach(l);else{const c=V(e),h=c&&as(n);if(c&&n==="length"){const a=Number(s);o.forEach((d,g)=>{(g==="length"||g===Jt||!ct(g)&&g>=a)&&l(d)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),h&&l(o.get(Jt)),t){case"add":c?h&&l(o.get("length")):(l(o.get(mt)),St(e)&&l(o.get(zn)));break;case"delete":c||(l(o.get(mt)),St(e)&&l(o.get(zn)));break;case"set":St(e)&&l(o.get(mt));break}}gs()}function Et(e){const t=$(e);return t===e?t:(oe(t,"iterate",Jt),Ce(e)?t:t.map(Je))}function vs(e){return oe(e=$(e),"iterate",Jt),e}function tt(e,t){return lt(e)?Ct(e)?Qt(Je(t)):Qt(t):Je(t)}const oo={__proto__:null,[Symbol.iterator](){return Dn(this,Symbol.iterator,e=>tt(this,e))},concat(...e){return Et(this).concat(...e.map(t=>V(t)?Et(t):t))},entries(){return Dn(this,"entries",e=>(e[1]=tt(this,e[1]),e))},every(e,t){return Ue(this,"every",e,t,void 0,arguments)},filter(e,t){return Ue(this,"filter",e,t,n=>n.map(s=>tt(this,s)),arguments)},find(e,t){return Ue(this,"find",e,t,n=>tt(this,n),arguments)},findIndex(e,t){return Ue(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ue(this,"findLast",e,t,n=>tt(this,n),arguments)},findLastIndex(e,t){return Ue(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ue(this,"forEach",e,t,void 0,arguments)},includes(...e){return Ln(this,"includes",e)},indexOf(...e){return Ln(this,"indexOf",e)},join(e){return Et(this).join(e)},lastIndexOf(...e){return Ln(this,"lastIndexOf",e)},map(e,t){return Ue(this,"map",e,t,void 0,arguments)},pop(){return Ft(this,"pop")},push(...e){return Ft(this,"push",e)},reduce(e,...t){return Fs(this,"reduce",e,t)},reduceRight(e,...t){return Fs(this,"reduceRight",e,t)},shift(){return Ft(this,"shift")},some(e,t){return Ue(this,"some",e,t,void 0,arguments)},splice(...e){return Ft(this,"splice",e)},toReversed(){return Et(this).toReversed()},toSorted(e){return Et(this).toSorted(e)},toSpliced(...e){return Et(this).toSpliced(...e)},unshift(...e){return Ft(this,"unshift",e)},values(){return Dn(this,"values",e=>tt(this,e))}};function Dn(e,t,n){const s=vs(e),r=s[t]();return s!==e&&!Ce(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const lo=Array.prototype;function Ue(e,t,n,s,r,i){const o=vs(e),l=o!==e&&!Ce(e),c=o[t];if(c!==lo[t]){const d=c.apply(e,i);return l?Je(d):d}let h=n;o!==e&&(l?h=function(d,g){return n.call(this,tt(e,d),g,e)}:n.length>2&&(h=function(d,g){return n.call(this,d,g,e)}));const a=c.call(o,h,s);return l&&r?r(a):a}function Fs(e,t,n,s){const r=vs(e);let i=n;return r!==e&&(Ce(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,tt(e,l),c,e)}),r[t](i,...s)}function Ln(e,t,n){const s=$(e);oe(s,"iterate",Jt);const r=s[t](...n);return(r===-1||r===!1)&&Es(n[0])?(n[0]=$(n[0]),s[t](...n)):r}function Ft(e,t,n=[]){ke(),ps();const s=$(e)[t].apply(e,n);return gs(),ze(),s}const co=cs("__proto__,__v_isRef,__isVue"),Ur=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ct));function fo(e){ct(e)||(e=String(e));const t=$(this);return oe(t,"has",e),t.hasOwnProperty(e)}class Gr{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?bo:qr:i?$r:Wr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=V(t);if(!r){let c;if(o&&(c=oo[n]))return c;if(n==="hasOwnProperty")return fo}const l=Reflect.get(t,n,ce(t)?t:s);if((ct(n)?Ur.has(n):co(n))||(r||oe(t,"get",n),i))return l;if(ce(l)){const c=o&&as(n)?l:l.value;return r&&Z(c)?Qn(c):c}return Z(l)?r?Qn(l):An(l):l}}class Kr extends Gr{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const o=V(t)&&as(n);if(!this._isShallow){const h=lt(i);if(!Ce(s)&&!lt(s)&&(i=$(i),s=$(s)),!o&&ce(i)&&!ce(s))return h||(i.value=s),!0}const l=o?Number(n)e,sn=e=>Reflect.getPrototypeOf(e);function go(e,t,n){return function(...s){const r=this.__v_raw,i=$(r),o=St(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,h=r[e](...s),a=n?Jn:t?Qt:Je;return!t&&oe(i,"iterate",c?zn:mt),{next(){const{value:d,done:g}=h.next();return g?{value:d,done:g}:{value:l?[a(d[0]),a(d[1])]:a(d),done:g}},[Symbol.iterator](){return this}}}}function rn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function mo(e,t){const n={get(r){const i=this.__v_raw,o=$(i),l=$(r);e||(ot(r,l)&&oe(o,"get",r),oe(o,"get",l));const{has:c}=sn(o),h=t?Jn:e?Qt:Je;if(c.call(o,r))return h(i.get(r));if(c.call(o,l))return h(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&oe($(r),"iterate",mt),r.size},has(r){const i=this.__v_raw,o=$(i),l=$(r);return e||(ot(r,l)&&oe(o,"has",r),oe(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=$(l),h=t?Jn:e?Qt:Je;return!e&&oe(c,"iterate",mt),l.forEach((a,d)=>r.call(i,h(a),h(d),o))}};return fe(n,e?{add:rn("add"),set:rn("set"),delete:rn("delete"),clear:rn("clear")}:{add(r){!t&&!Ce(r)&&!lt(r)&&(r=$(r));const i=$(this);return sn(i).has.call(i,r)||(i.add(r),$e(i,"add",r,r)),this},set(r,i){!t&&!Ce(i)&&!lt(i)&&(i=$(i));const o=$(this),{has:l,get:c}=sn(o);let h=l.call(o,r);h||(r=$(r),h=l.call(o,r));const a=c.call(o,r);return o.set(r,i),h?ot(i,a)&&$e(o,"set",r,i):$e(o,"add",r,i),this},delete(r){const i=$(this),{has:o,get:l}=sn(i);let c=o.call(i,r);c||(r=$(r),c=o.call(i,r)),l&&l.call(i,r);const h=i.delete(r);return c&&$e(i,"delete",r,void 0),h},clear(){const r=$(this),i=r.size!==0,o=r.clear();return i&&$e(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=go(r,e,t)}),n}function ys(e,t){const n=mo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(q(n,r)&&r in s?n:s,r,i)}const _o={get:ys(!1,!1)},vo={get:ys(!1,!0)},yo={get:ys(!0,!1)};const Wr=new WeakMap,$r=new WeakMap,qr=new WeakMap,bo=new WeakMap;function Eo(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function xo(e){return e.__v_skip||!Object.isExtensible(e)?0:Eo(qi(e))}function An(e){return lt(e)?e:bs(e,!1,ao,_o,Wr)}function kr(e){return bs(e,!1,po,vo,$r)}function Qn(e){return bs(e,!0,ho,yo,qr)}function bs(e,t,n,s,r){if(!Z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=xo(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function Ct(e){return lt(e)?Ct(e.__v_raw):!!(e&&e.__v_isReactive)}function lt(e){return!!(e&&e.__v_isReadonly)}function Ce(e){return!!(e&&e.__v_isShallow)}function Es(e){return e?!!e.__v_raw:!1}function $(e){const t=e&&e.__v_raw;return t?$(t):e}function Ro(e){return!q(e,"__v_skip")&&Object.isExtensible(e)&&Pr(e,"__v_skip",!0),e}const Je=e=>Z(e)?An(e):e,Qt=e=>Z(e)?Qn(e):e;function ce(e){return e?e.__v_isRef===!0:!1}function Ao(e){return zr(e,!1)}function So(e){return zr(e,!0)}function zr(e,t){return ce(e)?e:new Co(e,t)}class Co{constructor(t,n){this.dep=new _s,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:$(t),this._value=n?t:Je(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ce(t)||lt(t);t=s?t:$(t),ot(t,n)&&(this._rawValue=t,this._value=s?t:Je(t),this.dep.trigger())}}function wt(e){return ce(e)?e.value:e}const wo={get:(e,t,n)=>t==="__v_raw"?e:wt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return ce(r)&&!ce(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Jr(e){return Ct(e)?e:new Proxy(e,wo)}class Oo{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new _s(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=zt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&Y!==this)return Lr(this,!0),!0}get value(){const t=this.dep.track();return Br(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Po(e,t,n=!1){let s,r;return B(e)?s=e:(s=e.get,r=e.set),new Oo(s,r,n)}const on={},an=new WeakMap;let ht;function To(e,t=!1,n=ht){if(n){let s=an.get(n);s||an.set(n,s=[]),s.push(e)}}function Io(e,t,n=X){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,h=I=>r?I:Ce(I)||r===!1||r===0?it(I,1):it(I);let a,d,g,m,w=!1,P=!1;if(ce(e)?(d=()=>e.value,w=Ce(e)):Ct(e)?(d=()=>h(e),w=!0):V(e)?(P=!0,w=e.some(I=>Ct(I)||Ce(I)),d=()=>e.map(I=>{if(ce(I))return I.value;if(Ct(I))return h(I);if(B(I))return c?c(I,2):I()})):B(e)?t?d=c?()=>c(e,2):e:d=()=>{if(g){ke();try{g()}finally{ze()}}const I=ht;ht=a;try{return c?c(e,3,[m]):e(m)}finally{ht=I}}:d=Be,t&&r){const I=d,J=r===!0?1/0:r;d=()=>it(I(),J)}const j=so(),N=()=>{a.stop(),j&&j.active&&us(j.effects,a)};if(i&&t){const I=t;t=(...J)=>{I(...J),N()}}let T=P?new Array(e.length).fill(on):on;const L=I=>{if(!(!(a.flags&1)||!a.dirty&&!I))if(t){const J=a.run();if(r||w||(P?J.some((ie,ee)=>ot(ie,T[ee])):ot(J,T))){g&&g();const ie=ht;ht=a;try{const ee=[J,T===on?void 0:P&&T[0]===on?[]:T,m];T=J,c?c(t,3,ee):t(...ee)}finally{ht=ie}}}else a.run()};return l&&l(L),a=new Mr(d),a.scheduler=o?()=>o(L,!1):L,m=I=>To(I,!1,a),g=a.onStop=()=>{const I=an.get(a);if(I){if(c)c(I,4);else for(const J of I)J();an.delete(a)}},t?s?L(!0):T=a.run():o?o(L.bind(null,!0),!0):a.run(),N.pause=a.pause.bind(a),N.resume=a.resume.bind(a),N.stop=N,N}function it(e,t=1/0,n){if(t<=0||!Z(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,ce(e))it(e.value,t,n);else if(V(e))for(let s=0;s{it(s,t,n)});else if(Or(e)){for(const s in e)it(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&it(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function tn(e,t,n,s){try{return s?e(...s):e()}catch(r){Sn(r,t,n)}}function Ve(e,t,n,s){if(B(e)){const r=tn(e,t,n,s);return r&&Cr(r)&&r.catch(i=>{Sn(i,t,n)}),r}if(V(e)){const r=[];for(let i=0;i>>1,r=de[s],i=Yt(r);i=Yt(n)?de.push(e):de.splice(Mo(t),0,e),e.flags|=1,Xr()}}function Xr(){dn||(dn=Qr.then(ei))}function Do(e){V(e)?Ot.push(...e):nt&&e.id===-1?nt.splice(xt+1,0,e):e.flags&1||(Ot.push(e),e.flags|=1),Xr()}function Hs(e,t,n=Fe+1){for(;nYt(n)-Yt(s));if(Ot.length=0,nt){nt.push(...t);return}for(nt=t,xt=0;xte.id==null?e.flags&2?-1:1/0:e.id;function ei(e){try{for(Fe=0;Fe{s._d&&mn(-1);const i=hn(t);let o;try{o=e(...r)}finally{hn(i),s._d&&mn(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function at(e,t,n,s){const r=e.dirs,i=t&&t.dirs;for(let o=0;oe.__isTeleport,Bo=Symbol("_leaveCb");function Rs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Rs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function As(e,t){return B(e)?fe({name:e.name},t,{setup:e}):e}function ni(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const pn=new WeakMap;function Kt(e,t,n,s,r=!1){if(V(e)){e.forEach((w,P)=>Kt(w,t&&(V(t)?t[P]:t),n,s,r));return}if(Wt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Kt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Os(s.component):s.el,o=r?null:i,{i:l,r:c}=e,h=t&&t.r,a=l.refs===X?l.refs={}:l.refs,d=l.setupState,g=$(d),m=d===X?Ar:w=>q(g,w);if(h!=null&&h!==c){if(Bs(t),se(h))a[h]=null,m(h)&&(d[h]=null);else if(ce(h)){h.value=null;const w=t;w.k&&(a[w.k]=null)}}if(B(c))tn(c,l,12,[o,a]);else{const w=se(c),P=ce(c);if(w||P){const j=()=>{if(e.f){const N=w?m(c)?d[c]:a[c]:c.value;if(r)V(N)&&us(N,i);else if(V(N))N.includes(i)||N.push(i);else if(w)a[c]=[i],m(c)&&(d[c]=a[c]);else{const T=[i];c.value=T,e.k&&(a[e.k]=T)}}else w?(a[c]=o,m(c)&&(d[c]=o)):P&&(c.value=o,e.k&&(a[e.k]=o))};if(o){const N=()=>{j(),pn.delete(e)};N.id=-1,pn.set(e,N),ve(N,n)}else Bs(e),j()}}}function Bs(e){const t=pn.get(e);t&&(t.flags|=8,pn.delete(e))}Rn().requestIdleCallback;Rn().cancelIdleCallback;const Wt=e=>!!e.type.__asyncLoader,si=e=>e.type.__isKeepAlive;function Vo(e,t){ri(e,"a",t)}function jo(e,t){ri(e,"da",t)}function ri(e,t,n=le){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Cn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)si(r.parent.vnode)&&Uo(s,t,n,r),r=r.parent}}function Uo(e,t,n,s){const r=Cn(t,e,s,!0);ii(()=>{us(s[t],r)},n)}function Cn(e,t,n=le,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{ke();const l=nn(n),c=Ve(t,n,e,o);return l(),ze(),c});return s?r.unshift(i):r.push(i),i}}const Qe=e=>(t,n=le)=>{(!Zt||e==="sp")&&Cn(e,(...s)=>t(...s),n)},Go=Qe("bm"),Ko=Qe("m"),Wo=Qe("bu"),$o=Qe("u"),qo=Qe("bum"),ii=Qe("um"),ko=Qe("sp"),zo=Qe("rtg"),Jo=Qe("rtc");function Qo(e,t=le){Cn("ec",e,t)}const Yo="components";function Xo(e,t){return el(Yo,e,!0,t)||e}const Zo=Symbol.for("v-ndc");function el(e,t,n=!0,s=!1){const r=Ae||le;if(r){const i=r.type;{const l=Wl(i,!1);if(l&&(l===t||l===Re(t)||l===xn(Re(t))))return i}const o=Vs(r[e]||i[e],t)||Vs(r.appContext[e],t);return!o&&s?i:o}}function Vs(e,t){return e&&(e[t]||e[Re(t)]||e[xn(Re(t))])}const Yn=e=>e?Ci(e)?Os(e):Yn(e.parent):null,$t=fe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Yn(e.parent),$root:e=>Yn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>li(e),$forceUpdate:e=>e.f||(e.f=()=>{xs(e.update)}),$nextTick:e=>e.n||(e.n=Yr.bind(e.proxy)),$watch:e=>al.bind(e)}),Fn=(e,t)=>e!==X&&!e.__isScriptSetup&&q(e,t),tl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;if(t[0]!=="$"){const g=o[t];if(g!==void 0)switch(g){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Fn(s,t))return o[t]=1,s[t];if(r!==X&&q(r,t))return o[t]=2,r[t];if(q(i,t))return o[t]=3,i[t];if(n!==X&&q(n,t))return o[t]=4,n[t];Xn&&(o[t]=0)}}const h=$t[t];let a,d;if(h)return t==="$attrs"&&oe(e.attrs,"get",""),h(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(n!==X&&q(n,t))return o[t]=4,n[t];if(d=c.config.globalProperties,q(d,t))return d[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Fn(r,t)?(r[t]=n,!0):s!==X&&q(s,t)?(s[t]=n,!0):q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:o}},l){let c;return!!(n[l]||e!==X&&l[0]!=="$"&&q(e,l)||Fn(t,l)||q(i,l)||q(s,l)||q($t,l)||q(r.config.globalProperties,l)||(c=o.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function js(e){return V(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Xn=!0;function nl(e){const t=li(e),n=e.proxy,s=e.ctx;Xn=!1,t.beforeCreate&&Us(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:h,created:a,beforeMount:d,mounted:g,beforeUpdate:m,updated:w,activated:P,deactivated:j,beforeDestroy:N,beforeUnmount:T,destroyed:L,unmounted:I,render:J,renderTracked:ie,renderTriggered:ee,errorCaptured:Oe,serverPrefetch:Ye,expose:Pe,inheritAttrs:Xe,components:ft,directives:Te,filters:Dt}=t;if(h&&sl(h,s,null),o)for(const z in o){const K=o[z];B(K)&&(s[z]=K.bind(n))}if(r){const z=r.call(n,n);Z(z)&&(e.data=An(z))}if(Xn=!0,i)for(const z in i){const K=i[z],je=B(K)?K.bind(n,n):B(K.get)?K.get.bind(n,n):Be,Ze=!B(K)&&B(K.set)?K.set.bind(n):Be,Ie=ye({get:je,set:Ze});Object.defineProperty(s,z,{enumerable:!0,configurable:!0,get:()=>Ie.value,set:he=>Ie.value=he})}if(l)for(const z in l)oi(l[z],s,n,z);if(c){const z=B(c)?c.call(n):c;Reflect.ownKeys(z).forEach(K=>{ln(K,z[K])})}a&&Us(a,e,"c");function re(z,K){V(K)?K.forEach(je=>z(je.bind(n))):K&&z(K.bind(n))}if(re(Go,d),re(Ko,g),re(Wo,m),re($o,w),re(Vo,P),re(jo,j),re(Qo,Oe),re(Jo,ie),re(zo,ee),re(qo,T),re(ii,I),re(ko,Ye),V(Pe))if(Pe.length){const z=e.exposed||(e.exposed={});Pe.forEach(K=>{Object.defineProperty(z,K,{get:()=>n[K],set:je=>n[K]=je,enumerable:!0})})}else e.exposed||(e.exposed={});J&&e.render===Be&&(e.render=J),Xe!=null&&(e.inheritAttrs=Xe),ft&&(e.components=ft),Te&&(e.directives=Te),Ye&&ni(e)}function sl(e,t,n=Be){V(e)&&(e=Zn(e));for(const s in e){const r=e[s];let i;Z(r)?"default"in r?i=qe(r.from||s,r.default,!0):i=qe(r.from||s):i=qe(r),ce(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function Us(e,t,n){Ve(V(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function oi(e,t,n,s){let r=s.includes(".")?ui(n,s):()=>n[s];if(se(e)){const i=t[e];B(i)&&cn(r,i)}else if(B(e))cn(r,e.bind(n));else if(Z(e))if(V(e))e.forEach(i=>oi(i,t,n,s));else{const i=B(e.handler)?e.handler.bind(n):t[e.handler];B(i)&&cn(r,i,e)}}function li(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(h=>gn(c,h,o,!0)),gn(c,t,o)),Z(t)&&i.set(t,c),c}function gn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&gn(e,i,n,!0),r&&r.forEach(o=>gn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=rl[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const rl={data:Gs,props:Ks,emits:Ks,methods:Vt,computed:Vt,beforeCreate:ue,created:ue,beforeMount:ue,mounted:ue,beforeUpdate:ue,updated:ue,beforeDestroy:ue,beforeUnmount:ue,destroyed:ue,unmounted:ue,activated:ue,deactivated:ue,errorCaptured:ue,serverPrefetch:ue,components:Vt,directives:Vt,watch:ol,provide:Gs,inject:il};function Gs(e,t){return t?e?function(){return fe(B(e)?e.call(this,this):e,B(t)?t.call(this,this):t)}:t:e}function il(e,t){return Vt(Zn(e),Zn(t))}function Zn(e){if(V(e)){const t={};for(let n=0;n1)return n&&B(t)?t.call(s&&s.proxy):t}}const fl=Symbol.for("v-scx"),ul=()=>qe(fl);function cn(e,t,n){return fi(e,t,n)}function fi(e,t,n=X){const{immediate:s,deep:r,flush:i,once:o}=n,l=fe({},n),c=t&&s||!t&&i!=="post";let h;if(Zt){if(i==="sync"){const m=ul();h=m.__watcherHandles||(m.__watcherHandles=[])}else if(!c){const m=()=>{};return m.stop=Be,m.resume=Be,m.pause=Be,m}}const a=le;l.call=(m,w,P)=>Ve(m,a,w,P);let d=!1;i==="post"?l.scheduler=m=>{ve(m,a&&a.suspense)}:i!=="sync"&&(d=!0,l.scheduler=(m,w)=>{w?m():xs(m)}),l.augmentJob=m=>{t&&(m.flags|=4),d&&(m.flags|=2,a&&(m.id=a.uid,m.i=a))};const g=Io(e,t,l);return Zt&&(h?h.push(g):c&&g()),g}function al(e,t,n){const s=this.proxy,r=se(e)?e.includes(".")?ui(s,e):()=>s[e]:e.bind(s,s);let i;B(t)?i=t:(i=t.handler,n=t);const o=nn(this),l=fi(r,i.bind(s),n);return o(),l}function ui(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Re(t)}Modifiers`]||e[`${_t(t)}Modifiers`];function hl(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||X;let r=n;const i=t.startsWith("update:"),o=i&&dl(s,t.slice(7));o&&(o.trim&&(r=n.map(a=>se(a)?a.trim():a)),o.number&&(r=n.map(Ji)));let l,c=s[l=Tn(t)]||s[l=Tn(Re(t))];!c&&i&&(c=s[l=Tn(_t(t))]),c&&Ve(c,e,6,r);const h=s[l+"Once"];if(h){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ve(h,e,6,r)}}const pl=new WeakMap;function ai(e,t,n=!1){const s=n?pl:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!B(e)){const c=h=>{const a=ai(h,t,!0);a&&(l=!0,fe(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(Z(e)&&s.set(e,null),null):(V(i)?i.forEach(c=>o[c]=null):fe(o,i),Z(e)&&s.set(e,o),o)}function wn(e,t){return!e||!yn(t)?!1:(t=t.slice(2).replace(/Once$/,""),q(e,t[0].toLowerCase()+t.slice(1))||q(e,_t(t))||q(e,t))}function Ws(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:h,renderCache:a,props:d,data:g,setupState:m,ctx:w,inheritAttrs:P}=e,j=hn(e);let N,T;try{if(n.shapeFlag&4){const I=r||s,J=I;N=He(h.call(J,I,a,d,m,g,w)),T=l}else{const I=t;N=He(I.length>1?I(d,{attrs:l,slots:o,emit:c}):I(d,null)),T=t.props?l:gl(l)}}catch(I){qt.length=0,Sn(I,e,1),N=xe(Tt)}let L=N;if(T&&P!==!1){const I=Object.keys(T),{shapeFlag:J}=L;I.length&&J&7&&(i&&I.some(fs)&&(T=ml(T,i)),L=It(L,T,!1,!0))}return n.dirs&&(L=It(L,null,!1,!0),L.dirs=L.dirs?L.dirs.concat(n.dirs):n.dirs),n.transition&&Rs(L,n.transition),N=L,hn(j),N}const gl=e=>{let t;for(const n in e)(n==="class"||n==="style"||yn(n))&&((t||(t={}))[n]=e[n]);return t},ml=(e,t)=>{const n={};for(const s in e)(!fs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function _l(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,h=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?$s(s,o,h):!!o;if(c&8){const a=t.dynamicProps;for(let d=0;dObject.create(di),pi=e=>Object.getPrototypeOf(e)===di;function yl(e,t,n,s=!1){const r={},i=hi();e.propsDefaults=Object.create(null),gi(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:kr(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function bl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=$(r),[c]=e.propsOptions;let h=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let d=0;d{c=!0;const[g,m]=mi(d,t,!0);fe(o,g),m&&l.push(...m)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return Z(e)&&s.set(e,At),At;if(V(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",Cs=e=>V(e)?e.map(He):[He(e)],xl=(e,t,n)=>{if(t._n)return t;const s=Lo((...r)=>Cs(t(...r)),n);return s._c=!1,s},_i=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Ss(r))continue;const i=e[r];if(B(i))t[r]=xl(r,i,s);else if(i!=null){const o=Cs(i);t[r]=()=>o}}},vi=(e,t)=>{const n=Cs(t);e.slots.default=()=>n},yi=(e,t,n)=>{for(const s in t)(n||!Ss(s))&&(e[s]=t[s])},Rl=(e,t,n)=>{const s=e.slots=hi();if(e.vnode.shapeFlag&32){const r=t._;r?(yi(s,t,n),n&&Pr(s,"_",r,!0)):_i(t,s)}else t&&vi(e,t)},Al=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=X;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:yi(r,t,n):(i=!t.$stable,_i(t,r)),o=t}else t&&(vi(e,t),o={default:1});if(i)for(const l in r)!Ss(l)&&o[l]==null&&delete r[l]},ve=Pl;function Sl(e){return Cl(e)}function Cl(e,t){const n=Rn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:h,setElementText:a,parentNode:d,nextSibling:g,setScopeId:m=Be,insertStaticContent:w}=e,P=(f,u,p,v=null,b=null,_=null,A=void 0,R=null,x=!!u.dynamicChildren)=>{if(f===u)return;f&&!Ht(f,u)&&(v=y(f),he(f,b,_,!0),f=null),u.patchFlag===-2&&(x=!1,u.dynamicChildren=null);const{type:E,ref:F,shapeFlag:C}=u;switch(E){case On:j(f,u,p,v);break;case Tt:N(f,u,p,v);break;case Bn:f==null&&T(u,p,v,A);break;case We:ft(f,u,p,v,b,_,A,R,x);break;default:C&1?J(f,u,p,v,b,_,A,R,x):C&6?Te(f,u,p,v,b,_,A,R,x):(C&64||C&128)&&E.process(f,u,p,v,b,_,A,R,x,M)}F!=null&&b?Kt(F,f&&f.ref,_,u||f,!u):F==null&&f&&f.ref!=null&&Kt(f.ref,null,_,f,!0)},j=(f,u,p,v)=>{if(f==null)s(u.el=l(u.children),p,v);else{const b=u.el=f.el;u.children!==f.children&&h(b,u.children)}},N=(f,u,p,v)=>{f==null?s(u.el=c(u.children||""),p,v):u.el=f.el},T=(f,u,p,v)=>{[f.el,f.anchor]=w(f.children,u,p,v,f.el,f.anchor)},L=({el:f,anchor:u},p,v)=>{let b;for(;f&&f!==u;)b=g(f),s(f,p,v),f=b;s(u,p,v)},I=({el:f,anchor:u})=>{let p;for(;f&&f!==u;)p=g(f),r(f),f=p;r(u)},J=(f,u,p,v,b,_,A,R,x)=>{if(u.type==="svg"?A="svg":u.type==="math"&&(A="mathml"),f==null)ie(u,p,v,b,_,A,R,x);else{const E=f.el&&f.el._isVueCE?f.el:null;try{E&&E._beginPatch(),Ye(f,u,b,_,A,R,x)}finally{E&&E._endPatch()}}},ie=(f,u,p,v,b,_,A,R)=>{let x,E;const{props:F,shapeFlag:C,transition:D,dirs:H}=f;if(x=f.el=o(f.type,_,F&&F.is,F),C&8?a(x,f.children):C&16&&Oe(f.children,x,null,v,b,Hn(f,_),A,R),H&&at(f,null,v,"created"),ee(x,f,f.scopeId,A,v),F){for(const Q in F)Q!=="value"&&!jt(Q)&&i(x,Q,null,F[Q],_,v);"value"in F&&i(x,"value",null,F.value,_),(E=F.onVnodeBeforeMount)&&Le(E,v,f)}H&&at(f,null,v,"beforeMount");const G=wl(b,D);G&&D.beforeEnter(x),s(x,u,p),((E=F&&F.onVnodeMounted)||G||H)&&ve(()=>{E&&Le(E,v,f),G&&D.enter(x),H&&at(f,null,v,"mounted")},b)},ee=(f,u,p,v,b)=>{if(p&&m(f,p),v)for(let _=0;_{for(let E=x;E{const R=u.el=f.el;let{patchFlag:x,dynamicChildren:E,dirs:F}=u;x|=f.patchFlag&16;const C=f.props||X,D=u.props||X;let H;if(p&&dt(p,!1),(H=D.onVnodeBeforeUpdate)&&Le(H,p,u,f),F&&at(u,f,p,"beforeUpdate"),p&&dt(p,!0),(C.innerHTML&&D.innerHTML==null||C.textContent&&D.textContent==null)&&a(R,""),E?Pe(f.dynamicChildren,E,R,p,v,Hn(u,b),_):A||K(f,u,R,null,p,v,Hn(u,b),_,!1),x>0){if(x&16)Xe(R,C,D,p,b);else if(x&2&&C.class!==D.class&&i(R,"class",null,D.class,b),x&4&&i(R,"style",C.style,D.style,b),x&8){const G=u.dynamicProps;for(let Q=0;Q{H&&Le(H,p,u,f),F&&at(u,f,p,"updated")},v)},Pe=(f,u,p,v,b,_,A)=>{for(let R=0;R{if(u!==p){if(u!==X)for(const _ in u)!jt(_)&&!(_ in p)&&i(f,_,u[_],null,b,v);for(const _ in p){if(jt(_))continue;const A=p[_],R=u[_];A!==R&&_!=="value"&&i(f,_,R,A,b,v)}"value"in p&&i(f,"value",u.value,p.value,b)}},ft=(f,u,p,v,b,_,A,R,x)=>{const E=u.el=f?f.el:l(""),F=u.anchor=f?f.anchor:l("");let{patchFlag:C,dynamicChildren:D,slotScopeIds:H}=u;H&&(R=R?R.concat(H):H),f==null?(s(E,p,v),s(F,p,v),Oe(u.children||[],p,F,b,_,A,R,x)):C>0&&C&64&&D&&f.dynamicChildren?(Pe(f.dynamicChildren,D,p,b,_,A,R),(u.key!=null||b&&u===b.subTree)&&bi(f,u,!0)):K(f,u,p,F,b,_,A,R,x)},Te=(f,u,p,v,b,_,A,R,x)=>{u.slotScopeIds=R,f==null?u.shapeFlag&512?b.ctx.activate(u,p,v,A,x):Dt(u,p,v,b,_,A,x):vt(f,u,x)},Dt=(f,u,p,v,b,_,A)=>{const R=f.component=Bl(f,v,b);if(si(f)&&(R.ctx.renderer=M),jl(R,!1,A),R.asyncDep){if(b&&b.registerDep(R,re,A),!f.el){const x=R.subTree=xe(Tt);N(null,x,u,p),f.placeholder=x.el}}else re(R,f,u,p,b,_,A)},vt=(f,u,p)=>{const v=u.component=f.component;if(_l(f,u,p))if(v.asyncDep&&!v.asyncResolved){z(v,u,p);return}else v.next=u,v.update();else u.el=f.el,v.vnode=u},re=(f,u,p,v,b,_,A)=>{const R=()=>{if(f.isMounted){let{next:C,bu:D,u:H,parent:G,vnode:Q}=f;{const Me=Ei(f);if(Me){C&&(C.el=Q.el,z(f,C,A)),Me.asyncDep.then(()=>{f.isUnmounted||R()});return}}let k=C,pe;dt(f,!1),C?(C.el=Q.el,z(f,C,A)):C=Q,D&&In(D),(pe=C.props&&C.props.onVnodeBeforeUpdate)&&Le(pe,G,C,Q),dt(f,!0);const ge=Ws(f),Ne=f.subTree;f.subTree=ge,P(Ne,ge,d(Ne.el),y(Ne),f,b,_),C.el=ge.el,k===null&&vl(f,ge.el),H&&ve(H,b),(pe=C.props&&C.props.onVnodeUpdated)&&ve(()=>Le(pe,G,C,Q),b)}else{let C;const{el:D,props:H}=u,{bm:G,m:Q,parent:k,root:pe,type:ge}=f,Ne=Wt(u);dt(f,!1),G&&In(G),!Ne&&(C=H&&H.onVnodeBeforeMount)&&Le(C,k,u),dt(f,!0);{pe.ce&&pe.ce._def.shadowRoot!==!1&&pe.ce._injectChildStyle(ge);const Me=f.subTree=Ws(f);P(null,Me,p,v,f,b,_),u.el=Me.el}if(Q&&ve(Q,b),!Ne&&(C=H&&H.onVnodeMounted)){const Me=u;ve(()=>Le(C,k,Me),b)}(u.shapeFlag&256||k&&Wt(k.vnode)&&k.vnode.shapeFlag&256)&&f.a&&ve(f.a,b),f.isMounted=!0,u=p=v=null}};f.scope.on();const x=f.effect=new Mr(R);f.scope.off();const E=f.update=x.run.bind(x),F=f.job=x.runIfDirty.bind(x);F.i=f,F.id=f.uid,x.scheduler=()=>xs(F),dt(f,!0),E()},z=(f,u,p)=>{u.component=f;const v=f.vnode.props;f.vnode=u,f.next=null,bl(f,u.props,v,p),Al(f,u.children,p),ke(),Hs(f),ze()},K=(f,u,p,v,b,_,A,R,x=!1)=>{const E=f&&f.children,F=f?f.shapeFlag:0,C=u.children,{patchFlag:D,shapeFlag:H}=u;if(D>0){if(D&128){Ze(E,C,p,v,b,_,A,R,x);return}else if(D&256){je(E,C,p,v,b,_,A,R,x);return}}H&8?(F&16&&Ee(E,b,_),C!==E&&a(p,C)):F&16?H&16?Ze(E,C,p,v,b,_,A,R,x):Ee(E,b,_,!0):(F&8&&a(p,""),H&16&&Oe(C,p,v,b,_,A,R,x))},je=(f,u,p,v,b,_,A,R,x)=>{f=f||At,u=u||At;const E=f.length,F=u.length,C=Math.min(E,F);let D;for(D=0;DF?Ee(f,b,_,!0,!1,C):Oe(u,p,v,b,_,A,R,x,C)},Ze=(f,u,p,v,b,_,A,R,x)=>{let E=0;const F=u.length;let C=f.length-1,D=F-1;for(;E<=C&&E<=D;){const H=f[E],G=u[E]=x?st(u[E]):He(u[E]);if(Ht(H,G))P(H,G,p,null,b,_,A,R,x);else break;E++}for(;E<=C&&E<=D;){const H=f[C],G=u[D]=x?st(u[D]):He(u[D]);if(Ht(H,G))P(H,G,p,null,b,_,A,R,x);else break;C--,D--}if(E>C){if(E<=D){const H=D+1,G=HD)for(;E<=C;)he(f[E],b,_,!0),E++;else{const H=E,G=E,Q=new Map;for(E=G;E<=D;E++){const _e=u[E]=x?st(u[E]):He(u[E]);_e.key!=null&&Q.set(_e.key,E)}let k,pe=0;const ge=D-G+1;let Ne=!1,Me=0;const Lt=new Array(ge);for(E=0;E=ge){he(_e,b,_,!0);continue}let De;if(_e.key!=null)De=Q.get(_e.key);else for(k=G;k<=D;k++)if(Lt[k-G]===0&&Ht(_e,u[k])){De=k;break}De===void 0?he(_e,b,_,!0):(Lt[De-G]=E+1,De>=Me?Me=De:Ne=!0,P(_e,u[De],p,null,b,_,A,R,x),pe++)}const Is=Ne?Ol(Lt):At;for(k=Is.length-1,E=ge-1;E>=0;E--){const _e=G+E,De=u[_e],Ns=u[_e+1],Ms=_e+1{const{el:_,type:A,transition:R,children:x,shapeFlag:E}=f;if(E&6){Ie(f.component.subTree,u,p,v);return}if(E&128){f.suspense.move(u,p,v);return}if(E&64){A.move(f,u,p,M);return}if(A===We){s(_,u,p);for(let C=0;CR.enter(_),b);else{const{leave:C,delayLeave:D,afterLeave:H}=R,G=()=>{f.ctx.isUnmounted?r(_):s(_,u,p)},Q=()=>{_._isLeaving&&_[Bo](!0),C(_,()=>{G(),H&&H()})};D?D(_,G,Q):Q()}else s(_,u,p)},he=(f,u,p,v=!1,b=!1)=>{const{type:_,props:A,ref:R,children:x,dynamicChildren:E,shapeFlag:F,patchFlag:C,dirs:D,cacheIndex:H}=f;if(C===-2&&(b=!1),R!=null&&(ke(),Kt(R,null,p,f,!0),ze()),H!=null&&(u.renderCache[H]=void 0),F&256){u.ctx.deactivate(f);return}const G=F&1&&D,Q=!Wt(f);let k;if(Q&&(k=A&&A.onVnodeBeforeUnmount)&&Le(k,u,f),F&6)ut(f.component,p,v);else{if(F&128){f.suspense.unmount(p,v);return}G&&at(f,null,u,"beforeUnmount"),F&64?f.type.remove(f,u,p,M,v):E&&!E.hasOnce&&(_!==We||C>0&&C&64)?Ee(E,u,p,!1,!0):(_===We&&C&384||!b&&F&16)&&Ee(x,u,p),v&&yt(f)}(Q&&(k=A&&A.onVnodeUnmounted)||G)&&ve(()=>{k&&Le(k,u,f),G&&at(f,null,u,"unmounted")},p)},yt=f=>{const{type:u,el:p,anchor:v,transition:b}=f;if(u===We){bt(p,v);return}if(u===Bn){I(f);return}const _=()=>{r(p),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(f.shapeFlag&1&&b&&!b.persisted){const{leave:A,delayLeave:R}=b,x=()=>A(p,_);R?R(f.el,_,x):x()}else _()},bt=(f,u)=>{let p;for(;f!==u;)p=g(f),r(f),f=p;r(u)},ut=(f,u,p)=>{const{bum:v,scope:b,job:_,subTree:A,um:R,m:x,a:E}=f;ks(x),ks(E),v&&In(v),b.stop(),_&&(_.flags|=8,he(A,f,u,p)),R&&ve(R,u),ve(()=>{f.isUnmounted=!0},u)},Ee=(f,u,p,v=!1,b=!1,_=0)=>{for(let A=_;A{if(f.shapeFlag&6)return y(f.component.subTree);if(f.shapeFlag&128)return f.suspense.next();const u=g(f.anchor||f.el),p=u&&u[Fo];return p?g(p):u};let O=!1;const S=(f,u,p)=>{f==null?u._vnode&&he(u._vnode,null,null,!0):P(u._vnode||null,f,u,null,null,null,p),u._vnode=f,O||(O=!0,Hs(),Zr(),O=!1)},M={p:P,um:he,m:Ie,r:yt,mt:Dt,mc:Oe,pc:K,pbc:Pe,n:y,o:e};return{render:S,hydrate:void 0,createApp:cl(S)}}function Hn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function dt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function wl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function bi(e,t,n=!1){const s=e.children,r=t.children;if(V(s)&&V(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Ei(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ei(t)}function ks(e){if(e)for(let t=0;te.__isSuspense;function Pl(e,t){t&&t.pendingBranch?V(e)?t.effects.push(...e):t.effects.push(e):Do(e)}const We=Symbol.for("v-fgt"),On=Symbol.for("v-txt"),Tt=Symbol.for("v-cmt"),Bn=Symbol.for("v-stc"),qt=[];let be=null;function Ri(e=!1){qt.push(be=e?null:[])}function Tl(){qt.pop(),be=qt[qt.length-1]||null}let Xt=1;function mn(e,t=!1){Xt+=e,e<0&&be&&t&&(be.hasOnce=!0)}function Ai(e){return e.dynamicChildren=Xt>0?be||At:null,Tl(),Xt>0&&be&&be.push(e),e}function Il(e,t,n,s,r,i){return Ai(pt(e,t,n,s,r,i,!0))}function Nl(e,t,n,s,r){return Ai(xe(e,t,n,s,r,!0))}function _n(e){return e?e.__v_isVNode===!0:!1}function Ht(e,t){return e.type===t.type&&e.key===t.key}const Si=({key:e})=>e??null,fn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?se(e)||ce(e)||B(e)?{i:Ae,r:e,k:t,f:!!n}:e:null);function pt(e,t=null,n=null,s=0,r=null,i=e===We?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Si(t),ref:t&&fn(t),scopeId:ti,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Ae};return l?(ws(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=se(n)?8:16),Xt>0&&!o&&be&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&be.push(c),c}const xe=Ml;function Ml(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Zo)&&(e=Tt),_n(e)){const l=It(e,t,!0);return n&&ws(l,n),Xt>0&&!i&&be&&(l.shapeFlag&6?be[be.indexOf(e)]=l:be.push(l)),l.patchFlag=-2,l}if($l(e)&&(e=e.__vccOpts),t){t=Dl(t);let{class:l,style:c}=t;l&&!se(l)&&(t.class=hs(l)),Z(c)&&(Es(c)&&!V(c)&&(c=fe({},c)),t.style=ds(c))}const o=se(e)?1:xi(e)?128:Ho(e)?64:Z(e)?4:B(e)?2:0;return pt(e,t,n,s,r,o,i,!0)}function Dl(e){return e?Es(e)||pi(e)?fe({},e):e:null}function It(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,h=t?Ll(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:h,key:h&&Si(h),ref:t&&t.ref?n&&i?V(i)?i.concat(fn(t)):[i,fn(t)]:fn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==We?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&It(e.ssContent),ssFallback:e.ssFallback&&It(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Rs(a,c.clone(a)),a}function ts(e=" ",t=0){return xe(On,null,e,t)}function He(e){return e==null||typeof e=="boolean"?xe(Tt):V(e)?xe(We,null,e.slice()):_n(e)?st(e):xe(On,null,String(e))}function st(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:It(e)}function ws(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(V(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),ws(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!pi(t)?t._ctx=Ae:r===3&&Ae&&(Ae.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else B(t)?(t={default:t,_ctx:Ae},n=32):(t=String(t),s&64?(n=16,t=[ts(t)]):n=8);e.children=t,e.shapeFlag|=n}function Ll(...e){const t={};for(let n=0;nle||Ae;let vn,ns;{const e=Rn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};vn=t("__VUE_INSTANCE_SETTERS__",n=>le=n),ns=t("__VUE_SSR_SETTERS__",n=>Zt=n)}const nn=e=>{const t=le;return vn(e),e.scope.on(),()=>{e.scope.off(),vn(t)}},zs=()=>{le&&le.scope.off(),vn(null)};function Ci(e){return e.vnode.shapeFlag&4}let Zt=!1;function jl(e,t=!1,n=!1){t&&ns(t);const{props:s,children:r}=e.vnode,i=Ci(e);yl(e,s,i,t),Rl(e,r,n||t);const o=i?Ul(e,t):void 0;return t&&ns(!1),o}function Ul(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,tl);const{setup:s}=n;if(s){ke();const r=e.setupContext=s.length>1?Kl(e):null,i=nn(e),o=tn(s,e,0,[e.props,r]),l=Cr(o);if(ze(),i(),(l||e.sp)&&!Wt(e)&&ni(e),l){if(o.then(zs,zs),t)return o.then(c=>{Js(e,c)}).catch(c=>{Sn(c,e,0)});e.asyncDep=o}else Js(e,o)}else wi(e)}function Js(e,t,n){B(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Z(t)&&(e.setupState=Jr(t)),wi(e)}function wi(e,t,n){const s=e.type;e.render||(e.render=s.render||Be);{const r=nn(e);ke();try{nl(e)}finally{ze(),r()}}}const Gl={get(e,t){return oe(e,"get",""),e[t]}};function Kl(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Gl),slots:e.slots,emit:e.emit,expose:t}}function Os(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Jr(Ro(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in $t)return $t[n](e)},has(t,n){return n in t||n in $t}})):e.proxy}function Wl(e,t=!0){return B(e)?e.displayName||e.name:e.name||t&&e.__name}function $l(e){return B(e)&&"__vccOpts"in e}const ye=(e,t)=>Po(e,t,Zt);function Oi(e,t,n){try{mn(-1);const s=arguments.length;return s===2?Z(t)&&!V(t)?_n(t)?xe(e,null,[t]):xe(e,t):xe(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&_n(n)&&(n=[n]),xe(e,t,n))}finally{mn(1)}}const ql="3.5.25";/** +* @vue/runtime-dom v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ss;const Qs=typeof window<"u"&&window.trustedTypes;if(Qs)try{ss=Qs.createPolicy("vue",{createHTML:e=>e})}catch{}const Pi=ss?e=>ss.createHTML(e):e=>e,kl="http://www.w3.org/2000/svg",zl="http://www.w3.org/1998/Math/MathML",Ke=typeof document<"u"?document:null,Ys=Ke&&Ke.createElement("template"),Jl={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ke.createElementNS(kl,e):t==="mathml"?Ke.createElementNS(zl,e):n?Ke.createElement(e,{is:n}):Ke.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ke.createTextNode(e),createComment:e=>Ke.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ke.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Ys.innerHTML=Pi(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Ys.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ql=Symbol("_vtc");function Yl(e,t,n){const s=e[Ql];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Xs=Symbol("_vod"),Xl=Symbol("_vsh"),Zl=Symbol(""),ec=/(?:^|;)\s*display\s*:/;function tc(e,t,n){const s=e.style,r=se(n);let i=!1;if(n&&!r){if(t)if(se(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&un(s,l,"")}else for(const o in t)n[o]==null&&un(s,o,"");for(const o in n)o==="display"&&(i=!0),un(s,o,n[o])}else if(r){if(t!==n){const o=s[Zl];o&&(n+=";"+o),s.cssText=n,i=ec.test(n)}}else t&&e.removeAttribute("style");Xs in e&&(e[Xs]=i?s.display:"",e[Xl]&&(s.display="none"))}const Zs=/\s*!important$/;function un(e,t,n){if(V(n))n.forEach(s=>un(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=nc(e,t);Zs.test(n)?e.setProperty(_t(s),n.replace(Zs,""),"important"):e[s]=n}}const er=["Webkit","Moz","ms"],Vn={};function nc(e,t){const n=Vn[t];if(n)return n;let s=Re(t);if(s!=="filter"&&s in e)return Vn[t]=s;s=xn(s);for(let r=0;rjn||(lc.then(()=>jn=0),jn=Date.now());function fc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Ve(uc(s,n.value),t,5,[s])};return n.value=e,n.attached=cc(),n}function uc(e,t){if(V(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const or=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ac=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?Yl(e,s,o):t==="style"?tc(e,n,s):yn(t)?fs(t)||ic(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):dc(e,t,s,o))?(sr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&nr(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!se(s))?sr(e,Re(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),nr(e,t,s,o))};function dc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&or(t)&&B(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return or(t)&&se(n)?!1:t in e}const hc=fe({patchProp:ac},Jl);let lr;function pc(){return lr||(lr=Sl(hc))}const gc=((...e)=>{const t=pc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=_c(s);if(!r)return;const i=t._component;!B(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,mc(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function mc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function _c(e){return se(e)?document.querySelector(e):e}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Rt=typeof document<"u";function Ti(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function vc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Ti(e.default)}const W=Object.assign;function Un(e,t){const n={};for(const s in t){const r=t[s];n[s]=we(r)?r.map(e):e(r)}return n}const kt=()=>{},we=Array.isArray;function cr(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const Ii=/#/g,yc=/&/g,bc=/\//g,Ec=/=/g,xc=/\?/g,Ni=/\+/g,Rc=/%5B/g,Ac=/%5D/g,Mi=/%5E/g,Sc=/%60/g,Di=/%7B/g,Cc=/%7C/g,Li=/%7D/g,wc=/%20/g;function Ps(e){return e==null?"":encodeURI(""+e).replace(Cc,"|").replace(Rc,"[").replace(Ac,"]")}function Oc(e){return Ps(e).replace(Di,"{").replace(Li,"}").replace(Mi,"^")}function rs(e){return Ps(e).replace(Ni,"%2B").replace(wc,"+").replace(Ii,"%23").replace(yc,"%26").replace(Sc,"`").replace(Di,"{").replace(Li,"}").replace(Mi,"^")}function Pc(e){return rs(e).replace(Ec,"%3D")}function Tc(e){return Ps(e).replace(Ii,"%23").replace(xc,"%3F")}function Ic(e){return Tc(e).replace(bc,"%2F")}function en(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const Nc=/\/$/,Mc=e=>e.replace(Nc,"");function Gn(e,t,n="/"){let s,r={},i="",o="";const l=t.indexOf("#");let c=t.indexOf("?");return c=l>=0&&c>l?-1:c,c>=0&&(s=t.slice(0,c),i=t.slice(c,l>0?l:t.length),r=e(i.slice(1))),l>=0&&(s=s||t.slice(0,l),o=t.slice(l,t.length)),s=Hc(s??t,n),{fullPath:s+i+o,path:s,query:r,hash:en(o)}}function Dc(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function fr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Lc(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&Nt(t.matched[s],n.matched[r])&&Fi(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Nt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Fi(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Fc(e[n],t[n]))return!1;return!0}function Fc(e,t){return we(e)?ur(e,t):we(t)?ur(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function ur(e,t){return we(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function Hc(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let i=n.length-1,o,l;for(o=0;o1&&i--;else break;return n.slice(0,i).join("/")+"/"+s.slice(o).join("/")}const et={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let is=(function(e){return e.pop="pop",e.push="push",e})({}),Kn=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function Bc(e){if(!e)if(Rt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Mc(e)}const Vc=/^[^#]+#/;function jc(e,t){return e.replace(Vc,"#")+t}function Uc(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const Pn=()=>({left:window.scrollX,top:window.scrollY});function Gc(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Uc(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function ar(e,t){return(history.state?history.state.position-t:-1)+e}const os=new Map;function Kc(e,t){os.set(e,t)}function Wc(e){const t=os.get(e);return os.delete(e),t}function $c(e){return typeof e=="string"||e&&typeof e=="object"}function Hi(e){return typeof e=="string"||typeof e=="symbol"}let te=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const Bi=Symbol("");te.MATCHER_NOT_FOUND+"",te.NAVIGATION_GUARD_REDIRECT+"",te.NAVIGATION_ABORTED+"",te.NAVIGATION_CANCELLED+"",te.NAVIGATION_DUPLICATED+"";function Mt(e,t){return W(new Error,{type:e,[Bi]:!0},t)}function Ge(e,t){return e instanceof Error&&Bi in e&&(t==null||!!(e.type&t))}const qc=["params","query","hash"];function kc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of qc)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function zc(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&rs(r)):[s&&rs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function Jc(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=we(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const Qc=Symbol(""),hr=Symbol(""),Ts=Symbol(""),Vi=Symbol(""),ls=Symbol("");function Bt(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function rt(e,t,n,s,r,i=o=>o()){const o=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const h=g=>{g===!1?c(Mt(te.NAVIGATION_ABORTED,{from:n,to:t})):g instanceof Error?c(g):$c(g)?c(Mt(te.NAVIGATION_GUARD_REDIRECT,{from:t,to:g})):(o&&s.enterCallbacks[r]===o&&typeof g=="function"&&o.push(g),l())},a=i(()=>e.call(s&&s.instances[r],t,n,h));let d=Promise.resolve(a);e.length<3&&(d=d.then(h)),d.catch(g=>c(g))})}function Wn(e,t,n,s,r=i=>i()){const i=[];for(const o of e)for(const l in o.components){let c=o.components[l];if(!(t!=="beforeRouteEnter"&&!o.instances[l]))if(Ti(c)){const h=(c.__vccOpts||c)[t];h&&i.push(rt(h,n,s,o,l,r))}else{let h=c();i.push(()=>h.then(a=>{if(!a)throw new Error(`Couldn't resolve component "${l}" at "${o.path}"`);const d=vc(a)?a.default:a;o.mods[l]=a,o.components[l]=d;const g=(d.__vccOpts||d)[t];return g&&rt(g,n,s,o,l,r)()}))}}return i}function Yc(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let o=0;oNt(h,l))?s.push(l):n.push(l));const c=e.matched[o];c&&(t.matched.find(h=>Nt(h,c))||r.push(c))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let Xc=()=>location.protocol+"//"+location.host;function ji(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let o=r.includes(e.slice(i))?e.slice(i).length:1,l=r.slice(o);return l[0]!=="/"&&(l="/"+l),fr(l,"")}return fr(n,e)+s+r}function Zc(e,t,n,s){let r=[],i=[],o=null;const l=({state:g})=>{const m=ji(e,location),w=n.value,P=t.value;let j=0;if(g){if(n.value=m,t.value=g,o&&o===w){o=null;return}j=P?g.position-P.position:0}else s(m);r.forEach(N=>{N(n.value,w,{delta:j,type:is.pop,direction:j?j>0?Kn.forward:Kn.back:Kn.unknown})})};function c(){o=n.value}function h(g){r.push(g);const m=()=>{const w=r.indexOf(g);w>-1&&r.splice(w,1)};return i.push(m),m}function a(){if(document.visibilityState==="hidden"){const{history:g}=window;if(!g.state)return;g.replaceState(W({},g.state,{scroll:Pn()}),"")}}function d(){for(const g of i)g();i=[],window.removeEventListener("popstate",l),window.removeEventListener("pagehide",a),document.removeEventListener("visibilitychange",a)}return window.addEventListener("popstate",l),window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",a),{pauseListeners:c,listen:h,destroy:d}}function pr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?Pn():null}}function ef(e){const{history:t,location:n}=window,s={value:ji(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(c,h,a){const d=e.indexOf("#"),g=d>-1?(n.host&&document.querySelector("base")?e:e.slice(d))+c:Xc()+e+c;try{t[a?"replaceState":"pushState"](h,"",g),r.value=h}catch(m){console.error(m),n[a?"replace":"assign"](g)}}function o(c,h){i(c,W({},t.state,pr(r.value.back,c,r.value.forward,!0),h,{position:r.value.position}),!0),s.value=c}function l(c,h){const a=W({},r.value,t.state,{forward:c,scroll:Pn()});i(a.current,a,!0),i(c,W({},pr(s.value,c,null),{position:a.position+1},h),!1),s.value=c}return{location:s,state:r,push:l,replace:o}}function tf(e){e=Bc(e);const t=ef(e),n=Zc(e,t.state,t.location,t.replace);function s(i,o=!0){o||n.pauseListeners(),history.go(i)}const r=W({location:"",base:e,go:s,createHref:jc.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let gt=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var ne=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(ne||{});const nf={type:gt.Static,value:""},sf=/[a-zA-Z0-9_]/;function rf(e){if(!e)return[[]];if(e==="/")return[[nf]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(m){throw new Error(`ERR (${n})/"${h}": ${m}`)}let n=ne.Static,s=n;const r=[];let i;function o(){i&&r.push(i),i=[]}let l=0,c,h="",a="";function d(){h&&(n===ne.Static?i.push({type:gt.Static,value:h}):n===ne.Param||n===ne.ParamRegExp||n===ne.ParamRegExpEnd?(i.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${h}) must be alone in its segment. eg: '/:ids+.`),i.push({type:gt.Param,value:h,regexp:a,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),h="")}function g(){h+=c}for(;lt.length?t.length===1&&t[0]===ae.Static+ae.Segment?1:-1:0}function Ui(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const uf={strict:!1,end:!0,sensitive:!1};function af(e,t,n){const s=cf(rf(e.path),n),r=W(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function df(e,t){const n=[],s=new Map;t=cr(uf,t);function r(d){return s.get(d)}function i(d,g,m){const w=!m,P=vr(d);P.aliasOf=m&&m.record;const j=cr(t,d),N=[P];if("alias"in d){const I=typeof d.alias=="string"?[d.alias]:d.alias;for(const J of I)N.push(vr(W({},P,{components:m?m.record.components:P.components,path:J,aliasOf:m?m.record:P})))}let T,L;for(const I of N){const{path:J}=I;if(g&&J[0]!=="/"){const ie=g.record.path,ee=ie[ie.length-1]==="/"?"":"/";I.path=g.record.path+(J&&ee+J)}if(T=af(I,g,j),m?m.alias.push(T):(L=L||T,L!==T&&L.alias.push(T),w&&d.name&&!yr(T)&&o(d.name)),Gi(T)&&c(T),P.children){const ie=P.children;for(let ee=0;ee{o(L)}:kt}function o(d){if(Hi(d)){const g=s.get(d);g&&(s.delete(d),n.splice(n.indexOf(g),1),g.children.forEach(o),g.alias.forEach(o))}else{const g=n.indexOf(d);g>-1&&(n.splice(g,1),d.record.name&&s.delete(d.record.name),d.children.forEach(o),d.alias.forEach(o))}}function l(){return n}function c(d){const g=gf(d,n);n.splice(g,0,d),d.record.name&&!yr(d)&&s.set(d.record.name,d)}function h(d,g){let m,w={},P,j;if("name"in d&&d.name){if(m=s.get(d.name),!m)throw Mt(te.MATCHER_NOT_FOUND,{location:d});j=m.record.name,w=W(_r(g.params,m.keys.filter(L=>!L.optional).concat(m.parent?m.parent.keys.filter(L=>L.optional):[]).map(L=>L.name)),d.params&&_r(d.params,m.keys.map(L=>L.name))),P=m.stringify(w)}else if(d.path!=null)P=d.path,m=n.find(L=>L.re.test(P)),m&&(w=m.parse(P),j=m.record.name);else{if(m=g.name?s.get(g.name):n.find(L=>L.re.test(g.path)),!m)throw Mt(te.MATCHER_NOT_FOUND,{location:d,currentLocation:g});j=m.record.name,w=W({},g.params,d.params),P=m.stringify(w)}const N=[];let T=m;for(;T;)N.unshift(T.record),T=T.parent;return{name:j,path:P,params:w,matched:N,meta:pf(N)}}e.forEach(d=>i(d));function a(){n.length=0,s.clear()}return{addRoute:i,resolve:h,removeRoute:o,clearRoutes:a,getRoutes:l,getRecordMatcher:r}}function _r(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function vr(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:hf(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function hf(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function yr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function pf(e){return e.reduce((t,n)=>W(t,n.meta),{})}function gf(e,t){let n=0,s=t.length;for(;n!==s;){const i=n+s>>1;Ui(e,t[i])<0?s=i:n=i+1}const r=mf(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function mf(e){let t=e;for(;t=t.parent;)if(Gi(t)&&Ui(e,t)===0)return t}function Gi({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function br(e){const t=qe(Ts),n=qe(Vi),s=ye(()=>{const c=wt(e.to);return t.resolve(c)}),r=ye(()=>{const{matched:c}=s.value,{length:h}=c,a=c[h-1],d=n.matched;if(!a||!d.length)return-1;const g=d.findIndex(Nt.bind(null,a));if(g>-1)return g;const m=Er(c[h-2]);return h>1&&Er(a)===m&&d[d.length-1].path!==m?d.findIndex(Nt.bind(null,c[h-2])):g}),i=ye(()=>r.value>-1&&Ef(n.params,s.value.params)),o=ye(()=>r.value>-1&&r.value===n.matched.length-1&&Fi(n.params,s.value.params));function l(c={}){if(bf(c)){const h=t[wt(e.replace)?"replace":"push"](wt(e.to)).catch(kt);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>h),h}return Promise.resolve()}return{route:s,href:ye(()=>s.value.href),isActive:i,isExactActive:o,navigate:l}}function _f(e){return e.length===1?e[0]:e}const vf=As({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:br,setup(e,{slots:t}){const n=An(br(e)),{options:s}=qe(Ts),r=ye(()=>({[xr(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[xr(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&_f(t.default(n));return e.custom?i:Oi("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),yf=vf;function bf(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Ef(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!we(r)||r.length!==s.length||s.some((i,o)=>i.valueOf()!==r[o].valueOf()))return!1}return!0}function Er(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const xr=(e,t,n)=>e??t??n,xf=As({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=qe(ls),r=ye(()=>e.route||s.value),i=qe(hr,0),o=ye(()=>{let h=wt(i);const{matched:a}=r.value;let d;for(;(d=a[h])&&!d.components;)h++;return h}),l=ye(()=>r.value.matched[o.value]);ln(hr,ye(()=>o.value+1)),ln(Qc,l),ln(ls,r);const c=Ao();return cn(()=>[c.value,l.value,e.name],([h,a,d],[g,m,w])=>{a&&(a.instances[d]=h,m&&m!==a&&h&&h===g&&(a.leaveGuards.size||(a.leaveGuards=m.leaveGuards),a.updateGuards.size||(a.updateGuards=m.updateGuards))),h&&a&&(!m||!Nt(a,m)||!g)&&(a.enterCallbacks[d]||[]).forEach(P=>P(h))},{flush:"post"}),()=>{const h=r.value,a=e.name,d=l.value,g=d&&d.components[a];if(!g)return Rr(n.default,{Component:g,route:h});const m=d.props[a],w=m?m===!0?h.params:typeof m=="function"?m(h):m:null,j=Oi(g,W({},w,t,{onVnodeUnmounted:N=>{N.component.isUnmounted&&(d.instances[a]=null)},ref:c}));return Rr(n.default,{Component:j,route:h})||j}}});function Rr(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Rf=xf;function Af(e){const t=df(e.routes,e),n=e.parseQuery||zc,s=e.stringifyQuery||dr,r=e.history,i=Bt(),o=Bt(),l=Bt(),c=So(et);let h=et;Rt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const a=Un.bind(null,y=>""+y),d=Un.bind(null,Ic),g=Un.bind(null,en);function m(y,O){let S,M;return Hi(y)?(S=t.getRecordMatcher(y),M=O):M=y,t.addRoute(M,S)}function w(y){const O=t.getRecordMatcher(y);O&&t.removeRoute(O)}function P(){return t.getRoutes().map(y=>y.record)}function j(y){return!!t.getRecordMatcher(y)}function N(y,O){if(O=W({},O||c.value),typeof y=="string"){const p=Gn(n,y,O.path),v=t.resolve({path:p.path},O),b=r.createHref(p.fullPath);return W(p,v,{params:g(v.params),hash:en(p.hash),redirectedFrom:void 0,href:b})}let S;if(y.path!=null)S=W({},y,{path:Gn(n,y.path,O.path).path});else{const p=W({},y.params);for(const v in p)p[v]==null&&delete p[v];S=W({},y,{params:d(p)}),O.params=d(O.params)}const M=t.resolve(S,O),U=y.hash||"";M.params=a(g(M.params));const f=Dc(s,W({},y,{hash:Oc(U),path:M.path})),u=r.createHref(f);return W({fullPath:f,hash:U,query:s===dr?Jc(y.query):y.query||{}},M,{redirectedFrom:void 0,href:u})}function T(y){return typeof y=="string"?Gn(n,y,c.value.path):W({},y)}function L(y,O){if(h!==y)return Mt(te.NAVIGATION_CANCELLED,{from:O,to:y})}function I(y){return ee(y)}function J(y){return I(W(T(y),{replace:!0}))}function ie(y,O){const S=y.matched[y.matched.length-1];if(S&&S.redirect){const{redirect:M}=S;let U=typeof M=="function"?M(y,O):M;return typeof U=="string"&&(U=U.includes("?")||U.includes("#")?U=T(U):{path:U},U.params={}),W({query:y.query,hash:y.hash,params:U.path!=null?{}:y.params},U)}}function ee(y,O){const S=h=N(y),M=c.value,U=y.state,f=y.force,u=y.replace===!0,p=ie(S,M);if(p)return ee(W(T(p),{state:typeof p=="object"?W({},U,p.state):U,force:f,replace:u}),O||S);const v=S;v.redirectedFrom=O;let b;return!f&&Lc(s,M,S)&&(b=Mt(te.NAVIGATION_DUPLICATED,{to:v,from:M}),Ie(M,M,!0,!1)),(b?Promise.resolve(b):Pe(v,M)).catch(_=>Ge(_)?Ge(_,te.NAVIGATION_GUARD_REDIRECT)?_:Ze(_):K(_,v,M)).then(_=>{if(_){if(Ge(_,te.NAVIGATION_GUARD_REDIRECT))return ee(W({replace:u},T(_.to),{state:typeof _.to=="object"?W({},U,_.to.state):U,force:f}),O||v)}else _=ft(v,M,!0,u,U);return Xe(v,M,_),_})}function Oe(y,O){const S=L(y,O);return S?Promise.reject(S):Promise.resolve()}function Ye(y){const O=bt.values().next().value;return O&&typeof O.runWithContext=="function"?O.runWithContext(y):y()}function Pe(y,O){let S;const[M,U,f]=Yc(y,O);S=Wn(M.reverse(),"beforeRouteLeave",y,O);for(const p of M)p.leaveGuards.forEach(v=>{S.push(rt(v,y,O))});const u=Oe.bind(null,y,O);return S.push(u),Ee(S).then(()=>{S=[];for(const p of i.list())S.push(rt(p,y,O));return S.push(u),Ee(S)}).then(()=>{S=Wn(U,"beforeRouteUpdate",y,O);for(const p of U)p.updateGuards.forEach(v=>{S.push(rt(v,y,O))});return S.push(u),Ee(S)}).then(()=>{S=[];for(const p of f)if(p.beforeEnter)if(we(p.beforeEnter))for(const v of p.beforeEnter)S.push(rt(v,y,O));else S.push(rt(p.beforeEnter,y,O));return S.push(u),Ee(S)}).then(()=>(y.matched.forEach(p=>p.enterCallbacks={}),S=Wn(f,"beforeRouteEnter",y,O,Ye),S.push(u),Ee(S))).then(()=>{S=[];for(const p of o.list())S.push(rt(p,y,O));return S.push(u),Ee(S)}).catch(p=>Ge(p,te.NAVIGATION_CANCELLED)?p:Promise.reject(p))}function Xe(y,O,S){l.list().forEach(M=>Ye(()=>M(y,O,S)))}function ft(y,O,S,M,U){const f=L(y,O);if(f)return f;const u=O===et,p=Rt?history.state:{};S&&(M||u?r.replace(y.fullPath,W({scroll:u&&p&&p.scroll},U)):r.push(y.fullPath,U)),c.value=y,Ie(y,O,S,u),Ze()}let Te;function Dt(){Te||(Te=r.listen((y,O,S)=>{if(!ut.listening)return;const M=N(y),U=ie(M,ut.currentRoute.value);if(U){ee(W(U,{replace:!0,force:!0}),M).catch(kt);return}h=M;const f=c.value;Rt&&Kc(ar(f.fullPath,S.delta),Pn()),Pe(M,f).catch(u=>Ge(u,te.NAVIGATION_ABORTED|te.NAVIGATION_CANCELLED)?u:Ge(u,te.NAVIGATION_GUARD_REDIRECT)?(ee(W(T(u.to),{force:!0}),M).then(p=>{Ge(p,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&!S.delta&&S.type===is.pop&&r.go(-1,!1)}).catch(kt),Promise.reject()):(S.delta&&r.go(-S.delta,!1),K(u,M,f))).then(u=>{u=u||ft(M,f,!1),u&&(S.delta&&!Ge(u,te.NAVIGATION_CANCELLED)?r.go(-S.delta,!1):S.type===is.pop&&Ge(u,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),Xe(M,f,u)}).catch(kt)}))}let vt=Bt(),re=Bt(),z;function K(y,O,S){Ze(y);const M=re.list();return M.length?M.forEach(U=>U(y,O,S)):console.error(y),Promise.reject(y)}function je(){return z&&c.value!==et?Promise.resolve():new Promise((y,O)=>{vt.add([y,O])})}function Ze(y){return z||(z=!y,Dt(),vt.list().forEach(([O,S])=>y?S(y):O()),vt.reset()),y}function Ie(y,O,S,M){const{scrollBehavior:U}=e;if(!Rt||!U)return Promise.resolve();const f=!S&&Wc(ar(y.fullPath,0))||(M||!S)&&history.state&&history.state.scroll||null;return Yr().then(()=>U(y,O,f)).then(u=>u&&Gc(u)).catch(u=>K(u,y,O))}const he=y=>r.go(y);let yt;const bt=new Set,ut={currentRoute:c,listening:!0,addRoute:m,removeRoute:w,clearRoutes:t.clearRoutes,hasRoute:j,getRoutes:P,resolve:N,options:e,push:I,replace:J,go:he,back:()=>he(-1),forward:()=>he(1),beforeEach:i.add,beforeResolve:o.add,afterEach:l.add,onError:re.add,isReady:je,install(y){y.component("RouterLink",yf),y.component("RouterView",Rf),y.config.globalProperties.$router=ut,Object.defineProperty(y.config.globalProperties,"$route",{enumerable:!0,get:()=>wt(c)}),Rt&&!yt&&c.value===et&&(yt=!0,I(r.location).catch(M=>{}));const O={};for(const M in et)Object.defineProperty(O,M,{get:()=>c.value[M],enumerable:!0});y.provide(Ts,ut),y.provide(Vi,kr(O)),y.provide(ls,c);const S=y.unmount;bt.add(y),y.unmount=function(){bt.delete(y),bt.size<1&&(h=et,Te&&Te(),Te=null,c.value=et,yt=!1,z=!1),S()}}};function Ee(y){return y.reduce((O,S)=>O.then(()=>Ye(S)),Promise.resolve())}return ut}function Ki(e=window.location.pathname){const t=e.split("/").filter(Boolean);return t.length<2||t[0]!=="t"?"":decodeURIComponent(t[1]||"").toLowerCase()}function Wi(e=window.location.pathname){return`/t/${Ki(e)}/admin/`}function Sf(e=window.location.pathname){return`/t/${Ki(e)}/v1`}const Cf={style:{padding:"16px"}},wf={style:{"margin-top":"8px",color:"#666"}},Of={style:{"margin-top":"4px",color:"#666"}},Pf=As({__name:"DashboardPage",setup(e){const t=ye(()=>Wi()),n=ye(()=>Sf());return(s,r)=>(Ri(),Il("main",Cf,[r[2]||(r[2]=pt("h1",{style:{"font-size":"20px","font-weight":"600"}},"QuyUn Admin",-1)),pt("p",wf,[r[0]||(r[0]=ts(" Router base: ",-1)),pt("code",null,$n(t.value),1)]),pt("p",Of,[r[1]||(r[1]=ts(" API baseURL: ",-1)),pt("code",null,$n(n.value),1)])]))}}),Tf=Af({history:tf(Wi()),routes:[{path:"/",component:Pf},{path:"/:pathMatch(.*)*",redirect:"/"}]}),If=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Nf={};function Mf(e,t){const n=Xo("router-view");return Ri(),Nl(n)}const Df=If(Nf,[["render",Mf]]);gc(Df).use(Tf).mount("#app"); diff --git a/frontend/admin/dist/index.html b/frontend/admin/dist/index.html new file mode 100644 index 0000000..85638c0 --- /dev/null +++ b/frontend/admin/dist/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn Admin + + + +
+ + + diff --git a/frontend/admin/index.html b/frontend/admin/index.html new file mode 100644 index 0000000..cd7d82b --- /dev/null +++ b/frontend/admin/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn Admin + + +
+ + + + diff --git a/frontend/admin/package-lock.json b/frontend/admin/package-lock.json new file mode 100644 index 0000000..22d43c1 --- /dev/null +++ b/frontend/admin/package-lock.json @@ -0,0 +1,1614 @@ +{ + "name": "@quyun/admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@quyun/admin", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/frontend/admin/package.json b/frontend/admin/package.json new file mode 100644 index 0000000..23e1d57 --- /dev/null +++ b/frontend/admin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@quyun/admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } +} + diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue new file mode 100644 index 0000000..c2549a1 --- /dev/null +++ b/frontend/admin/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/frontend/admin/src/api.ts b/frontend/admin/src/api.ts new file mode 100644 index 0000000..1347060 --- /dev/null +++ b/frontend/admin/src/api.ts @@ -0,0 +1,7 @@ +import axios from 'axios' +import { getApiBaseURL } from './tenant' + +export const api = axios.create({ + baseURL: getApiBaseURL(), +}) + diff --git a/frontend/admin/src/env.d.ts b/frontend/admin/src/env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/frontend/admin/src/env.d.ts @@ -0,0 +1,2 @@ +/// + diff --git a/frontend/admin/src/main.ts b/frontend/admin/src/main.ts new file mode 100644 index 0000000..c80f0ac --- /dev/null +++ b/frontend/admin/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import { router } from './router' +import App from './App.vue' + +createApp(App).use(router).mount('#app') + diff --git a/frontend/admin/src/router.ts b/frontend/admin/src/router.ts new file mode 100644 index 0000000..0a3e9cc --- /dev/null +++ b/frontend/admin/src/router.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { getAdminRouterBase } from './tenant' +import DashboardPage from './views/DashboardPage.vue' + +export const router = createRouter({ + history: createWebHistory(getAdminRouterBase()), + routes: [ + { path: '/', component: DashboardPage }, + { path: '/:pathMatch(.*)*', redirect: '/' }, + ], +}) + diff --git a/frontend/admin/src/tenant.ts b/frontend/admin/src/tenant.ts new file mode 100644 index 0000000..a4129bf --- /dev/null +++ b/frontend/admin/src/tenant.ts @@ -0,0 +1,16 @@ +export function getTenantCodeFromPath(pathname = window.location.pathname): string { + const parts = pathname.split('/').filter(Boolean) + if (parts.length < 2 || parts[0] !== 't') return '' + return decodeURIComponent(parts[1] || '').toLowerCase() +} + +export function getAdminRouterBase(pathname = window.location.pathname): string { + const tenantCode = getTenantCodeFromPath(pathname) + return `/t/${tenantCode}/admin/` +} + +export function getApiBaseURL(pathname = window.location.pathname): string { + const tenantCode = getTenantCodeFromPath(pathname) + return `/t/${tenantCode}/v1` +} + diff --git a/frontend/admin/src/views/DashboardPage.vue b/frontend/admin/src/views/DashboardPage.vue new file mode 100644 index 0000000..295f86f --- /dev/null +++ b/frontend/admin/src/views/DashboardPage.vue @@ -0,0 +1,20 @@ + + + + diff --git a/frontend/admin/tsconfig.json b/frontend/admin/tsconfig.json new file mode 100644 index 0000000..d415b8f --- /dev/null +++ b/frontend/admin/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "types": ["vite/client"] + }, + "include": ["src"] +} + diff --git a/frontend/admin/vite.config.ts b/frontend/admin/vite.config.ts new file mode 100644 index 0000000..ac8285c --- /dev/null +++ b/frontend/admin/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + base: './', + server: { + port: 5173, + }, +}) + diff --git a/frontend/superadmin/dist/assets/index-CFAqJvax.js b/frontend/superadmin/dist/assets/index-CFAqJvax.js new file mode 100644 index 0000000..88f05fc --- /dev/null +++ b/frontend/superadmin/dist/assets/index-CFAqJvax.js @@ -0,0 +1,30 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&s(i)}).observe(document,{childList:!0,subtree:!0});function n(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(r){if(r.ep)return;r.ep=!0;const o=n(r);fetch(r.href,o)}})();/** +* @vue/shared v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function er(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const te={},jt=[],rt=()=>{},Ho=()=>!1,Qn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),tr=e=>e.startsWith("onUpdate:"),ye=Object.assign,nr=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Tl=Object.prototype.hasOwnProperty,Q=(e,t)=>Tl.call(e,t),H=Array.isArray,Ht=e=>Yn(e)==="[object Map]",ko=e=>Yn(e)==="[object Set]",k=e=>typeof e=="function",ae=e=>typeof e=="string",St=e=>typeof e=="symbol",oe=e=>e!==null&&typeof e=="object",Vo=e=>(oe(e)||k(e))&&k(e.then)&&k(e.catch),qo=Object.prototype.toString,Yn=e=>qo.call(e),Pl=e=>Yn(e).slice(8,-1),$o=e=>Yn(e)==="[object Object]",sr=e=>ae(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,rn=er(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Zn=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Nl=/-\w/g,je=Zn(e=>e.replace(Nl,t=>t.slice(1).toUpperCase())),Il=/\B([A-Z])/g,Ft=Zn(e=>e.replace(Il,"-$1").toLowerCase()),es=Zn(e=>e.charAt(0).toUpperCase()+e.slice(1)),ms=Zn(e=>e?`on${es(e)}`:""),xt=(e,t)=>!Object.is(e,t),Dn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},rr=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Tr;const ts=()=>Tr||(Tr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function or(e){if(H(e)){const t={};for(let n=0;n{if(n){const s=n.split(Fl);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function ir(e){let t="";if(ae(e))t=e;else if(H(e))for(let n=0;n!!(e&&e.__v_isRef===!0),bt=e=>ae(e)?e:e==null?"":H(e)||oe(e)&&(e.toString===qo||!k(e.toString))?Go(e)?bt(e.value):JSON.stringify(e,zo,2):String(e),zo=(e,t)=>Go(t)?zo(e,t.value):Ht(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],o)=>(n[gs(s,o)+" =>"]=r,n),{})}:ko(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>gs(n))}:St(t)?gs(t):oe(t)&&!H(t)&&!$o(t)?String(t):t,gs=(e,t="")=>{var n;return St(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Se;class jl{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=Se,!t&&Se&&(this.index=(Se.scopes||(Se.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(Se=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(ln){let t=ln;for(ln=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;on;){let t=on;for(on=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Yo(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Zo(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),ar(s),kl(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Ds(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(ei(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function ei(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===mn)||(e.globalVersion=mn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Ds(e))))return;e.flags|=2;const t=e.dep,n=se,s=$e;se=e,$e=!0;try{Yo(e);const r=e.fn(e._value);(t.version===0||xt(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{se=n,$e=s,Zo(e),e.flags&=-3}}function ar(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)ar(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function kl(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let $e=!0;const ti=[];function ht(){ti.push($e),$e=!1}function pt(){const e=ti.pop();$e=e===void 0?!0:e}function Pr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=se;se=void 0;try{t()}finally{se=n}}}let mn=0;class Vl{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class ur{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!se||!$e||se===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==se)n=this.activeLink=new Vl(se,this),se.deps?(n.prevDep=se.depsTail,se.depsTail.nextDep=n,se.depsTail=n):se.deps=se.depsTail=n,ni(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=se.depsTail,n.nextDep=void 0,se.depsTail.nextDep=n,se.depsTail=n,se.deps===n&&(se.deps=s)}return n}trigger(t){this.version++,mn++,this.notify(t)}notify(t){lr();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{cr()}}}function ni(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)ni(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Fs=new WeakMap,Pt=Symbol(""),Ls=Symbol(""),gn=Symbol("");function he(e,t,n){if($e&&se){let s=Fs.get(e);s||Fs.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new ur),r.map=s,r.key=n),r.track()}}function ut(e,t,n,s,r,o){const i=Fs.get(e);if(!i){mn++;return}const l=c=>{c&&c.trigger()};if(lr(),t==="clear")i.forEach(l);else{const c=H(e),u=c&&sr(n);if(c&&n==="length"){const a=Number(s);i.forEach((f,p)=>{(p==="length"||p===gn||!St(p)&&p>=a)&&l(f)})}else switch((n!==void 0||i.has(void 0))&&l(i.get(n)),u&&l(i.get(gn)),t){case"add":c?u&&l(i.get("length")):(l(i.get(Pt)),Ht(e)&&l(i.get(Ls)));break;case"delete":c||(l(i.get(Pt)),Ht(e)&&l(i.get(Ls)));break;case"set":Ht(e)&&l(i.get(Pt));break}}cr()}function Lt(e){const t=X(e);return t===e?t:(he(t,"iterate",gn),Be(e)?t:t.map(Ke))}function ns(e){return he(e=X(e),"iterate",gn),e}function _t(e,t){return mt(e)?Nt(e)?$t(Ke(t)):$t(t):Ke(t)}const ql={__proto__:null,[Symbol.iterator](){return bs(this,Symbol.iterator,e=>_t(this,e))},concat(...e){return Lt(this).concat(...e.map(t=>H(t)?Lt(t):t))},entries(){return bs(this,"entries",e=>(e[1]=_t(this,e[1]),e))},every(e,t){return lt(this,"every",e,t,void 0,arguments)},filter(e,t){return lt(this,"filter",e,t,n=>n.map(s=>_t(this,s)),arguments)},find(e,t){return lt(this,"find",e,t,n=>_t(this,n),arguments)},findIndex(e,t){return lt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return lt(this,"findLast",e,t,n=>_t(this,n),arguments)},findLastIndex(e,t){return lt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return lt(this,"forEach",e,t,void 0,arguments)},includes(...e){return _s(this,"includes",e)},indexOf(...e){return _s(this,"indexOf",e)},join(e){return Lt(this).join(e)},lastIndexOf(...e){return _s(this,"lastIndexOf",e)},map(e,t){return lt(this,"map",e,t,void 0,arguments)},pop(){return Zt(this,"pop")},push(...e){return Zt(this,"push",e)},reduce(e,...t){return Nr(this,"reduce",e,t)},reduceRight(e,...t){return Nr(this,"reduceRight",e,t)},shift(){return Zt(this,"shift")},some(e,t){return lt(this,"some",e,t,void 0,arguments)},splice(...e){return Zt(this,"splice",e)},toReversed(){return Lt(this).toReversed()},toSorted(e){return Lt(this).toSorted(e)},toSpliced(...e){return Lt(this).toSpliced(...e)},unshift(...e){return Zt(this,"unshift",e)},values(){return bs(this,"values",e=>_t(this,e))}};function bs(e,t,n){const s=ns(e),r=s[t]();return s!==e&&!Be(e)&&(r._next=r.next,r.next=()=>{const o=r._next();return o.done||(o.value=n(o.value)),o}),r}const $l=Array.prototype;function lt(e,t,n,s,r,o){const i=ns(e),l=i!==e&&!Be(e),c=i[t];if(c!==$l[t]){const f=c.apply(e,o);return l?Ke(f):f}let u=n;i!==e&&(l?u=function(f,p){return n.call(this,_t(e,f),p,e)}:n.length>2&&(u=function(f,p){return n.call(this,f,p,e)}));const a=c.call(i,u,s);return l&&r?r(a):a}function Nr(e,t,n,s){const r=ns(e);let o=n;return r!==e&&(Be(e)?n.length>3&&(o=function(i,l,c){return n.call(this,i,l,c,e)}):o=function(i,l,c){return n.call(this,i,_t(e,l),c,e)}),r[t](o,...s)}function _s(e,t,n){const s=X(e);he(s,"iterate",gn);const r=s[t](...n);return(r===-1||r===!1)&&hr(n[0])?(n[0]=X(n[0]),s[t](...n)):r}function Zt(e,t,n=[]){ht(),lr();const s=X(e)[t].apply(e,n);return cr(),pt(),s}const Kl=er("__proto__,__v_isRef,__isVue"),si=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(St));function Wl(e){St(e)||(e=String(e));const t=X(this);return he(t,"has",e),t.hasOwnProperty(e)}class ri{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return s===(r?o?nc:ci:o?li:ii).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const i=H(t);if(!r){let c;if(i&&(c=ql[n]))return c;if(n==="hasOwnProperty")return Wl}const l=Reflect.get(t,n,ge(t)?t:s);if((St(n)?si.has(n):Kl(n))||(r||he(t,"get",n),o))return l;if(ge(l)){const c=i&&sr(n)?l:l.value;return r&&oe(c)?Us(c):c}return oe(l)?r?Us(l):ss(l):l}}class oi extends ri{constructor(t=!1){super(!1,t)}set(t,n,s,r){let o=t[n];const i=H(t)&&sr(n);if(!this._isShallow){const u=mt(o);if(!Be(s)&&!mt(s)&&(o=X(o),s=X(s)),!i&&ge(o)&&!ge(s))return u||(o.value=s),!0}const l=i?Number(n)e,Tn=e=>Reflect.getPrototypeOf(e);function Ql(e,t,n){return function(...s){const r=this.__v_raw,o=X(r),i=Ht(o),l=e==="entries"||e===Symbol.iterator&&i,c=e==="keys"&&i,u=r[e](...s),a=n?Ms:t?$t:Ke;return!t&&he(o,"iterate",c?Ls:Pt),{next(){const{value:f,done:p}=u.next();return p?{value:f,done:p}:{value:l?[a(f[0]),a(f[1])]:a(f),done:p}},[Symbol.iterator](){return this}}}}function Pn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Yl(e,t){const n={get(r){const o=this.__v_raw,i=X(o),l=X(r);e||(xt(r,l)&&he(i,"get",r),he(i,"get",l));const{has:c}=Tn(i),u=t?Ms:e?$t:Ke;if(c.call(i,r))return u(o.get(r));if(c.call(i,l))return u(o.get(l));o!==i&&o.get(r)},get size(){const r=this.__v_raw;return!e&&he(X(r),"iterate",Pt),r.size},has(r){const o=this.__v_raw,i=X(o),l=X(r);return e||(xt(r,l)&&he(i,"has",r),he(i,"has",l)),r===l?o.has(r):o.has(r)||o.has(l)},forEach(r,o){const i=this,l=i.__v_raw,c=X(l),u=t?Ms:e?$t:Ke;return!e&&he(c,"iterate",Pt),l.forEach((a,f)=>r.call(o,u(a),u(f),i))}};return ye(n,e?{add:Pn("add"),set:Pn("set"),delete:Pn("delete"),clear:Pn("clear")}:{add(r){!t&&!Be(r)&&!mt(r)&&(r=X(r));const o=X(this);return Tn(o).has.call(o,r)||(o.add(r),ut(o,"add",r,r)),this},set(r,o){!t&&!Be(o)&&!mt(o)&&(o=X(o));const i=X(this),{has:l,get:c}=Tn(i);let u=l.call(i,r);u||(r=X(r),u=l.call(i,r));const a=c.call(i,r);return i.set(r,o),u?xt(o,a)&&ut(i,"set",r,o):ut(i,"add",r,o),this},delete(r){const o=X(this),{has:i,get:l}=Tn(o);let c=i.call(o,r);c||(r=X(r),c=i.call(o,r)),l&&l.call(o,r);const u=o.delete(r);return c&&ut(o,"delete",r,void 0),u},clear(){const r=X(this),o=r.size!==0,i=r.clear();return o&&ut(r,"clear",void 0,void 0),i}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Ql(r,e,t)}),n}function fr(e,t){const n=Yl(e,t);return(s,r,o)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(Q(n,r)&&r in s?n:s,r,o)}const Zl={get:fr(!1,!1)},ec={get:fr(!1,!0)},tc={get:fr(!0,!1)};const ii=new WeakMap,li=new WeakMap,ci=new WeakMap,nc=new WeakMap;function sc(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function rc(e){return e.__v_skip||!Object.isExtensible(e)?0:sc(Pl(e))}function ss(e){return mt(e)?e:dr(e,!1,zl,Zl,ii)}function ai(e){return dr(e,!1,Xl,ec,li)}function Us(e){return dr(e,!0,Jl,tc,ci)}function dr(e,t,n,s,r){if(!oe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const o=rc(e);if(o===0)return e;const i=r.get(e);if(i)return i;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function Nt(e){return mt(e)?Nt(e.__v_raw):!!(e&&e.__v_isReactive)}function mt(e){return!!(e&&e.__v_isReadonly)}function Be(e){return!!(e&&e.__v_isShallow)}function hr(e){return e?!!e.__v_raw:!1}function X(e){const t=e&&e.__v_raw;return t?X(t):e}function oc(e){return!Q(e,"__v_skip")&&Object.isExtensible(e)&&Ko(e,"__v_skip",!0),e}const Ke=e=>oe(e)?ss(e):e,$t=e=>oe(e)?Us(e):e;function ge(e){return e?e.__v_isRef===!0:!1}function yn(e){return ui(e,!1)}function ic(e){return ui(e,!0)}function ui(e,t){return ge(e)?e:new lc(e,t)}class lc{constructor(t,n){this.dep=new ur,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:X(t),this._value=n?t:Ke(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Be(t)||mt(t);t=s?t:X(t),xt(t,n)&&(this._rawValue=t,this._value=s?t:Ke(t),this.dep.trigger())}}function kt(e){return ge(e)?e.value:e}const cc={get:(e,t,n)=>t==="__v_raw"?e:kt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return ge(r)&&!ge(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function fi(e){return Nt(e)?e:new Proxy(e,cc)}class ac{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new ur(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=mn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&se!==this)return Qo(this,!0),!0}get value(){const t=this.dep.track();return ei(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function uc(e,t,n=!1){let s,r;return k(e)?s=e:(s=e.get,r=e.set),new ac(s,r,n)}const Nn={},kn=new WeakMap;let Ot;function fc(e,t=!1,n=Ot){if(n){let s=kn.get(n);s||kn.set(n,s=[]),s.push(e)}}function dc(e,t,n=te){const{immediate:s,deep:r,once:o,scheduler:i,augmentJob:l,call:c}=n,u=P=>r?P:Be(P)||r===!1||r===0?ft(P,1):ft(P);let a,f,p,y,g=!1,R=!1;if(ge(e)?(f=()=>e.value,g=Be(e)):Nt(e)?(f=()=>u(e),g=!0):H(e)?(R=!0,g=e.some(P=>Nt(P)||Be(P)),f=()=>e.map(P=>{if(ge(P))return P.value;if(Nt(P))return u(P);if(k(P))return c?c(P,2):P()})):k(e)?t?f=c?()=>c(e,2):e:f=()=>{if(p){ht();try{p()}finally{pt()}}const P=Ot;Ot=a;try{return c?c(e,3,[y]):e(y)}finally{Ot=P}}:f=rt,t&&r){const P=f,q=r===!0?1/0:r;f=()=>ft(P(),q)}const S=Hl(),C=()=>{a.stop(),S&&S.active&&nr(S.effects,a)};if(o&&t){const P=t;t=(...q)=>{P(...q),C()}}let N=R?new Array(e.length).fill(Nn):Nn;const D=P=>{if(!(!(a.flags&1)||!a.dirty&&!P))if(t){const q=a.run();if(r||g||(R?q.some((re,K)=>xt(re,N[K])):xt(q,N))){p&&p();const re=Ot;Ot=a;try{const K=[q,N===Nn?void 0:R&&N[0]===Nn?[]:N,y];N=q,c?c(t,3,K):t(...K)}finally{Ot=re}}}else a.run()};return l&&l(D),a=new Jo(f),a.scheduler=i?()=>i(D,!1):D,y=P=>fc(P,!1,a),p=a.onStop=()=>{const P=kn.get(a);if(P){if(c)c(P,4);else for(const q of P)q();kn.delete(a)}},t?s?D(!0):N=a.run():i?i(D.bind(null,!0),!0):a.run(),C.pause=a.pause.bind(a),C.resume=a.resume.bind(a),C.stop=C,C}function ft(e,t=1/0,n){if(t<=0||!oe(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,ge(e))ft(e.value,t,n);else if(H(e))for(let s=0;s{ft(s,t,n)});else if($o(e)){for(const s in e)ft(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&ft(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Rn(e,t,n,s){try{return s?e(...s):e()}catch(r){rs(r,t,n)}}function ot(e,t,n,s){if(k(e)){const r=Rn(e,t,n,s);return r&&Vo(r)&&r.catch(o=>{rs(o,t,n)}),r}if(H(e)){const r=[];for(let o=0;o>>1,r=Ee[s],o=bn(r);o=bn(n)?Ee.push(e):Ee.splice(pc(t),0,e),e.flags|=1,pi()}}function pi(){Vn||(Vn=di.then(gi))}function mc(e){H(e)?Vt.push(...e):Et&&e.id===-1?Et.splice(Mt+1,0,e):e.flags&1||(Vt.push(e),e.flags|=1),pi()}function Ir(e,t,n=nt+1){for(;nbn(n)-bn(s));if(Vt.length=0,Et){Et.push(...t);return}for(Et=t,Mt=0;Mte.id==null?e.flags&2?-1:1/0:e.id;function gi(e){try{for(nt=0;nt{s._d&&Wn(-1);const o=qn(t);let i;try{i=e(...r)}finally{qn(o),s._d&&Wn(1)}return i};return s._n=!0,s._c=!0,s._d=!0,s}function bi(e,t){if(De===null)return e;const n=cs(De),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,bc=Symbol("_leaveCb");function mr(e,t){e.shapeFlag&6&&e.component?(e.transition=t,mr(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function xn(e,t){return k(e)?ye({name:e.name},t,{setup:e}):e}function _i(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const $n=new WeakMap;function cn(e,t,n,s,r=!1){if(H(e)){e.forEach((g,R)=>cn(g,t&&(H(t)?t[R]:t),n,s,r));return}if(an(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&cn(e,t,n,s.component.subTree);return}const o=s.shapeFlag&4?cs(s.component):s.el,i=r?null:o,{i:l,r:c}=e,u=t&&t.r,a=l.refs===te?l.refs={}:l.refs,f=l.setupState,p=X(f),y=f===te?Ho:g=>Q(p,g);if(u!=null&&u!==c){if(Dr(t),ae(u))a[u]=null,y(u)&&(f[u]=null);else if(ge(u)){u.value=null;const g=t;g.k&&(a[g.k]=null)}}if(k(c))Rn(c,l,12,[i,a]);else{const g=ae(c),R=ge(c);if(g||R){const S=()=>{if(e.f){const C=g?y(c)?f[c]:a[c]:c.value;if(r)H(C)&&nr(C,o);else if(H(C))C.includes(o)||C.push(o);else if(g)a[c]=[o],y(c)&&(f[c]=a[c]);else{const N=[o];c.value=N,e.k&&(a[e.k]=N)}}else g?(a[c]=i,y(c)&&(f[c]=i)):R&&(c.value=i,e.k&&(a[e.k]=i))};if(i){const C=()=>{S(),$n.delete(e)};C.id=-1,$n.set(e,C),Ie(C,n)}else Dr(e),S()}}}function Dr(e){const t=$n.get(e);t&&(t.flags|=8,$n.delete(e))}ts().requestIdleCallback;ts().cancelIdleCallback;const an=e=>!!e.type.__asyncLoader,Ei=e=>e.type.__isKeepAlive;function _c(e,t){wi(e,"a",t)}function Ec(e,t){wi(e,"da",t)}function wi(e,t,n=pe){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(os(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Ei(r.parent.vnode)&&wc(s,t,n,r),r=r.parent}}function wc(e,t,n,s){const r=os(t,e,s,!0);Ri(()=>{nr(s[t],r)},n)}function os(e,t,n=pe,s=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{ht();const l=Sn(n),c=ot(t,n,e,i);return l(),pt(),c});return s?r.unshift(o):r.push(o),o}}const gt=e=>(t,n=pe)=>{(!En||e==="sp")&&os(e,(...s)=>t(...s),n)},Rc=gt("bm"),gr=gt("m"),xc=gt("bu"),Sc=gt("u"),Ac=gt("bum"),Ri=gt("um"),vc=gt("sp"),Oc=gt("rtg"),Cc=gt("rtc");function Tc(e,t=pe){os("ec",e,t)}const Pc="components";function Fr(e,t){return Ic(Pc,e,!0,t)||e}const Nc=Symbol.for("v-ndc");function Ic(e,t,n=!0,s=!1){const r=De||pe;if(r){const o=r.type;{const l=Ra(o,!1);if(l&&(l===t||l===je(t)||l===es(je(t))))return o}const i=Lr(r[e]||o[e],t)||Lr(r.appContext[e],t);return!i&&s?o:i}}function Lr(e,t){return e&&(e[t]||e[je(t)]||e[es(je(t))])}function Dc(e,t,n,s){let r;const o=n,i=H(e);if(i||ae(e)){const l=i&&Nt(e);let c=!1,u=!1;l&&(c=!Be(e),u=mt(e),e=ns(e)),r=new Array(e.length);for(let a=0,f=e.length;at(l,c,void 0,o));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,u=l.length;ce?ki(e)?cs(e):js(e.parent):null,un=ye(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>js(e.parent),$root:e=>js(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Si(e),$forceUpdate:e=>e.f||(e.f=()=>{pr(e.update)}),$nextTick:e=>e.n||(e.n=hi.bind(e.proxy)),$watch:e=>$c.bind(e)}),Es=(e,t)=>e!==te&&!e.__isScriptSetup&&Q(e,t),Fc={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:o,accessCache:i,type:l,appContext:c}=e;if(t[0]!=="$"){const p=i[t];if(p!==void 0)switch(p){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return o[t]}else{if(Es(s,t))return i[t]=1,s[t];if(r!==te&&Q(r,t))return i[t]=2,r[t];if(Q(o,t))return i[t]=3,o[t];if(n!==te&&Q(n,t))return i[t]=4,n[t];Hs&&(i[t]=0)}}const u=un[t];let a,f;if(u)return t==="$attrs"&&he(e.attrs,"get",""),u(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(n!==te&&Q(n,t))return i[t]=4,n[t];if(f=c.config.globalProperties,Q(f,t))return f[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:o}=e;return Es(r,t)?(r[t]=n,!0):s!==te&&Q(s,t)?(s[t]=n,!0):Q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:o,type:i}},l){let c;return!!(n[l]||e!==te&&l[0]!=="$"&&Q(e,l)||Es(t,l)||Q(o,l)||Q(s,l)||Q(un,l)||Q(r.config.globalProperties,l)||(c=i.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Mr(e){return H(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Hs=!0;function Lc(e){const t=Si(e),n=e.proxy,s=e.ctx;Hs=!1,t.beforeCreate&&Ur(t.beforeCreate,e,"bc");const{data:r,computed:o,methods:i,watch:l,provide:c,inject:u,created:a,beforeMount:f,mounted:p,beforeUpdate:y,updated:g,activated:R,deactivated:S,beforeDestroy:C,beforeUnmount:N,destroyed:D,unmounted:P,render:q,renderTracked:re,renderTriggered:K,errorCaptured:we,serverPrefetch:Ce,expose:Te,inheritAttrs:He,components:Le,directives:de,filters:Pe}=t;if(u&&Mc(u,s,null),i)for(const z in i){const $=i[z];k($)&&(s[z]=$.bind(n))}if(r){const z=r.call(n,n);oe(z)&&(e.data=ss(z))}if(Hs=!0,o)for(const z in o){const $=o[z],Me=k($)?$.bind(n,n):k($.get)?$.get.bind(n,n):rt,ze=!k($)&&k($.set)?$.set.bind(n):rt,ue=qe({get:Me,set:ze});Object.defineProperty(s,z,{enumerable:!0,configurable:!0,get:()=>ue.value,set:le=>ue.value=le})}if(l)for(const z in l)xi(l[z],s,n,z);if(c){const z=k(c)?c.call(n):c;Reflect.ownKeys(z).forEach($=>{Fn($,z[$])})}a&&Ur(a,e,"c");function Y(z,$){H($)?$.forEach(Me=>z(Me.bind(n))):$&&z($.bind(n))}if(Y(Rc,f),Y(gr,p),Y(xc,y),Y(Sc,g),Y(_c,R),Y(Ec,S),Y(Tc,we),Y(Cc,re),Y(Oc,K),Y(Ac,N),Y(Ri,P),Y(vc,Ce),H(Te))if(Te.length){const z=e.exposed||(e.exposed={});Te.forEach($=>{Object.defineProperty(z,$,{get:()=>n[$],set:Me=>n[$]=Me,enumerable:!0})})}else e.exposed||(e.exposed={});q&&e.render===rt&&(e.render=q),He!=null&&(e.inheritAttrs=He),Le&&(e.components=Le),de&&(e.directives=de),Ce&&_i(e)}function Mc(e,t,n=rt){H(e)&&(e=ks(e));for(const s in e){const r=e[s];let o;oe(r)?"default"in r?o=dt(r.from||s,r.default,!0):o=dt(r.from||s):o=dt(r),ge(o)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[s]=o}}function Ur(e,t,n){ot(H(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function xi(e,t,n,s){let r=s.includes(".")?Oi(n,s):()=>n[s];if(ae(e)){const o=t[e];k(o)&&Ln(r,o)}else if(k(e))Ln(r,e.bind(n));else if(oe(e))if(H(e))e.forEach(o=>xi(o,t,n,s));else{const o=k(e.handler)?e.handler.bind(n):t[e.handler];k(o)&&Ln(r,o,e)}}function Si(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(u=>Kn(c,u,i,!0)),Kn(c,t,i)),oe(t)&&o.set(t,c),c}function Kn(e,t,n,s=!1){const{mixins:r,extends:o}=t;o&&Kn(e,o,n,!0),r&&r.forEach(i=>Kn(e,i,n,!0));for(const i in t)if(!(s&&i==="expose")){const l=Uc[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const Uc={data:Br,props:jr,emits:jr,methods:sn,computed:sn,beforeCreate:be,created:be,beforeMount:be,mounted:be,beforeUpdate:be,updated:be,beforeDestroy:be,beforeUnmount:be,destroyed:be,unmounted:be,activated:be,deactivated:be,errorCaptured:be,serverPrefetch:be,components:sn,directives:sn,watch:jc,provide:Br,inject:Bc};function Br(e,t){return t?e?function(){return ye(k(e)?e.call(this,this):e,k(t)?t.call(this,this):t)}:t:e}function Bc(e,t){return sn(ks(e),ks(t))}function ks(e){if(H(e)){const t={};for(let n=0;n1)return n&&k(t)?t.call(s&&s.proxy):t}}const Vc=Symbol.for("v-scx"),qc=()=>dt(Vc);function Ln(e,t,n){return vi(e,t,n)}function vi(e,t,n=te){const{immediate:s,deep:r,flush:o,once:i}=n,l=ye({},n),c=t&&s||!t&&o!=="post";let u;if(En){if(o==="sync"){const y=qc();u=y.__watcherHandles||(y.__watcherHandles=[])}else if(!c){const y=()=>{};return y.stop=rt,y.resume=rt,y.pause=rt,y}}const a=pe;l.call=(y,g,R)=>ot(y,a,g,R);let f=!1;o==="post"?l.scheduler=y=>{Ie(y,a&&a.suspense)}:o!=="sync"&&(f=!0,l.scheduler=(y,g)=>{g?y():pr(y)}),l.augmentJob=y=>{t&&(y.flags|=4),f&&(y.flags|=2,a&&(y.id=a.uid,y.i=a))};const p=dc(e,t,l);return En&&(u?u.push(p):c&&p()),p}function $c(e,t,n){const s=this.proxy,r=ae(e)?e.includes(".")?Oi(s,e):()=>s[e]:e.bind(s,s);let o;k(t)?o=t:(o=t.handler,n=t);const i=Sn(this),l=vi(r,o.bind(s),n);return i(),l}function Oi(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${je(t)}Modifiers`]||e[`${Ft(t)}Modifiers`];function Wc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||te;let r=n;const o=t.startsWith("update:"),i=o&&Kc(s,t.slice(7));i&&(i.trim&&(r=n.map(a=>ae(a)?a.trim():a)),i.number&&(r=n.map(rr)));let l,c=s[l=ms(t)]||s[l=ms(je(t))];!c&&o&&(c=s[l=ms(Ft(t))]),c&&ot(c,e,6,r);const u=s[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,ot(u,e,6,r)}}const Gc=new WeakMap;function Ci(e,t,n=!1){const s=n?Gc:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const o=e.emits;let i={},l=!1;if(!k(e)){const c=u=>{const a=Ci(u,t,!0);a&&(l=!0,ye(i,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!o&&!l?(oe(e)&&s.set(e,null),null):(H(o)?o.forEach(c=>i[c]=null):ye(i,o),oe(e)&&s.set(e,i),i)}function is(e,t){return!e||!Qn(t)?!1:(t=t.slice(2).replace(/Once$/,""),Q(e,t[0].toLowerCase()+t.slice(1))||Q(e,Ft(t))||Q(e,t))}function Hr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[o],slots:i,attrs:l,emit:c,render:u,renderCache:a,props:f,data:p,setupState:y,ctx:g,inheritAttrs:R}=e,S=qn(e);let C,N;try{if(n.shapeFlag&4){const P=r||s,q=P;C=st(u.call(q,P,a,f,y,p,g)),N=l}else{const P=t;C=st(P.length>1?P(f,{attrs:l,slots:i,emit:c}):P(f,null)),N=t.props?l:zc(l)}}catch(P){fn.length=0,rs(P,e,1),C=Ae(Kt)}let D=C;if(N&&R!==!1){const P=Object.keys(N),{shapeFlag:q}=D;P.length&&q&7&&(o&&P.some(tr)&&(N=Jc(N,o)),D=Wt(D,N,!1,!0))}return n.dirs&&(D=Wt(D,null,!1,!0),D.dirs=D.dirs?D.dirs.concat(n.dirs):n.dirs),n.transition&&mr(D,n.transition),C=D,qn(S),C}const zc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Qn(n))&&((t||(t={}))[n]=e[n]);return t},Jc=(e,t)=>{const n={};for(const s in e)(!tr(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Xc(e,t,n){const{props:s,children:r,component:o}=e,{props:i,children:l,patchFlag:c}=t,u=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?kr(s,i,u):!!i;if(c&8){const a=t.dynamicProps;for(let f=0;fObject.create(Ti),Ni=e=>Object.getPrototypeOf(e)===Ti;function Yc(e,t,n,s=!1){const r={},o=Pi();e.propsDefaults=Object.create(null),Ii(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=s?r:ai(r):e.type.props?e.props=r:e.props=o,e.attrs=o}function Zc(e,t,n,s){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=X(r),[c]=e.propsOptions;let u=!1;if((s||i>0)&&!(i&16)){if(i&8){const a=e.vnode.dynamicProps;for(let f=0;f{c=!0;const[p,y]=Di(f,t,!0);ye(i,p),y&&l.push(...y)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!o&&!c)return oe(e)&&s.set(e,jt),jt;if(H(o))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",br=e=>H(e)?e.map(st):[st(e)],ta=(e,t,n)=>{if(t._n)return t;const s=Bs((...r)=>br(t(...r)),n);return s._c=!1,s},Fi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(yr(r))continue;const o=e[r];if(k(o))t[r]=ta(r,o,s);else if(o!=null){const i=br(o);t[r]=()=>i}}},Li=(e,t)=>{const n=br(t);e.slots.default=()=>n},Mi=(e,t,n)=>{for(const s in t)(n||!yr(s))&&(e[s]=t[s])},na=(e,t,n)=>{const s=e.slots=Pi();if(e.vnode.shapeFlag&32){const r=t._;r?(Mi(s,t,n),n&&Ko(s,"_",r,!0)):Fi(t,s)}else t&&Li(e,t)},sa=(e,t,n)=>{const{vnode:s,slots:r}=e;let o=!0,i=te;if(s.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:Mi(r,t,n):(o=!t.$stable,Fi(t,r)),i=t}else t&&(Li(e,t),i={default:1});if(o)for(const l in r)!yr(l)&&i[l]==null&&delete r[l]},Ie=ca;function ra(e){return oa(e)}function oa(e,t){const n=ts();n.__VUE__=!0;const{insert:s,remove:r,patchProp:o,createElement:i,createText:l,createComment:c,setText:u,setElementText:a,parentNode:f,nextSibling:p,setScopeId:y=rt,insertStaticContent:g}=e,R=(d,h,m,E=null,x=null,_=null,T=void 0,O=null,v=!!h.dynamicChildren)=>{if(d===h)return;d&&!en(d,h)&&(E=w(d),le(d,x,_,!0),d=null),h.patchFlag===-2&&(v=!1,h.dynamicChildren=null);const{type:A,ref:B,shapeFlag:F}=h;switch(A){case ls:S(d,h,m,E);break;case Kt:C(d,h,m,E);break;case Rs:d==null&&N(h,m,E,T);break;case Ve:Le(d,h,m,E,x,_,T,O,v);break;default:F&1?q(d,h,m,E,x,_,T,O,v):F&6?de(d,h,m,E,x,_,T,O,v):(F&64||F&128)&&A.process(d,h,m,E,x,_,T,O,v,M)}B!=null&&x?cn(B,d&&d.ref,_,h||d,!h):B==null&&d&&d.ref!=null&&cn(d.ref,null,_,d,!0)},S=(d,h,m,E)=>{if(d==null)s(h.el=l(h.children),m,E);else{const x=h.el=d.el;h.children!==d.children&&u(x,h.children)}},C=(d,h,m,E)=>{d==null?s(h.el=c(h.children||""),m,E):h.el=d.el},N=(d,h,m,E)=>{[d.el,d.anchor]=g(d.children,h,m,E,d.el,d.anchor)},D=({el:d,anchor:h},m,E)=>{let x;for(;d&&d!==h;)x=p(d),s(d,m,E),d=x;s(h,m,E)},P=({el:d,anchor:h})=>{let m;for(;d&&d!==h;)m=p(d),r(d),d=m;r(h)},q=(d,h,m,E,x,_,T,O,v)=>{if(h.type==="svg"?T="svg":h.type==="math"&&(T="mathml"),d==null)re(h,m,E,x,_,T,O,v);else{const A=d.el&&d.el._isVueCE?d.el:null;try{A&&A._beginPatch(),Ce(d,h,x,_,T,O,v)}finally{A&&A._endPatch()}}},re=(d,h,m,E,x,_,T,O)=>{let v,A;const{props:B,shapeFlag:F,transition:U,dirs:j}=d;if(v=d.el=i(d.type,_,B&&B.is,B),F&8?a(v,d.children):F&16&&we(d.children,v,null,E,x,ws(d,_),T,O),j&&At(d,null,E,"created"),K(v,d,d.scopeId,T,E),B){for(const ne in B)ne!=="value"&&!rn(ne)&&o(v,ne,null,B[ne],_,E);"value"in B&&o(v,"value",null,B.value,_),(A=B.onVnodeBeforeMount)&&et(A,E,d)}j&&At(d,null,E,"beforeMount");const G=ia(x,U);G&&U.beforeEnter(v),s(v,h,m),((A=B&&B.onVnodeMounted)||G||j)&&Ie(()=>{A&&et(A,E,d),G&&U.enter(v),j&&At(d,null,E,"mounted")},x)},K=(d,h,m,E,x)=>{if(m&&y(d,m),E)for(let _=0;_{for(let A=v;A{const O=h.el=d.el;let{patchFlag:v,dynamicChildren:A,dirs:B}=h;v|=d.patchFlag&16;const F=d.props||te,U=h.props||te;let j;if(m&&vt(m,!1),(j=U.onVnodeBeforeUpdate)&&et(j,m,h,d),B&&At(h,d,m,"beforeUpdate"),m&&vt(m,!0),(F.innerHTML&&U.innerHTML==null||F.textContent&&U.textContent==null)&&a(O,""),A?Te(d.dynamicChildren,A,O,m,E,ws(h,x),_):T||$(d,h,O,null,m,E,ws(h,x),_,!1),v>0){if(v&16)He(O,F,U,m,x);else if(v&2&&F.class!==U.class&&o(O,"class",null,U.class,x),v&4&&o(O,"style",F.style,U.style,x),v&8){const G=h.dynamicProps;for(let ne=0;ne{j&&et(j,m,h,d),B&&At(h,d,m,"updated")},E)},Te=(d,h,m,E,x,_,T)=>{for(let O=0;O{if(h!==m){if(h!==te)for(const _ in h)!rn(_)&&!(_ in m)&&o(d,_,h[_],null,x,E);for(const _ in m){if(rn(_))continue;const T=m[_],O=h[_];T!==O&&_!=="value"&&o(d,_,O,T,x,E)}"value"in m&&o(d,"value",h.value,m.value,x)}},Le=(d,h,m,E,x,_,T,O,v)=>{const A=h.el=d?d.el:l(""),B=h.anchor=d?d.anchor:l("");let{patchFlag:F,dynamicChildren:U,slotScopeIds:j}=h;j&&(O=O?O.concat(j):j),d==null?(s(A,m,E),s(B,m,E),we(h.children||[],m,B,x,_,T,O,v)):F>0&&F&64&&U&&d.dynamicChildren?(Te(d.dynamicChildren,U,m,x,_,T,O),(h.key!=null||x&&h===x.subTree)&&Ui(d,h,!0)):$(d,h,m,B,x,_,T,O,v)},de=(d,h,m,E,x,_,T,O,v)=>{h.slotScopeIds=O,d==null?h.shapeFlag&512?x.ctx.activate(h,m,E,T,v):Pe(h,m,E,x,_,T,v):it(d,h,v)},Pe=(d,h,m,E,x,_,T)=>{const O=d.component=ga(d,E,x);if(Ei(d)&&(O.ctx.renderer=M),ba(O,!1,T),O.asyncDep){if(x&&x.registerDep(O,Y,T),!d.el){const v=O.subTree=Ae(Kt);C(null,v,h,m),d.placeholder=v.el}}else Y(O,d,h,m,x,_,T)},it=(d,h,m)=>{const E=h.component=d.component;if(Xc(d,h,m))if(E.asyncDep&&!E.asyncResolved){z(E,h,m);return}else E.next=h,E.update();else h.el=d.el,E.vnode=h},Y=(d,h,m,E,x,_,T)=>{const O=()=>{if(d.isMounted){let{next:F,bu:U,u:j,parent:G,vnode:ne}=d;{const Ye=Bi(d);if(Ye){F&&(F.el=ne.el,z(d,F,T)),Ye.asyncDep.then(()=>{d.isUnmounted||O()});return}}let Z=F,Re;vt(d,!1),F?(F.el=ne.el,z(d,F,T)):F=ne,U&&Dn(U),(Re=F.props&&F.props.onVnodeBeforeUpdate)&&et(Re,G,F,ne),vt(d,!0);const xe=Hr(d),Qe=d.subTree;d.subTree=xe,R(Qe,xe,f(Qe.el),w(Qe),d,x,_),F.el=xe.el,Z===null&&Qc(d,xe.el),j&&Ie(j,x),(Re=F.props&&F.props.onVnodeUpdated)&&Ie(()=>et(Re,G,F,ne),x)}else{let F;const{el:U,props:j}=h,{bm:G,m:ne,parent:Z,root:Re,type:xe}=d,Qe=an(h);vt(d,!1),G&&Dn(G),!Qe&&(F=j&&j.onVnodeBeforeMount)&&et(F,Z,h),vt(d,!0);{Re.ce&&Re.ce._def.shadowRoot!==!1&&Re.ce._injectChildStyle(xe);const Ye=d.subTree=Hr(d);R(null,Ye,m,E,d,x,_),h.el=Ye.el}if(ne&&Ie(ne,x),!Qe&&(F=j&&j.onVnodeMounted)){const Ye=h;Ie(()=>et(F,Z,Ye),x)}(h.shapeFlag&256||Z&&an(Z.vnode)&&Z.vnode.shapeFlag&256)&&d.a&&Ie(d.a,x),d.isMounted=!0,h=m=E=null}};d.scope.on();const v=d.effect=new Jo(O);d.scope.off();const A=d.update=v.run.bind(v),B=d.job=v.runIfDirty.bind(v);B.i=d,B.id=d.uid,v.scheduler=()=>pr(B),vt(d,!0),A()},z=(d,h,m)=>{h.component=d;const E=d.vnode.props;d.vnode=h,d.next=null,Zc(d,h.props,E,m),sa(d,h.children,m),ht(),Ir(d),pt()},$=(d,h,m,E,x,_,T,O,v=!1)=>{const A=d&&d.children,B=d?d.shapeFlag:0,F=h.children,{patchFlag:U,shapeFlag:j}=h;if(U>0){if(U&128){ze(A,F,m,E,x,_,T,O,v);return}else if(U&256){Me(A,F,m,E,x,_,T,O,v);return}}j&8?(B&16&&Ue(A,x,_),F!==A&&a(m,F)):B&16?j&16?ze(A,F,m,E,x,_,T,O,v):Ue(A,x,_,!0):(B&8&&a(m,""),j&16&&we(F,m,E,x,_,T,O,v))},Me=(d,h,m,E,x,_,T,O,v)=>{d=d||jt,h=h||jt;const A=d.length,B=h.length,F=Math.min(A,B);let U;for(U=0;UB?Ue(d,x,_,!0,!1,F):we(h,m,E,x,_,T,O,v,F)},ze=(d,h,m,E,x,_,T,O,v)=>{let A=0;const B=h.length;let F=d.length-1,U=B-1;for(;A<=F&&A<=U;){const j=d[A],G=h[A]=v?wt(h[A]):st(h[A]);if(en(j,G))R(j,G,m,null,x,_,T,O,v);else break;A++}for(;A<=F&&A<=U;){const j=d[F],G=h[U]=v?wt(h[U]):st(h[U]);if(en(j,G))R(j,G,m,null,x,_,T,O,v);else break;F--,U--}if(A>F){if(A<=U){const j=U+1,G=jU)for(;A<=F;)le(d[A],x,_,!0),A++;else{const j=A,G=A,ne=new Map;for(A=G;A<=U;A++){const Ne=h[A]=v?wt(h[A]):st(h[A]);Ne.key!=null&&ne.set(Ne.key,A)}let Z,Re=0;const xe=U-G+1;let Qe=!1,Ye=0;const Yt=new Array(xe);for(A=0;A=xe){le(Ne,x,_,!0);continue}let Ze;if(Ne.key!=null)Ze=ne.get(Ne.key);else for(Z=G;Z<=U;Z++)if(Yt[Z-G]===0&&en(Ne,h[Z])){Ze=Z;break}Ze===void 0?le(Ne,x,_,!0):(Yt[Ze-G]=A+1,Ze>=Ye?Ye=Ze:Qe=!0,R(Ne,h[Ze],m,null,x,_,T,O,v),Re++)}const vr=Qe?la(Yt):jt;for(Z=vr.length-1,A=xe-1;A>=0;A--){const Ne=G+A,Ze=h[Ne],Or=h[Ne+1],Cr=Ne+1{const{el:_,type:T,transition:O,children:v,shapeFlag:A}=d;if(A&6){ue(d.component.subTree,h,m,E);return}if(A&128){d.suspense.move(h,m,E);return}if(A&64){T.move(d,h,m,M);return}if(T===Ve){s(_,h,m);for(let F=0;FO.enter(_),x);else{const{leave:F,delayLeave:U,afterLeave:j}=O,G=()=>{d.ctx.isUnmounted?r(_):s(_,h,m)},ne=()=>{_._isLeaving&&_[bc](!0),F(_,()=>{G(),j&&j()})};U?U(_,G,ne):ne()}else s(_,h,m)},le=(d,h,m,E=!1,x=!1)=>{const{type:_,props:T,ref:O,children:v,dynamicChildren:A,shapeFlag:B,patchFlag:F,dirs:U,cacheIndex:j}=d;if(F===-2&&(x=!1),O!=null&&(ht(),cn(O,null,m,d,!0),pt()),j!=null&&(h.renderCache[j]=void 0),B&256){h.ctx.deactivate(d);return}const G=B&1&&U,ne=!an(d);let Z;if(ne&&(Z=T&&T.onVnodeBeforeUnmount)&&et(Z,h,d),B&6)Xe(d.component,m,E);else{if(B&128){d.suspense.unmount(m,E);return}G&&At(d,null,h,"beforeUnmount"),B&64?d.type.remove(d,h,m,M,E):A&&!A.hasOnce&&(_!==Ve||F>0&&F&64)?Ue(A,h,m,!1,!0):(_===Ve&&F&384||!x&&B&16)&&Ue(v,h,m),E&&Je(d)}(ne&&(Z=T&&T.onVnodeUnmounted)||G)&&Ie(()=>{Z&&et(Z,h,d),G&&At(d,null,h,"unmounted")},m)},Je=d=>{const{type:h,el:m,anchor:E,transition:x}=d;if(h===Ve){ke(m,E);return}if(h===Rs){P(d);return}const _=()=>{r(m),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:T,delayLeave:O}=x,v=()=>T(m,_);O?O(d.el,_,v):v()}else _()},ke=(d,h)=>{let m;for(;d!==h;)m=p(d),r(d),d=m;r(h)},Xe=(d,h,m)=>{const{bum:E,scope:x,job:_,subTree:T,um:O,m:v,a:A}=d;qr(v),qr(A),E&&Dn(E),x.stop(),_&&(_.flags|=8,le(T,d,h,m)),O&&Ie(O,h),Ie(()=>{d.isUnmounted=!0},h)},Ue=(d,h,m,E=!1,x=!1,_=0)=>{for(let T=_;T{if(d.shapeFlag&6)return w(d.component.subTree);if(d.shapeFlag&128)return d.suspense.next();const h=p(d.anchor||d.el),m=h&&h[gc];return m?p(m):h};let L=!1;const I=(d,h,m)=>{d==null?h._vnode&&le(h._vnode,null,null,!0):R(h._vnode||null,d,h,null,null,null,m),h._vnode=d,L||(L=!0,Ir(),mi(),L=!1)},M={p:R,um:le,m:ue,r:Je,mt:Pe,mc:we,pc:$,pbc:Te,n:w,o:e};return{render:I,hydrate:void 0,createApp:kc(I)}}function ws({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function vt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function ia(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Ui(e,t,n=!1){const s=e.children,r=t.children;if(H(s)&&H(r))for(let o=0;o>1,e[n[l]]0&&(t[s]=n[o-1]),n[o]=s)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function Bi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Bi(t)}function qr(e){if(e)for(let t=0;te.__isSuspense;function ca(e,t){t&&t.pendingBranch?H(e)?t.effects.push(...e):t.effects.push(e):mc(e)}const Ve=Symbol.for("v-fgt"),ls=Symbol.for("v-txt"),Kt=Symbol.for("v-cmt"),Rs=Symbol.for("v-stc"),fn=[];let Fe=null;function dn(e=!1){fn.push(Fe=e?null:[])}function aa(){fn.pop(),Fe=fn[fn.length-1]||null}let _n=1;function Wn(e,t=!1){_n+=e,e<0&&Fe&&t&&(Fe.hasOnce=!0)}function ua(e){return e.dynamicChildren=_n>0?Fe||jt:null,aa(),_n>0&&Fe&&Fe.push(e),e}function hn(e,t,n,s,r,o){return ua(ee(e,t,n,s,r,o,!0))}function Gn(e){return e?e.__v_isVNode===!0:!1}function en(e,t){return e.type===t.type&&e.key===t.key}const Hi=({key:e})=>e??null,Mn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ae(e)||ge(e)||k(e)?{i:De,r:e,k:t,f:!!n}:e:null);function ee(e,t=null,n=null,s=0,r=null,o=e===Ve?0:1,i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Hi(t),ref:t&&Mn(t),scopeId:yi,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:De};return l?(_r(c,n),o&128&&e.normalize(c)):n&&(c.shapeFlag|=ae(n)?8:16),_n>0&&!i&&Fe&&(c.patchFlag>0||o&6)&&c.patchFlag!==32&&Fe.push(c),c}const Ae=fa;function fa(e,t=null,n=null,s=0,r=null,o=!1){if((!e||e===Nc)&&(e=Kt),Gn(e)){const l=Wt(e,t,!0);return n&&_r(l,n),_n>0&&!o&&Fe&&(l.shapeFlag&6?Fe[Fe.indexOf(e)]=l:Fe.push(l)),l.patchFlag=-2,l}if(xa(e)&&(e=e.__vccOpts),t){t=da(t);let{class:l,style:c}=t;l&&!ae(l)&&(t.class=ir(l)),oe(c)&&(hr(c)&&!H(c)&&(c=ye({},c)),t.style=or(c))}const i=ae(e)?1:ji(e)?128:yc(e)?64:oe(e)?4:k(e)?2:0;return ee(e,t,n,s,r,i,o,!0)}function da(e){return e?hr(e)||Ni(e)?ye({},e):e:null}function Wt(e,t,n=!1,s=!1){const{props:r,ref:o,patchFlag:i,children:l,transition:c}=e,u=t?ha(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&Hi(u),ref:t&&t.ref?n&&o?H(o)?o.concat(Mn(t)):[o,Mn(t)]:Mn(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ve?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Wt(e.ssContent),ssFallback:e.ssFallback&&Wt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&mr(a,c.clone(a)),a}function qs(e=" ",t=0){return Ae(ls,null,e,t)}function st(e){return e==null||typeof e=="boolean"?Ae(Kt):H(e)?Ae(Ve,null,e.slice()):Gn(e)?wt(e):Ae(ls,null,String(e))}function wt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Wt(e)}function _r(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(H(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),_r(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Ni(t)?t._ctx=De:r===3&&De&&(De.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else k(t)?(t={default:t,_ctx:De},n=32):(t=String(t),s&64?(n=16,t=[qs(t)]):n=8);e.children=t,e.shapeFlag|=n}function ha(...e){const t={};for(let n=0;npe||De;let zn,$s;{const e=ts(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),o=>{r.length>1?r.forEach(i=>i(o)):r[0](o)}};zn=t("__VUE_INSTANCE_SETTERS__",n=>pe=n),$s=t("__VUE_SSR_SETTERS__",n=>En=n)}const Sn=e=>{const t=pe;return zn(e),e.scope.on(),()=>{e.scope.off(),zn(t)}},$r=()=>{pe&&pe.scope.off(),zn(null)};function ki(e){return e.vnode.shapeFlag&4}let En=!1;function ba(e,t=!1,n=!1){t&&$s(t);const{props:s,children:r}=e.vnode,o=ki(e);Yc(e,s,o,t),na(e,r,n||t);const i=o?_a(e,t):void 0;return t&&$s(!1),i}function _a(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Fc);const{setup:s}=n;if(s){ht();const r=e.setupContext=s.length>1?wa(e):null,o=Sn(e),i=Rn(s,e,0,[e.props,r]),l=Vo(i);if(pt(),o(),(l||e.sp)&&!an(e)&&_i(e),l){if(i.then($r,$r),t)return i.then(c=>{Kr(e,c)}).catch(c=>{rs(c,e,0)});e.asyncDep=i}else Kr(e,i)}else Vi(e)}function Kr(e,t,n){k(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:oe(t)&&(e.setupState=fi(t)),Vi(e)}function Vi(e,t,n){const s=e.type;e.render||(e.render=s.render||rt);{const r=Sn(e);ht();try{Lc(e)}finally{pt(),r()}}}const Ea={get(e,t){return he(e,"get",""),e[t]}};function wa(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Ea),slots:e.slots,emit:e.emit,expose:t}}function cs(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(fi(oc(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in un)return un[n](e)},has(t,n){return n in t||n in un}})):e.proxy}function Ra(e,t=!0){return k(e)?e.displayName||e.name:e.name||t&&e.__name}function xa(e){return k(e)&&"__vccOpts"in e}const qe=(e,t)=>uc(e,t,En);function qi(e,t,n){try{Wn(-1);const s=arguments.length;return s===2?oe(t)&&!H(t)?Gn(t)?Ae(e,null,[t]):Ae(e,t):Ae(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&Gn(n)&&(n=[n]),Ae(e,t,n))}finally{Wn(1)}}const Sa="3.5.25";/** +* @vue/runtime-dom v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ks;const Wr=typeof window<"u"&&window.trustedTypes;if(Wr)try{Ks=Wr.createPolicy("vue",{createHTML:e=>e})}catch{}const $i=Ks?e=>Ks.createHTML(e):e=>e,Aa="http://www.w3.org/2000/svg",va="http://www.w3.org/1998/Math/MathML",at=typeof document<"u"?document:null,Gr=at&&at.createElement("template"),Oa={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?at.createElementNS(Aa,e):t==="mathml"?at.createElementNS(va,e):n?at.createElement(e,{is:n}):at.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>at.createTextNode(e),createComment:e=>at.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>at.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{Gr.innerHTML=$i(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Gr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ca=Symbol("_vtc");function Ta(e,t,n){const s=e[Ca];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const zr=Symbol("_vod"),Pa=Symbol("_vsh"),Na=Symbol(""),Ia=/(?:^|;)\s*display\s*:/;function Da(e,t,n){const s=e.style,r=ae(n);let o=!1;if(n&&!r){if(t)if(ae(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&Un(s,l,"")}else for(const i in t)n[i]==null&&Un(s,i,"");for(const i in n)i==="display"&&(o=!0),Un(s,i,n[i])}else if(r){if(t!==n){const i=s[Na];i&&(n+=";"+i),s.cssText=n,o=Ia.test(n)}}else t&&e.removeAttribute("style");zr in e&&(e[zr]=o?s.display:"",e[Pa]&&(s.display="none"))}const Jr=/\s*!important$/;function Un(e,t,n){if(H(n))n.forEach(s=>Un(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Fa(e,t);Jr.test(n)?e.setProperty(Ft(s),n.replace(Jr,""),"important"):e[s]=n}}const Xr=["Webkit","Moz","ms"],xs={};function Fa(e,t){const n=xs[t];if(n)return n;let s=je(t);if(s!=="filter"&&s in e)return xs[t]=s;s=es(s);for(let r=0;rSs||(Ba.then(()=>Ss=0),Ss=Date.now());function Ha(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;ot(ka(s,n.value),t,5,[s])};return n.value=e,n.attached=ja(),n}function ka(e,t){if(H(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const no=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Va=(e,t,n,s,r,o)=>{const i=r==="svg";t==="class"?Ta(e,s,i):t==="style"?Da(e,n,s):Qn(t)?tr(t)||Ma(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):qa(e,t,s,i))?(Zr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Yr(e,t,s,i,o,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!ae(s))?Zr(e,je(t),s,o,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Yr(e,t,s,i))};function qa(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&no(t)&&k(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return no(t)&&ae(n)?!1:t in e}const so=e=>{const t=e.props["onUpdate:modelValue"]||!1;return H(t)?n=>Dn(t,n):t};function $a(e){e.target.composing=!0}function ro(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const As=Symbol("_assign");function oo(e,t,n){return t&&(e=e.trim()),n&&(e=rr(e)),e}const Ki={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[As]=so(r);const o=s||r.props&&r.props.type==="number";Ut(e,t?"change":"input",i=>{i.target.composing||e[As](oo(e.value,n,o))}),(n||o)&&Ut(e,"change",()=>{e.value=oo(e.value,n,o)}),t||(Ut(e,"compositionstart",$a),Ut(e,"compositionend",ro),Ut(e,"change",ro))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},i){if(e[As]=so(i),e.composing)return;const l=(o||e.type==="number")&&!/^0\d/.test(e.value)?rr(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},Ka=ye({patchProp:Va},Oa);let io;function Wa(){return io||(io=ra(Ka))}const Ga=((...e)=>{const t=Wa().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Ja(s);if(!r)return;const o=t._component;!k(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const i=n(r,!1,za(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t});function za(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Ja(e){return ae(e)?document.querySelector(e):e}function Wi(e,t){return function(){return e.apply(t,arguments)}}const{toString:Xa}=Object.prototype,{getPrototypeOf:Er}=Object,{iterator:as,toStringTag:Gi}=Symbol,us=(e=>t=>{const n=Xa.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),Ge=e=>(e=e.toLowerCase(),t=>us(t)===e),fs=e=>t=>typeof t===e,{isArray:Xt}=Array,Gt=fs("undefined");function An(e){return e!==null&&!Gt(e)&&e.constructor!==null&&!Gt(e.constructor)&&ve(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const zi=Ge("ArrayBuffer");function Qa(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&zi(e.buffer),t}const Ya=fs("string"),ve=fs("function"),Ji=fs("number"),vn=e=>e!==null&&typeof e=="object",Za=e=>e===!0||e===!1,Bn=e=>{if(us(e)!=="object")return!1;const t=Er(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Gi in e)&&!(as in e)},eu=e=>{if(!vn(e)||An(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},tu=Ge("Date"),nu=Ge("File"),su=Ge("Blob"),ru=Ge("FileList"),ou=e=>vn(e)&&ve(e.pipe),iu=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||ve(e.append)&&((t=us(e))==="formdata"||t==="object"&&ve(e.toString)&&e.toString()==="[object FormData]"))},lu=Ge("URLSearchParams"),[cu,au,uu,fu]=["ReadableStream","Request","Response","Headers"].map(Ge),du=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function On(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let s,r;if(typeof e!="object"&&(e=[e]),Xt(e))for(s=0,r=e.length;s0;)if(r=n[s],t===r.toLowerCase())return r;return null}const Ct=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Qi=e=>!Gt(e)&&e!==Ct;function Ws(){const{caseless:e,skipUndefined:t}=Qi(this)&&this||{},n={},s=(r,o)=>{const i=e&&Xi(n,o)||o;Bn(n[i])&&Bn(r)?n[i]=Ws(n[i],r):Bn(r)?n[i]=Ws({},r):Xt(r)?n[i]=r.slice():(!t||!Gt(r))&&(n[i]=r)};for(let r=0,o=arguments.length;r(On(t,(r,o)=>{n&&ve(r)?e[o]=Wi(r,n):e[o]=r},{allOwnKeys:s}),e),pu=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),mu=(e,t,n,s)=>{e.prototype=Object.create(t.prototype,s),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},gu=(e,t,n,s)=>{let r,o,i;const l={};if(t=t||{},e==null)return t;do{for(r=Object.getOwnPropertyNames(e),o=r.length;o-- >0;)i=r[o],(!s||s(i,e,t))&&!l[i]&&(t[i]=e[i],l[i]=!0);e=n!==!1&&Er(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},yu=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const s=e.indexOf(t,n);return s!==-1&&s===n},bu=e=>{if(!e)return null;if(Xt(e))return e;let t=e.length;if(!Ji(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},_u=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&Er(Uint8Array)),Eu=(e,t)=>{const s=(e&&e[as]).call(e);let r;for(;(r=s.next())&&!r.done;){const o=r.value;t.call(e,o[0],o[1])}},wu=(e,t)=>{let n;const s=[];for(;(n=e.exec(t))!==null;)s.push(n);return s},Ru=Ge("HTMLFormElement"),xu=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,s,r){return s.toUpperCase()+r}),lo=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),Su=Ge("RegExp"),Yi=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),s={};On(n,(r,o)=>{let i;(i=t(r,o,e))!==!1&&(s[o]=i||r)}),Object.defineProperties(e,s)},Au=e=>{Yi(e,(t,n)=>{if(ve(e)&&["arguments","caller","callee"].indexOf(n)!==-1)return!1;const s=e[n];if(ve(s)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},vu=(e,t)=>{const n={},s=r=>{r.forEach(o=>{n[o]=!0})};return Xt(e)?s(e):s(String(e).split(t)),n},Ou=()=>{},Cu=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function Tu(e){return!!(e&&ve(e.append)&&e[Gi]==="FormData"&&e[as])}const Pu=e=>{const t=new Array(10),n=(s,r)=>{if(vn(s)){if(t.indexOf(s)>=0)return;if(An(s))return s;if(!("toJSON"in s)){t[r]=s;const o=Xt(s)?[]:{};return On(s,(i,l)=>{const c=n(i,r+1);!Gt(c)&&(o[l]=c)}),t[r]=void 0,o}}return s};return n(e,0)},Nu=Ge("AsyncFunction"),Iu=e=>e&&(vn(e)||ve(e))&&ve(e.then)&&ve(e.catch),Zi=((e,t)=>e?setImmediate:t?((n,s)=>(Ct.addEventListener("message",({source:r,data:o})=>{r===Ct&&o===n&&s.length&&s.shift()()},!1),r=>{s.push(r),Ct.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",ve(Ct.postMessage)),Du=typeof queueMicrotask<"u"?queueMicrotask.bind(Ct):typeof process<"u"&&process.nextTick||Zi,Fu=e=>e!=null&&ve(e[as]),b={isArray:Xt,isArrayBuffer:zi,isBuffer:An,isFormData:iu,isArrayBufferView:Qa,isString:Ya,isNumber:Ji,isBoolean:Za,isObject:vn,isPlainObject:Bn,isEmptyObject:eu,isReadableStream:cu,isRequest:au,isResponse:uu,isHeaders:fu,isUndefined:Gt,isDate:tu,isFile:nu,isBlob:su,isRegExp:Su,isFunction:ve,isStream:ou,isURLSearchParams:lu,isTypedArray:_u,isFileList:ru,forEach:On,merge:Ws,extend:hu,trim:du,stripBOM:pu,inherits:mu,toFlatObject:gu,kindOf:us,kindOfTest:Ge,endsWith:yu,toArray:bu,forEachEntry:Eu,matchAll:wu,isHTMLForm:Ru,hasOwnProperty:lo,hasOwnProp:lo,reduceDescriptors:Yi,freezeMethods:Au,toObjectSet:vu,toCamelCase:xu,noop:Ou,toFiniteNumber:Cu,findKey:Xi,global:Ct,isContextDefined:Qi,isSpecCompliantForm:Tu,toJSONObject:Pu,isAsyncFn:Nu,isThenable:Iu,setImmediate:Zi,asap:Du,isIterable:Fu};function V(e,t,n,s,r){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),s&&(this.request=s),r&&(this.response=r,this.status=r.status?r.status:null)}b.inherits(V,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:b.toJSONObject(this.config),code:this.code,status:this.status}}});const el=V.prototype,tl={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(e=>{tl[e]={value:e}});Object.defineProperties(V,tl);Object.defineProperty(el,"isAxiosError",{value:!0});V.from=(e,t,n,s,r,o)=>{const i=Object.create(el);b.toFlatObject(e,i,function(a){return a!==Error.prototype},u=>u!=="isAxiosError");const l=e&&e.message?e.message:"Error",c=t==null&&e?e.code:t;return V.call(i,l,c,n,s,r),e&&i.cause==null&&Object.defineProperty(i,"cause",{value:e,configurable:!0}),i.name=e&&e.name||"Error",o&&Object.assign(i,o),i};const Lu=null;function Gs(e){return b.isPlainObject(e)||b.isArray(e)}function nl(e){return b.endsWith(e,"[]")?e.slice(0,-2):e}function co(e,t,n){return e?e.concat(t).map(function(r,o){return r=nl(r),!n&&o?"["+r+"]":r}).join(n?".":""):t}function Mu(e){return b.isArray(e)&&!e.some(Gs)}const Uu=b.toFlatObject(b,{},null,function(t){return/^is[A-Z]/.test(t)});function ds(e,t,n){if(!b.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=b.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(R,S){return!b.isUndefined(S[R])});const s=n.metaTokens,r=n.visitor||a,o=n.dots,i=n.indexes,c=(n.Blob||typeof Blob<"u"&&Blob)&&b.isSpecCompliantForm(t);if(!b.isFunction(r))throw new TypeError("visitor must be a function");function u(g){if(g===null)return"";if(b.isDate(g))return g.toISOString();if(b.isBoolean(g))return g.toString();if(!c&&b.isBlob(g))throw new V("Blob is not supported. Use a Buffer instead.");return b.isArrayBuffer(g)||b.isTypedArray(g)?c&&typeof Blob=="function"?new Blob([g]):Buffer.from(g):g}function a(g,R,S){let C=g;if(g&&!S&&typeof g=="object"){if(b.endsWith(R,"{}"))R=s?R:R.slice(0,-2),g=JSON.stringify(g);else if(b.isArray(g)&&Mu(g)||(b.isFileList(g)||b.endsWith(R,"[]"))&&(C=b.toArray(g)))return R=nl(R),C.forEach(function(D,P){!(b.isUndefined(D)||D===null)&&t.append(i===!0?co([R],P,o):i===null?R:R+"[]",u(D))}),!1}return Gs(g)?!0:(t.append(co(S,R,o),u(g)),!1)}const f=[],p=Object.assign(Uu,{defaultVisitor:a,convertValue:u,isVisitable:Gs});function y(g,R){if(!b.isUndefined(g)){if(f.indexOf(g)!==-1)throw Error("Circular reference detected in "+R.join("."));f.push(g),b.forEach(g,function(C,N){(!(b.isUndefined(C)||C===null)&&r.call(t,C,b.isString(N)?N.trim():N,R,p))===!0&&y(C,R?R.concat(N):[N])}),f.pop()}}if(!b.isObject(e))throw new TypeError("data must be an object");return y(e),t}function ao(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(s){return t[s]})}function wr(e,t){this._pairs=[],e&&ds(e,this,t)}const sl=wr.prototype;sl.append=function(t,n){this._pairs.push([t,n])};sl.toString=function(t){const n=t?function(s){return t.call(this,s,ao)}:ao;return this._pairs.map(function(r){return n(r[0])+"="+n(r[1])},"").join("&")};function Bu(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function rl(e,t,n){if(!t)return e;const s=n&&n.encode||Bu;b.isFunction(n)&&(n={serialize:n});const r=n&&n.serialize;let o;if(r?o=r(t,n):o=b.isURLSearchParams(t)?t.toString():new wr(t,n).toString(s),o){const i=e.indexOf("#");i!==-1&&(e=e.slice(0,i)),e+=(e.indexOf("?")===-1?"?":"&")+o}return e}class uo{constructor(){this.handlers=[]}use(t,n,s){return this.handlers.push({fulfilled:t,rejected:n,synchronous:s?s.synchronous:!1,runWhen:s?s.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){b.forEach(this.handlers,function(s){s!==null&&t(s)})}}const ol={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},ju=typeof URLSearchParams<"u"?URLSearchParams:wr,Hu=typeof FormData<"u"?FormData:null,ku=typeof Blob<"u"?Blob:null,Vu={isBrowser:!0,classes:{URLSearchParams:ju,FormData:Hu,Blob:ku},protocols:["http","https","file","blob","url","data"]},Rr=typeof window<"u"&&typeof document<"u",zs=typeof navigator=="object"&&navigator||void 0,qu=Rr&&(!zs||["ReactNative","NativeScript","NS"].indexOf(zs.product)<0),$u=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",Ku=Rr&&window.location.href||"http://localhost",Wu=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Rr,hasStandardBrowserEnv:qu,hasStandardBrowserWebWorkerEnv:$u,navigator:zs,origin:Ku},Symbol.toStringTag,{value:"Module"})),me={...Wu,...Vu};function Gu(e,t){return ds(e,new me.classes.URLSearchParams,{visitor:function(n,s,r,o){return me.isNode&&b.isBuffer(n)?(this.append(s,n.toString("base64")),!1):o.defaultVisitor.apply(this,arguments)},...t})}function zu(e){return b.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function Ju(e){const t={},n=Object.keys(e);let s;const r=n.length;let o;for(s=0;s=n.length;return i=!i&&b.isArray(r)?r.length:i,c?(b.hasOwnProp(r,i)?r[i]=[r[i],s]:r[i]=s,!l):((!r[i]||!b.isObject(r[i]))&&(r[i]=[]),t(n,s,r[i],o)&&b.isArray(r[i])&&(r[i]=Ju(r[i])),!l)}if(b.isFormData(e)&&b.isFunction(e.entries)){const n={};return b.forEachEntry(e,(s,r)=>{t(zu(s),r,n,0)}),n}return null}function Xu(e,t,n){if(b.isString(e))try{return(t||JSON.parse)(e),b.trim(e)}catch(s){if(s.name!=="SyntaxError")throw s}return(n||JSON.stringify)(e)}const Cn={transitional:ol,adapter:["xhr","http","fetch"],transformRequest:[function(t,n){const s=n.getContentType()||"",r=s.indexOf("application/json")>-1,o=b.isObject(t);if(o&&b.isHTMLForm(t)&&(t=new FormData(t)),b.isFormData(t))return r?JSON.stringify(il(t)):t;if(b.isArrayBuffer(t)||b.isBuffer(t)||b.isStream(t)||b.isFile(t)||b.isBlob(t)||b.isReadableStream(t))return t;if(b.isArrayBufferView(t))return t.buffer;if(b.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let l;if(o){if(s.indexOf("application/x-www-form-urlencoded")>-1)return Gu(t,this.formSerializer).toString();if((l=b.isFileList(t))||s.indexOf("multipart/form-data")>-1){const c=this.env&&this.env.FormData;return ds(l?{"files[]":t}:t,c&&new c,this.formSerializer)}}return o||r?(n.setContentType("application/json",!1),Xu(t)):t}],transformResponse:[function(t){const n=this.transitional||Cn.transitional,s=n&&n.forcedJSONParsing,r=this.responseType==="json";if(b.isResponse(t)||b.isReadableStream(t))return t;if(t&&b.isString(t)&&(s&&!this.responseType||r)){const i=!(n&&n.silentJSONParsing)&&r;try{return JSON.parse(t,this.parseReviver)}catch(l){if(i)throw l.name==="SyntaxError"?V.from(l,V.ERR_BAD_RESPONSE,this,null,this.response):l}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:me.classes.FormData,Blob:me.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};b.forEach(["delete","get","head","post","put","patch"],e=>{Cn.headers[e]={}});const Qu=b.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),Yu=e=>{const t={};let n,s,r;return e&&e.split(` +`).forEach(function(i){r=i.indexOf(":"),n=i.substring(0,r).trim().toLowerCase(),s=i.substring(r+1).trim(),!(!n||t[n]&&Qu[n])&&(n==="set-cookie"?t[n]?t[n].push(s):t[n]=[s]:t[n]=t[n]?t[n]+", "+s:s)}),t},fo=Symbol("internals");function tn(e){return e&&String(e).trim().toLowerCase()}function jn(e){return e===!1||e==null?e:b.isArray(e)?e.map(jn):String(e)}function Zu(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let s;for(;s=n.exec(e);)t[s[1]]=s[2];return t}const ef=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function vs(e,t,n,s,r){if(b.isFunction(s))return s.call(this,t,n);if(r&&(t=n),!!b.isString(t)){if(b.isString(s))return t.indexOf(s)!==-1;if(b.isRegExp(s))return s.test(t)}}function tf(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,s)=>n.toUpperCase()+s)}function nf(e,t){const n=b.toCamelCase(" "+t);["get","set","has"].forEach(s=>{Object.defineProperty(e,s+n,{value:function(r,o,i){return this[s].call(this,t,r,o,i)},configurable:!0})})}let Oe=class{constructor(t){t&&this.set(t)}set(t,n,s){const r=this;function o(l,c,u){const a=tn(c);if(!a)throw new Error("header name must be a non-empty string");const f=b.findKey(r,a);(!f||r[f]===void 0||u===!0||u===void 0&&r[f]!==!1)&&(r[f||c]=jn(l))}const i=(l,c)=>b.forEach(l,(u,a)=>o(u,a,c));if(b.isPlainObject(t)||t instanceof this.constructor)i(t,n);else if(b.isString(t)&&(t=t.trim())&&!ef(t))i(Yu(t),n);else if(b.isObject(t)&&b.isIterable(t)){let l={},c,u;for(const a of t){if(!b.isArray(a))throw TypeError("Object iterator must return a key-value pair");l[u=a[0]]=(c=l[u])?b.isArray(c)?[...c,a[1]]:[c,a[1]]:a[1]}i(l,n)}else t!=null&&o(n,t,s);return this}get(t,n){if(t=tn(t),t){const s=b.findKey(this,t);if(s){const r=this[s];if(!n)return r;if(n===!0)return Zu(r);if(b.isFunction(n))return n.call(this,r,s);if(b.isRegExp(n))return n.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=tn(t),t){const s=b.findKey(this,t);return!!(s&&this[s]!==void 0&&(!n||vs(this,this[s],s,n)))}return!1}delete(t,n){const s=this;let r=!1;function o(i){if(i=tn(i),i){const l=b.findKey(s,i);l&&(!n||vs(s,s[l],l,n))&&(delete s[l],r=!0)}}return b.isArray(t)?t.forEach(o):o(t),r}clear(t){const n=Object.keys(this);let s=n.length,r=!1;for(;s--;){const o=n[s];(!t||vs(this,this[o],o,t,!0))&&(delete this[o],r=!0)}return r}normalize(t){const n=this,s={};return b.forEach(this,(r,o)=>{const i=b.findKey(s,o);if(i){n[i]=jn(r),delete n[o];return}const l=t?tf(o):String(o).trim();l!==o&&delete n[o],n[l]=jn(r),s[l]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return b.forEach(this,(s,r)=>{s!=null&&s!==!1&&(n[r]=t&&b.isArray(s)?s.join(", "):s)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const s=new this(t);return n.forEach(r=>s.set(r)),s}static accessor(t){const s=(this[fo]=this[fo]={accessors:{}}).accessors,r=this.prototype;function o(i){const l=tn(i);s[l]||(nf(r,i),s[l]=!0)}return b.isArray(t)?t.forEach(o):o(t),this}};Oe.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);b.reduceDescriptors(Oe.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(s){this[n]=s}}});b.freezeMethods(Oe);function Os(e,t){const n=this||Cn,s=t||n,r=Oe.from(s.headers);let o=s.data;return b.forEach(e,function(l){o=l.call(n,o,r.normalize(),t?t.status:void 0)}),r.normalize(),o}function ll(e){return!!(e&&e.__CANCEL__)}function Qt(e,t,n){V.call(this,e??"canceled",V.ERR_CANCELED,t,n),this.name="CanceledError"}b.inherits(Qt,V,{__CANCEL__:!0});function cl(e,t,n){const s=n.config.validateStatus;!n.status||!s||s(n.status)?e(n):t(new V("Request failed with status code "+n.status,[V.ERR_BAD_REQUEST,V.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function sf(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function rf(e,t){e=e||10;const n=new Array(e),s=new Array(e);let r=0,o=0,i;return t=t!==void 0?t:1e3,function(c){const u=Date.now(),a=s[o];i||(i=u),n[r]=c,s[r]=u;let f=o,p=0;for(;f!==r;)p+=n[f++],f=f%e;if(r=(r+1)%e,r===o&&(o=(o+1)%e),u-i{n=a,r=null,o&&(clearTimeout(o),o=null),e(...u)};return[(...u)=>{const a=Date.now(),f=a-n;f>=s?i(u,a):(r=u,o||(o=setTimeout(()=>{o=null,i(r)},s-f)))},()=>r&&i(r)]}const Jn=(e,t,n=3)=>{let s=0;const r=rf(50,250);return of(o=>{const i=o.loaded,l=o.lengthComputable?o.total:void 0,c=i-s,u=r(c),a=i<=l;s=i;const f={loaded:i,total:l,progress:l?i/l:void 0,bytes:c,rate:u||void 0,estimated:u&&l&&a?(l-i)/u:void 0,event:o,lengthComputable:l!=null,[t?"download":"upload"]:!0};e(f)},n)},ho=(e,t)=>{const n=e!=null;return[s=>t[0]({lengthComputable:n,total:e,loaded:s}),t[1]]},po=e=>(...t)=>b.asap(()=>e(...t)),lf=me.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,me.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(me.origin),me.navigator&&/(msie|trident)/i.test(me.navigator.userAgent)):()=>!0,cf=me.hasStandardBrowserEnv?{write(e,t,n,s,r,o,i){if(typeof document>"u")return;const l=[`${e}=${encodeURIComponent(t)}`];b.isNumber(n)&&l.push(`expires=${new Date(n).toUTCString()}`),b.isString(s)&&l.push(`path=${s}`),b.isString(r)&&l.push(`domain=${r}`),o===!0&&l.push("secure"),b.isString(i)&&l.push(`SameSite=${i}`),document.cookie=l.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function af(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function uf(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function al(e,t,n){let s=!af(t);return e&&(s||n==!1)?uf(e,t):t}const mo=e=>e instanceof Oe?{...e}:e;function Dt(e,t){t=t||{};const n={};function s(u,a,f,p){return b.isPlainObject(u)&&b.isPlainObject(a)?b.merge.call({caseless:p},u,a):b.isPlainObject(a)?b.merge({},a):b.isArray(a)?a.slice():a}function r(u,a,f,p){if(b.isUndefined(a)){if(!b.isUndefined(u))return s(void 0,u,f,p)}else return s(u,a,f,p)}function o(u,a){if(!b.isUndefined(a))return s(void 0,a)}function i(u,a){if(b.isUndefined(a)){if(!b.isUndefined(u))return s(void 0,u)}else return s(void 0,a)}function l(u,a,f){if(f in t)return s(u,a);if(f in e)return s(void 0,u)}const c={url:o,method:o,data:o,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:l,headers:(u,a,f)=>r(mo(u),mo(a),f,!0)};return b.forEach(Object.keys({...e,...t}),function(a){const f=c[a]||r,p=f(e[a],t[a],a);b.isUndefined(p)&&f!==l||(n[a]=p)}),n}const ul=e=>{const t=Dt({},e);let{data:n,withXSRFToken:s,xsrfHeaderName:r,xsrfCookieName:o,headers:i,auth:l}=t;if(t.headers=i=Oe.from(i),t.url=rl(al(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),l&&i.set("Authorization","Basic "+btoa((l.username||"")+":"+(l.password?unescape(encodeURIComponent(l.password)):""))),b.isFormData(n)){if(me.hasStandardBrowserEnv||me.hasStandardBrowserWebWorkerEnv)i.setContentType(void 0);else if(b.isFunction(n.getHeaders)){const c=n.getHeaders(),u=["content-type","content-length"];Object.entries(c).forEach(([a,f])=>{u.includes(a.toLowerCase())&&i.set(a,f)})}}if(me.hasStandardBrowserEnv&&(s&&b.isFunction(s)&&(s=s(t)),s||s!==!1&&lf(t.url))){const c=r&&o&&cf.read(o);c&&i.set(r,c)}return t},ff=typeof XMLHttpRequest<"u",df=ff&&function(e){return new Promise(function(n,s){const r=ul(e);let o=r.data;const i=Oe.from(r.headers).normalize();let{responseType:l,onUploadProgress:c,onDownloadProgress:u}=r,a,f,p,y,g;function R(){y&&y(),g&&g(),r.cancelToken&&r.cancelToken.unsubscribe(a),r.signal&&r.signal.removeEventListener("abort",a)}let S=new XMLHttpRequest;S.open(r.method.toUpperCase(),r.url,!0),S.timeout=r.timeout;function C(){if(!S)return;const D=Oe.from("getAllResponseHeaders"in S&&S.getAllResponseHeaders()),q={data:!l||l==="text"||l==="json"?S.responseText:S.response,status:S.status,statusText:S.statusText,headers:D,config:e,request:S};cl(function(K){n(K),R()},function(K){s(K),R()},q),S=null}"onloadend"in S?S.onloadend=C:S.onreadystatechange=function(){!S||S.readyState!==4||S.status===0&&!(S.responseURL&&S.responseURL.indexOf("file:")===0)||setTimeout(C)},S.onabort=function(){S&&(s(new V("Request aborted",V.ECONNABORTED,e,S)),S=null)},S.onerror=function(P){const q=P&&P.message?P.message:"Network Error",re=new V(q,V.ERR_NETWORK,e,S);re.event=P||null,s(re),S=null},S.ontimeout=function(){let P=r.timeout?"timeout of "+r.timeout+"ms exceeded":"timeout exceeded";const q=r.transitional||ol;r.timeoutErrorMessage&&(P=r.timeoutErrorMessage),s(new V(P,q.clarifyTimeoutError?V.ETIMEDOUT:V.ECONNABORTED,e,S)),S=null},o===void 0&&i.setContentType(null),"setRequestHeader"in S&&b.forEach(i.toJSON(),function(P,q){S.setRequestHeader(q,P)}),b.isUndefined(r.withCredentials)||(S.withCredentials=!!r.withCredentials),l&&l!=="json"&&(S.responseType=r.responseType),u&&([p,g]=Jn(u,!0),S.addEventListener("progress",p)),c&&S.upload&&([f,y]=Jn(c),S.upload.addEventListener("progress",f),S.upload.addEventListener("loadend",y)),(r.cancelToken||r.signal)&&(a=D=>{S&&(s(!D||D.type?new Qt(null,e,S):D),S.abort(),S=null)},r.cancelToken&&r.cancelToken.subscribe(a),r.signal&&(r.signal.aborted?a():r.signal.addEventListener("abort",a)));const N=sf(r.url);if(N&&me.protocols.indexOf(N)===-1){s(new V("Unsupported protocol "+N+":",V.ERR_BAD_REQUEST,e));return}S.send(o||null)})},hf=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let s=new AbortController,r;const o=function(u){if(!r){r=!0,l();const a=u instanceof Error?u:this.reason;s.abort(a instanceof V?a:new Qt(a instanceof Error?a.message:a))}};let i=t&&setTimeout(()=>{i=null,o(new V(`timeout ${t} of ms exceeded`,V.ETIMEDOUT))},t);const l=()=>{e&&(i&&clearTimeout(i),i=null,e.forEach(u=>{u.unsubscribe?u.unsubscribe(o):u.removeEventListener("abort",o)}),e=null)};e.forEach(u=>u.addEventListener("abort",o));const{signal:c}=s;return c.unsubscribe=()=>b.asap(l),c}},pf=function*(e,t){let n=e.byteLength;if(n{const r=mf(e,t);let o=0,i,l=c=>{i||(i=!0,s&&s(c))};return new ReadableStream({async pull(c){try{const{done:u,value:a}=await r.next();if(u){l(),c.close();return}let f=a.byteLength;if(n){let p=o+=f;n(p)}c.enqueue(new Uint8Array(a))}catch(u){throw l(u),u}},cancel(c){return l(c),r.return()}},{highWaterMark:2})},yo=64*1024,{isFunction:In}=b,yf=(({Request:e,Response:t})=>({Request:e,Response:t}))(b.global),{ReadableStream:bo,TextEncoder:_o}=b.global,Eo=(e,...t)=>{try{return!!e(...t)}catch{return!1}},bf=e=>{e=b.merge.call({skipUndefined:!0},yf,e);const{fetch:t,Request:n,Response:s}=e,r=t?In(t):typeof fetch=="function",o=In(n),i=In(s);if(!r)return!1;const l=r&&In(bo),c=r&&(typeof _o=="function"?(g=>R=>g.encode(R))(new _o):async g=>new Uint8Array(await new n(g).arrayBuffer())),u=o&&l&&Eo(()=>{let g=!1;const R=new n(me.origin,{body:new bo,method:"POST",get duplex(){return g=!0,"half"}}).headers.has("Content-Type");return g&&!R}),a=i&&l&&Eo(()=>b.isReadableStream(new s("").body)),f={stream:a&&(g=>g.body)};r&&["text","arrayBuffer","blob","formData","stream"].forEach(g=>{!f[g]&&(f[g]=(R,S)=>{let C=R&&R[g];if(C)return C.call(R);throw new V(`Response type '${g}' is not supported`,V.ERR_NOT_SUPPORT,S)})});const p=async g=>{if(g==null)return 0;if(b.isBlob(g))return g.size;if(b.isSpecCompliantForm(g))return(await new n(me.origin,{method:"POST",body:g}).arrayBuffer()).byteLength;if(b.isArrayBufferView(g)||b.isArrayBuffer(g))return g.byteLength;if(b.isURLSearchParams(g)&&(g=g+""),b.isString(g))return(await c(g)).byteLength},y=async(g,R)=>{const S=b.toFiniteNumber(g.getContentLength());return S??p(R)};return async g=>{let{url:R,method:S,data:C,signal:N,cancelToken:D,timeout:P,onDownloadProgress:q,onUploadProgress:re,responseType:K,headers:we,withCredentials:Ce="same-origin",fetchOptions:Te}=ul(g),He=t||fetch;K=K?(K+"").toLowerCase():"text";let Le=hf([N,D&&D.toAbortSignal()],P),de=null;const Pe=Le&&Le.unsubscribe&&(()=>{Le.unsubscribe()});let it;try{if(re&&u&&S!=="get"&&S!=="head"&&(it=await y(we,C))!==0){let ue=new n(R,{method:"POST",body:C,duplex:"half"}),le;if(b.isFormData(C)&&(le=ue.headers.get("content-type"))&&we.setContentType(le),ue.body){const[Je,ke]=ho(it,Jn(po(re)));C=go(ue.body,yo,Je,ke)}}b.isString(Ce)||(Ce=Ce?"include":"omit");const Y=o&&"credentials"in n.prototype,z={...Te,signal:Le,method:S.toUpperCase(),headers:we.normalize().toJSON(),body:C,duplex:"half",credentials:Y?Ce:void 0};de=o&&new n(R,z);let $=await(o?He(de,Te):He(R,z));const Me=a&&(K==="stream"||K==="response");if(a&&(q||Me&&Pe)){const ue={};["status","statusText","headers"].forEach(Xe=>{ue[Xe]=$[Xe]});const le=b.toFiniteNumber($.headers.get("content-length")),[Je,ke]=q&&ho(le,Jn(po(q),!0))||[];$=new s(go($.body,yo,Je,()=>{ke&&ke(),Pe&&Pe()}),ue)}K=K||"text";let ze=await f[b.findKey(f,K)||"text"]($,g);return!Me&&Pe&&Pe(),await new Promise((ue,le)=>{cl(ue,le,{data:ze,headers:Oe.from($.headers),status:$.status,statusText:$.statusText,config:g,request:de})})}catch(Y){throw Pe&&Pe(),Y&&Y.name==="TypeError"&&/Load failed|fetch/i.test(Y.message)?Object.assign(new V("Network Error",V.ERR_NETWORK,g,de),{cause:Y.cause||Y}):V.from(Y,Y&&Y.code,g,de)}}},_f=new Map,fl=e=>{let t=e&&e.env||{};const{fetch:n,Request:s,Response:r}=t,o=[s,r,n];let i=o.length,l=i,c,u,a=_f;for(;l--;)c=o[l],u=a.get(c),u===void 0&&a.set(c,u=l?new Map:bf(t)),a=u;return u};fl();const xr={http:Lu,xhr:df,fetch:{get:fl}};b.forEach(xr,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const wo=e=>`- ${e}`,Ef=e=>b.isFunction(e)||e===null||e===!1;function wf(e,t){e=b.isArray(e)?e:[e];const{length:n}=e;let s,r;const o={};for(let i=0;i`adapter ${c} `+(u===!1?"is not supported by the environment":"is not available in the build"));let l=n?i.length>1?`since : +`+i.map(wo).join(` +`):" "+wo(i[0]):"as no adapter specified";throw new V("There is no suitable adapter to dispatch the request "+l,"ERR_NOT_SUPPORT")}return r}const dl={getAdapter:wf,adapters:xr};function Cs(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Qt(null,e)}function Ro(e){return Cs(e),e.headers=Oe.from(e.headers),e.data=Os.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),dl.getAdapter(e.adapter||Cn.adapter,e)(e).then(function(s){return Cs(e),s.data=Os.call(e,e.transformResponse,s),s.headers=Oe.from(s.headers),s},function(s){return ll(s)||(Cs(e),s&&s.response&&(s.response.data=Os.call(e,e.transformResponse,s.response),s.response.headers=Oe.from(s.response.headers))),Promise.reject(s)})}const hl="1.13.2",hs={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{hs[e]=function(s){return typeof s===e||"a"+(t<1?"n ":" ")+e}});const xo={};hs.transitional=function(t,n,s){function r(o,i){return"[Axios v"+hl+"] Transitional option '"+o+"'"+i+(s?". "+s:"")}return(o,i,l)=>{if(t===!1)throw new V(r(i," has been removed"+(n?" in "+n:"")),V.ERR_DEPRECATED);return n&&!xo[i]&&(xo[i]=!0,console.warn(r(i," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(o,i,l):!0}};hs.spelling=function(t){return(n,s)=>(console.warn(`${s} is likely a misspelling of ${t}`),!0)};function Rf(e,t,n){if(typeof e!="object")throw new V("options must be an object",V.ERR_BAD_OPTION_VALUE);const s=Object.keys(e);let r=s.length;for(;r-- >0;){const o=s[r],i=t[o];if(i){const l=e[o],c=l===void 0||i(l,o,e);if(c!==!0)throw new V("option "+o+" must be "+c,V.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new V("Unknown option "+o,V.ERR_BAD_OPTION)}}const Hn={assertOptions:Rf,validators:hs},tt=Hn.validators;let It=class{constructor(t){this.defaults=t||{},this.interceptors={request:new uo,response:new uo}}async request(t,n){try{return await this._request(t,n)}catch(s){if(s instanceof Error){let r={};Error.captureStackTrace?Error.captureStackTrace(r):r=new Error;const o=r.stack?r.stack.replace(/^.+\n/,""):"";try{s.stack?o&&!String(s.stack).endsWith(o.replace(/^.+\n.+\n/,""))&&(s.stack+=` +`+o):s.stack=o}catch{}}throw s}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=Dt(this.defaults,n);const{transitional:s,paramsSerializer:r,headers:o}=n;s!==void 0&&Hn.assertOptions(s,{silentJSONParsing:tt.transitional(tt.boolean),forcedJSONParsing:tt.transitional(tt.boolean),clarifyTimeoutError:tt.transitional(tt.boolean)},!1),r!=null&&(b.isFunction(r)?n.paramsSerializer={serialize:r}:Hn.assertOptions(r,{encode:tt.function,serialize:tt.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),Hn.assertOptions(n,{baseUrl:tt.spelling("baseURL"),withXsrfToken:tt.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=o&&b.merge(o.common,o[n.method]);o&&b.forEach(["delete","get","head","post","put","patch","common"],g=>{delete o[g]}),n.headers=Oe.concat(i,o);const l=[];let c=!0;this.interceptors.request.forEach(function(R){typeof R.runWhen=="function"&&R.runWhen(n)===!1||(c=c&&R.synchronous,l.unshift(R.fulfilled,R.rejected))});const u=[];this.interceptors.response.forEach(function(R){u.push(R.fulfilled,R.rejected)});let a,f=0,p;if(!c){const g=[Ro.bind(this),void 0];for(g.unshift(...l),g.push(...u),p=g.length,a=Promise.resolve(n);f{if(!s._listeners)return;let o=s._listeners.length;for(;o-- >0;)s._listeners[o](r);s._listeners=null}),this.promise.then=r=>{let o;const i=new Promise(l=>{s.subscribe(l),o=l}).then(r);return i.cancel=function(){s.unsubscribe(o)},i},t(function(o,i,l){s.reason||(s.reason=new Qt(o,i,l),n(s.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=s=>{t.abort(s)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new pl(function(r){t=r}),cancel:t}}};function Sf(e){return function(n){return e.apply(null,n)}}function Af(e){return b.isObject(e)&&e.isAxiosError===!0}const Js={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Js).forEach(([e,t])=>{Js[t]=e});function ml(e){const t=new It(e),n=Wi(It.prototype.request,t);return b.extend(n,It.prototype,t,{allOwnKeys:!0}),b.extend(n,t,null,{allOwnKeys:!0}),n.create=function(r){return ml(Dt(e,r))},n}const ce=ml(Cn);ce.Axios=It;ce.CanceledError=Qt;ce.CancelToken=xf;ce.isCancel=ll;ce.VERSION=hl;ce.toFormData=ds;ce.AxiosError=V;ce.Cancel=ce.CanceledError;ce.all=function(t){return Promise.all(t)};ce.spread=Sf;ce.isAxiosError=Af;ce.mergeConfig=Dt;ce.AxiosHeaders=Oe;ce.formToJSON=e=>il(b.isHTMLForm(e)?new FormData(e):e);ce.getAdapter=dl.getAdapter;ce.HttpStatusCode=Js;ce.default=ce;const{Axios:Qd,AxiosError:Yd,CanceledError:Zd,isCancel:eh,CancelToken:th,VERSION:nh,all:sh,Cancel:rh,isAxiosError:oh,spread:ih,toFormData:lh,AxiosHeaders:ch,HttpStatusCode:ah,formToJSON:uh,getAdapter:fh,mergeConfig:dh}=ce,Xn=ce.create({baseURL:"/super/v1"});function vf(e){const t=e.trim();if(!t){delete Xn.defaults.headers.common.Authorization;return}Xn.defaults.headers.common.Authorization=`Bearer ${t}`}const Of={style:{display:"flex",gap:"12px",padding:"12px 16px","border-bottom":"1px solid #eee"}},Cf=xn({__name:"App",setup(e){const t=yn("");function n(){vf(t.value)}return(s,r)=>{const o=Fr("router-link"),i=Fr("router-view");return dn(),hn(Ve,null,[ee("header",Of,[r[3]||(r[3]=ee("strong",null,"Super Admin",-1)),Ae(o,{to:"/"},{default:Bs(()=>[...r[1]||(r[1]=[qs("统计",-1)])]),_:1}),Ae(o,{to:"/tenants"},{default:Bs(()=>[...r[2]||(r[2]=[qs("租户",-1)])]),_:1}),r[4]||(r[4]=ee("span",{style:{flex:"1"}},null,-1)),bi(ee("input",{"onUpdate:modelValue":r[0]||(r[0]=l=>t.value=l),placeholder:"Bearer token(可选)",style:{width:"320px"}},null,512),[[Ki,t.value]]),ee("button",{onClick:n},"应用")]),Ae(i)],64)}}});/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Bt=typeof document<"u";function gl(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Tf(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&gl(e.default)}const J=Object.assign;function Ts(e,t){const n={};for(const s in t){const r=t[s];n[s]=We(r)?r.map(e):e(r)}return n}const pn=()=>{},We=Array.isArray;function So(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const yl=/#/g,Pf=/&/g,Nf=/\//g,If=/=/g,Df=/\?/g,bl=/\+/g,Ff=/%5B/g,Lf=/%5D/g,_l=/%5E/g,Mf=/%60/g,El=/%7B/g,Uf=/%7C/g,wl=/%7D/g,Bf=/%20/g;function Sr(e){return e==null?"":encodeURI(""+e).replace(Uf,"|").replace(Ff,"[").replace(Lf,"]")}function jf(e){return Sr(e).replace(El,"{").replace(wl,"}").replace(_l,"^")}function Xs(e){return Sr(e).replace(bl,"%2B").replace(Bf,"+").replace(yl,"%23").replace(Pf,"%26").replace(Mf,"`").replace(El,"{").replace(wl,"}").replace(_l,"^")}function Hf(e){return Xs(e).replace(If,"%3D")}function kf(e){return Sr(e).replace(yl,"%23").replace(Df,"%3F")}function Vf(e){return kf(e).replace(Nf,"%2F")}function wn(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const qf=/\/$/,$f=e=>e.replace(qf,"");function Ps(e,t,n="/"){let s,r={},o="",i="";const l=t.indexOf("#");let c=t.indexOf("?");return c=l>=0&&c>l?-1:c,c>=0&&(s=t.slice(0,c),o=t.slice(c,l>0?l:t.length),r=e(o.slice(1))),l>=0&&(s=s||t.slice(0,l),i=t.slice(l,t.length)),s=zf(s??t,n),{fullPath:s+o+i,path:s,query:r,hash:wn(i)}}function Kf(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Ao(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Wf(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&zt(t.matched[s],n.matched[r])&&Rl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function zt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Rl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Gf(e[n],t[n]))return!1;return!0}function Gf(e,t){return We(e)?vo(e,t):We(t)?vo(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function vo(e,t){return We(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function zf(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let o=n.length-1,i,l;for(i=0;i1&&o--;else break;return n.slice(0,o).join("/")+"/"+s.slice(i).join("/")}const yt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Qs=(function(e){return e.pop="pop",e.push="push",e})({}),Ns=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function Jf(e){if(!e)if(Bt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),$f(e)}const Xf=/^[^#]+#/;function Qf(e,t){return e.replace(Xf,"#")+t}function Yf(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const ps=()=>({left:window.scrollX,top:window.scrollY});function Zf(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Yf(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Oo(e,t){return(history.state?history.state.position-t:-1)+e}const Ys=new Map;function ed(e,t){Ys.set(e,t)}function td(e){const t=Ys.get(e);return Ys.delete(e),t}function nd(e){return typeof e=="string"||e&&typeof e=="object"}function xl(e){return typeof e=="string"||typeof e=="symbol"}let ie=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const Sl=Symbol("");ie.MATCHER_NOT_FOUND+"",ie.NAVIGATION_GUARD_REDIRECT+"",ie.NAVIGATION_ABORTED+"",ie.NAVIGATION_CANCELLED+"",ie.NAVIGATION_DUPLICATED+"";function Jt(e,t){return J(new Error,{type:e,[Sl]:!0},t)}function ct(e,t){return e instanceof Error&&Sl in e&&(t==null||!!(e.type&t))}const sd=["params","query","hash"];function rd(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of sd)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function od(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&Xs(r)):[s&&Xs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function id(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=We(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const ld=Symbol(""),To=Symbol(""),Ar=Symbol(""),Al=Symbol(""),Zs=Symbol("");function nn(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Rt(e,t,n,s,r,o=i=>i()){const i=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const u=p=>{p===!1?c(Jt(ie.NAVIGATION_ABORTED,{from:n,to:t})):p instanceof Error?c(p):nd(p)?c(Jt(ie.NAVIGATION_GUARD_REDIRECT,{from:t,to:p})):(i&&s.enterCallbacks[r]===i&&typeof p=="function"&&i.push(p),l())},a=o(()=>e.call(s&&s.instances[r],t,n,u));let f=Promise.resolve(a);e.length<3&&(f=f.then(u)),f.catch(p=>c(p))})}function Is(e,t,n,s,r=o=>o()){const o=[];for(const i of e)for(const l in i.components){let c=i.components[l];if(!(t!=="beforeRouteEnter"&&!i.instances[l]))if(gl(c)){const u=(c.__vccOpts||c)[t];u&&o.push(Rt(u,n,s,i,l,r))}else{let u=c();o.push(()=>u.then(a=>{if(!a)throw new Error(`Couldn't resolve component "${l}" at "${i.path}"`);const f=Tf(a)?a.default:a;i.mods[l]=a,i.components[l]=f;const p=(f.__vccOpts||f)[t];return p&&Rt(p,n,s,i,l,r)()}))}}return o}function cd(e,t){const n=[],s=[],r=[],o=Math.max(t.matched.length,e.matched.length);for(let i=0;izt(u,l))?s.push(l):n.push(l));const c=e.matched[i];c&&(t.matched.find(u=>zt(u,c))||r.push(c))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let ad=()=>location.protocol+"//"+location.host;function vl(e,t){const{pathname:n,search:s,hash:r}=t,o=e.indexOf("#");if(o>-1){let i=r.includes(e.slice(o))?e.slice(o).length:1,l=r.slice(i);return l[0]!=="/"&&(l="/"+l),Ao(l,"")}return Ao(n,e)+s+r}function ud(e,t,n,s){let r=[],o=[],i=null;const l=({state:p})=>{const y=vl(e,location),g=n.value,R=t.value;let S=0;if(p){if(n.value=y,t.value=p,i&&i===g){i=null;return}S=R?p.position-R.position:0}else s(y);r.forEach(C=>{C(n.value,g,{delta:S,type:Qs.pop,direction:S?S>0?Ns.forward:Ns.back:Ns.unknown})})};function c(){i=n.value}function u(p){r.push(p);const y=()=>{const g=r.indexOf(p);g>-1&&r.splice(g,1)};return o.push(y),y}function a(){if(document.visibilityState==="hidden"){const{history:p}=window;if(!p.state)return;p.replaceState(J({},p.state,{scroll:ps()}),"")}}function f(){for(const p of o)p();o=[],window.removeEventListener("popstate",l),window.removeEventListener("pagehide",a),document.removeEventListener("visibilitychange",a)}return window.addEventListener("popstate",l),window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",a),{pauseListeners:c,listen:u,destroy:f}}function Po(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?ps():null}}function fd(e){const{history:t,location:n}=window,s={value:vl(e,n)},r={value:t.state};r.value||o(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function o(c,u,a){const f=e.indexOf("#"),p=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+c:ad()+e+c;try{t[a?"replaceState":"pushState"](u,"",p),r.value=u}catch(y){console.error(y),n[a?"replace":"assign"](p)}}function i(c,u){o(c,J({},t.state,Po(r.value.back,c,r.value.forward,!0),u,{position:r.value.position}),!0),s.value=c}function l(c,u){const a=J({},r.value,t.state,{forward:c,scroll:ps()});o(a.current,a,!0),o(c,J({},Po(s.value,c,null),{position:a.position+1},u),!1),s.value=c}return{location:s,state:r,push:l,replace:i}}function dd(e){e=Jf(e);const t=fd(e),n=ud(e,t.state,t.location,t.replace);function s(o,i=!0){i||n.pauseListeners(),history.go(o)}const r=J({location:"",base:e,go:s,createHref:Qf.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let Tt=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var fe=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(fe||{});const hd={type:Tt.Static,value:""},pd=/[a-zA-Z0-9_]/;function md(e){if(!e)return[[]];if(e==="/")return[[hd]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(y){throw new Error(`ERR (${n})/"${u}": ${y}`)}let n=fe.Static,s=n;const r=[];let o;function i(){o&&r.push(o),o=[]}let l=0,c,u="",a="";function f(){u&&(n===fe.Static?o.push({type:Tt.Static,value:u}):n===fe.Param||n===fe.ParamRegExp||n===fe.ParamRegExpEnd?(o.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),o.push({type:Tt.Param,value:u,regexp:a,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),u="")}function p(){u+=c}for(;lt.length?t.length===1&&t[0]===_e.Static+_e.Segment?1:-1:0}function Ol(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const Ed={strict:!1,end:!0,sensitive:!1};function wd(e,t,n){const s=bd(md(e.path),n),r=J(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Rd(e,t){const n=[],s=new Map;t=So(Ed,t);function r(f){return s.get(f)}function o(f,p,y){const g=!y,R=Fo(f);R.aliasOf=y&&y.record;const S=So(t,f),C=[R];if("alias"in f){const P=typeof f.alias=="string"?[f.alias]:f.alias;for(const q of P)C.push(Fo(J({},R,{components:y?y.record.components:R.components,path:q,aliasOf:y?y.record:R})))}let N,D;for(const P of C){const{path:q}=P;if(p&&q[0]!=="/"){const re=p.record.path,K=re[re.length-1]==="/"?"":"/";P.path=p.record.path+(q&&K+q)}if(N=wd(P,p,S),y?y.alias.push(N):(D=D||N,D!==N&&D.alias.push(N),g&&f.name&&!Lo(N)&&i(f.name)),Cl(N)&&c(N),R.children){const re=R.children;for(let K=0;K{i(D)}:pn}function i(f){if(xl(f)){const p=s.get(f);p&&(s.delete(f),n.splice(n.indexOf(p),1),p.children.forEach(i),p.alias.forEach(i))}else{const p=n.indexOf(f);p>-1&&(n.splice(p,1),f.record.name&&s.delete(f.record.name),f.children.forEach(i),f.alias.forEach(i))}}function l(){return n}function c(f){const p=Ad(f,n);n.splice(p,0,f),f.record.name&&!Lo(f)&&s.set(f.record.name,f)}function u(f,p){let y,g={},R,S;if("name"in f&&f.name){if(y=s.get(f.name),!y)throw Jt(ie.MATCHER_NOT_FOUND,{location:f});S=y.record.name,g=J(Do(p.params,y.keys.filter(D=>!D.optional).concat(y.parent?y.parent.keys.filter(D=>D.optional):[]).map(D=>D.name)),f.params&&Do(f.params,y.keys.map(D=>D.name))),R=y.stringify(g)}else if(f.path!=null)R=f.path,y=n.find(D=>D.re.test(R)),y&&(g=y.parse(R),S=y.record.name);else{if(y=p.name?s.get(p.name):n.find(D=>D.re.test(p.path)),!y)throw Jt(ie.MATCHER_NOT_FOUND,{location:f,currentLocation:p});S=y.record.name,g=J({},p.params,f.params),R=y.stringify(g)}const C=[];let N=y;for(;N;)C.unshift(N.record),N=N.parent;return{name:S,path:R,params:g,matched:C,meta:Sd(C)}}e.forEach(f=>o(f));function a(){n.length=0,s.clear()}return{addRoute:o,resolve:u,removeRoute:i,clearRoutes:a,getRoutes:l,getRecordMatcher:r}}function Do(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function Fo(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:xd(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function xd(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function Lo(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Sd(e){return e.reduce((t,n)=>J(t,n.meta),{})}function Ad(e,t){let n=0,s=t.length;for(;n!==s;){const o=n+s>>1;Ol(e,t[o])<0?s=o:n=o+1}const r=vd(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function vd(e){let t=e;for(;t=t.parent;)if(Cl(t)&&Ol(e,t)===0)return t}function Cl({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Mo(e){const t=dt(Ar),n=dt(Al),s=qe(()=>{const c=kt(e.to);return t.resolve(c)}),r=qe(()=>{const{matched:c}=s.value,{length:u}=c,a=c[u-1],f=n.matched;if(!a||!f.length)return-1;const p=f.findIndex(zt.bind(null,a));if(p>-1)return p;const y=Uo(c[u-2]);return u>1&&Uo(a)===y&&f[f.length-1].path!==y?f.findIndex(zt.bind(null,c[u-2])):p}),o=qe(()=>r.value>-1&&Nd(n.params,s.value.params)),i=qe(()=>r.value>-1&&r.value===n.matched.length-1&&Rl(n.params,s.value.params));function l(c={}){if(Pd(c)){const u=t[kt(e.replace)?"replace":"push"](kt(e.to)).catch(pn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>u),u}return Promise.resolve()}return{route:s,href:qe(()=>s.value.href),isActive:o,isExactActive:i,navigate:l}}function Od(e){return e.length===1?e[0]:e}const Cd=xn({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Mo,setup(e,{slots:t}){const n=ss(Mo(e)),{options:s}=dt(Ar),r=qe(()=>({[Bo(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[Bo(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const o=t.default&&Od(t.default(n));return e.custom?o:qi("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},o)}}}),Td=Cd;function Pd(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Nd(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!We(r)||r.length!==s.length||s.some((o,i)=>o.valueOf()!==r[i].valueOf()))return!1}return!0}function Uo(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Bo=(e,t,n)=>e??t??n,Id=xn({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=dt(Zs),r=qe(()=>e.route||s.value),o=dt(To,0),i=qe(()=>{let u=kt(o);const{matched:a}=r.value;let f;for(;(f=a[u])&&!f.components;)u++;return u}),l=qe(()=>r.value.matched[i.value]);Fn(To,qe(()=>i.value+1)),Fn(ld,l),Fn(Zs,r);const c=yn();return Ln(()=>[c.value,l.value,e.name],([u,a,f],[p,y,g])=>{a&&(a.instances[f]=u,y&&y!==a&&u&&u===p&&(a.leaveGuards.size||(a.leaveGuards=y.leaveGuards),a.updateGuards.size||(a.updateGuards=y.updateGuards))),u&&a&&(!y||!zt(a,y)||!p)&&(a.enterCallbacks[f]||[]).forEach(R=>R(u))},{flush:"post"}),()=>{const u=r.value,a=e.name,f=l.value,p=f&&f.components[a];if(!p)return jo(n.default,{Component:p,route:u});const y=f.props[a],g=y?y===!0?u.params:typeof y=="function"?y(u):y:null,S=qi(p,J({},g,t,{onVnodeUnmounted:C=>{C.component.isUnmounted&&(f.instances[a]=null)},ref:c}));return jo(n.default,{Component:S,route:u})||S}}});function jo(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Dd=Id;function Fd(e){const t=Rd(e.routes,e),n=e.parseQuery||od,s=e.stringifyQuery||Co,r=e.history,o=nn(),i=nn(),l=nn(),c=ic(yt);let u=yt;Bt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const a=Ts.bind(null,w=>""+w),f=Ts.bind(null,Vf),p=Ts.bind(null,wn);function y(w,L){let I,M;return xl(w)?(I=t.getRecordMatcher(w),M=L):M=w,t.addRoute(M,I)}function g(w){const L=t.getRecordMatcher(w);L&&t.removeRoute(L)}function R(){return t.getRoutes().map(w=>w.record)}function S(w){return!!t.getRecordMatcher(w)}function C(w,L){if(L=J({},L||c.value),typeof w=="string"){const m=Ps(n,w,L.path),E=t.resolve({path:m.path},L),x=r.createHref(m.fullPath);return J(m,E,{params:p(E.params),hash:wn(m.hash),redirectedFrom:void 0,href:x})}let I;if(w.path!=null)I=J({},w,{path:Ps(n,w.path,L.path).path});else{const m=J({},w.params);for(const E in m)m[E]==null&&delete m[E];I=J({},w,{params:f(m)}),L.params=f(L.params)}const M=t.resolve(I,L),W=w.hash||"";M.params=a(p(M.params));const d=Kf(s,J({},w,{hash:jf(W),path:M.path})),h=r.createHref(d);return J({fullPath:d,hash:W,query:s===Co?id(w.query):w.query||{}},M,{redirectedFrom:void 0,href:h})}function N(w){return typeof w=="string"?Ps(n,w,c.value.path):J({},w)}function D(w,L){if(u!==w)return Jt(ie.NAVIGATION_CANCELLED,{from:L,to:w})}function P(w){return K(w)}function q(w){return P(J(N(w),{replace:!0}))}function re(w,L){const I=w.matched[w.matched.length-1];if(I&&I.redirect){const{redirect:M}=I;let W=typeof M=="function"?M(w,L):M;return typeof W=="string"&&(W=W.includes("?")||W.includes("#")?W=N(W):{path:W},W.params={}),J({query:w.query,hash:w.hash,params:W.path!=null?{}:w.params},W)}}function K(w,L){const I=u=C(w),M=c.value,W=w.state,d=w.force,h=w.replace===!0,m=re(I,M);if(m)return K(J(N(m),{state:typeof m=="object"?J({},W,m.state):W,force:d,replace:h}),L||I);const E=I;E.redirectedFrom=L;let x;return!d&&Wf(s,M,I)&&(x=Jt(ie.NAVIGATION_DUPLICATED,{to:E,from:M}),ue(M,M,!0,!1)),(x?Promise.resolve(x):Te(E,M)).catch(_=>ct(_)?ct(_,ie.NAVIGATION_GUARD_REDIRECT)?_:ze(_):$(_,E,M)).then(_=>{if(_){if(ct(_,ie.NAVIGATION_GUARD_REDIRECT))return K(J({replace:h},N(_.to),{state:typeof _.to=="object"?J({},W,_.to.state):W,force:d}),L||E)}else _=Le(E,M,!0,h,W);return He(E,M,_),_})}function we(w,L){const I=D(w,L);return I?Promise.reject(I):Promise.resolve()}function Ce(w){const L=ke.values().next().value;return L&&typeof L.runWithContext=="function"?L.runWithContext(w):w()}function Te(w,L){let I;const[M,W,d]=cd(w,L);I=Is(M.reverse(),"beforeRouteLeave",w,L);for(const m of M)m.leaveGuards.forEach(E=>{I.push(Rt(E,w,L))});const h=we.bind(null,w,L);return I.push(h),Ue(I).then(()=>{I=[];for(const m of o.list())I.push(Rt(m,w,L));return I.push(h),Ue(I)}).then(()=>{I=Is(W,"beforeRouteUpdate",w,L);for(const m of W)m.updateGuards.forEach(E=>{I.push(Rt(E,w,L))});return I.push(h),Ue(I)}).then(()=>{I=[];for(const m of d)if(m.beforeEnter)if(We(m.beforeEnter))for(const E of m.beforeEnter)I.push(Rt(E,w,L));else I.push(Rt(m.beforeEnter,w,L));return I.push(h),Ue(I)}).then(()=>(w.matched.forEach(m=>m.enterCallbacks={}),I=Is(d,"beforeRouteEnter",w,L,Ce),I.push(h),Ue(I))).then(()=>{I=[];for(const m of i.list())I.push(Rt(m,w,L));return I.push(h),Ue(I)}).catch(m=>ct(m,ie.NAVIGATION_CANCELLED)?m:Promise.reject(m))}function He(w,L,I){l.list().forEach(M=>Ce(()=>M(w,L,I)))}function Le(w,L,I,M,W){const d=D(w,L);if(d)return d;const h=L===yt,m=Bt?history.state:{};I&&(M||h?r.replace(w.fullPath,J({scroll:h&&m&&m.scroll},W)):r.push(w.fullPath,W)),c.value=w,ue(w,L,I,h),ze()}let de;function Pe(){de||(de=r.listen((w,L,I)=>{if(!Xe.listening)return;const M=C(w),W=re(M,Xe.currentRoute.value);if(W){K(J(W,{replace:!0,force:!0}),M).catch(pn);return}u=M;const d=c.value;Bt&&ed(Oo(d.fullPath,I.delta),ps()),Te(M,d).catch(h=>ct(h,ie.NAVIGATION_ABORTED|ie.NAVIGATION_CANCELLED)?h:ct(h,ie.NAVIGATION_GUARD_REDIRECT)?(K(J(N(h.to),{force:!0}),M).then(m=>{ct(m,ie.NAVIGATION_ABORTED|ie.NAVIGATION_DUPLICATED)&&!I.delta&&I.type===Qs.pop&&r.go(-1,!1)}).catch(pn),Promise.reject()):(I.delta&&r.go(-I.delta,!1),$(h,M,d))).then(h=>{h=h||Le(M,d,!1),h&&(I.delta&&!ct(h,ie.NAVIGATION_CANCELLED)?r.go(-I.delta,!1):I.type===Qs.pop&&ct(h,ie.NAVIGATION_ABORTED|ie.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),He(M,d,h)}).catch(pn)}))}let it=nn(),Y=nn(),z;function $(w,L,I){ze(w);const M=Y.list();return M.length?M.forEach(W=>W(w,L,I)):console.error(w),Promise.reject(w)}function Me(){return z&&c.value!==yt?Promise.resolve():new Promise((w,L)=>{it.add([w,L])})}function ze(w){return z||(z=!w,Pe(),it.list().forEach(([L,I])=>w?I(w):L()),it.reset()),w}function ue(w,L,I,M){const{scrollBehavior:W}=e;if(!Bt||!W)return Promise.resolve();const d=!I&&td(Oo(w.fullPath,0))||(M||!I)&&history.state&&history.state.scroll||null;return hi().then(()=>W(w,L,d)).then(h=>h&&Zf(h)).catch(h=>$(h,w,L))}const le=w=>r.go(w);let Je;const ke=new Set,Xe={currentRoute:c,listening:!0,addRoute:y,removeRoute:g,clearRoutes:t.clearRoutes,hasRoute:S,getRoutes:R,resolve:C,options:e,push:P,replace:q,go:le,back:()=>le(-1),forward:()=>le(1),beforeEach:o.add,beforeResolve:i.add,afterEach:l.add,onError:Y.add,isReady:Me,install(w){w.component("RouterLink",Td),w.component("RouterView",Dd),w.config.globalProperties.$router=Xe,Object.defineProperty(w.config.globalProperties,"$route",{enumerable:!0,get:()=>kt(c)}),Bt&&!Je&&c.value===yt&&(Je=!0,P(r.location).catch(M=>{}));const L={};for(const M in yt)Object.defineProperty(L,M,{get:()=>c.value[M],enumerable:!0});w.provide(Ar,Xe),w.provide(Al,ai(L)),w.provide(Zs,c);const I=w.unmount;ke.add(w),w.unmount=function(){ke.delete(w),ke.size<1&&(u=yt,de&&de(),de=null,c.value=yt,Je=!1,z=!1),I()}}};function Ue(w){return w.reduce((L,I)=>L.then(()=>Ce(I)),Promise.resolve())}return Xe}const Ld={style:{padding:"16px"}},Md={style:{"margin-top":"12px",background:"#f7f7f7",padding:"12px","border-radius":"8px"}},Ud=xn({__name:"DashboardPage",setup(e){const t=yn(null);return gr(async()=>{const n=await Xn.get("/statistics");t.value=n.data}),(n,s)=>(dn(),hn("main",Ld,[s[0]||(s[0]=ee("h1",{style:{"font-size":"18px","font-weight":"600"}},"统计",-1)),ee("pre",Md,bt(t.value),1)]))}}),Bd={style:{padding:"16px"}},jd={style:{"margin-top":"12px",display:"flex",gap:"8px"}},Hd={style:{"margin-top":"12px",width:"100%","border-collapse":"collapse"}},kd={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},Vd={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},qd={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},$d={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},Kd={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},Wd={style:{"border-bottom":"1px solid #f2f2f2",padding:"8px"}},Gd=xn({__name:"TenantsPage",setup(e){const t=yn(""),n=yn([]);async function s(){const r=await Xn.get("/tenants",{params:{page:1,limit:50,keyword:t.value}});n.value=r.data.items||[]}return gr(s),(r,o)=>(dn(),hn("main",Bd,[o[2]||(o[2]=ee("h1",{style:{"font-size":"18px","font-weight":"600"}},"租户",-1)),ee("div",jd,[bi(ee("input",{"onUpdate:modelValue":o[0]||(o[0]=i=>t.value=i),placeholder:"keyword"},null,512),[[Ki,t.value]]),ee("button",{onClick:s},"查询")]),ee("table",Hd,[o[1]||(o[1]=ee("thead",null,[ee("tr",null,[ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"ID"),ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"Code"),ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"Name"),ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"Admins"),ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"Admin Expire At(max)"),ee("th",{style:{"text-align":"left","border-bottom":"1px solid #eee",padding:"8px"}},"Status")])],-1)),ee("tbody",null,[(dn(!0),hn(Ve,null,Dc(n.value,i=>(dn(),hn("tr",{key:i.id},[ee("td",kd,bt(i.id),1),ee("td",Vd,bt(i.tenant_code),1),ee("td",qd,bt(i.name),1),ee("td",$d,bt(i.admin_count),1),ee("td",Kd,bt(i.admin_expire_at||"-"),1),ee("td",Wd,bt(i.status),1)]))),128))])])]))}}),zd=Fd({history:dd("/super/"),routes:[{path:"/",component:Ud},{path:"/tenants",component:Gd},{path:"/:pathMatch(.*)*",redirect:"/"}]});Ga(Cf).use(zd).mount("#app"); diff --git a/frontend/superadmin/dist/index.html b/frontend/superadmin/dist/index.html new file mode 100644 index 0000000..e450a8c --- /dev/null +++ b/frontend/superadmin/dist/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn Super Admin + + + +
+ + + diff --git a/frontend/superadmin/index.html b/frontend/superadmin/index.html new file mode 100644 index 0000000..e7685ab --- /dev/null +++ b/frontend/superadmin/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn Super Admin + + +
+ + + + diff --git a/frontend/superadmin/package-lock.json b/frontend/superadmin/package-lock.json new file mode 100644 index 0000000..f9c57c6 --- /dev/null +++ b/frontend/superadmin/package-lock.json @@ -0,0 +1,1614 @@ +{ + "name": "@quyun/superadmin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@quyun/superadmin", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/frontend/superadmin/package.json b/frontend/superadmin/package.json new file mode 100644 index 0000000..c2e4a28 --- /dev/null +++ b/frontend/superadmin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@quyun/superadmin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } +} + diff --git a/frontend/superadmin/src/App.vue b/frontend/superadmin/src/App.vue new file mode 100644 index 0000000..4f31bc1 --- /dev/null +++ b/frontend/superadmin/src/App.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/superadmin/src/api.ts b/frontend/superadmin/src/api.ts new file mode 100644 index 0000000..6dc8ae9 --- /dev/null +++ b/frontend/superadmin/src/api.ts @@ -0,0 +1,15 @@ +import axios from 'axios' + +export const api = axios.create({ + baseURL: '/super/v1', +}) + +export function setSuperToken(token: string) { + const t = token.trim() + if (!t) { + delete api.defaults.headers.common.Authorization + return + } + api.defaults.headers.common.Authorization = `Bearer ${t}` +} + diff --git a/frontend/superadmin/src/env.d.ts b/frontend/superadmin/src/env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/frontend/superadmin/src/env.d.ts @@ -0,0 +1,2 @@ +/// + diff --git a/frontend/superadmin/src/main.ts b/frontend/superadmin/src/main.ts new file mode 100644 index 0000000..18fa598 --- /dev/null +++ b/frontend/superadmin/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import App from './App.vue' +import { router } from './router' + +createApp(App).use(router).mount('#app') + diff --git a/frontend/superadmin/src/router.ts b/frontend/superadmin/src/router.ts new file mode 100644 index 0000000..9fb2032 --- /dev/null +++ b/frontend/superadmin/src/router.ts @@ -0,0 +1,14 @@ +import { createRouter, createWebHistory } from 'vue-router' +import DashboardPage from './views/DashboardPage.vue' +import TenantsPage from './views/TenantsPage.vue' +import RolesPage from './views/RolesPage.vue' + +export const router = createRouter({ + history: createWebHistory('/super/'), + routes: [ + { path: '/', component: DashboardPage }, + { path: '/tenants', component: TenantsPage }, + { path: '/roles', component: RolesPage }, + { path: '/:pathMatch(.*)*', redirect: '/' }, + ], +}) diff --git a/frontend/superadmin/src/views/DashboardPage.vue b/frontend/superadmin/src/views/DashboardPage.vue new file mode 100644 index 0000000..9af5e36 --- /dev/null +++ b/frontend/superadmin/src/views/DashboardPage.vue @@ -0,0 +1,19 @@ + + + + diff --git a/frontend/superadmin/src/views/RolesPage.vue b/frontend/superadmin/src/views/RolesPage.vue new file mode 100644 index 0000000..04f0349 --- /dev/null +++ b/frontend/superadmin/src/views/RolesPage.vue @@ -0,0 +1,76 @@ + + + + diff --git a/frontend/superadmin/src/views/TenantsPage.vue b/frontend/superadmin/src/views/TenantsPage.vue new file mode 100644 index 0000000..c4ad15b --- /dev/null +++ b/frontend/superadmin/src/views/TenantsPage.vue @@ -0,0 +1,47 @@ + + + + diff --git a/frontend/superadmin/tsconfig.json b/frontend/superadmin/tsconfig.json new file mode 100644 index 0000000..d415b8f --- /dev/null +++ b/frontend/superadmin/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "types": ["vite/client"] + }, + "include": ["src"] +} + diff --git a/frontend/superadmin/vite.config.ts b/frontend/superadmin/vite.config.ts new file mode 100644 index 0000000..39a6fd4 --- /dev/null +++ b/frontend/superadmin/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + base: './', + server: { port: 5175 }, +}) + diff --git a/frontend/user/dist/assets/index-wt7BFVvy.js b/frontend/user/dist/assets/index-wt7BFVvy.js new file mode 100644 index 0000000..1f19daa --- /dev/null +++ b/frontend/user/dist/assets/index-wt7BFVvy.js @@ -0,0 +1,25 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();/** +* @vue/shared v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function cs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const X={},At=[],Be=()=>{},Ar=()=>!1,yn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),fs=e=>e.startsWith("onUpdate:"),fe=Object.assign,us=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},$i=Object.prototype.hasOwnProperty,q=(e,t)=>$i.call(e,t),V=Array.isArray,St=e=>bn(e)==="[object Map]",Sr=e=>bn(e)==="[object Set]",B=e=>typeof e=="function",se=e=>typeof e=="string",ct=e=>typeof e=="symbol",Z=e=>e!==null&&typeof e=="object",Cr=e=>(Z(e)||B(e))&&B(e.then)&&B(e.catch),wr=Object.prototype.toString,bn=e=>wr.call(e),qi=e=>bn(e).slice(8,-1),Or=e=>bn(e)==="[object Object]",as=e=>se(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,jt=cs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),En=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},ki=/-\w/g,Re=En(e=>e.replace(ki,t=>t.slice(1).toUpperCase())),zi=/\B([A-Z])/g,_t=En(e=>e.replace(zi,"-$1").toLowerCase()),xn=En(e=>e.charAt(0).toUpperCase()+e.slice(1)),Tn=En(e=>e?`on${xn(e)}`:""),ot=(e,t)=>!Object.is(e,t),In=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Ji=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Ds;const Rn=()=>Ds||(Ds=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ds(e){if(V(e)){const t={};for(let n=0;n{if(n){const s=n.split(Yi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function hs(e){let t="";if(se(e))t=e;else if(V(e))for(let n=0;n!!(e&&e.__v_isRef===!0),$n=e=>se(e)?e:e==null?"":V(e)||Z(e)&&(e.toString===wr||!B(e.toString))?Ir(e)?$n(e.value):JSON.stringify(e,Nr,2):String(e),Nr=(e,t)=>Ir(t)?Nr(e,t.value):St(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Nn(s,i)+" =>"]=r,n),{})}:Sr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Nn(n))}:ct(t)?Nn(t):Z(t)&&!V(t)&&!Or(t)?String(t):t,Nn=(e,t="")=>{var n;return ct(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let me;class no{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=me,!t&&me&&(this.index=(me.scopes||(me.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(me=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Gt){let t=Gt;for(Gt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Ut;){let t=Ut;for(Ut=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Fr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Hr(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),ms(s),ro(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function qn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Br(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Br(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===zt)||(e.globalVersion=zt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!qn(e))))return;e.flags|=2;const t=e.dep,n=Y,s=Se;Y=e,Se=!0;try{Fr(e);const r=e.fn(e._value);(t.version===0||ot(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{Y=n,Se=s,Hr(e),e.flags&=-3}}function ms(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)ms(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function ro(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Se=!0;const Vr=[];function ke(){Vr.push(Se),Se=!1}function ze(){const e=Vr.pop();Se=e===void 0?!0:e}function Ls(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Y;Y=void 0;try{t()}finally{Y=n}}}let zt=0;class io{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class _s{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Y||!Se||Y===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Y)n=this.activeLink=new io(Y,this),Y.deps?(n.prevDep=Y.depsTail,Y.depsTail.nextDep=n,Y.depsTail=n):Y.deps=Y.depsTail=n,jr(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=Y.depsTail,n.nextDep=void 0,Y.depsTail.nextDep=n,Y.depsTail=n,Y.deps===n&&(Y.deps=s)}return n}trigger(t){this.version++,zt++,this.notify(t)}notify(t){ps();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{gs()}}}function jr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)jr(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const kn=new WeakMap,mt=Symbol(""),zn=Symbol(""),Jt=Symbol("");function oe(e,t,n){if(Se&&Y){let s=kn.get(e);s||kn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new _s),r.map=s,r.key=n),r.track()}}function $e(e,t,n,s,r,i){const o=kn.get(e);if(!o){zt++;return}const l=c=>{c&&c.trigger()};if(ps(),t==="clear")o.forEach(l);else{const c=V(e),h=c&&as(n);if(c&&n==="length"){const a=Number(s);o.forEach((d,g)=>{(g==="length"||g===Jt||!ct(g)&&g>=a)&&l(d)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),h&&l(o.get(Jt)),t){case"add":c?h&&l(o.get("length")):(l(o.get(mt)),St(e)&&l(o.get(zn)));break;case"delete":c||(l(o.get(mt)),St(e)&&l(o.get(zn)));break;case"set":St(e)&&l(o.get(mt));break}}gs()}function Et(e){const t=$(e);return t===e?t:(oe(t,"iterate",Jt),Ce(e)?t:t.map(Je))}function vs(e){return oe(e=$(e),"iterate",Jt),e}function tt(e,t){return lt(e)?Ct(e)?Qt(Je(t)):Qt(t):Je(t)}const oo={__proto__:null,[Symbol.iterator](){return Dn(this,Symbol.iterator,e=>tt(this,e))},concat(...e){return Et(this).concat(...e.map(t=>V(t)?Et(t):t))},entries(){return Dn(this,"entries",e=>(e[1]=tt(this,e[1]),e))},every(e,t){return Ue(this,"every",e,t,void 0,arguments)},filter(e,t){return Ue(this,"filter",e,t,n=>n.map(s=>tt(this,s)),arguments)},find(e,t){return Ue(this,"find",e,t,n=>tt(this,n),arguments)},findIndex(e,t){return Ue(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ue(this,"findLast",e,t,n=>tt(this,n),arguments)},findLastIndex(e,t){return Ue(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ue(this,"forEach",e,t,void 0,arguments)},includes(...e){return Ln(this,"includes",e)},indexOf(...e){return Ln(this,"indexOf",e)},join(e){return Et(this).join(e)},lastIndexOf(...e){return Ln(this,"lastIndexOf",e)},map(e,t){return Ue(this,"map",e,t,void 0,arguments)},pop(){return Ft(this,"pop")},push(...e){return Ft(this,"push",e)},reduce(e,...t){return Fs(this,"reduce",e,t)},reduceRight(e,...t){return Fs(this,"reduceRight",e,t)},shift(){return Ft(this,"shift")},some(e,t){return Ue(this,"some",e,t,void 0,arguments)},splice(...e){return Ft(this,"splice",e)},toReversed(){return Et(this).toReversed()},toSorted(e){return Et(this).toSorted(e)},toSpliced(...e){return Et(this).toSpliced(...e)},unshift(...e){return Ft(this,"unshift",e)},values(){return Dn(this,"values",e=>tt(this,e))}};function Dn(e,t,n){const s=vs(e),r=s[t]();return s!==e&&!Ce(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const lo=Array.prototype;function Ue(e,t,n,s,r,i){const o=vs(e),l=o!==e&&!Ce(e),c=o[t];if(c!==lo[t]){const d=c.apply(e,i);return l?Je(d):d}let h=n;o!==e&&(l?h=function(d,g){return n.call(this,tt(e,d),g,e)}:n.length>2&&(h=function(d,g){return n.call(this,d,g,e)}));const a=c.call(o,h,s);return l&&r?r(a):a}function Fs(e,t,n,s){const r=vs(e);let i=n;return r!==e&&(Ce(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,tt(e,l),c,e)}),r[t](i,...s)}function Ln(e,t,n){const s=$(e);oe(s,"iterate",Jt);const r=s[t](...n);return(r===-1||r===!1)&&Es(n[0])?(n[0]=$(n[0]),s[t](...n)):r}function Ft(e,t,n=[]){ke(),ps();const s=$(e)[t].apply(e,n);return gs(),ze(),s}const co=cs("__proto__,__v_isRef,__isVue"),Ur=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ct));function fo(e){ct(e)||(e=String(e));const t=$(this);return oe(t,"has",e),t.hasOwnProperty(e)}class Gr{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?bo:qr:i?$r:Wr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=V(t);if(!r){let c;if(o&&(c=oo[n]))return c;if(n==="hasOwnProperty")return fo}const l=Reflect.get(t,n,ce(t)?t:s);if((ct(n)?Ur.has(n):co(n))||(r||oe(t,"get",n),i))return l;if(ce(l)){const c=o&&as(n)?l:l.value;return r&&Z(c)?Qn(c):c}return Z(l)?r?Qn(l):An(l):l}}class Kr extends Gr{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const o=V(t)&&as(n);if(!this._isShallow){const h=lt(i);if(!Ce(s)&&!lt(s)&&(i=$(i),s=$(s)),!o&&ce(i)&&!ce(s))return h||(i.value=s),!0}const l=o?Number(n)e,sn=e=>Reflect.getPrototypeOf(e);function go(e,t,n){return function(...s){const r=this.__v_raw,i=$(r),o=St(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,h=r[e](...s),a=n?Jn:t?Qt:Je;return!t&&oe(i,"iterate",c?zn:mt),{next(){const{value:d,done:g}=h.next();return g?{value:d,done:g}:{value:l?[a(d[0]),a(d[1])]:a(d),done:g}},[Symbol.iterator](){return this}}}}function rn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function mo(e,t){const n={get(r){const i=this.__v_raw,o=$(i),l=$(r);e||(ot(r,l)&&oe(o,"get",r),oe(o,"get",l));const{has:c}=sn(o),h=t?Jn:e?Qt:Je;if(c.call(o,r))return h(i.get(r));if(c.call(o,l))return h(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&oe($(r),"iterate",mt),r.size},has(r){const i=this.__v_raw,o=$(i),l=$(r);return e||(ot(r,l)&&oe(o,"has",r),oe(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=$(l),h=t?Jn:e?Qt:Je;return!e&&oe(c,"iterate",mt),l.forEach((a,d)=>r.call(i,h(a),h(d),o))}};return fe(n,e?{add:rn("add"),set:rn("set"),delete:rn("delete"),clear:rn("clear")}:{add(r){!t&&!Ce(r)&&!lt(r)&&(r=$(r));const i=$(this);return sn(i).has.call(i,r)||(i.add(r),$e(i,"add",r,r)),this},set(r,i){!t&&!Ce(i)&&!lt(i)&&(i=$(i));const o=$(this),{has:l,get:c}=sn(o);let h=l.call(o,r);h||(r=$(r),h=l.call(o,r));const a=c.call(o,r);return o.set(r,i),h?ot(i,a)&&$e(o,"set",r,i):$e(o,"add",r,i),this},delete(r){const i=$(this),{has:o,get:l}=sn(i);let c=o.call(i,r);c||(r=$(r),c=o.call(i,r)),l&&l.call(i,r);const h=i.delete(r);return c&&$e(i,"delete",r,void 0),h},clear(){const r=$(this),i=r.size!==0,o=r.clear();return i&&$e(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=go(r,e,t)}),n}function ys(e,t){const n=mo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(q(n,r)&&r in s?n:s,r,i)}const _o={get:ys(!1,!1)},vo={get:ys(!1,!0)},yo={get:ys(!0,!1)};const Wr=new WeakMap,$r=new WeakMap,qr=new WeakMap,bo=new WeakMap;function Eo(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function xo(e){return e.__v_skip||!Object.isExtensible(e)?0:Eo(qi(e))}function An(e){return lt(e)?e:bs(e,!1,ao,_o,Wr)}function kr(e){return bs(e,!1,po,vo,$r)}function Qn(e){return bs(e,!0,ho,yo,qr)}function bs(e,t,n,s,r){if(!Z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=xo(e);if(i===0)return e;const o=r.get(e);if(o)return o;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function Ct(e){return lt(e)?Ct(e.__v_raw):!!(e&&e.__v_isReactive)}function lt(e){return!!(e&&e.__v_isReadonly)}function Ce(e){return!!(e&&e.__v_isShallow)}function Es(e){return e?!!e.__v_raw:!1}function $(e){const t=e&&e.__v_raw;return t?$(t):e}function Ro(e){return!q(e,"__v_skip")&&Object.isExtensible(e)&&Pr(e,"__v_skip",!0),e}const Je=e=>Z(e)?An(e):e,Qt=e=>Z(e)?Qn(e):e;function ce(e){return e?e.__v_isRef===!0:!1}function Ao(e){return zr(e,!1)}function So(e){return zr(e,!0)}function zr(e,t){return ce(e)?e:new Co(e,t)}class Co{constructor(t,n){this.dep=new _s,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:$(t),this._value=n?t:Je(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ce(t)||lt(t);t=s?t:$(t),ot(t,n)&&(this._rawValue=t,this._value=s?t:Je(t),this.dep.trigger())}}function wt(e){return ce(e)?e.value:e}const wo={get:(e,t,n)=>t==="__v_raw"?e:wt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return ce(r)&&!ce(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Jr(e){return Ct(e)?e:new Proxy(e,wo)}class Oo{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new _s(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=zt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&Y!==this)return Lr(this,!0),!0}get value(){const t=this.dep.track();return Br(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Po(e,t,n=!1){let s,r;return B(e)?s=e:(s=e.get,r=e.set),new Oo(s,r,n)}const on={},an=new WeakMap;let ht;function To(e,t=!1,n=ht){if(n){let s=an.get(n);s||an.set(n,s=[]),s.push(e)}}function Io(e,t,n=X){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,h=I=>r?I:Ce(I)||r===!1||r===0?it(I,1):it(I);let a,d,g,m,w=!1,P=!1;if(ce(e)?(d=()=>e.value,w=Ce(e)):Ct(e)?(d=()=>h(e),w=!0):V(e)?(P=!0,w=e.some(I=>Ct(I)||Ce(I)),d=()=>e.map(I=>{if(ce(I))return I.value;if(Ct(I))return h(I);if(B(I))return c?c(I,2):I()})):B(e)?t?d=c?()=>c(e,2):e:d=()=>{if(g){ke();try{g()}finally{ze()}}const I=ht;ht=a;try{return c?c(e,3,[m]):e(m)}finally{ht=I}}:d=Be,t&&r){const I=d,J=r===!0?1/0:r;d=()=>it(I(),J)}const j=so(),N=()=>{a.stop(),j&&j.active&&us(j.effects,a)};if(i&&t){const I=t;t=(...J)=>{I(...J),N()}}let T=P?new Array(e.length).fill(on):on;const L=I=>{if(!(!(a.flags&1)||!a.dirty&&!I))if(t){const J=a.run();if(r||w||(P?J.some((ie,ee)=>ot(ie,T[ee])):ot(J,T))){g&&g();const ie=ht;ht=a;try{const ee=[J,T===on?void 0:P&&T[0]===on?[]:T,m];T=J,c?c(t,3,ee):t(...ee)}finally{ht=ie}}}else a.run()};return l&&l(L),a=new Mr(d),a.scheduler=o?()=>o(L,!1):L,m=I=>To(I,!1,a),g=a.onStop=()=>{const I=an.get(a);if(I){if(c)c(I,4);else for(const J of I)J();an.delete(a)}},t?s?L(!0):T=a.run():o?o(L.bind(null,!0),!0):a.run(),N.pause=a.pause.bind(a),N.resume=a.resume.bind(a),N.stop=N,N}function it(e,t=1/0,n){if(t<=0||!Z(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,ce(e))it(e.value,t,n);else if(V(e))for(let s=0;s{it(s,t,n)});else if(Or(e)){for(const s in e)it(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&it(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function tn(e,t,n,s){try{return s?e(...s):e()}catch(r){Sn(r,t,n)}}function Ve(e,t,n,s){if(B(e)){const r=tn(e,t,n,s);return r&&Cr(r)&&r.catch(i=>{Sn(i,t,n)}),r}if(V(e)){const r=[];for(let i=0;i>>1,r=de[s],i=Yt(r);i=Yt(n)?de.push(e):de.splice(Mo(t),0,e),e.flags|=1,Xr()}}function Xr(){dn||(dn=Qr.then(ei))}function Do(e){V(e)?Ot.push(...e):nt&&e.id===-1?nt.splice(xt+1,0,e):e.flags&1||(Ot.push(e),e.flags|=1),Xr()}function Hs(e,t,n=Fe+1){for(;nYt(n)-Yt(s));if(Ot.length=0,nt){nt.push(...t);return}for(nt=t,xt=0;xte.id==null?e.flags&2?-1:1/0:e.id;function ei(e){try{for(Fe=0;Fe{s._d&&mn(-1);const i=hn(t);let o;try{o=e(...r)}finally{hn(i),s._d&&mn(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function at(e,t,n,s){const r=e.dirs,i=t&&t.dirs;for(let o=0;oe.__isTeleport,Bo=Symbol("_leaveCb");function Rs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Rs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function As(e,t){return B(e)?fe({name:e.name},t,{setup:e}):e}function ni(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const pn=new WeakMap;function Kt(e,t,n,s,r=!1){if(V(e)){e.forEach((w,P)=>Kt(w,t&&(V(t)?t[P]:t),n,s,r));return}if(Wt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Kt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Os(s.component):s.el,o=r?null:i,{i:l,r:c}=e,h=t&&t.r,a=l.refs===X?l.refs={}:l.refs,d=l.setupState,g=$(d),m=d===X?Ar:w=>q(g,w);if(h!=null&&h!==c){if(Bs(t),se(h))a[h]=null,m(h)&&(d[h]=null);else if(ce(h)){h.value=null;const w=t;w.k&&(a[w.k]=null)}}if(B(c))tn(c,l,12,[o,a]);else{const w=se(c),P=ce(c);if(w||P){const j=()=>{if(e.f){const N=w?m(c)?d[c]:a[c]:c.value;if(r)V(N)&&us(N,i);else if(V(N))N.includes(i)||N.push(i);else if(w)a[c]=[i],m(c)&&(d[c]=a[c]);else{const T=[i];c.value=T,e.k&&(a[e.k]=T)}}else w?(a[c]=o,m(c)&&(d[c]=o)):P&&(c.value=o,e.k&&(a[e.k]=o))};if(o){const N=()=>{j(),pn.delete(e)};N.id=-1,pn.set(e,N),ve(N,n)}else Bs(e),j()}}}function Bs(e){const t=pn.get(e);t&&(t.flags|=8,pn.delete(e))}Rn().requestIdleCallback;Rn().cancelIdleCallback;const Wt=e=>!!e.type.__asyncLoader,si=e=>e.type.__isKeepAlive;function Vo(e,t){ri(e,"a",t)}function jo(e,t){ri(e,"da",t)}function ri(e,t,n=le){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Cn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)si(r.parent.vnode)&&Uo(s,t,n,r),r=r.parent}}function Uo(e,t,n,s){const r=Cn(t,e,s,!0);ii(()=>{us(s[t],r)},n)}function Cn(e,t,n=le,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{ke();const l=nn(n),c=Ve(t,n,e,o);return l(),ze(),c});return s?r.unshift(i):r.push(i),i}}const Qe=e=>(t,n=le)=>{(!Zt||e==="sp")&&Cn(e,(...s)=>t(...s),n)},Go=Qe("bm"),Ko=Qe("m"),Wo=Qe("bu"),$o=Qe("u"),qo=Qe("bum"),ii=Qe("um"),ko=Qe("sp"),zo=Qe("rtg"),Jo=Qe("rtc");function Qo(e,t=le){Cn("ec",e,t)}const Yo="components";function Xo(e,t){return el(Yo,e,!0,t)||e}const Zo=Symbol.for("v-ndc");function el(e,t,n=!0,s=!1){const r=Ae||le;if(r){const i=r.type;{const l=Wl(i,!1);if(l&&(l===t||l===Re(t)||l===xn(Re(t))))return i}const o=Vs(r[e]||i[e],t)||Vs(r.appContext[e],t);return!o&&s?i:o}}function Vs(e,t){return e&&(e[t]||e[Re(t)]||e[xn(Re(t))])}const Yn=e=>e?Ci(e)?Os(e):Yn(e.parent):null,$t=fe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Yn(e.parent),$root:e=>Yn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>li(e),$forceUpdate:e=>e.f||(e.f=()=>{xs(e.update)}),$nextTick:e=>e.n||(e.n=Yr.bind(e.proxy)),$watch:e=>al.bind(e)}),Fn=(e,t)=>e!==X&&!e.__isScriptSetup&&q(e,t),tl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;if(t[0]!=="$"){const g=o[t];if(g!==void 0)switch(g){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Fn(s,t))return o[t]=1,s[t];if(r!==X&&q(r,t))return o[t]=2,r[t];if(q(i,t))return o[t]=3,i[t];if(n!==X&&q(n,t))return o[t]=4,n[t];Xn&&(o[t]=0)}}const h=$t[t];let a,d;if(h)return t==="$attrs"&&oe(e.attrs,"get",""),h(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(n!==X&&q(n,t))return o[t]=4,n[t];if(d=c.config.globalProperties,q(d,t))return d[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Fn(r,t)?(r[t]=n,!0):s!==X&&q(s,t)?(s[t]=n,!0):q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:o}},l){let c;return!!(n[l]||e!==X&&l[0]!=="$"&&q(e,l)||Fn(t,l)||q(i,l)||q(s,l)||q($t,l)||q(r.config.globalProperties,l)||(c=o.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function js(e){return V(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Xn=!0;function nl(e){const t=li(e),n=e.proxy,s=e.ctx;Xn=!1,t.beforeCreate&&Us(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:h,created:a,beforeMount:d,mounted:g,beforeUpdate:m,updated:w,activated:P,deactivated:j,beforeDestroy:N,beforeUnmount:T,destroyed:L,unmounted:I,render:J,renderTracked:ie,renderTriggered:ee,errorCaptured:Oe,serverPrefetch:Ye,expose:Pe,inheritAttrs:Xe,components:ft,directives:Te,filters:Dt}=t;if(h&&sl(h,s,null),o)for(const z in o){const K=o[z];B(K)&&(s[z]=K.bind(n))}if(r){const z=r.call(n,n);Z(z)&&(e.data=An(z))}if(Xn=!0,i)for(const z in i){const K=i[z],je=B(K)?K.bind(n,n):B(K.get)?K.get.bind(n,n):Be,Ze=!B(K)&&B(K.set)?K.set.bind(n):Be,Ie=ye({get:je,set:Ze});Object.defineProperty(s,z,{enumerable:!0,configurable:!0,get:()=>Ie.value,set:he=>Ie.value=he})}if(l)for(const z in l)oi(l[z],s,n,z);if(c){const z=B(c)?c.call(n):c;Reflect.ownKeys(z).forEach(K=>{ln(K,z[K])})}a&&Us(a,e,"c");function re(z,K){V(K)?K.forEach(je=>z(je.bind(n))):K&&z(K.bind(n))}if(re(Go,d),re(Ko,g),re(Wo,m),re($o,w),re(Vo,P),re(jo,j),re(Qo,Oe),re(Jo,ie),re(zo,ee),re(qo,T),re(ii,I),re(ko,Ye),V(Pe))if(Pe.length){const z=e.exposed||(e.exposed={});Pe.forEach(K=>{Object.defineProperty(z,K,{get:()=>n[K],set:je=>n[K]=je,enumerable:!0})})}else e.exposed||(e.exposed={});J&&e.render===Be&&(e.render=J),Xe!=null&&(e.inheritAttrs=Xe),ft&&(e.components=ft),Te&&(e.directives=Te),Ye&&ni(e)}function sl(e,t,n=Be){V(e)&&(e=Zn(e));for(const s in e){const r=e[s];let i;Z(r)?"default"in r?i=qe(r.from||s,r.default,!0):i=qe(r.from||s):i=qe(r),ce(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function Us(e,t,n){Ve(V(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function oi(e,t,n,s){let r=s.includes(".")?ui(n,s):()=>n[s];if(se(e)){const i=t[e];B(i)&&cn(r,i)}else if(B(e))cn(r,e.bind(n));else if(Z(e))if(V(e))e.forEach(i=>oi(i,t,n,s));else{const i=B(e.handler)?e.handler.bind(n):t[e.handler];B(i)&&cn(r,i,e)}}function li(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(h=>gn(c,h,o,!0)),gn(c,t,o)),Z(t)&&i.set(t,c),c}function gn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&gn(e,i,n,!0),r&&r.forEach(o=>gn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=rl[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const rl={data:Gs,props:Ks,emits:Ks,methods:Vt,computed:Vt,beforeCreate:ue,created:ue,beforeMount:ue,mounted:ue,beforeUpdate:ue,updated:ue,beforeDestroy:ue,beforeUnmount:ue,destroyed:ue,unmounted:ue,activated:ue,deactivated:ue,errorCaptured:ue,serverPrefetch:ue,components:Vt,directives:Vt,watch:ol,provide:Gs,inject:il};function Gs(e,t){return t?e?function(){return fe(B(e)?e.call(this,this):e,B(t)?t.call(this,this):t)}:t:e}function il(e,t){return Vt(Zn(e),Zn(t))}function Zn(e){if(V(e)){const t={};for(let n=0;n1)return n&&B(t)?t.call(s&&s.proxy):t}}const fl=Symbol.for("v-scx"),ul=()=>qe(fl);function cn(e,t,n){return fi(e,t,n)}function fi(e,t,n=X){const{immediate:s,deep:r,flush:i,once:o}=n,l=fe({},n),c=t&&s||!t&&i!=="post";let h;if(Zt){if(i==="sync"){const m=ul();h=m.__watcherHandles||(m.__watcherHandles=[])}else if(!c){const m=()=>{};return m.stop=Be,m.resume=Be,m.pause=Be,m}}const a=le;l.call=(m,w,P)=>Ve(m,a,w,P);let d=!1;i==="post"?l.scheduler=m=>{ve(m,a&&a.suspense)}:i!=="sync"&&(d=!0,l.scheduler=(m,w)=>{w?m():xs(m)}),l.augmentJob=m=>{t&&(m.flags|=4),d&&(m.flags|=2,a&&(m.id=a.uid,m.i=a))};const g=Io(e,t,l);return Zt&&(h?h.push(g):c&&g()),g}function al(e,t,n){const s=this.proxy,r=se(e)?e.includes(".")?ui(s,e):()=>s[e]:e.bind(s,s);let i;B(t)?i=t:(i=t.handler,n=t);const o=nn(this),l=fi(r,i.bind(s),n);return o(),l}function ui(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Re(t)}Modifiers`]||e[`${_t(t)}Modifiers`];function hl(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||X;let r=n;const i=t.startsWith("update:"),o=i&&dl(s,t.slice(7));o&&(o.trim&&(r=n.map(a=>se(a)?a.trim():a)),o.number&&(r=n.map(Ji)));let l,c=s[l=Tn(t)]||s[l=Tn(Re(t))];!c&&i&&(c=s[l=Tn(_t(t))]),c&&Ve(c,e,6,r);const h=s[l+"Once"];if(h){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ve(h,e,6,r)}}const pl=new WeakMap;function ai(e,t,n=!1){const s=n?pl:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!B(e)){const c=h=>{const a=ai(h,t,!0);a&&(l=!0,fe(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(Z(e)&&s.set(e,null),null):(V(i)?i.forEach(c=>o[c]=null):fe(o,i),Z(e)&&s.set(e,o),o)}function wn(e,t){return!e||!yn(t)?!1:(t=t.slice(2).replace(/Once$/,""),q(e,t[0].toLowerCase()+t.slice(1))||q(e,_t(t))||q(e,t))}function Ws(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:h,renderCache:a,props:d,data:g,setupState:m,ctx:w,inheritAttrs:P}=e,j=hn(e);let N,T;try{if(n.shapeFlag&4){const I=r||s,J=I;N=He(h.call(J,I,a,d,m,g,w)),T=l}else{const I=t;N=He(I.length>1?I(d,{attrs:l,slots:o,emit:c}):I(d,null)),T=t.props?l:gl(l)}}catch(I){qt.length=0,Sn(I,e,1),N=xe(Tt)}let L=N;if(T&&P!==!1){const I=Object.keys(T),{shapeFlag:J}=L;I.length&&J&7&&(i&&I.some(fs)&&(T=ml(T,i)),L=It(L,T,!1,!0))}return n.dirs&&(L=It(L,null,!1,!0),L.dirs=L.dirs?L.dirs.concat(n.dirs):n.dirs),n.transition&&Rs(L,n.transition),N=L,hn(j),N}const gl=e=>{let t;for(const n in e)(n==="class"||n==="style"||yn(n))&&((t||(t={}))[n]=e[n]);return t},ml=(e,t)=>{const n={};for(const s in e)(!fs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function _l(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,h=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?$s(s,o,h):!!o;if(c&8){const a=t.dynamicProps;for(let d=0;dObject.create(di),pi=e=>Object.getPrototypeOf(e)===di;function yl(e,t,n,s=!1){const r={},i=hi();e.propsDefaults=Object.create(null),gi(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:kr(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function bl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=$(r),[c]=e.propsOptions;let h=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let d=0;d{c=!0;const[g,m]=mi(d,t,!0);fe(o,g),m&&l.push(...m)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return Z(e)&&s.set(e,At),At;if(V(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",Cs=e=>V(e)?e.map(He):[He(e)],xl=(e,t,n)=>{if(t._n)return t;const s=Lo((...r)=>Cs(t(...r)),n);return s._c=!1,s},_i=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Ss(r))continue;const i=e[r];if(B(i))t[r]=xl(r,i,s);else if(i!=null){const o=Cs(i);t[r]=()=>o}}},vi=(e,t)=>{const n=Cs(t);e.slots.default=()=>n},yi=(e,t,n)=>{for(const s in t)(n||!Ss(s))&&(e[s]=t[s])},Rl=(e,t,n)=>{const s=e.slots=hi();if(e.vnode.shapeFlag&32){const r=t._;r?(yi(s,t,n),n&&Pr(s,"_",r,!0)):_i(t,s)}else t&&vi(e,t)},Al=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=X;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:yi(r,t,n):(i=!t.$stable,_i(t,r)),o=t}else t&&(vi(e,t),o={default:1});if(i)for(const l in r)!Ss(l)&&o[l]==null&&delete r[l]},ve=Pl;function Sl(e){return Cl(e)}function Cl(e,t){const n=Rn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:h,setElementText:a,parentNode:d,nextSibling:g,setScopeId:m=Be,insertStaticContent:w}=e,P=(f,u,p,v=null,b=null,_=null,A=void 0,R=null,x=!!u.dynamicChildren)=>{if(f===u)return;f&&!Ht(f,u)&&(v=y(f),he(f,b,_,!0),f=null),u.patchFlag===-2&&(x=!1,u.dynamicChildren=null);const{type:E,ref:F,shapeFlag:C}=u;switch(E){case On:j(f,u,p,v);break;case Tt:N(f,u,p,v);break;case Bn:f==null&&T(u,p,v,A);break;case We:ft(f,u,p,v,b,_,A,R,x);break;default:C&1?J(f,u,p,v,b,_,A,R,x):C&6?Te(f,u,p,v,b,_,A,R,x):(C&64||C&128)&&E.process(f,u,p,v,b,_,A,R,x,M)}F!=null&&b?Kt(F,f&&f.ref,_,u||f,!u):F==null&&f&&f.ref!=null&&Kt(f.ref,null,_,f,!0)},j=(f,u,p,v)=>{if(f==null)s(u.el=l(u.children),p,v);else{const b=u.el=f.el;u.children!==f.children&&h(b,u.children)}},N=(f,u,p,v)=>{f==null?s(u.el=c(u.children||""),p,v):u.el=f.el},T=(f,u,p,v)=>{[f.el,f.anchor]=w(f.children,u,p,v,f.el,f.anchor)},L=({el:f,anchor:u},p,v)=>{let b;for(;f&&f!==u;)b=g(f),s(f,p,v),f=b;s(u,p,v)},I=({el:f,anchor:u})=>{let p;for(;f&&f!==u;)p=g(f),r(f),f=p;r(u)},J=(f,u,p,v,b,_,A,R,x)=>{if(u.type==="svg"?A="svg":u.type==="math"&&(A="mathml"),f==null)ie(u,p,v,b,_,A,R,x);else{const E=f.el&&f.el._isVueCE?f.el:null;try{E&&E._beginPatch(),Ye(f,u,b,_,A,R,x)}finally{E&&E._endPatch()}}},ie=(f,u,p,v,b,_,A,R)=>{let x,E;const{props:F,shapeFlag:C,transition:D,dirs:H}=f;if(x=f.el=o(f.type,_,F&&F.is,F),C&8?a(x,f.children):C&16&&Oe(f.children,x,null,v,b,Hn(f,_),A,R),H&&at(f,null,v,"created"),ee(x,f,f.scopeId,A,v),F){for(const Q in F)Q!=="value"&&!jt(Q)&&i(x,Q,null,F[Q],_,v);"value"in F&&i(x,"value",null,F.value,_),(E=F.onVnodeBeforeMount)&&Le(E,v,f)}H&&at(f,null,v,"beforeMount");const G=wl(b,D);G&&D.beforeEnter(x),s(x,u,p),((E=F&&F.onVnodeMounted)||G||H)&&ve(()=>{E&&Le(E,v,f),G&&D.enter(x),H&&at(f,null,v,"mounted")},b)},ee=(f,u,p,v,b)=>{if(p&&m(f,p),v)for(let _=0;_{for(let E=x;E{const R=u.el=f.el;let{patchFlag:x,dynamicChildren:E,dirs:F}=u;x|=f.patchFlag&16;const C=f.props||X,D=u.props||X;let H;if(p&&dt(p,!1),(H=D.onVnodeBeforeUpdate)&&Le(H,p,u,f),F&&at(u,f,p,"beforeUpdate"),p&&dt(p,!0),(C.innerHTML&&D.innerHTML==null||C.textContent&&D.textContent==null)&&a(R,""),E?Pe(f.dynamicChildren,E,R,p,v,Hn(u,b),_):A||K(f,u,R,null,p,v,Hn(u,b),_,!1),x>0){if(x&16)Xe(R,C,D,p,b);else if(x&2&&C.class!==D.class&&i(R,"class",null,D.class,b),x&4&&i(R,"style",C.style,D.style,b),x&8){const G=u.dynamicProps;for(let Q=0;Q{H&&Le(H,p,u,f),F&&at(u,f,p,"updated")},v)},Pe=(f,u,p,v,b,_,A)=>{for(let R=0;R{if(u!==p){if(u!==X)for(const _ in u)!jt(_)&&!(_ in p)&&i(f,_,u[_],null,b,v);for(const _ in p){if(jt(_))continue;const A=p[_],R=u[_];A!==R&&_!=="value"&&i(f,_,R,A,b,v)}"value"in p&&i(f,"value",u.value,p.value,b)}},ft=(f,u,p,v,b,_,A,R,x)=>{const E=u.el=f?f.el:l(""),F=u.anchor=f?f.anchor:l("");let{patchFlag:C,dynamicChildren:D,slotScopeIds:H}=u;H&&(R=R?R.concat(H):H),f==null?(s(E,p,v),s(F,p,v),Oe(u.children||[],p,F,b,_,A,R,x)):C>0&&C&64&&D&&f.dynamicChildren?(Pe(f.dynamicChildren,D,p,b,_,A,R),(u.key!=null||b&&u===b.subTree)&&bi(f,u,!0)):K(f,u,p,F,b,_,A,R,x)},Te=(f,u,p,v,b,_,A,R,x)=>{u.slotScopeIds=R,f==null?u.shapeFlag&512?b.ctx.activate(u,p,v,A,x):Dt(u,p,v,b,_,A,x):vt(f,u,x)},Dt=(f,u,p,v,b,_,A)=>{const R=f.component=Bl(f,v,b);if(si(f)&&(R.ctx.renderer=M),jl(R,!1,A),R.asyncDep){if(b&&b.registerDep(R,re,A),!f.el){const x=R.subTree=xe(Tt);N(null,x,u,p),f.placeholder=x.el}}else re(R,f,u,p,b,_,A)},vt=(f,u,p)=>{const v=u.component=f.component;if(_l(f,u,p))if(v.asyncDep&&!v.asyncResolved){z(v,u,p);return}else v.next=u,v.update();else u.el=f.el,v.vnode=u},re=(f,u,p,v,b,_,A)=>{const R=()=>{if(f.isMounted){let{next:C,bu:D,u:H,parent:G,vnode:Q}=f;{const Me=Ei(f);if(Me){C&&(C.el=Q.el,z(f,C,A)),Me.asyncDep.then(()=>{f.isUnmounted||R()});return}}let k=C,pe;dt(f,!1),C?(C.el=Q.el,z(f,C,A)):C=Q,D&&In(D),(pe=C.props&&C.props.onVnodeBeforeUpdate)&&Le(pe,G,C,Q),dt(f,!0);const ge=Ws(f),Ne=f.subTree;f.subTree=ge,P(Ne,ge,d(Ne.el),y(Ne),f,b,_),C.el=ge.el,k===null&&vl(f,ge.el),H&&ve(H,b),(pe=C.props&&C.props.onVnodeUpdated)&&ve(()=>Le(pe,G,C,Q),b)}else{let C;const{el:D,props:H}=u,{bm:G,m:Q,parent:k,root:pe,type:ge}=f,Ne=Wt(u);dt(f,!1),G&&In(G),!Ne&&(C=H&&H.onVnodeBeforeMount)&&Le(C,k,u),dt(f,!0);{pe.ce&&pe.ce._def.shadowRoot!==!1&&pe.ce._injectChildStyle(ge);const Me=f.subTree=Ws(f);P(null,Me,p,v,f,b,_),u.el=Me.el}if(Q&&ve(Q,b),!Ne&&(C=H&&H.onVnodeMounted)){const Me=u;ve(()=>Le(C,k,Me),b)}(u.shapeFlag&256||k&&Wt(k.vnode)&&k.vnode.shapeFlag&256)&&f.a&&ve(f.a,b),f.isMounted=!0,u=p=v=null}};f.scope.on();const x=f.effect=new Mr(R);f.scope.off();const E=f.update=x.run.bind(x),F=f.job=x.runIfDirty.bind(x);F.i=f,F.id=f.uid,x.scheduler=()=>xs(F),dt(f,!0),E()},z=(f,u,p)=>{u.component=f;const v=f.vnode.props;f.vnode=u,f.next=null,bl(f,u.props,v,p),Al(f,u.children,p),ke(),Hs(f),ze()},K=(f,u,p,v,b,_,A,R,x=!1)=>{const E=f&&f.children,F=f?f.shapeFlag:0,C=u.children,{patchFlag:D,shapeFlag:H}=u;if(D>0){if(D&128){Ze(E,C,p,v,b,_,A,R,x);return}else if(D&256){je(E,C,p,v,b,_,A,R,x);return}}H&8?(F&16&&Ee(E,b,_),C!==E&&a(p,C)):F&16?H&16?Ze(E,C,p,v,b,_,A,R,x):Ee(E,b,_,!0):(F&8&&a(p,""),H&16&&Oe(C,p,v,b,_,A,R,x))},je=(f,u,p,v,b,_,A,R,x)=>{f=f||At,u=u||At;const E=f.length,F=u.length,C=Math.min(E,F);let D;for(D=0;DF?Ee(f,b,_,!0,!1,C):Oe(u,p,v,b,_,A,R,x,C)},Ze=(f,u,p,v,b,_,A,R,x)=>{let E=0;const F=u.length;let C=f.length-1,D=F-1;for(;E<=C&&E<=D;){const H=f[E],G=u[E]=x?st(u[E]):He(u[E]);if(Ht(H,G))P(H,G,p,null,b,_,A,R,x);else break;E++}for(;E<=C&&E<=D;){const H=f[C],G=u[D]=x?st(u[D]):He(u[D]);if(Ht(H,G))P(H,G,p,null,b,_,A,R,x);else break;C--,D--}if(E>C){if(E<=D){const H=D+1,G=HD)for(;E<=C;)he(f[E],b,_,!0),E++;else{const H=E,G=E,Q=new Map;for(E=G;E<=D;E++){const _e=u[E]=x?st(u[E]):He(u[E]);_e.key!=null&&Q.set(_e.key,E)}let k,pe=0;const ge=D-G+1;let Ne=!1,Me=0;const Lt=new Array(ge);for(E=0;E=ge){he(_e,b,_,!0);continue}let De;if(_e.key!=null)De=Q.get(_e.key);else for(k=G;k<=D;k++)if(Lt[k-G]===0&&Ht(_e,u[k])){De=k;break}De===void 0?he(_e,b,_,!0):(Lt[De-G]=E+1,De>=Me?Me=De:Ne=!0,P(_e,u[De],p,null,b,_,A,R,x),pe++)}const Is=Ne?Ol(Lt):At;for(k=Is.length-1,E=ge-1;E>=0;E--){const _e=G+E,De=u[_e],Ns=u[_e+1],Ms=_e+1{const{el:_,type:A,transition:R,children:x,shapeFlag:E}=f;if(E&6){Ie(f.component.subTree,u,p,v);return}if(E&128){f.suspense.move(u,p,v);return}if(E&64){A.move(f,u,p,M);return}if(A===We){s(_,u,p);for(let C=0;CR.enter(_),b);else{const{leave:C,delayLeave:D,afterLeave:H}=R,G=()=>{f.ctx.isUnmounted?r(_):s(_,u,p)},Q=()=>{_._isLeaving&&_[Bo](!0),C(_,()=>{G(),H&&H()})};D?D(_,G,Q):Q()}else s(_,u,p)},he=(f,u,p,v=!1,b=!1)=>{const{type:_,props:A,ref:R,children:x,dynamicChildren:E,shapeFlag:F,patchFlag:C,dirs:D,cacheIndex:H}=f;if(C===-2&&(b=!1),R!=null&&(ke(),Kt(R,null,p,f,!0),ze()),H!=null&&(u.renderCache[H]=void 0),F&256){u.ctx.deactivate(f);return}const G=F&1&&D,Q=!Wt(f);let k;if(Q&&(k=A&&A.onVnodeBeforeUnmount)&&Le(k,u,f),F&6)ut(f.component,p,v);else{if(F&128){f.suspense.unmount(p,v);return}G&&at(f,null,u,"beforeUnmount"),F&64?f.type.remove(f,u,p,M,v):E&&!E.hasOnce&&(_!==We||C>0&&C&64)?Ee(E,u,p,!1,!0):(_===We&&C&384||!b&&F&16)&&Ee(x,u,p),v&&yt(f)}(Q&&(k=A&&A.onVnodeUnmounted)||G)&&ve(()=>{k&&Le(k,u,f),G&&at(f,null,u,"unmounted")},p)},yt=f=>{const{type:u,el:p,anchor:v,transition:b}=f;if(u===We){bt(p,v);return}if(u===Bn){I(f);return}const _=()=>{r(p),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(f.shapeFlag&1&&b&&!b.persisted){const{leave:A,delayLeave:R}=b,x=()=>A(p,_);R?R(f.el,_,x):x()}else _()},bt=(f,u)=>{let p;for(;f!==u;)p=g(f),r(f),f=p;r(u)},ut=(f,u,p)=>{const{bum:v,scope:b,job:_,subTree:A,um:R,m:x,a:E}=f;ks(x),ks(E),v&&In(v),b.stop(),_&&(_.flags|=8,he(A,f,u,p)),R&&ve(R,u),ve(()=>{f.isUnmounted=!0},u)},Ee=(f,u,p,v=!1,b=!1,_=0)=>{for(let A=_;A{if(f.shapeFlag&6)return y(f.component.subTree);if(f.shapeFlag&128)return f.suspense.next();const u=g(f.anchor||f.el),p=u&&u[Fo];return p?g(p):u};let O=!1;const S=(f,u,p)=>{f==null?u._vnode&&he(u._vnode,null,null,!0):P(u._vnode||null,f,u,null,null,null,p),u._vnode=f,O||(O=!0,Hs(),Zr(),O=!1)},M={p:P,um:he,m:Ie,r:yt,mt:Dt,mc:Oe,pc:K,pbc:Pe,n:y,o:e};return{render:S,hydrate:void 0,createApp:cl(S)}}function Hn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function dt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function wl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function bi(e,t,n=!1){const s=e.children,r=t.children;if(V(s)&&V(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Ei(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ei(t)}function ks(e){if(e)for(let t=0;te.__isSuspense;function Pl(e,t){t&&t.pendingBranch?V(e)?t.effects.push(...e):t.effects.push(e):Do(e)}const We=Symbol.for("v-fgt"),On=Symbol.for("v-txt"),Tt=Symbol.for("v-cmt"),Bn=Symbol.for("v-stc"),qt=[];let be=null;function Ri(e=!1){qt.push(be=e?null:[])}function Tl(){qt.pop(),be=qt[qt.length-1]||null}let Xt=1;function mn(e,t=!1){Xt+=e,e<0&&be&&t&&(be.hasOnce=!0)}function Ai(e){return e.dynamicChildren=Xt>0?be||At:null,Tl(),Xt>0&&be&&be.push(e),e}function Il(e,t,n,s,r,i){return Ai(pt(e,t,n,s,r,i,!0))}function Nl(e,t,n,s,r){return Ai(xe(e,t,n,s,r,!0))}function _n(e){return e?e.__v_isVNode===!0:!1}function Ht(e,t){return e.type===t.type&&e.key===t.key}const Si=({key:e})=>e??null,fn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?se(e)||ce(e)||B(e)?{i:Ae,r:e,k:t,f:!!n}:e:null);function pt(e,t=null,n=null,s=0,r=null,i=e===We?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Si(t),ref:t&&fn(t),scopeId:ti,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Ae};return l?(ws(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=se(n)?8:16),Xt>0&&!o&&be&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&be.push(c),c}const xe=Ml;function Ml(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Zo)&&(e=Tt),_n(e)){const l=It(e,t,!0);return n&&ws(l,n),Xt>0&&!i&&be&&(l.shapeFlag&6?be[be.indexOf(e)]=l:be.push(l)),l.patchFlag=-2,l}if($l(e)&&(e=e.__vccOpts),t){t=Dl(t);let{class:l,style:c}=t;l&&!se(l)&&(t.class=hs(l)),Z(c)&&(Es(c)&&!V(c)&&(c=fe({},c)),t.style=ds(c))}const o=se(e)?1:xi(e)?128:Ho(e)?64:Z(e)?4:B(e)?2:0;return pt(e,t,n,s,r,o,i,!0)}function Dl(e){return e?Es(e)||pi(e)?fe({},e):e:null}function It(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,h=t?Ll(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:h,key:h&&Si(h),ref:t&&t.ref?n&&i?V(i)?i.concat(fn(t)):[i,fn(t)]:fn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==We?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&It(e.ssContent),ssFallback:e.ssFallback&&It(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Rs(a,c.clone(a)),a}function ts(e=" ",t=0){return xe(On,null,e,t)}function He(e){return e==null||typeof e=="boolean"?xe(Tt):V(e)?xe(We,null,e.slice()):_n(e)?st(e):xe(On,null,String(e))}function st(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:It(e)}function ws(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(V(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),ws(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!pi(t)?t._ctx=Ae:r===3&&Ae&&(Ae.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else B(t)?(t={default:t,_ctx:Ae},n=32):(t=String(t),s&64?(n=16,t=[ts(t)]):n=8);e.children=t,e.shapeFlag|=n}function Ll(...e){const t={};for(let n=0;nle||Ae;let vn,ns;{const e=Rn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};vn=t("__VUE_INSTANCE_SETTERS__",n=>le=n),ns=t("__VUE_SSR_SETTERS__",n=>Zt=n)}const nn=e=>{const t=le;return vn(e),e.scope.on(),()=>{e.scope.off(),vn(t)}},zs=()=>{le&&le.scope.off(),vn(null)};function Ci(e){return e.vnode.shapeFlag&4}let Zt=!1;function jl(e,t=!1,n=!1){t&&ns(t);const{props:s,children:r}=e.vnode,i=Ci(e);yl(e,s,i,t),Rl(e,r,n||t);const o=i?Ul(e,t):void 0;return t&&ns(!1),o}function Ul(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,tl);const{setup:s}=n;if(s){ke();const r=e.setupContext=s.length>1?Kl(e):null,i=nn(e),o=tn(s,e,0,[e.props,r]),l=Cr(o);if(ze(),i(),(l||e.sp)&&!Wt(e)&&ni(e),l){if(o.then(zs,zs),t)return o.then(c=>{Js(e,c)}).catch(c=>{Sn(c,e,0)});e.asyncDep=o}else Js(e,o)}else wi(e)}function Js(e,t,n){B(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Z(t)&&(e.setupState=Jr(t)),wi(e)}function wi(e,t,n){const s=e.type;e.render||(e.render=s.render||Be);{const r=nn(e);ke();try{nl(e)}finally{ze(),r()}}}const Gl={get(e,t){return oe(e,"get",""),e[t]}};function Kl(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Gl),slots:e.slots,emit:e.emit,expose:t}}function Os(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Jr(Ro(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in $t)return $t[n](e)},has(t,n){return n in t||n in $t}})):e.proxy}function Wl(e,t=!0){return B(e)?e.displayName||e.name:e.name||t&&e.__name}function $l(e){return B(e)&&"__vccOpts"in e}const ye=(e,t)=>Po(e,t,Zt);function Oi(e,t,n){try{mn(-1);const s=arguments.length;return s===2?Z(t)&&!V(t)?_n(t)?xe(e,null,[t]):xe(e,t):xe(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&_n(n)&&(n=[n]),xe(e,t,n))}finally{mn(1)}}const ql="3.5.25";/** +* @vue/runtime-dom v3.5.25 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ss;const Qs=typeof window<"u"&&window.trustedTypes;if(Qs)try{ss=Qs.createPolicy("vue",{createHTML:e=>e})}catch{}const Pi=ss?e=>ss.createHTML(e):e=>e,kl="http://www.w3.org/2000/svg",zl="http://www.w3.org/1998/Math/MathML",Ke=typeof document<"u"?document:null,Ys=Ke&&Ke.createElement("template"),Jl={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ke.createElementNS(kl,e):t==="mathml"?Ke.createElementNS(zl,e):n?Ke.createElement(e,{is:n}):Ke.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ke.createTextNode(e),createComment:e=>Ke.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ke.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Ys.innerHTML=Pi(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Ys.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ql=Symbol("_vtc");function Yl(e,t,n){const s=e[Ql];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Xs=Symbol("_vod"),Xl=Symbol("_vsh"),Zl=Symbol(""),ec=/(?:^|;)\s*display\s*:/;function tc(e,t,n){const s=e.style,r=se(n);let i=!1;if(n&&!r){if(t)if(se(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&un(s,l,"")}else for(const o in t)n[o]==null&&un(s,o,"");for(const o in n)o==="display"&&(i=!0),un(s,o,n[o])}else if(r){if(t!==n){const o=s[Zl];o&&(n+=";"+o),s.cssText=n,i=ec.test(n)}}else t&&e.removeAttribute("style");Xs in e&&(e[Xs]=i?s.display:"",e[Xl]&&(s.display="none"))}const Zs=/\s*!important$/;function un(e,t,n){if(V(n))n.forEach(s=>un(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=nc(e,t);Zs.test(n)?e.setProperty(_t(s),n.replace(Zs,""),"important"):e[s]=n}}const er=["Webkit","Moz","ms"],Vn={};function nc(e,t){const n=Vn[t];if(n)return n;let s=Re(t);if(s!=="filter"&&s in e)return Vn[t]=s;s=xn(s);for(let r=0;rjn||(lc.then(()=>jn=0),jn=Date.now());function fc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Ve(uc(s,n.value),t,5,[s])};return n.value=e,n.attached=cc(),n}function uc(e,t){if(V(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const or=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ac=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?Yl(e,s,o):t==="style"?tc(e,n,s):yn(t)?fs(t)||ic(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):dc(e,t,s,o))?(sr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&nr(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!se(s))?sr(e,Re(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),nr(e,t,s,o))};function dc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&or(t)&&B(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return or(t)&&se(n)?!1:t in e}const hc=fe({patchProp:ac},Jl);let lr;function pc(){return lr||(lr=Sl(hc))}const gc=((...e)=>{const t=pc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=_c(s);if(!r)return;const i=t._component;!B(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,mc(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function mc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function _c(e){return se(e)?document.querySelector(e):e}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Rt=typeof document<"u";function Ti(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function vc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Ti(e.default)}const W=Object.assign;function Un(e,t){const n={};for(const s in t){const r=t[s];n[s]=we(r)?r.map(e):e(r)}return n}const kt=()=>{},we=Array.isArray;function cr(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const Ii=/#/g,yc=/&/g,bc=/\//g,Ec=/=/g,xc=/\?/g,Ni=/\+/g,Rc=/%5B/g,Ac=/%5D/g,Mi=/%5E/g,Sc=/%60/g,Di=/%7B/g,Cc=/%7C/g,Li=/%7D/g,wc=/%20/g;function Ps(e){return e==null?"":encodeURI(""+e).replace(Cc,"|").replace(Rc,"[").replace(Ac,"]")}function Oc(e){return Ps(e).replace(Di,"{").replace(Li,"}").replace(Mi,"^")}function rs(e){return Ps(e).replace(Ni,"%2B").replace(wc,"+").replace(Ii,"%23").replace(yc,"%26").replace(Sc,"`").replace(Di,"{").replace(Li,"}").replace(Mi,"^")}function Pc(e){return rs(e).replace(Ec,"%3D")}function Tc(e){return Ps(e).replace(Ii,"%23").replace(xc,"%3F")}function Ic(e){return Tc(e).replace(bc,"%2F")}function en(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const Nc=/\/$/,Mc=e=>e.replace(Nc,"");function Gn(e,t,n="/"){let s,r={},i="",o="";const l=t.indexOf("#");let c=t.indexOf("?");return c=l>=0&&c>l?-1:c,c>=0&&(s=t.slice(0,c),i=t.slice(c,l>0?l:t.length),r=e(i.slice(1))),l>=0&&(s=s||t.slice(0,l),o=t.slice(l,t.length)),s=Hc(s??t,n),{fullPath:s+i+o,path:s,query:r,hash:en(o)}}function Dc(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function fr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Lc(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&Nt(t.matched[s],n.matched[r])&&Fi(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Nt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Fi(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Fc(e[n],t[n]))return!1;return!0}function Fc(e,t){return we(e)?ur(e,t):we(t)?ur(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function ur(e,t){return we(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function Hc(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let i=n.length-1,o,l;for(o=0;o1&&i--;else break;return n.slice(0,i).join("/")+"/"+s.slice(o).join("/")}const et={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let is=(function(e){return e.pop="pop",e.push="push",e})({}),Kn=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function Bc(e){if(!e)if(Rt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Mc(e)}const Vc=/^[^#]+#/;function jc(e,t){return e.replace(Vc,"#")+t}function Uc(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const Pn=()=>({left:window.scrollX,top:window.scrollY});function Gc(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Uc(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function ar(e,t){return(history.state?history.state.position-t:-1)+e}const os=new Map;function Kc(e,t){os.set(e,t)}function Wc(e){const t=os.get(e);return os.delete(e),t}function $c(e){return typeof e=="string"||e&&typeof e=="object"}function Hi(e){return typeof e=="string"||typeof e=="symbol"}let te=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const Bi=Symbol("");te.MATCHER_NOT_FOUND+"",te.NAVIGATION_GUARD_REDIRECT+"",te.NAVIGATION_ABORTED+"",te.NAVIGATION_CANCELLED+"",te.NAVIGATION_DUPLICATED+"";function Mt(e,t){return W(new Error,{type:e,[Bi]:!0},t)}function Ge(e,t){return e instanceof Error&&Bi in e&&(t==null||!!(e.type&t))}const qc=["params","query","hash"];function kc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of qc)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function zc(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&rs(r)):[s&&rs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function Jc(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=we(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const Qc=Symbol(""),hr=Symbol(""),Ts=Symbol(""),Vi=Symbol(""),ls=Symbol("");function Bt(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function rt(e,t,n,s,r,i=o=>o()){const o=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const h=g=>{g===!1?c(Mt(te.NAVIGATION_ABORTED,{from:n,to:t})):g instanceof Error?c(g):$c(g)?c(Mt(te.NAVIGATION_GUARD_REDIRECT,{from:t,to:g})):(o&&s.enterCallbacks[r]===o&&typeof g=="function"&&o.push(g),l())},a=i(()=>e.call(s&&s.instances[r],t,n,h));let d=Promise.resolve(a);e.length<3&&(d=d.then(h)),d.catch(g=>c(g))})}function Wn(e,t,n,s,r=i=>i()){const i=[];for(const o of e)for(const l in o.components){let c=o.components[l];if(!(t!=="beforeRouteEnter"&&!o.instances[l]))if(Ti(c)){const h=(c.__vccOpts||c)[t];h&&i.push(rt(h,n,s,o,l,r))}else{let h=c();i.push(()=>h.then(a=>{if(!a)throw new Error(`Couldn't resolve component "${l}" at "${o.path}"`);const d=vc(a)?a.default:a;o.mods[l]=a,o.components[l]=d;const g=(d.__vccOpts||d)[t];return g&&rt(g,n,s,o,l,r)()}))}}return i}function Yc(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let o=0;oNt(h,l))?s.push(l):n.push(l));const c=e.matched[o];c&&(t.matched.find(h=>Nt(h,c))||r.push(c))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let Xc=()=>location.protocol+"//"+location.host;function ji(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let o=r.includes(e.slice(i))?e.slice(i).length:1,l=r.slice(o);return l[0]!=="/"&&(l="/"+l),fr(l,"")}return fr(n,e)+s+r}function Zc(e,t,n,s){let r=[],i=[],o=null;const l=({state:g})=>{const m=ji(e,location),w=n.value,P=t.value;let j=0;if(g){if(n.value=m,t.value=g,o&&o===w){o=null;return}j=P?g.position-P.position:0}else s(m);r.forEach(N=>{N(n.value,w,{delta:j,type:is.pop,direction:j?j>0?Kn.forward:Kn.back:Kn.unknown})})};function c(){o=n.value}function h(g){r.push(g);const m=()=>{const w=r.indexOf(g);w>-1&&r.splice(w,1)};return i.push(m),m}function a(){if(document.visibilityState==="hidden"){const{history:g}=window;if(!g.state)return;g.replaceState(W({},g.state,{scroll:Pn()}),"")}}function d(){for(const g of i)g();i=[],window.removeEventListener("popstate",l),window.removeEventListener("pagehide",a),document.removeEventListener("visibilitychange",a)}return window.addEventListener("popstate",l),window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",a),{pauseListeners:c,listen:h,destroy:d}}function pr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?Pn():null}}function ef(e){const{history:t,location:n}=window,s={value:ji(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(c,h,a){const d=e.indexOf("#"),g=d>-1?(n.host&&document.querySelector("base")?e:e.slice(d))+c:Xc()+e+c;try{t[a?"replaceState":"pushState"](h,"",g),r.value=h}catch(m){console.error(m),n[a?"replace":"assign"](g)}}function o(c,h){i(c,W({},t.state,pr(r.value.back,c,r.value.forward,!0),h,{position:r.value.position}),!0),s.value=c}function l(c,h){const a=W({},r.value,t.state,{forward:c,scroll:Pn()});i(a.current,a,!0),i(c,W({},pr(s.value,c,null),{position:a.position+1},h),!1),s.value=c}return{location:s,state:r,push:l,replace:o}}function tf(e){e=Bc(e);const t=ef(e),n=Zc(e,t.state,t.location,t.replace);function s(i,o=!0){o||n.pauseListeners(),history.go(i)}const r=W({location:"",base:e,go:s,createHref:jc.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let gt=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var ne=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(ne||{});const nf={type:gt.Static,value:""},sf=/[a-zA-Z0-9_]/;function rf(e){if(!e)return[[]];if(e==="/")return[[nf]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(m){throw new Error(`ERR (${n})/"${h}": ${m}`)}let n=ne.Static,s=n;const r=[];let i;function o(){i&&r.push(i),i=[]}let l=0,c,h="",a="";function d(){h&&(n===ne.Static?i.push({type:gt.Static,value:h}):n===ne.Param||n===ne.ParamRegExp||n===ne.ParamRegExpEnd?(i.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${h}) must be alone in its segment. eg: '/:ids+.`),i.push({type:gt.Param,value:h,regexp:a,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),h="")}function g(){h+=c}for(;lt.length?t.length===1&&t[0]===ae.Static+ae.Segment?1:-1:0}function Ui(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const uf={strict:!1,end:!0,sensitive:!1};function af(e,t,n){const s=cf(rf(e.path),n),r=W(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function df(e,t){const n=[],s=new Map;t=cr(uf,t);function r(d){return s.get(d)}function i(d,g,m){const w=!m,P=vr(d);P.aliasOf=m&&m.record;const j=cr(t,d),N=[P];if("alias"in d){const I=typeof d.alias=="string"?[d.alias]:d.alias;for(const J of I)N.push(vr(W({},P,{components:m?m.record.components:P.components,path:J,aliasOf:m?m.record:P})))}let T,L;for(const I of N){const{path:J}=I;if(g&&J[0]!=="/"){const ie=g.record.path,ee=ie[ie.length-1]==="/"?"":"/";I.path=g.record.path+(J&&ee+J)}if(T=af(I,g,j),m?m.alias.push(T):(L=L||T,L!==T&&L.alias.push(T),w&&d.name&&!yr(T)&&o(d.name)),Gi(T)&&c(T),P.children){const ie=P.children;for(let ee=0;ee{o(L)}:kt}function o(d){if(Hi(d)){const g=s.get(d);g&&(s.delete(d),n.splice(n.indexOf(g),1),g.children.forEach(o),g.alias.forEach(o))}else{const g=n.indexOf(d);g>-1&&(n.splice(g,1),d.record.name&&s.delete(d.record.name),d.children.forEach(o),d.alias.forEach(o))}}function l(){return n}function c(d){const g=gf(d,n);n.splice(g,0,d),d.record.name&&!yr(d)&&s.set(d.record.name,d)}function h(d,g){let m,w={},P,j;if("name"in d&&d.name){if(m=s.get(d.name),!m)throw Mt(te.MATCHER_NOT_FOUND,{location:d});j=m.record.name,w=W(_r(g.params,m.keys.filter(L=>!L.optional).concat(m.parent?m.parent.keys.filter(L=>L.optional):[]).map(L=>L.name)),d.params&&_r(d.params,m.keys.map(L=>L.name))),P=m.stringify(w)}else if(d.path!=null)P=d.path,m=n.find(L=>L.re.test(P)),m&&(w=m.parse(P),j=m.record.name);else{if(m=g.name?s.get(g.name):n.find(L=>L.re.test(g.path)),!m)throw Mt(te.MATCHER_NOT_FOUND,{location:d,currentLocation:g});j=m.record.name,w=W({},g.params,d.params),P=m.stringify(w)}const N=[];let T=m;for(;T;)N.unshift(T.record),T=T.parent;return{name:j,path:P,params:w,matched:N,meta:pf(N)}}e.forEach(d=>i(d));function a(){n.length=0,s.clear()}return{addRoute:i,resolve:h,removeRoute:o,clearRoutes:a,getRoutes:l,getRecordMatcher:r}}function _r(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function vr(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:hf(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function hf(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function yr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function pf(e){return e.reduce((t,n)=>W(t,n.meta),{})}function gf(e,t){let n=0,s=t.length;for(;n!==s;){const i=n+s>>1;Ui(e,t[i])<0?s=i:n=i+1}const r=mf(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function mf(e){let t=e;for(;t=t.parent;)if(Gi(t)&&Ui(e,t)===0)return t}function Gi({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function br(e){const t=qe(Ts),n=qe(Vi),s=ye(()=>{const c=wt(e.to);return t.resolve(c)}),r=ye(()=>{const{matched:c}=s.value,{length:h}=c,a=c[h-1],d=n.matched;if(!a||!d.length)return-1;const g=d.findIndex(Nt.bind(null,a));if(g>-1)return g;const m=Er(c[h-2]);return h>1&&Er(a)===m&&d[d.length-1].path!==m?d.findIndex(Nt.bind(null,c[h-2])):g}),i=ye(()=>r.value>-1&&Ef(n.params,s.value.params)),o=ye(()=>r.value>-1&&r.value===n.matched.length-1&&Fi(n.params,s.value.params));function l(c={}){if(bf(c)){const h=t[wt(e.replace)?"replace":"push"](wt(e.to)).catch(kt);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>h),h}return Promise.resolve()}return{route:s,href:ye(()=>s.value.href),isActive:i,isExactActive:o,navigate:l}}function _f(e){return e.length===1?e[0]:e}const vf=As({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:br,setup(e,{slots:t}){const n=An(br(e)),{options:s}=qe(Ts),r=ye(()=>({[xr(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[xr(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&_f(t.default(n));return e.custom?i:Oi("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),yf=vf;function bf(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Ef(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!we(r)||r.length!==s.length||s.some((i,o)=>i.valueOf()!==r[o].valueOf()))return!1}return!0}function Er(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const xr=(e,t,n)=>e??t??n,xf=As({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=qe(ls),r=ye(()=>e.route||s.value),i=qe(hr,0),o=ye(()=>{let h=wt(i);const{matched:a}=r.value;let d;for(;(d=a[h])&&!d.components;)h++;return h}),l=ye(()=>r.value.matched[o.value]);ln(hr,ye(()=>o.value+1)),ln(Qc,l),ln(ls,r);const c=Ao();return cn(()=>[c.value,l.value,e.name],([h,a,d],[g,m,w])=>{a&&(a.instances[d]=h,m&&m!==a&&h&&h===g&&(a.leaveGuards.size||(a.leaveGuards=m.leaveGuards),a.updateGuards.size||(a.updateGuards=m.updateGuards))),h&&a&&(!m||!Nt(a,m)||!g)&&(a.enterCallbacks[d]||[]).forEach(P=>P(h))},{flush:"post"}),()=>{const h=r.value,a=e.name,d=l.value,g=d&&d.components[a];if(!g)return Rr(n.default,{Component:g,route:h});const m=d.props[a],w=m?m===!0?h.params:typeof m=="function"?m(h):m:null,j=Oi(g,W({},w,t,{onVnodeUnmounted:N=>{N.component.isUnmounted&&(d.instances[a]=null)},ref:c}));return Rr(n.default,{Component:j,route:h})||j}}});function Rr(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Rf=xf;function Af(e){const t=df(e.routes,e),n=e.parseQuery||zc,s=e.stringifyQuery||dr,r=e.history,i=Bt(),o=Bt(),l=Bt(),c=So(et);let h=et;Rt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const a=Un.bind(null,y=>""+y),d=Un.bind(null,Ic),g=Un.bind(null,en);function m(y,O){let S,M;return Hi(y)?(S=t.getRecordMatcher(y),M=O):M=y,t.addRoute(M,S)}function w(y){const O=t.getRecordMatcher(y);O&&t.removeRoute(O)}function P(){return t.getRoutes().map(y=>y.record)}function j(y){return!!t.getRecordMatcher(y)}function N(y,O){if(O=W({},O||c.value),typeof y=="string"){const p=Gn(n,y,O.path),v=t.resolve({path:p.path},O),b=r.createHref(p.fullPath);return W(p,v,{params:g(v.params),hash:en(p.hash),redirectedFrom:void 0,href:b})}let S;if(y.path!=null)S=W({},y,{path:Gn(n,y.path,O.path).path});else{const p=W({},y.params);for(const v in p)p[v]==null&&delete p[v];S=W({},y,{params:d(p)}),O.params=d(O.params)}const M=t.resolve(S,O),U=y.hash||"";M.params=a(g(M.params));const f=Dc(s,W({},y,{hash:Oc(U),path:M.path})),u=r.createHref(f);return W({fullPath:f,hash:U,query:s===dr?Jc(y.query):y.query||{}},M,{redirectedFrom:void 0,href:u})}function T(y){return typeof y=="string"?Gn(n,y,c.value.path):W({},y)}function L(y,O){if(h!==y)return Mt(te.NAVIGATION_CANCELLED,{from:O,to:y})}function I(y){return ee(y)}function J(y){return I(W(T(y),{replace:!0}))}function ie(y,O){const S=y.matched[y.matched.length-1];if(S&&S.redirect){const{redirect:M}=S;let U=typeof M=="function"?M(y,O):M;return typeof U=="string"&&(U=U.includes("?")||U.includes("#")?U=T(U):{path:U},U.params={}),W({query:y.query,hash:y.hash,params:U.path!=null?{}:y.params},U)}}function ee(y,O){const S=h=N(y),M=c.value,U=y.state,f=y.force,u=y.replace===!0,p=ie(S,M);if(p)return ee(W(T(p),{state:typeof p=="object"?W({},U,p.state):U,force:f,replace:u}),O||S);const v=S;v.redirectedFrom=O;let b;return!f&&Lc(s,M,S)&&(b=Mt(te.NAVIGATION_DUPLICATED,{to:v,from:M}),Ie(M,M,!0,!1)),(b?Promise.resolve(b):Pe(v,M)).catch(_=>Ge(_)?Ge(_,te.NAVIGATION_GUARD_REDIRECT)?_:Ze(_):K(_,v,M)).then(_=>{if(_){if(Ge(_,te.NAVIGATION_GUARD_REDIRECT))return ee(W({replace:u},T(_.to),{state:typeof _.to=="object"?W({},U,_.to.state):U,force:f}),O||v)}else _=ft(v,M,!0,u,U);return Xe(v,M,_),_})}function Oe(y,O){const S=L(y,O);return S?Promise.reject(S):Promise.resolve()}function Ye(y){const O=bt.values().next().value;return O&&typeof O.runWithContext=="function"?O.runWithContext(y):y()}function Pe(y,O){let S;const[M,U,f]=Yc(y,O);S=Wn(M.reverse(),"beforeRouteLeave",y,O);for(const p of M)p.leaveGuards.forEach(v=>{S.push(rt(v,y,O))});const u=Oe.bind(null,y,O);return S.push(u),Ee(S).then(()=>{S=[];for(const p of i.list())S.push(rt(p,y,O));return S.push(u),Ee(S)}).then(()=>{S=Wn(U,"beforeRouteUpdate",y,O);for(const p of U)p.updateGuards.forEach(v=>{S.push(rt(v,y,O))});return S.push(u),Ee(S)}).then(()=>{S=[];for(const p of f)if(p.beforeEnter)if(we(p.beforeEnter))for(const v of p.beforeEnter)S.push(rt(v,y,O));else S.push(rt(p.beforeEnter,y,O));return S.push(u),Ee(S)}).then(()=>(y.matched.forEach(p=>p.enterCallbacks={}),S=Wn(f,"beforeRouteEnter",y,O,Ye),S.push(u),Ee(S))).then(()=>{S=[];for(const p of o.list())S.push(rt(p,y,O));return S.push(u),Ee(S)}).catch(p=>Ge(p,te.NAVIGATION_CANCELLED)?p:Promise.reject(p))}function Xe(y,O,S){l.list().forEach(M=>Ye(()=>M(y,O,S)))}function ft(y,O,S,M,U){const f=L(y,O);if(f)return f;const u=O===et,p=Rt?history.state:{};S&&(M||u?r.replace(y.fullPath,W({scroll:u&&p&&p.scroll},U)):r.push(y.fullPath,U)),c.value=y,Ie(y,O,S,u),Ze()}let Te;function Dt(){Te||(Te=r.listen((y,O,S)=>{if(!ut.listening)return;const M=N(y),U=ie(M,ut.currentRoute.value);if(U){ee(W(U,{replace:!0,force:!0}),M).catch(kt);return}h=M;const f=c.value;Rt&&Kc(ar(f.fullPath,S.delta),Pn()),Pe(M,f).catch(u=>Ge(u,te.NAVIGATION_ABORTED|te.NAVIGATION_CANCELLED)?u:Ge(u,te.NAVIGATION_GUARD_REDIRECT)?(ee(W(T(u.to),{force:!0}),M).then(p=>{Ge(p,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&!S.delta&&S.type===is.pop&&r.go(-1,!1)}).catch(kt),Promise.reject()):(S.delta&&r.go(-S.delta,!1),K(u,M,f))).then(u=>{u=u||ft(M,f,!1),u&&(S.delta&&!Ge(u,te.NAVIGATION_CANCELLED)?r.go(-S.delta,!1):S.type===is.pop&&Ge(u,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),Xe(M,f,u)}).catch(kt)}))}let vt=Bt(),re=Bt(),z;function K(y,O,S){Ze(y);const M=re.list();return M.length?M.forEach(U=>U(y,O,S)):console.error(y),Promise.reject(y)}function je(){return z&&c.value!==et?Promise.resolve():new Promise((y,O)=>{vt.add([y,O])})}function Ze(y){return z||(z=!y,Dt(),vt.list().forEach(([O,S])=>y?S(y):O()),vt.reset()),y}function Ie(y,O,S,M){const{scrollBehavior:U}=e;if(!Rt||!U)return Promise.resolve();const f=!S&&Wc(ar(y.fullPath,0))||(M||!S)&&history.state&&history.state.scroll||null;return Yr().then(()=>U(y,O,f)).then(u=>u&&Gc(u)).catch(u=>K(u,y,O))}const he=y=>r.go(y);let yt;const bt=new Set,ut={currentRoute:c,listening:!0,addRoute:m,removeRoute:w,clearRoutes:t.clearRoutes,hasRoute:j,getRoutes:P,resolve:N,options:e,push:I,replace:J,go:he,back:()=>he(-1),forward:()=>he(1),beforeEach:i.add,beforeResolve:o.add,afterEach:l.add,onError:re.add,isReady:je,install(y){y.component("RouterLink",yf),y.component("RouterView",Rf),y.config.globalProperties.$router=ut,Object.defineProperty(y.config.globalProperties,"$route",{enumerable:!0,get:()=>wt(c)}),Rt&&!yt&&c.value===et&&(yt=!0,I(r.location).catch(M=>{}));const O={};for(const M in et)Object.defineProperty(O,M,{get:()=>c.value[M],enumerable:!0});y.provide(Ts,ut),y.provide(Vi,kr(O)),y.provide(ls,c);const S=y.unmount;bt.add(y),y.unmount=function(){bt.delete(y),bt.size<1&&(h=et,Te&&Te(),Te=null,c.value=et,yt=!1,z=!1),S()}}};function Ee(y){return y.reduce((O,S)=>O.then(()=>Ye(S)),Promise.resolve())}return ut}function Ki(e=window.location.pathname){const t=e.split("/").filter(Boolean);return t.length<2||t[0]!=="t"?"":decodeURIComponent(t[1]||"").toLowerCase()}function Wi(e=window.location.pathname){return`/t/${Ki(e)}/`}function Sf(e=window.location.pathname){return`/t/${Ki(e)}/v1`}const Cf={style:{padding:"16px"}},wf={style:{"margin-top":"8px",color:"#666"}},Of={style:{"margin-top":"4px",color:"#666"}},Pf=As({__name:"HomePage",setup(e){const t=ye(()=>Wi()),n=ye(()=>Sf());return(s,r)=>(Ri(),Il("main",Cf,[r[2]||(r[2]=pt("h1",{style:{"font-size":"20px","font-weight":"600"}},"QuyUn",-1)),pt("p",wf,[r[0]||(r[0]=ts(" Router base: ",-1)),pt("code",null,$n(t.value),1)]),pt("p",Of,[r[1]||(r[1]=ts(" API baseURL: ",-1)),pt("code",null,$n(n.value),1)])]))}}),Tf=Af({history:tf(Wi()),routes:[{path:"/",component:Pf},{path:"/:pathMatch(.*)*",redirect:"/"}]}),If=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Nf={};function Mf(e,t){const n=Xo("router-view");return Ri(),Nl(n)}const Df=If(Nf,[["render",Mf]]);gc(Df).use(Tf).mount("#app"); diff --git a/frontend/user/dist/index.html b/frontend/user/dist/index.html new file mode 100644 index 0000000..1369fd5 --- /dev/null +++ b/frontend/user/dist/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn + + + +
+ + + diff --git a/frontend/user/index.html b/frontend/user/index.html new file mode 100644 index 0000000..7e215be --- /dev/null +++ b/frontend/user/index.html @@ -0,0 +1,13 @@ + + + + + + QuyUn + + +
+ + + + diff --git a/frontend/user/package-lock.json b/frontend/user/package-lock.json new file mode 100644 index 0000000..b0d754e --- /dev/null +++ b/frontend/user/package-lock.json @@ -0,0 +1,1614 @@ +{ + "name": "@quyun/user", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@quyun/user", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/frontend/user/package.json b/frontend/user/package.json new file mode 100644 index 0000000..a18ec62 --- /dev/null +++ b/frontend/user/package.json @@ -0,0 +1,23 @@ +{ + "name": "@quyun/user", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } +} + diff --git a/frontend/user/src/App.vue b/frontend/user/src/App.vue new file mode 100644 index 0000000..c2549a1 --- /dev/null +++ b/frontend/user/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/frontend/user/src/api.ts b/frontend/user/src/api.ts new file mode 100644 index 0000000..5b35228 --- /dev/null +++ b/frontend/user/src/api.ts @@ -0,0 +1,8 @@ +import axios from 'axios' +import { getApiBaseURL } from './tenant' + +export const api = axios.create({ + baseURL: getApiBaseURL(), + withCredentials: true, +}) + diff --git a/frontend/user/src/env.d.ts b/frontend/user/src/env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/frontend/user/src/env.d.ts @@ -0,0 +1,2 @@ +/// + diff --git a/frontend/user/src/main.ts b/frontend/user/src/main.ts new file mode 100644 index 0000000..c80f0ac --- /dev/null +++ b/frontend/user/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import { router } from './router' +import App from './App.vue' + +createApp(App).use(router).mount('#app') + diff --git a/frontend/user/src/router.ts b/frontend/user/src/router.ts new file mode 100644 index 0000000..92c7eb1 --- /dev/null +++ b/frontend/user/src/router.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { getUserRouterBase } from './tenant' +import HomePage from './views/HomePage.vue' + +export const router = createRouter({ + history: createWebHistory(getUserRouterBase()), + routes: [ + { path: '/', component: HomePage }, + { path: '/:pathMatch(.*)*', redirect: '/' }, + ], +}) + diff --git a/frontend/user/src/tenant.ts b/frontend/user/src/tenant.ts new file mode 100644 index 0000000..846ce9c --- /dev/null +++ b/frontend/user/src/tenant.ts @@ -0,0 +1,16 @@ +export function getTenantCodeFromPath(pathname = window.location.pathname): string { + const parts = pathname.split('/').filter(Boolean) + if (parts.length < 2 || parts[0] !== 't') return '' + return decodeURIComponent(parts[1] || '').toLowerCase() +} + +export function getUserRouterBase(pathname = window.location.pathname): string { + const tenantCode = getTenantCodeFromPath(pathname) + return `/t/${tenantCode}/` +} + +export function getApiBaseURL(pathname = window.location.pathname): string { + const tenantCode = getTenantCodeFromPath(pathname) + return `/t/${tenantCode}/v1` +} + diff --git a/frontend/user/src/views/HomePage.vue b/frontend/user/src/views/HomePage.vue new file mode 100644 index 0000000..a30148a --- /dev/null +++ b/frontend/user/src/views/HomePage.vue @@ -0,0 +1,20 @@ + + + + diff --git a/frontend/user/tsconfig.json b/frontend/user/tsconfig.json new file mode 100644 index 0000000..d415b8f --- /dev/null +++ b/frontend/user/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "types": ["vite/client"] + }, + "include": ["src"] +} + diff --git a/frontend/user/vite.config.ts b/frontend/user/vite.config.ts new file mode 100644 index 0000000..932c59c --- /dev/null +++ b/frontend/user/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + base: './', + server: { + port: 5174, + }, +}) + diff --git a/specs/API.md b/specs/API.md new file mode 100644 index 0000000..9a0de73 --- /dev/null +++ b/specs/API.md @@ -0,0 +1,248 @@ +# 新项目 API 规格(/t/:tenant_code/v1) + +## 0. 通用约定 + +### 0.1 Base URL 与租户 + +- Base:`/t/:tenant_code/v1` +- `tenant_code` 校验:服务端对路径段做 `lower()` 后校验 `^[a-z0-9_-]+$`,并查表确认租户存在且启用 + +### 0.2 认证 + +- WeChat H5:Cookie 会话(例如 `token`),请求需携带 `withCredentials` +- Admin:`Authorization: Bearer ` + +### 0.3 响应 + +- 成功:`200`(或 201/204),JSON +- 业务错误:`400`(含错误信息) +- 未登录:`401` +- 无权限:`403` +- 不存在:`404` + +--- + +## 1. WeChat OAuth + +### 1.1 发起授权 + +`GET /auth/wechat?redirect=` + +- 行为:302 跳转到微信授权 URL;回调为 `/t/:tenant_code/v1/auth/login` + +### 1.2 授权回调 + +`GET /auth/login?code=&state=&redirect=` + +- 行为: + - 获取 openid 与用户资料 + - `(tenant_id, open_id)` 获取或创建 `users` + - 写入 Cookie 会话 + - 302 回跳 `redirect` + +--- + +## 2. WeChat H5 内容与用户 + +### 2.1 曲谱列表(仅已发布) + +`GET /posts` + +Query: +- `page`(默认 1) +- `limit`(默认 10) +- `keyword`(可选) + +Response(示例结构): +```json +{ + "items": [ + { + "id": 1, + "title": "标题", + "description": "简介", + "price": 19900, + "discount": 80, + "views": 10, + "likes": 0, + "tags": ["tag"], + "head_images": ["https://signed-url..."], + "bought": false, + "recharge_wechat": "联系微信(可选)" + } + ], + "total": 123, + "page": 1, + "limit": 10 +} +``` + +### 2.2 曲谱详情(仅已发布) + +`GET /posts/:id/show` + +Response:同 `PostItem`,额外含 `content` + +### 2.3 获取播放 URL + +`GET /posts/:id/play` + +Response: +```json +{ "url": "https://signed-url..." } +``` + +规则:未购买返回 `metas.short=true` 的视频 URL;已购买返回 `metas.short=false` 的视频 URL。 + +### 2.4 我的已购 + +`GET /posts/mine` + +Query:`page`、`limit`、`keyword`(可选) + +Response:同分页结构 + +### 2.5 余额购买 + +`POST /posts/:id/buy` + +Response(余额支付成功): +- 可直接返回 `{ "ok": true }` 或返回订单信息(由你最终 UI/交互决定) + +错误: +- 余额不足:`400`,message 建议为“余额不足,请联系管理员充值” +- 已购买:`400` + +### 2.6 用户资料 + +`GET /users/profile` + +Response: +```json +{ "id": 1001, "created_at": "2025-01-01T00:00:00Z", "username": "xx", "avatar": "url", "balance": 10000 } +``` + +### 2.7 修改用户名 + +`PUT /users/username` + +Body: +```json +{ "username": "新昵称" } +``` + +约束:trim 后不能为空;最大 12 个字符(按 rune 计)。 + +### 2.8 JS-SDK 配置 + +`GET /wechats/js-sdk?url=` + +Response:JS-SDK 签名数据(结构按前端 `weixin-js-sdk` 需要输出) + +--- + +## 3. Admin(租户后台) + +### 3.1 登录 + +`POST /admin/auth` + +Body: +```json +{ "username": "admin", "password": "******" } +``` + +Response: +```json +{ "token": "..." } +``` + +### 3.2 仪表盘统计 + +`GET /admin/statistics` + +Response: +```json +{ + "post_draft": 0, + "post_published": 0, + "media": 0, + "order": 0, + "user": 0, + "amount": 0 +} +``` + +### 3.3 媒体库 + +`GET /admin/medias?page=&limit=&keyword=` + +`GET /admin/medias/:id`:302 跳转到 OSS 签名 URL + +`DELETE /admin/medias/:id`:删除 OSS 对象并软删/删 DB 记录(最终由你决定) + +### 3.4 上传(预签名) + +`GET /admin/uploads/pre-uploaded-check/:md5.:ext?mime=` + +Response: +```json +{ "exists": false, "pre_sign": { "...": "..." } } +``` + +说明: +- `md5` 在租户内去重 +- OSS Key:`quyun//.` + +`POST /admin/uploads/post-uploaded-action` + +Body: +```json +{ "originalName": "a.mp4", "md5": "...", "mimeType": "video/mp4", "size": 123 } +``` + +### 3.5 曲谱管理 + +`GET /admin/posts?page=&limit=&keyword=` + +`POST /admin/posts`、`PUT /admin/posts/:id` Body(示例): +```json +{ + "title": "标题", + "head_images": [1,2,3], + "price": 19900, + "discount": 80, + "introduction": "简介", + "content": "正文", + "status": 1, + "medias": [10,11,12] +} +``` + +`GET /admin/posts/:id`:返回曲谱 + medias 列表 + +`DELETE /admin/posts/:id` + +`POST /admin/posts/:id/send-to/:userId`:赠送曲谱(写入授权记录) + +### 3.6 用户管理 + +`GET /admin/users?page=&limit=&keyword=` + +`GET /admin/users/:id` + +`GET /admin/users/:id/articles?page=&limit=` + +`POST /admin/users/:id/balance` + +Body: +```json +{ "balance": 10000 } +``` + +### 3.7 订单管理 + +`GET /admin/orders?page=&limit=&order_number=&user_id=` + +`POST /admin/orders/:id/refund`:仅余额订单可退款 + diff --git a/specs/DB.sql b/specs/DB.sql new file mode 100644 index 0000000..fd0335c --- /dev/null +++ b/specs/DB.sql @@ -0,0 +1,151 @@ +-- 新项目数据库 DDL(PostgreSQL) +-- 目标:多租户(tenant_id 全表隔离)+ tenant_uuid 用于 OSS Key:quyun//. +-- 注意:tenant_uuid 由业务代码生成写入(不使用 DB 扩展默认值) + +BEGIN; + +-- 1) 租户 +CREATE TABLE IF NOT EXISTS tenants ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(64) NOT NULL, + tenant_uuid UUID NOT NULL, + name VARCHAR(128) NOT NULL DEFAULT '', + status INT2 NOT NULL DEFAULT 0, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- tenant_code:不区分大小写(写入/查询均 lower),并限制字符集 a-z0-9_- +ALTER TABLE tenants + ADD CONSTRAINT tenants_tenant_code_format + CHECK (tenant_code ~ '^[A-Za-z0-9_-]+$'); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_tenant_code_lower + ON tenants (lower(tenant_code)); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_tenant_uuid + ON tenants (tenant_uuid); + +-- 2) 租户后台账号 +CREATE TABLE IF NOT EXISTS admin_users ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + username VARCHAR(64) NOT NULL, + password_hash TEXT NOT NULL, + role VARCHAR(32) NOT NULL DEFAULT 'admin', + status INT2 NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_admin_users_tenant_username + ON admin_users (tenant_id, lower(username)); + +-- 3) 用户(微信 openid 用户) +CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ, + status INT2 NOT NULL DEFAULT 0, + open_id VARCHAR(128) NOT NULL, + username VARCHAR(128) NOT NULL DEFAULT '', + avatar TEXT, + metas JSONB NOT NULL DEFAULT '{}'::jsonb, + auth_token JSONB NOT NULL DEFAULT '{}'::jsonb, + balance BIGINT NOT NULL DEFAULT 0 +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_users_tenant_openid + ON users (tenant_id, open_id); + +CREATE INDEX IF NOT EXISTS ix_users_tenant_id + ON users (tenant_id, id); + +-- 4) 媒体 +CREATE TABLE IF NOT EXISTS medias ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + name VARCHAR(255) NOT NULL DEFAULT '', + mime_type VARCHAR(128) NOT NULL DEFAULT '', + size BIGINT NOT NULL DEFAULT 0, + path VARCHAR(512) NOT NULL DEFAULT '', + metas JSONB NOT NULL DEFAULT '{}'::jsonb, + hash VARCHAR(64) NOT NULL DEFAULT '' +); + +-- 租户内按 md5 去重 +CREATE UNIQUE INDEX IF NOT EXISTS ux_medias_tenant_hash + ON medias (tenant_id, hash); + +CREATE INDEX IF NOT EXISTS ix_medias_tenant_id + ON medias (tenant_id, id); + +-- 5) 曲谱/内容 +CREATE TABLE IF NOT EXISTS posts ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ, + status INT2 NOT NULL DEFAULT 0, + title VARCHAR(128) NOT NULL, + head_images JSONB NOT NULL DEFAULT '[]'::jsonb, + description VARCHAR(256) NOT NULL DEFAULT '', + content TEXT NOT NULL DEFAULT '', + price BIGINT NOT NULL DEFAULT 0, + discount INT2 NOT NULL DEFAULT 100, + views BIGINT NOT NULL DEFAULT 0, + likes BIGINT NOT NULL DEFAULT 0, + tags JSONB NOT NULL DEFAULT '[]'::jsonb, + assets JSONB NOT NULL DEFAULT '[]'::jsonb +); + +CREATE INDEX IF NOT EXISTS ix_posts_tenant_status_deleted + ON posts (tenant_id, status, deleted_at); + +-- 6) 授权关系(购买/赠送) +CREATE TABLE IF NOT EXISTS user_posts ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + price BIGINT NOT NULL DEFAULT 0 +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_user_posts_tenant_user_post + ON user_posts (tenant_id, user_id, post_id); + +CREATE INDEX IF NOT EXISTS ix_user_posts_tenant_user + ON user_posts (tenant_id, user_id, id); + +-- 7) 订单(仅余额) +CREATE TABLE IF NOT EXISTS orders ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + order_no VARCHAR(64) NOT NULL, + price BIGINT NOT NULL DEFAULT 0, + discount INT2 NOT NULL DEFAULT 100, + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + payment_method VARCHAR(50) NOT NULL DEFAULT 'balance', + post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + status INT2 NOT NULL DEFAULT 0, + meta JSONB NOT NULL DEFAULT '{}'::jsonb +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_orders_tenant_order_no + ON orders (tenant_id, order_no); + +CREATE INDEX IF NOT EXISTS ix_orders_tenant_user + ON orders (tenant_id, user_id, id); + +COMMIT; + diff --git a/specs/PRD.md b/specs/PRD.md new file mode 100644 index 0000000..d457448 --- /dev/null +++ b/specs/PRD.md @@ -0,0 +1,126 @@ +# 新项目 PRD(多租户 + 微信登录/分享 + 余额支付) + +## 1. 范围与约束 + +### 1.1 多租户(从第一天开始) + +- 所有站点与 API 都在路径前缀下:`/t/:tenant_code/...` +- `tenant_code` 规则:不区分大小写,允许字符集 `a-z0-9_-` +- 系统内部以 `tenant_uuid`(UUID)作为租户在 OSS 的存储分区标识 + - OSS Key:`quyun//.` +- 数据隔离:所有业务表均包含 `tenant_id`,所有查询必须带 `tenant_id` 过滤 + +### 1.2 微信能力 + +- 保留:微信网页授权登录(OAuth)、JS-SDK 签名与分享 +- 移除:微信支付/退款/回调(多租户版本完全不支持,历史也不兼容) + +### 1.3 支付能力 + +- 仅支持:余额支付(单位:分),后台可充值余额 +- 可退款:仅余额订单(退款 = 返还余额 + 撤销授权) + +### 1.4 技术栈约束 + +- 后端开发语言:Golang(Go) +- 前端技术栈:Vue 3 + Vite +- UI 组件库(推荐):PrimeVue(配合 TailwindCSS,性能较好且扩展/主题定制能力强) + +--- + +## 2. 角色与端 + +### 2.1 WeChat H5 端(C 端用户) + +- 访问曲谱列表、搜索 +- 查看曲谱详情 +- 播放:未购买只能播放“预览版(short=true)”,已购买播放“完整版(short=false)” +- 余额购买 +- 查看已购列表(快速播放) +- 查看个人资料与余额 +- 分享(JS-SDK) + +### 2.2 Admin 管理端(租户运营人员) + +- 登录(租户维度账号) +- 仪表盘统计 +- 媒体库管理:上传(预签名)、列表、预览、删除 +- 曲谱管理:创建/编辑/发布/草稿、绑定媒体资源、设置封面(≤3) +- 用户管理:列表、详情、查看已购、给用户充值余额 +- 订单管理:列表、退款(余额订单) +- 运营操作:赠送曲谱给指定用户 + +--- + +## 3. 核心业务对象 + +### 3.1 曲谱(Post) + +- 字段:标题、简介、正文、价格(分)、折扣(0~100)、状态(draft/published) +- 关联媒体: + - `head_images`:封面媒体 ID 列表(≤3) + - `assets`:媒体资产数组(见 MediaAsset),包含视频/音频/文件等 + +### 3.2 媒体(Media) + +- 字段:name、mime_type、size、hash(md5)、path(OSS key)、metas +- metas:`short`(是否预览资源)、`duration`、`parent_hash` +- hash 去重:在租户维度内去重(同租户相同 md5 视为同一资源) + +### 3.3 授权(UserPosts) + +- 记录用户购买/赠送后获得的曲谱访问权 +- 唯一约束:同一租户同一用户同一曲谱只能有一条 + +### 3.4 订单(Order) + +- 仅余额订单:`payment_method = balance` +- 状态:`pending/completed/refund_success/cancelled`(具体枚举在技术规格中确定) + +--- + +## 4. 关键流程 + +### 4.1 租户识别 + +- 进入任意页面时,根据 URL 中的 `:tenant_code` 识别租户 +- 服务端需校验: + - code 正则:`^[a-z0-9_-]+$`(不区分大小写) + - code 存在且启用 +- 解析成功后,将 `tenant_id`、`tenant_uuid` 注入到请求上下文 + +### 4.2 微信登录 + +1) 未登录请求 API → 401(XHR)或 302 → `/t/:tenant/v1/auth/wechat?redirect=...` +2) `/auth/wechat` 生成微信授权地址(回调到 `/t/:tenant/v1/auth/login` 并透传 redirect) +3) `/auth/login` 换取 openid + 用户信息,按 `(tenant_id, open_id)` 获取或创建用户,签发会话 token(cookie) + +### 4.3 播放策略(预览/完整版) + +- assets 中选择 `type == "video/mp4"` 的资源: + - 未购买:选择 `metas.short == true` + - 已购买:选择 `metas.short == false` +- 媒体 URL 通过 OSS 预签名下发 + +### 4.4 余额购买与授权 + +- 下单:创建订单 `pending` +- 校验余额足够: + - 足够:扣减余额 → 订单 `completed` → 写入 `user_posts` + - 不足:返回业务错误(提示联系管理员充值) + +### 4.5 余额退款 + +- 仅允许对 `completed + payment_method=balance` 的订单退款 +- 退款动作: + - 返还余额 + - 删除/撤销 `user_posts` 授权 + - 订单标记为 `refund_success` + +--- + +## 5. 非功能性要求(简版) + +- 所有写操作必须带租户隔离与鉴权校验 +- 关键唯一约束在 DB 层实现(避免并发重复授权/重复订单号) +- 租户 UUID 由业务代码生成并写入(不依赖 DB 扩展) diff --git a/specs/ROUTING.md b/specs/ROUTING.md new file mode 100644 index 0000000..7d40477 --- /dev/null +++ b/specs/ROUTING.md @@ -0,0 +1,80 @@ +# 新项目路由与部署规则(多租户路径前缀) + +## 1. 路由总览 + +### 1.1 约定 + +- 租户前缀:`/t/:tenant_code/`(不区分大小写;服务端统一按 `lower()` 识别) +- API:`/t/:tenant_code/v1/...` +- Admin SPA:`/t/:tenant_code/admin/...` +- WeChat SPA:`/t/:tenant_code/...`(除 `v1` 与 `admin` 子路径) + +### 1.2 为什么必须这样分层 + +- WeChat/后台前端都是 SPA,通常需要一个 catch-all 回退到 `index.html` +- 同时 API 又需要精确匹配,必须确保 API 路由优先于静态路由 + +--- + +## 2. 后端(HTTP Server)路由注册顺序建议 + +以 Fiber 为例,推荐顺序: + +1) `GET /t/:tenant_code/v1/...`:注册所有 API(并注入 TenantContext 中间件) +2) `GET /t/:tenant_code/admin*`:回退到 Admin 的 `index.html`(并正确设置静态资源 base) +3) `GET /t/:tenant_code/*`:回退到 WeChat 的 `index.html` + +> 重点:不要用全局 `GET /*` 直接接管,否则会吞掉 API 与 admin。 + +--- + +## 3. 前端(Admin) + +### 3.1 Router base + +- `createWebHistory("/t//admin/")` + +### 3.2 API baseURL 推导 + +- 运行时从 `location.pathname` 提取 `tenant_code` +- axios `baseURL = "/t//v1"` + +### 3.3 Token + +- `Authorization: Bearer `(租户后台登录返回) + +--- + +## 4. 前端(WeChat H5) + +### 4.1 Router base + +- `createWebHistory("/t//")` + +### 4.2 API baseURL 与 Cookie + +- axios `baseURL = "/t//v1"` +- `withCredentials = true`(携带 cookie 会话) + +### 4.3 未登录跳转 + +- 401 时跳:`/t//v1/auth/wechat?redirect=` + +--- + +## 5. tenant_code 提取规则(前端共用) + +从路径 `/t//...` 提取第二段: + +- `tenant_code = decodeURIComponent(pathname.split('/')[2] || '')` +- 使用时建议 `tenant_code.toLowerCase()` +- 允许字符集:`a-z0-9_-`(不允许空) + +--- + +## 6. OSS Key 规则 + +- 统一格式:`quyun//.` +- `tenant_uuid` 来自 `tenants.tenant_uuid` +- `md5` 来自上传文件内容 hash +