RankLand 的 bwcx + Vue 3 SSR 前端/后端一体项目。当前前端已从
rankland-fe 迁移到本仓库,保留 bwcx class-component 开发范式和 vite-ssr
渲染层,并使用 shadcn-vue radix track + Tailwind v3 替代原 Umi/React/antd 页面。
包含技术栈:
- Vue 3 (Class Component First)
- TypeScript
- Vite 4 + vite-ssr
- Tailwind CSS v3 + shadcn-vue radix track
- Playwright e2e parity tests
- 安装并使用 Node.js 20.19.1(推荐使用 fnm 自动切换版本)。
- 安装并使用 pnpm 9.15.9。
- 准备本机 MySQL。默认开发连接配置为:
host: 127.0.0.1
port: 3306
user: blue
password: test
database: rankland
- 首次参与开发时,需要先创建 database。TypeORM migration 会创建表,但不会创建 database:
CREATE DATABASE rankland
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;- 安装依赖:
fnm exec --using v20.19.1 corepack pnpm@9.15.9 install --frozen-lockfile- 初始化或更新数据库结构:
pnpm run db:migration:runTypeORM 会在 typeorm_migrations 表记录已经执行过的 migration。再次执行该命令时,只会按时间戳顺序运行尚未记录的新 migration。
- 运行开发服务:
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run dev- 在浏览器中打开 http://127.0.0.1:3000/。
开发环境可以通过环境变量覆盖默认 MySQL / Redis 配置:
MYSQL_HOST=127.0.0.1 \
MYSQL_PORT=3306 \
MYSQL_USER=blue \
MYSQL_PASS=test \
MYSQL_DB=rankland \
REDIS_HOST=127.0.0.1 \
REDIS_PORT=6379 \
REDIS_DB=0 \
REDIS_PASS= \
pnpm run devRedis 用于开发和生产 SSR 页面结果缓存。Redis 不可用时服务会跳过缓存并继续未缓存 SSR;不会因为缓存层异常中断页面响应。
文件上传默认使用 FS Provider,将对象写入当前工作目录的 temp/file/,并通过同源 /file/ 路径下载;FS_BASE_PATH 和 FILE_BASE_URL 均可覆盖。Compose 模板为该目录配置了多实例共享的持久卷。选择 TencentCloud 时,下载 URL 默认使用 https://cdn.algoux.cn/rankland/file/,并且必须同时提供完整 COS 凭据、bucket 和 region。开发和生产共用同一个环境变量配置逻辑。
运行时不会自动执行 migration。切换分支、拉取代码或新增 migration 后,请手动运行:
pnpm run db:migration:run如果修改了 TypeORM Entity 并需要生成 schema 迁移文件,运行:
pnpm run db:migration:generate生成的 migration 需要人工 review 后提交。
- Contest 事件架构
- Contest v2 API 文档
- 比赛文件存储与 Provider
- 生产者 / 消费者实现指导
- Contest 事件实现进度
- MySQL DATETIME UTC 约定
- Redis SSR 页面缓存
迁移后的公开页面:
/:首页统计和榜单入口/search?kw=:榜单搜索/ranklist/:id:单榜渲染,支持focus=yes或聚焦=是进入无站点 chrome 的聚焦模式/collection/:id?rankId=:合集页和侧边导航/live/:id:实时榜单,支持轮询和滚榜 WebSocket 配置/playground:CSR-only SRK playground,包含 Monaco 编辑器和实时预览
RankLand 外部 API 配置:
LEGACY_API_BASE_SERVER=https://rl-api.algoux.cn
LEGACY_API_BASE_CLIENT=https://rl-api.algoux.cn
SRK_STORAGE_BASE=https://srk-assets.algoux.cn
VIN_URL=https://cdn.algoux.cn/rankland/vin.txt
HOST_GLOBAL=rl.algoux.org
HOST_CN=rl.algoux.cn
SITE_ALIAS=global
LIVE_POLLING_INTERVAL=10000
WS_BASE=wss://rl-api.algoux.cn
GTAG=
BEIAN=LEGACY_API_BASE_SERVER 在 SSR 服务运行时读取;LEGACY_API_BASE_CLIENT 和 VIN_URL 会在 Vite 构建时写入浏览器包。
这两个地址只用于 /rank/search、/ranking/* 等尚未迁移的旧接口;contest、collection、statistics 与文件元数据统一使用同源 /api/v2。如果切换 legacy mock 或环境地址,需要重新运行 client build。
运行单元测试:
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run test运行依赖本机 MySQL 的集成测试:
RUN_MYSQL_TESTS=true fnm exec --using v20.19.1 corepack pnpm@9.15.9 run test运行 RankLand 前端 e2e parity tests:
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run test:e2etest:e2e 会先用 mock RankLand API 环境构建前端,再启动 mock API
(127.0.0.1:4322) 和生产 SSR 服务 (127.0.0.1:4321) 执行 Playwright。
在 Codex/macOS 沙箱内 Chromium 可能因系统权限失败,需要在沙箱外运行该命令。
比赛相关接口统一使用 /api/v2 前缀。创建和更新比赛时使用 users 字段表示比赛用户。需要管理权限的接口使用 x-token: <AUTH_TOKEN>,事件追加还需要 x-producer-id:
POST /api/v2/contests x-token
PATCH /api/v2/contests/:uk x-token
GET /api/v2/contests x-token
GET /api/v2/contests/:uk x-token
DELETE /api/v2/contests/:uk x-token
GET /api/v2/public/contests
GET /api/v2/public/contests/:uk
POST /api/v2/public/contests/:uk/views
POST /api/v2/files x-token, multipart
GET /api/v2/files/:id x-token
DELETE /api/v2/files/:id x-token
GET /api/v2/contests/:uk/users x-token
GET /api/v2/contests/:uk/users/:userId x-token
PATCH /api/v2/contests/:uk/users/:userId x-token
GET /api/v2/public/contests/:uk/users
GET /api/v2/public/contests/:uk/users/:userId
POST /api/v2/contests/:uk/events x-token, x-producer-id
GET /api/v2/public/contests/:uk/events
GET /api/v2/public/contests/:uk/event-stream/notifications
GET /api/v2/contests/:uk/event-stream x-token
GET /api/v2/public/contests/:uk/event-stream
DELETE /api/v2/contests/:uk/event-stream/producer-lock x-token
POST /api/v2/contests/:uk/events/reset x-token
POST /api/v2/contests/:uk/events 支持直接传 BatchProducerEvent 等价 JSON,也支持 raw protobuf bytes;protobuf content type 仅允许 application/x-protobuf 或 application/protobuf。GET /api/v2/public/contests/:uk/events 会按 Accept 返回 GetContestEventsResponse protobuf bytes 或等价 JSON。
uk 是唯一比赛标识。MySQL 表字段使用 snake_case;例如 contest_event_stream 只保存 contest_id、事件高水位和 producer lock,不保存冗余 uk。接口、DTO 和业务代码中的字段仍使用 camelCase,并由 TypeORM 映射到 snake_case 列。
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run build服务以单个前台 Node 进程运行,不再使用 PM2 cluster。生产环境应在启动服务前显式执行 migration:
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run db:migration:run
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run build
MYSQL_HOST=... \
MYSQL_PORT=3306 \
MYSQL_USER=... \
MYSQL_PASS=... \
MYSQL_DB=... \
REDIS_HOST=... \
REDIS_PORT=6379 \
REDIS_DB=0 \
REDIS_PASS=... \
REDIS_NAMESPACE=rankland-prod \
AUTH_TOKEN=... \
FILE_PROVIDER=TencentCloud \
FILE_BASE_URL=https://cdn.algoux.cn/rankland/file/ \
FS_BASE_PATH= \
COS_SECRET_ID=... \
COS_SECRET_KEY=... \
COS_DOMAIN= \
COS_BUCKET=... \
COS_REGION=... \
COS_BASE_PATH=rankland/file/ \
fnm exec --using v20.19.1 corepack pnpm@9.15.9 run startcompose-template.yml 只列出启动所需的拓扑、凭据与存储配置。业务服务已经内置经过本机 loadtest
选择的性能默认值,常规部署无需复制这些环境变量;全部可覆盖项、约束、压测依据和回滚值集中记录在
服务端性能配置。
NODE_ENV=production 下必须提供 MYSQL_HOST、MYSQL_USER、MYSQL_PASS、MYSQL_DB 和非空
REDIS_NAMESPACE。同一部署的所有应用实例必须连接同一个 MySQL write primary、同一个 standalone
Redis endpoint,并使用完全相同的 namespace;不同环境和独立部署必须使用不同 namespace。
Redis Pub/Sub 不按 logical database 隔离,因此不能用 REDIS_DB 代替 namespace。非生产环境未配置
namespace 时默认使用 rankland:local。
文件 Provider 默认是 FS;Compose 使用 rankland-files 命名卷持久化并在多实例间共享文件。
显式选择 TencentCloud 时还必须提供 COS secret、bucket 和 region,COS_DOMAIN 可选。生产启动
不会自动迁移数据库。覆盖性能项需要整批重启实例,并按性能配置文档记录调优证据与回滚值。
本项目采用了基于运行时 -> 模块的目录分层结构:
src/
client/
common/
server/
为了规避混合运行时带来的潜在错误风险,前端和后端代码应放置在对应目录下,而涉及在前后端共享的模块(如错误码枚举、公共接口、公共服务、DTO、RPO)推荐放置在 common。
服务端使用 bwcx,提供简洁的 OOP 开发体验,参考 bwcx 文档。
前端使用 Vue 3。本仓库的页面组件(位于 src/client/modules/)沿用类组件方式开发,但你仍可以使用选项式 API 或 setup 语法糖进行开发(不推荐)。
要使用兼容 SSR/CSR 的数据预取,请在类组件中使用 asyncData 钩子获取数据。其返回值可以直接提供给页面组件的 props 使用:
export default class SomeComponent extends Vue {
// 从 asyncData 返回对象中获取 SSR 或页面导航时所需的初始数据
@Prop() public msg: string;
async asyncData({ to }: AsyncDataOptions) {
return {
msg: `hello ${to.name}`,
};
}
}asyncData 中不可访问组件 this,这是由于 Vue Class Component 的限制,我们不得不将 asyncData 作为组件实例方法提供,但实际上你应该将它视为静态方法。
后端开发的接口可以借助 @Api.Summary 和 @Contract 装饰器声明为 API。这样你可以在前端通过下列方式一键调用接口,且带有完备的类型提示。
开发后端接口:
@Controller('/api')
export default class SomeController {
@Api.Summary('接口描述')
@Get()
@Contract(SomeApiReqDTO, SomeApiRespDTO)
public async someApi(@Data() data: SomeApiReqDTO): Promise<SomeApiRespDTO> {
return ...
}
}在前端直接调用接口:
// 直接通过组件内 asyncData 或 $api 调用
export default class SomeView extends Vue {
async asyncData({ apiClient }) {
const res = await apiClient.someApi({ ... });
return res;
}
async mounted() {
const res = await this.$api.someApi({ ... });
}
}
// setup 或组合式 API 函数
const apiClient = useApiClient();
const res = await apiClient.someApi({ ... });使用 @View 和 @RenderMethod 为页面视图组件声明路由,这样可以无需定义前端路由配置并自动在后端装配该路由。
要导航到其他页面,请通过组件实例上的 $$router 进行导航。本项目约定页面路由组件以 .view.vue 结尾,以和其他组件进行区分。
示例:
@View('/ranklist/:id')
@RenderMethod(RenderMethodKind.SSR)
export default class RanklistView extends Vue {
public goToRanklist(id: string) {
return this.$$router.to('Ranklist').push({
id,
});
}
}