feat: initialize Ballet Island 微信小程序 with basic structure and functionality
- Add project configuration for WeChat mini program. - Create skills lock file for Taroify integration. - Set up app configuration and styles. - Implement main app component and index page with welcome message. - Add health check service to verify backend readiness. - Configure TypeScript settings and global types.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: taroify
|
||||
description: >
|
||||
Use when a task involves Taroify (@taroify/core, @taroify/icons,
|
||||
@taroify/hooks, or @taroify/commerce), including writing Taro React UI,
|
||||
selecting components, checking props or subcomponents, debugging usage,
|
||||
finding examples, or customizing themes.
|
||||
allowed-tools:
|
||||
- Bash(taroify *)
|
||||
- Bash(npx -y @taroify/cli *)
|
||||
- Bash(which taroify)
|
||||
---
|
||||
|
||||
# Taroify CLI
|
||||
|
||||
Use the bundled offline Taroify Catalog before writing or changing Taroify code. It contains
|
||||
component and Hook APIs, complete Markdown documentation, focused examples, full Taro Demo pages,
|
||||
and theme variables.
|
||||
|
||||
## Setup
|
||||
|
||||
Check for a global binary, otherwise use `npx`:
|
||||
|
||||
```bash
|
||||
which taroify || echo "use npx -y @taroify/cli <command>"
|
||||
```
|
||||
|
||||
Always pass `--format json` when consuming output programmatically.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. If the correct component is unknown, search by scenario:
|
||||
|
||||
```bash
|
||||
taroify search 上传 --format json
|
||||
taroify list --category form --format json
|
||||
```
|
||||
|
||||
2. Before writing code, inspect the real API:
|
||||
|
||||
```bash
|
||||
taroify info Button --format json
|
||||
taroify info Button.Group --format json
|
||||
```
|
||||
|
||||
3. Get a focused example, or request the full Taro page when imports and surrounding context matter:
|
||||
|
||||
```bash
|
||||
taroify demo Button --format json
|
||||
taroify demo Button demo1 --format json
|
||||
taroify demo Button --full --format json
|
||||
```
|
||||
|
||||
4. Read the full document only when the structured API and examples are insufficient:
|
||||
|
||||
```bash
|
||||
taroify doc Button --format json
|
||||
```
|
||||
|
||||
5. Query theme variables before customizing component styles:
|
||||
|
||||
```bash
|
||||
taroify token --format json
|
||||
taroify token Button --format json
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Query before writing; never guess Taroify component names, Props, enum values, events, or
|
||||
subcomponents.
|
||||
- Treat Taroify Props and inherited Taro native capabilities separately. Platform support is
|
||||
determined by the target Taro adapter.
|
||||
- Use public imports from `@taroify/core`, `@taroify/icons`, `@taroify/hooks`, or
|
||||
`@taroify/commerce`.
|
||||
- Prefer `ConfigProvider` keys or returned CSS variables over internal selectors and hardcoded
|
||||
component values.
|
||||
- Do not repeat a tool call with identical arguments.
|
||||
- Taroify examples use Taro React semantics; do not replace Taro components with Web-only DOM
|
||||
elements unless the user explicitly targets H5.
|
||||
|
||||
## MCP
|
||||
|
||||
When `taroify_*` MCP tools are available, prefer them over shell commands. They expose the same
|
||||
offline Catalog through six read-only tools and also provide document/demo resources.
|
||||
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
bin/
|
||||
.cache/
|
||||
.swc/
|
||||
coverage/
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
project.private.config.json
|
||||
*.tsbuildinfo
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 项目约定
|
||||
|
||||
本仓库包含两个独立项目,按目录分别管理代码、依赖、配置与运行命令。
|
||||
|
||||
- `miniprogram/`:Taro React 小程序。Node 配置、`package.json`、pnpm 锁文件、`node_modules/` 和 Taroify skill 均归属此目录。涉及小程序时,先读取 [小程序约定](miniprogram/AGENTS.md),在该目录执行 pnpm 命令。
|
||||
- `backend/`:Go + PostgreSQL 后端。Go 模块、Dockerfile、Compose、环境配置及开发脚本均归属此目录。使用 Go、Docker 和 shell 命令开发,不依赖 Node.js。
|
||||
|
||||
根目录仅保留版本控制、说明文档和通用仓库约定,不建立根级 Node 包或 pnpm workspace。
|
||||
|
||||
修改小程序后,在 `miniprogram/` 执行 `pnpm typecheck` 和 `pnpm build`。修改后端后,在 `backend/` 执行 `go vet ./...`、`go test -race ./...` 和 `go build -o bin/api ./cmd/server`;部署配置变更另需检查 Compose 和镜像构建。仅报告实际执行的验证。
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Ballet Island
|
||||
|
||||
同一 Git 仓库中的两个独立项目,分别安装依赖、运行与构建。当前提供项目骨架、欢迎页和服务健康检查,业务需求后续补充。
|
||||
|
||||
## 目录
|
||||
|
||||
```text
|
||||
miniprogram/ Taro 4.2.1 + React 18 + TypeScript + Taroify 1.0.6
|
||||
src/ 小程序源码
|
||||
config/ 小程序构建配置
|
||||
package.json 小程序依赖与命令
|
||||
pnpm-lock.yaml 小程序依赖锁文件
|
||||
node_modules/ 小程序本地依赖(不提交)
|
||||
.nvmrc Node 版本
|
||||
.agents/skills/taroify/ Taroify Agent Skill
|
||||
backend/ Go 1.26 + PostgreSQL 18
|
||||
cmd/server/ 服务入口、数据库连接与优雅退出
|
||||
internal/httpapi/ HTTP 路由与测试
|
||||
go.mod / go.sum Go 模块与依赖
|
||||
Dockerfile 后端镜像构建
|
||||
compose.yaml API 与数据库编排
|
||||
.env.example 后端环境变量示例
|
||||
scripts/dev.sh 本机 Go 开发启动脚本
|
||||
```
|
||||
|
||||
小程序的 Node、pnpm 配置和依赖均位于 `miniprogram/`。后端通过 Go、Docker 和 shell 独立运行。根目录保留 Git、说明文档和通用仓库约定。
|
||||
|
||||
## 小程序开发
|
||||
|
||||
需要 Node.js 22.12+(`miniprogram/.nvmrc` 指定 22.22.1)、pnpm 9.15.0,以及具有该小程序开发权限的微信开发者工具。
|
||||
|
||||
从仓库根目录进入小程序项目:
|
||||
|
||||
```sh
|
||||
cd miniprogram
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
微信开发者工具导入 `miniprogram/`,AppID 为 `wx6dfbf1021db8aee0`,编译目录为 `dist/`。首页“检查连接”请求后端 `/readyz`,同时检查数据库是否可用。
|
||||
|
||||
默认 API 为 `http://127.0.0.1:8080`。使用本地 HTTP 地址时,在开发者工具的本地设置中开启“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,仅用于本地调试。真机需使用能够访问的接口地址,并按微信要求配置域名。
|
||||
|
||||
在 `miniprogram/` 中通过构建时环境变量指定接口地址,修改后重启构建:
|
||||
|
||||
```sh
|
||||
TARO_APP_API_BASE_URL=https://api.example.com pnpm dev
|
||||
TARO_APP_API_BASE_URL=https://api.example.com pnpm build
|
||||
```
|
||||
|
||||
`TARO_APP_API_BASE_URL` 是公开接口地址,不自动读取后端 `.env`;未设置时使用本机默认地址。
|
||||
|
||||
## 后端开发
|
||||
|
||||
需要 Go 1.26 或更高版本,以及 Docker Compose。仅使用 Docker 运行后端时无需本机 Go。
|
||||
|
||||
另开终端,从仓库根目录进入后端项目:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
test -f .env || cp .env.example .env
|
||||
docker compose up -d --wait postgres
|
||||
./scripts/dev.sh
|
||||
```
|
||||
|
||||
`backend/.env` 已被 Git 忽略。启动脚本读取其中的 PostgreSQL 标准 `PG*` 变量与 `HTTP_ADDR`,先构建再启动 Go 服务;修改代码后需重启命令。脚本只应读取可信的本地配置,`.env` 使用 shell 赋值语法,含空格或 `$` 等特殊字符的值须用单引号包裹。直接运行 Go 或二进制时需自行注入环境变量。
|
||||
|
||||
后端全程无需 Node.js 或 pnpm。
|
||||
|
||||
## Docker 运行与部署
|
||||
|
||||
以下命令在 `backend/` 中执行:
|
||||
|
||||
```sh
|
||||
docker compose up -d --build --wait
|
||||
curl http://127.0.0.1:8080/healthz
|
||||
curl http://127.0.0.1:8080/readyz
|
||||
docker compose logs -f
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Compose 等待数据库健康后启动 API,并等待两个服务就绪。API 容器使用非 root 用户;数据库写入命名卷,`docker compose down` 保留数据。Compose 项目名固定为 `ballet-island`,沿用现有数据库卷。
|
||||
|
||||
默认仅向宿主机回环地址开放 API 和数据库。部署到服务器时,可让 HTTPS 反向代理连接 `127.0.0.1:${API_PORT}`;如确需其他机器直接访问 API,可在 `backend/.env` 调整 `API_BIND`。数据库端口始终只绑定本机。Compose 内部使用 `PGHOST=postgres`、`PGPORT=5432`,宿主机数据库端口由 `.env` 的 `PGPORT` 控制。
|
||||
|
||||
`API_PORT` 控制 Docker API 的宿主机端口,`HTTP_ADDR` 控制本机 Go 服务的监听地址;修改后应同步小程序的 API 地址。本机 Go 服务与 Docker API 不应同时占用同一端口。
|
||||
|
||||
正式上线需配置 HTTPS、微信 request 合法域名、环境密码与数据库备份。小程序由微信开发者工具发布,Docker 部署后端和数据库。
|
||||
|
||||
PostgreSQL 18 数据卷按[官方镜像说明](https://hub.docker.com/_/postgres)挂载到 `/var/lib/postgresql`。已有数据库卷的密码不会因修改 `.env` 自动更新;需按数据库流程修改,不要为更新密码删除数据卷。
|
||||
|
||||
## 验证与构建
|
||||
|
||||
在 `miniprogram/` 中:
|
||||
|
||||
```sh
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
```
|
||||
|
||||
在 `backend/` 中:
|
||||
|
||||
```sh
|
||||
go vet ./...
|
||||
go test -race ./...
|
||||
go build -o bin/api ./cmd/server
|
||||
```
|
||||
|
||||
产物分别在 `miniprogram/dist/` 和 `backend/bin/api`,均不进入版本控制。
|
||||
|
||||
| 接口 | 用途 | 响应 |
|
||||
| --- | --- | --- |
|
||||
| `GET /healthz` | HTTP 服务存活检查 | `200 {"status":"ok"}` |
|
||||
| `GET /readyz` | 实际检查 PostgreSQL 连接 | 可用时 200,不可用时 `503 {"status":"unavailable"}` |
|
||||
|
||||
Go 服务启动时检查数据库连接,收到 `SIGINT` / `SIGTERM` 时停止接收请求并关闭连接池。
|
||||
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.env*
|
||||
bin
|
||||
coverage
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Local development only. Set a strong password for a deployed environment.
|
||||
PGHOST=127.0.0.1
|
||||
PGPORT=5432
|
||||
PGDATABASE=ballet_island
|
||||
PGUSER=ballet_island
|
||||
PGPASSWORD=change-me-for-local-development
|
||||
PGSSLMODE=disable
|
||||
|
||||
# Host Go process address; Docker always listens on :8080 inside its container.
|
||||
HTTP_ADDR=127.0.0.1:8080
|
||||
API_PORT=8080
|
||||
API_BIND=127.0.0.1
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM golang:1.26-alpine AS builder
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/server
|
||||
|
||||
FROM alpine:3.23
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /out/api /usr/local/bin/api
|
||||
USER 65532:65532
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/local/bin/api"]
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ballet-island/backend/internal/httpapi"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
if err := run(); err != nil {
|
||||
slog.Error("server stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
for _, key := range []string{"PGHOST", "PGDATABASE", "PGUSER", "PGPASSWORD"} {
|
||||
if os.Getenv(key) == "" {
|
||||
return fmt.Errorf("%s is required", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Use libpq environment variables so passwords need no URL escaping.
|
||||
pool, err := pgxpool.New(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure database: %w", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
startupCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
err = pool.Ping(startupCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database: %w", err)
|
||||
}
|
||||
|
||||
addr := os.Getenv("HTTP_ADDR")
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1:8080"
|
||||
}
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: httpapi.NewHandler(pool.Ping),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
slog.Info("server starting", "address", addr)
|
||||
serverErr <- server.ListenAndServe()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-serverErr:
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
slog.Info("server shutting down")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
_ = server.Close()
|
||||
return fmt.Errorf("shutdown server: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
name: ballet-island
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${PGDATABASE:?Set PGDATABASE in .env}
|
||||
POSTGRES_USER: ${PGUSER:?Set PGUSER in .env}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD:?Set PGPASSWORD in .env}
|
||||
ports:
|
||||
- "127.0.0.1:${PGPORT:-5432}:5432"
|
||||
volumes:
|
||||
# PostgreSQL 18 stores its versioned data directory under this mount.
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
HTTP_ADDR: :8080
|
||||
PGHOST: postgres
|
||||
PGPORT: 5432
|
||||
PGDATABASE: ${PGDATABASE:?Set PGDATABASE in .env}
|
||||
PGUSER: ${PGUSER:?Set PGUSER in .env}
|
||||
PGPASSWORD: ${PGPASSWORD:?Set PGPASSWORD in .env}
|
||||
PGSSLMODE: disable
|
||||
ports:
|
||||
- "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://127.0.0.1:8080/readyz"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
stop_grace_period: 15s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,13 @@
|
||||
module ballet-island/backend
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require github.com/jackc/pgx/v5 v5.11.0
|
||||
|
||||
require (
|
||||
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
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
@@ -0,0 +1,34 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewHandler returns health endpoints. ping must honor its context deadline.
|
||||
func NewHandler(ping func(context.Context) error) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeStatus(w, http.StatusOK, "ok")
|
||||
})
|
||||
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := ping(ctx); err != nil {
|
||||
// Do not expose database connection details through a public endpoint.
|
||||
writeStatus(w, http.StatusServiceUnavailable, "unavailable")
|
||||
return
|
||||
}
|
||||
writeStatus(w, http.StatusOK, "ok")
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func writeStatus(w http.ResponseWriter, code int, status string) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status": status})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHealthRemainsLiveWithoutDatabase(t *testing.T) {
|
||||
handler := NewHandler(func(context.Context) error {
|
||||
t.Fatal("liveness must not query the database")
|
||||
return nil
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
if response.Code != http.StatusOK || response.Body.String() != "{\"status\":\"ok\"}\n" {
|
||||
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadinessReflectsDatabaseState(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
code int
|
||||
body string
|
||||
}{
|
||||
{"ready", nil, http.StatusOK, "{\"status\":\"ok\"}\n"},
|
||||
{"unavailable", errors.New("private database details"), http.StatusServiceUnavailable, "{\"status\":\"unavailable\"}\n"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
called := false
|
||||
handler := NewHandler(func(ctx context.Context) error {
|
||||
called = true
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok || time.Until(deadline) > 2*time.Second {
|
||||
t.Fatal("database check must have a bounded deadline")
|
||||
}
|
||||
return tc.err
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
if !called || response.Code != tc.code || response.Body.String() != tc.body {
|
||||
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.HasPrefix(response.Header().Get("Content-Type"), "application/json") {
|
||||
t.Fatal("health response must be JSON")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadinessPreservesRequestCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
handler := NewHandler(func(ctx context.Context) error { return ctx.Err() })
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/readyz", nil).WithContext(ctx)
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("got %d, want 503", response.Code)
|
||||
}
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
backend_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$backend_dir"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
printf '%s\n' '缺少 backend/.env,请先从 .env.example 复制并配置。' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only source trusted local configuration; .env uses shell assignment syntax.
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
go build -o bin/api ./cmd/server
|
||||
# Replace the shell so shutdown signals reach the Go process directly.
|
||||
exec ./bin/api
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
22.22.1
|
||||
@@ -0,0 +1,34 @@
|
||||
# 小程序约定
|
||||
|
||||
本目录是独立的 Node 项目。安装依赖和执行下述命令时,以 `miniprogram/` 为工作目录,使用本目录 `package.json` 指定的 pnpm 版本,维护本目录的 `pnpm-lock.yaml`。
|
||||
|
||||
## Taroify 开发
|
||||
|
||||
涉及小程序中的 Taroify 组件、Hooks、事件、子组件或主题样式时,在编写或修改代码前执行以下流程;仅修改 Go 后端时无需加载这些资料。
|
||||
|
||||
1. 阅读 [官方 Agent 索引](https://taroify.com/agents/index.md)、[官方 Skill](https://raw.githubusercontent.com/mallfoundry/taroify/main/packages/cli/skills/taroify/SKILL.md) 和项目已安装的 [Taroify skill](.agents/skills/taroify/SKILL.md),按其中的查询流程核对实际 API 和示例。同一任务中已读且未变化的内容可复用。
|
||||
2. 核对项目实际安装的 Taroify 版本与查询结果中的版本。CLI 固定为小程序开发依赖;优先使用本项目的离线 Catalog。若文档版本与依赖版本不同,以对应版本的官方资料和已安装包的类型声明、源码核实兼容性。
|
||||
3. 存在 `taroify_*` MCP 工具时优先使用 MCP。否则使用 `pnpm exec taroify`,所有程序化查询必须添加 `--format json`。若依赖尚未安装,先执行 `pnpm install --frozen-lockfile`。
|
||||
4. 编码前查询目标组件的 `info` 和 `demo`,确认 Props、枚举值、事件、子组件及公开引入方式;需要完整页面上下文时使用 `demo --full`。定制样式前使用 `token` 查询主题变量;结构化信息不足时再使用 `doc`。组件未知时先按场景 `search` 或按分类 `list`。
|
||||
|
||||
查询示例:
|
||||
|
||||
```sh
|
||||
pnpm exec taroify info Button --format json
|
||||
pnpm exec taroify demo Button --full --format json
|
||||
pnpm exec taroify token Button --format json
|
||||
```
|
||||
|
||||
组件用法必须有查询依据,不根据训练记忆猜测 API。区分 Taroify Props 与 Taro 原生能力,平台支持以微信小程序适配器为准;保持 Taro React 语义和公开导入方式。主题定制优先使用查询返回的 `ConfigProvider` key 或 CSS 变量。
|
||||
|
||||
官网不可用时读取[官方仓库中的 Agent 索引源文件](https://raw.githubusercontent.com/mallfoundry/taroify/main/site/content/agents/index.md)。远程资料均不可用时,可使用项目内的 skill 和 CLI 离线 Catalog,并说明未核实的版本差异;没有证据的用法保持未确认。
|
||||
|
||||
## Skill 维护与验证
|
||||
|
||||
项目 skill 位于 `.agents/skills/taroify/`,保留官方随包内容。升级 `@taroify/cli` 后,从新的依赖包重新安装并检查 skill 变更:
|
||||
|
||||
```sh
|
||||
pnpm dlx skills@1.7.0 add ./node_modules/@taroify/cli/skills/taroify --agent codex --yes
|
||||
```
|
||||
|
||||
修改小程序代码后运行 `pnpm typecheck` 和 `pnpm build`;微信开发者工具或真机中的交互验证须另行说明,构建通过不能代替运行验证。
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
presets: [['taro', { framework: 'react', ts: true, reactJsxRuntime: 'automatic' }]],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from '@tarojs/cli'
|
||||
|
||||
export default defineConfig<'webpack5'>({
|
||||
projectName: 'ballet-island',
|
||||
designWidth: 375,
|
||||
deviceRatio: {
|
||||
375: 2,
|
||||
640: 1.17,
|
||||
750: 1,
|
||||
828: 0.905,
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
framework: 'react',
|
||||
compiler: {
|
||||
type: 'webpack5',
|
||||
prebundle: { enable: false },
|
||||
},
|
||||
cache: { enable: true },
|
||||
defineConstants: {
|
||||
__API_BASE_URL__: JSON.stringify(
|
||||
(process.env.TARO_APP_API_BASE_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''),
|
||||
),
|
||||
},
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: { enable: true },
|
||||
cssModules: { enable: false },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@ballet-island/miniprogram",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"engines": {
|
||||
"node": ">=22.12.0 <23",
|
||||
"pnpm": ">=9 <10"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "taro build --type weapp --watch",
|
||||
"build": "taro build --type weapp",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@taroify/core": "1.0.6",
|
||||
"@tarojs/components": "4.2.1",
|
||||
"@tarojs/helper": "4.2.1",
|
||||
"@tarojs/react": "4.2.1",
|
||||
"@tarojs/runtime": "4.2.1",
|
||||
"@tarojs/shared": "4.2.1",
|
||||
"@tarojs/taro": "4.2.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@taroify/cli": "1.0.6",
|
||||
"@tarojs/cli": "4.2.1",
|
||||
"@tarojs/plugin-framework-react": "4.2.1",
|
||||
"@tarojs/plugin-platform-weapp": "4.2.1",
|
||||
"@tarojs/taro-loader": "4.2.1",
|
||||
"@tarojs/webpack5-runner": "4.2.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"babel-preset-taro": "4.2.1",
|
||||
"less": "^4.2.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "1.105.0",
|
||||
"typescript": "5.9.3",
|
||||
"webpack": "5.91.0"
|
||||
}
|
||||
}
|
||||
Generated
+11235
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Ballet Island 微信小程序",
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "ballet-island",
|
||||
"appid": "wx6dfbf1021db8aee0",
|
||||
"compileType": "miniprogram",
|
||||
"setting": {
|
||||
"es6": false,
|
||||
"minified": true,
|
||||
"postcss": false,
|
||||
"urlCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"taroify": {
|
||||
"source": "./node_modules/@taroify/cli/skills/taroify",
|
||||
"sourceType": "local",
|
||||
"computedHash": "c1a0f9254edcf2d10ba8d0d6373cbb14a29bf054c8683e5a5855d37aa692b1b8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default defineAppConfig({
|
||||
pages: ['pages/index/index'],
|
||||
window: {
|
||||
navigationBarTitleText: 'Ballet Island',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f7f8fa',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
page {
|
||||
background: #f7f8fa;
|
||||
color: #323233;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import './app.scss'
|
||||
|
||||
export default function App({ children }: PropsWithChildren) {
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: 'Ballet Island',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.welcome {
|
||||
margin: 32px 20px;
|
||||
padding: 28px 24px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__message {
|
||||
display: block;
|
||||
margin-bottom: 28px;
|
||||
color: #646566;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useState } from 'react'
|
||||
import { Text, View } from '@tarojs/components'
|
||||
import Button from '@taroify/core/button'
|
||||
import '@taroify/core/button/style'
|
||||
import { checkServiceReady } from '../../services/health'
|
||||
import './index.scss'
|
||||
|
||||
export default function Index() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('欢迎来到 Ballet Island')
|
||||
|
||||
async function handleCheck() {
|
||||
setLoading(true)
|
||||
try {
|
||||
await checkServiceReady()
|
||||
setMessage('连接成功,服务已就绪')
|
||||
} catch {
|
||||
setMessage('暂时无法连接,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="welcome">
|
||||
<Text className="welcome__title">Ballet Island</Text>
|
||||
<Text className="welcome__message">{message}</Text>
|
||||
<Button color="primary" block loading={loading} disabled={loading} onClick={handleCheck}>
|
||||
检查连接
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export async function checkServiceReady(): Promise<void> {
|
||||
const response = await Taro.request<{ status: string }>({
|
||||
url: `${__API_BASE_URL__}/readyz`,
|
||||
method: 'GET',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
if (response.statusCode !== 200 || response.data?.status !== 'ok') {
|
||||
throw new Error('服务暂未就绪')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "@tarojs/taro"]
|
||||
},
|
||||
"include": ["src", "config", "types"]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare const __API_BASE_URL__: string
|
||||
|
||||
declare module '*.scss'
|
||||
|
||||
Reference in New Issue
Block a user