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