Skip to content

Latest commit

 

History

History
267 lines (224 loc) · 13.5 KB

api.md

File metadata and controls

267 lines (224 loc) · 13.5 KB

接口约定

返回 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
}

运营后台(需 ADMIN 角色)

演出发布与图片管理(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>[] }