返回 README · 系统设计
前端统一以 /api 为前缀访问,Vite 开发服务器与 Nginx 会去掉前缀后转发到后端。启动后端后,可在 http://localhost:8080/doc.html(Knife4j)在线调试。
| 项 |
约定 |
| 认证 |
请求头 authorization: <token>,token 由登录接口返回 |
| 响应 |
{ "code": 0, "msg": "ok", "data": ... },code = 0 为成功 |
| 64 位整数 |
所有 ID、订单号、支付单号以字符串返回(超出 JS 安全整数范围) |
| 金额 |
单位为分,数字类型 |
| 时间 |
毫秒时间戳(epoch ms),数字类型;需要倒计时的接口额外返回 serverTime 用于校准时钟 |
| code |
HTTP |
含义 |
前端建议处理 |
| 400 |
200 |
参数错误 |
提示 msg |
| 401 |
401 |
未登录或登录过期 |
跳转登录页 |
| 403 |
200 |
无权限 |
提示 |
| 404 |
200 |
资源不存在 |
提示 |
| 429 |
429 |
请求过于频繁 |
提示稍后再试 |
| 500 |
200 |
系统繁忙,请重试 |
提示 |
| 1001 |
200 |
未开售或已停售 |
提示 |
| 1002 |
200 |
购买令牌无效,请重新排队 |
回到等候室 |
| 1003 |
200 |
超过账号限购 |
提示 |
| 1004 |
200 |
观演人已购买本场,data: { viewerId } |
标出对应观演人 |
| 1005 |
200 |
已售罄,可登记候补 |
引导登记候补 |
| 1006 |
200 |
座位已被占用,data: { seatIds: string[] } |
刷新座位图并标出冲突座位 |
| 1100 |
200 |
账号未实名 |
引导实名认证 |
| 1101 |
200 |
没有观演人 |
引导添加观演人 |
| 1200 |
200 |
订单状态不允许此操作 |
刷新订单 |
| 1201 |
200 |
演出开始前 48 小时内不可退票 |
提示 |
| 1300 |
200 |
候补登记失败(已登记 / 票档不在候补状态等) |
提示 |
| 方法 |
路径 |
登录 |
说明 |
| POST |
/user/code |
否 |
{ phone } 发送验证码。开发环境返回 { devCode },便于直接填入 |
| POST |
/user/login |
否 |
{ phone, code } → { token, user: UserVO },手机号不存在则自动注册 |
| POST |
/user/logout |
是 |
退出 |
| GET |
/user/me |
是 |
→ UserVO |
| POST |
/user/real-name |
是 |
{ name, idNo } 模拟实名认证,成功后自动把本人加为观演人 → UserVO |
type UserVO = {
id: string; phone: string /* 138****0000 */; nickName: string;
role: 'USER' | 'ADMIN'; verified: boolean; realName: string | null /* 林* */
}
| 方法 |
路径 |
说明 |
| GET |
/viewers |
→ ViewerVO[] |
| POST |
/viewers |
{ name, idNo },证件号为 18 位身份证(校验位合法)→ ViewerVO |
| DELETE |
/viewers/{id} |
存在有效票的观演人不可删除 |
type ViewerVO = { id: string; name: string; idNoMasked: string /* 3101**********1234 */; self: boolean }
| 方法 |
路径 |
说明 |
| GET |
/shows/filters |
→ { cities: string[], categories: string[] } |
| GET |
/shows?city=&category=&keyword=&page=1&size=12 |
→ { list: ShowCardVO[], total, page, size } |
| GET |
/shows/{id} |
→ ShowDetailVO,同时计入演出页 UV |
| GET |
/sessions/{id} |
→ SessionDetailVO |
| GET |
/sessions/{id}/seat-layout |
选座场次的静态座位布局 → SeatLayoutVO |
| GET |
/sessions/{id}/seat-status?areaId= |
区域座位占用位图 → { areaId, bitmap, serverTime } |
type SaleMode = 'TIER' | 'SEAT_SELECT'
type SessionStatus = 'PREPARING' | 'LOBBY' | 'ON_SALE' | 'CLOSED' // LOBBY = 等候室已开放、尚未开售
type ShowCardVO = {
id: string; title: string; artist: string; city: string; category: string;
posterUrl: string | null; venueName: string;
minPrice: number; maxPrice: number;
firstSessionAt: number; sessionCount: number;
saleStatus: 'UPCOMING' | 'ON_SALE' | 'SOLD_OUT' | 'ENDED';
nextSaleAt: number | null; hot: boolean
}
type ShowDetailVO = {
id: string; title: string; artist: string; city: string; category: string;
description: string; posterUrl: string | null;
detailImages: string[]; seatingChartUrl: string | null;
venue: { id: string; name: string; city: string; address: string };
uv: number; sessions: SessionVO[]; serverTime: number
}
type SessionVO = {
id: string; showId: string; name: string /* 2026-12-24 周四 19:30 */;
startAt: number; saleStartAt: number; saleMode: SaleMode; status: SessionStatus;
perUserLimit: number; soldOut: boolean; tiers: TierVO[]
}
type TierVO = {
id: string; name: string; price: number;
availability: 'AVAILABLE' | 'LIMITED' | 'SOLD_OUT'; // 只展示三档,不暴露精确余量
state: 'ON_SALE' | 'WAITLIST' | 'CLOSED' | null; // WAITLIST = 售罄,可登记候补
areaId: string | null // 选座场次:票档对应的区域
}
type SessionDetailVO = SessionVO & {
show: { id: string; title: string; artist: string; posterUrl: string | null; city: string; venueName: string; category: string };
serverTime: number
}
type SeatLayoutVO = {
sessionId: string;
areas: {
areaId: string; name: string; tierId: string; tierName: string; price: number;
rows: number; cols: number;
seats: [seatId: string, row: number, col: number, idx: number][]
}[]
}
座位位图:bitmap 为 Base64,第 idx 个座位是否已占用为 (bytes[idx >> 3] >> (7 - (idx & 7))) & 1。位图可能比座位数短,越界部分视为空闲。
| 方法 |
路径 |
说明 |
| POST |
/queue/enter |
{ sessionId },需已实名且至少一个观演人 → QueueStatusVO |
| GET |
/queue/poll?sessionId=&voucher= |
按 pollIntervalMs 轮询 → QueueStatusVO;每用户每秒至多 1 次 |
| GET |
/queue/token?sessionId= |
status = RELEASED 后调用一次 → { token, expireAt, serverTime };令牌过期返回 1002 |
type QueueStatusVO = {
sessionId: string;
status: 'WAITING' | 'RELEASED' | 'SOLD_OUT' | 'CLOSED';
phase: 'LOBBY' | 'ON_SALE'; // 开售前 / 开售后
voucher: string | null; // 排队凭证,轮询时原样带回
aheadEstimate: number; // 估算的前方人数
pollIntervalMs: number; // 建议轮询间隔
saleStartAt: number; serverTime: number
}
| 方法 |
路径 |
说明 |
| POST |
/orders |
下单 { sessionId, tierId, viewerIds: string[], token?, seatIds?: string[] } → { orderNo } |
| GET |
/orders/{orderNo}/result |
轮询下单结果 → { status: 'PROCESSING' | 'SUCCESS' | 'FAILED', msg } |
| GET |
/orders?status=&cursor=&size=10 |
我的订单 → { list: OrderVO[], nextCursor: string | null } |
| GET |
/orders/{orderNo} |
→ OrderDetailVO |
| POST |
/orders/{orderNo}/cancel |
取消待支付订单 |
| POST |
/orders/{orderNo}/pay |
发起支付 → { payNo, orderNo, amount, expireAt },前端跳转收银台 /cashier/{payNo} |
| POST |
/orders/{orderNo}/refund |
申请退票(演出开始 48 小时前),订单进入 REFUNDING,约 1 秒后变为 REFUNDED |
- 票档模式:
token 必填(来自等候室),seatIds 不传。
- 选座模式:不需要排队和令牌,
seatIds 数量与 viewerIds 相同,且都属于该票档对应的区域。
- 下单返回
orderNo 只代表“已抢到,正在生成订单”,需轮询 /orders/{orderNo}/result 直到 SUCCESS 或 FAILED。
type OrderStatus = 'PENDING_PAY' | 'PAID' | 'CANCELLED' | 'REFUNDING' | 'REFUNDED'
type OrderVO = {
orderNo: string; status: OrderStatus; origin: 'PUBLIC' | 'WAITLIST';
qty: number; amount: number; tierName: string;
showId: string; showTitle: string; artist: string; posterUrl: string | null;
sessionId: string; sessionName: string; sessionStartAt: number; venueName: string; city: string;
createdAt: number; expireAt: number; paidAt: number | null
}
type OrderDetailVO = OrderVO & {
price: number; cancelReason: 'TIMEOUT' | 'USER_CANCEL' | null;
tickets: { id: string; viewerName: string; idNoMasked: string; seatLabel: string | null; ticketCode: string | null; active: boolean }[];
refundable: boolean; serverTime: number
}
| 方法 |
路径 |
说明 |
| GET |
/mock-pay/{payNo} |
收银台信息 → { payNo, orderNo, amount, status: 'CREATED' | 'SUCCESS' | 'REFUNDED', subject, expireAt, serverTime } |
| POST |
/mock-pay/{payNo}/confirm |
模拟用户在网关确认支付,网关随即签名回调 → { payStatus, orderStatus } |
| POST |
/pay/notify |
网关 → 应用的支付回调(签名校验,无需登录,前端不调用) |
| POST |
/pay/refund-notify |
网关 → 应用的退款回调(同上) |
| 方法 |
路径 |
说明 |
| POST |
/waitlist |
{ sessionId, tierId, viewerIds },票档 state = WAITLIST 时可登记,至多 2 人 → WaitlistVO |
| GET |
/waitlist |
我的候补 → WaitlistVO[] |
| DELETE |
/waitlist/{id} |
取消候补 |
| GET |
/notifications |
站内通知(候补成功等)→ { id, title, content, link, createdAt }[] |
type WaitlistVO = {
id: string; sessionId: string; tierId: string; showId: string; showTitle: string;
sessionName: string; tierName: string; price: number; qty: number;
status: 'WAITING' | 'ALLOCATED' | 'CANCELLED';
position: number | null; // 当前排位(从 1 开始),仅 WAITING 时有值
orderNo: string | null; createdAt: number
}
演出发布与图片管理(2026-10-21):
| 方法 |
路径 |
说明 |
| GET |
/admin/shows |
演出列表,包含草稿/已上架/已下架及修订号 |
| GET |
/admin/venues |
场馆及其区域、座位数量,供选座票档绑定 |
| POST |
/admin/shows |
{ title } 新建草稿,返回 ShowEditor |
| GET |
/admin/shows/{id}/editor |
返回完整编辑表单 ShowEditor |
| PUT |
/admin/shows/{id}/editor |
{ revision, form } 保存,返回更新后的 ShowEditor;修订号不符返回 400 |
| POST |
/admin/shows/{id}/publish |
{ revision } 校验后首次发布,或将 OFFLINE 演出重新上架;返回 ShowEditor |
| POST |
/admin/shows/{id}/offline |
{ revision } 下架并禁止新增购买;返回 ShowEditor |
| POST |
/admin/media |
multipart/form-data 字段 file,PNG/JPEG ≤5 MiB、≤2000 万像素;返回 { url, width, height } |
| GET |
/media/{name} |
无需登录的图片内容(非 JSON);前端返回的 URL 为 /api/media/{name} |
type ShowEditor = { id: string; publishStatus: 'DRAFT' | 'PUBLISHED' | 'OFFLINE'; revision: number; form: ShowForm }
type ShowForm = {
title: string; artist: string; venueId: string | null; category: string; description: string; hot: boolean;
posterUrl: string | null; detailImages: string[]; seatingChartUrl: string | null;
sessions: {
startAt: number; saleStartAt: number; saleMode: 'TIER' | 'SEAT_SELECT'; perUserLimit: number;
tiers: { name: string; price: number /* 分 */; total: number; areaId: string | null }[];
}[];
}
最多 20 场、每场 20 票档、8 张详情图。草稿允许未完成的场次配置;发布时必须校验完整信息、时间顺序、正数价格库存、区域归属与座位数。已发布或下架的演出只能修改内容和图片,场馆与场次结构不可改。图片引用必须来自上传接口;移除引用不会物理删除图片。草稿和下架演出从公开查询隐藏,新入队、下单、候补和选座访问被阻止;已有订单的读取、支付和退款继续可用。
| 方法 |
路径 |
说明 |
| GET |
/admin/sessions |
场次运行状态 → AdminSessionVO[] |
| POST |
/admin/sessions/{id}/warmup |
立即预热,进入等候室 |
| POST |
/admin/sessions/{id}/open |
立即开售 |
| POST |
/admin/sessions/{id}/reset |
{ saleInMinutes } 清空该场次的订单、票、候补并重新预热,开售时间设为 N 分钟后 |
| POST |
/admin/sessions/{id}/tiers/{tierId}/state |
{ state: 'ON_SALE' | 'WAITLIST' | 'CLOSED' } 运营切换票档状态(如候补清空后重新公开售卖) |
| POST |
/admin/reconcile |
立即对账 → ReconcileVO[] |
| GET |
/admin/checks |
执行正确性校验 SQL → CheckVO[] |
| GET |
/admin/metrics |
关键计数器 → { submit: Record<string, number>, compensation: Record<string, number>, reconcileMismatch: number, cancel: Record<string, number>, waitlistAllocated: number } |
| PUT |
/admin/shows/{id} |
{ title?, description? } 修改演出信息,触发两级缓存失效 |
| POST |
/admin/loadtest/users |
{ count, duplicateRate } 批量生成压测用户、观演人与登录 token → { created } |
| GET |
/admin/loadtest/users.csv |
导出 user_id,token,viewer_ids 供 JMeter 使用 |
type AdminSessionVO = {
id: string; showId: string; showTitle: string; name: string;
saleMode: SaleMode; status: SessionStatus; saleStartAt: number; startAt: number;
queueSize: number; tokensActive: number; presale: number; watermark: string | null;
tiers: { id: string; name: string; price: number; total: number; dbSold: number;
redisStock: number | null; state: string | null; waitlistSize: number }[]
}
type ReconcileVO = { sessionId: string; tierId: string; tierName: string; total: number;
redisRemain: number; dbSold: number; inflight: number; diff: number; action: string }
type CheckVO = { name: string; description: string; violations: number; sample: Record<string, unknown>[] }