App 端三个接口对接文档
适用:移动端 / 桌面端 App
维护:基于当前 internal/handler + internal/logic 代码反向梳理
时间:2026-05-27
涉及接口:
- 文件上传 —
POST /v1/public/file/upload
- 订阅列表(含促销 promo) —
GET /v1/public/subscribe/list
- 邀请赠送记录 —
GET /v1/public/user/invite_records
公共说明:
- BaseURL 示例:
https://tapi.hifast.biz
- 鉴权头:
Authorization: <JWT>(注意:不要写 Bearer 前缀,本项目 AuthMiddleware 直接取 token 值)
- 业务码包在
{ code, msg, data } 信封中,code = 200 为成功
- 默认
Accept: application/json,可选 lang: zh_CN
- 经过
AuthMiddleware + DeviceMiddleware 的接口都需要登录态 + 设备绑定校验
1. 文件上传
1.1 Endpoint
- Handler:
internal/handler/public/file/fileUploadHandler.go
- Logic:
internal/logic/public/file/fileuploadlogic.go:33
- 路由:
internal/handler/routes.go:934
- 中间件:
AuthMiddleware + DeviceMiddleware(必须登录)
1.2 请求
Form 参数
| 字段 |
位置 |
必填 |
说明 |
biz_type |
form |
是 |
业务分类标签,会作为对象 key 的一部分(如 app-package、avatar) |
file |
form file |
是 |
待上传文件二进制 |
文件约束(来自 etc/ppanel.yaml → S3,可调整)
| 项 |
默认值 |
| 单文件最大 |
104857600 字节(100 MiB) |
| 允许的 Content-Type |
application/zip, application/x-zip-compressed, application/gzip, application/x-gzip, application/octet-stream, text/plain, application/json, image/jpeg, image/jpg, image/png, image/webp, image/gif, image/heic, image/heif, image/bmp |
Content-Type 判定优先级:multipart 文件头里的 Content-Type → 文件嗅探(前 512 字节)→ 兜底 application/octet-stream。
前端 form 上传时尽量带上 Content-Type,否则被嗅探成 application/octet-stream 可能不在白名单里。
1.3 请求示例
1.4 响应
| 字段 |
类型 |
说明 |
data.url |
string |
上传完成后的可访问 URL,规则:{S3.PublicBaseURL or S3.Endpoint}/{bucket}/{prefix}/{YYYY}/{MM}/{DD}/{userId}/{safeFileName}__{fileId} |
成功示例:
1.5 错误码
| 业务码 |
触发场景 |
InvalidAccess |
未登录 / JWT 无效 |
ParamError |
缺少 biz_type 或 file |
InvalidParams |
biz_type 为空、文件名为空、size <= 0、超过 MaxUploadSize、Content-Type 不在白名单 |
ERROR |
S3 未启用(S3.Enable=false) / S3 写入失败 |
1.6 前端易踩坑
file 字段名必须是 file,写 image / upload 都不行。
biz_type 走 form 字段(form:"biz_type"),不要塞 query 里。
- 上传成功只返回
url,不返回 file_id / 大小等元数据;如需附加元数据,请走分片协议 POST /upload/init + POST /upload/complete。
- 想上传 PDF / DOC 不会成功——白名单里没有,需要后端调
S3.AllowedContentTypes。
2. 订阅列表(含促销 promo)
2.1 Endpoint
- Handler:
internal/handler/public/subscribe/querySubscribeListHandler.go
- Logic:
internal/logic/public/subscribe/querySubscribeListLogic.go:32
- Promo 合并逻辑:
internal/logic/public/subscribe/promo.go
- 路由:
internal/handler/routes.go:1026
- 中间件:
OptionalAuthMiddleware + DeviceMiddleware(未登录也能请求,但未登录时只能拿到 rule_type = campaign 的促销)
2.2 请求
Query 参数
| 字段 |
类型 |
必填 |
说明 |
language |
string |
否 |
语言筛选,传值后返回该语言版本;不传则按系统默认语言返回 |
头部说明(影响返回内容)
| Header |
影响 |
Authorization |
传则识别为登录态,能拿到 new_user / inactive_user 类型的个性化促销;不传只返回 campaign 类型 |
X-App-Id |
不传会被识别为"老版本客户端",每个套餐的 discount 列表会被截掉最后一个元素。新版 App 必须带 X-App-Id |
2.3 请求示例
2.4 响应
顶层
| 字段 |
类型 |
说明 |
data.total |
int64 |
返回的套餐数量(= len(list),不是数据库总数) |
data.list |
Subscribe[] |
套餐列表 |
Subscribe 关键字段
| 字段 |
类型 |
说明 |
id |
int64 |
套餐 ID |
name |
string |
套餐名 |
language |
string |
当前返回的语言版本 |
description |
string |
套餐描述(可能是富文本/Markdown) |
unit_price |
int64 |
单时间单位原价,单位:分 |
unit_time |
string |
时间单位,枚举:Day / Month / Year(注意首字母大写) |
discount |
SubscribeDiscount[] |
量级折扣 + 促销,按 quantity 升序 |
node_count |
int64 |
节点数 |
traffic |
int64 |
套餐总流量,单位:字节 |
speed_limit |
int64 |
限速,单位见后端约定 |
device_limit |
int64 |
同时在线设备数限制 |
quota |
int64 |
总配额 |
show |
bool |
是否在前端展示 |
sell |
bool |
是否可售卖(本接口只返回 sell=true) |
show_original_price |
bool |
是否展示划线原价 |
reset_cycle |
int64 |
流量重置周期 |
renewal_reset |
bool |
续费时是否重置流量 |
created_at / updated_at |
int64 |
秒级 Unix 时间戳 |
SubscribeDiscount 字段
| 字段 |
类型 |
说明 |
quantity |
int64 |
购买的时间单位数量(如 1 = 1 个月,3 = 3 个月) |
discount |
float64 |
量级折扣比例,0 表示无折扣,0.05 表示再优惠 5% |
map_apple |
string |
对应 Apple IAP 商品 ID |
promo |
SubscribePromo | null |
#77 新增的促销对象,命中促销规则时下发,否则为 null |
SubscribePromo 字段
| 字段 |
类型 |
说明 |
rule_name |
string |
促销规则名(运营在后台填写,可直接给用户展示,如"新人首单 8 折") |
rule_type |
string |
规则类型枚举(见下表) |
promo_price |
int64 |
促销价,单位:分。优先级高于 unit_price * discount,前端命中促销时按此价显示 |
expires_at |
int64 |
该促销对当前用户的失效时间(秒级 Unix),0 表示无明确截止 |
rule_type 枚举
| 值 |
含义 |
资格判定 |
campaign |
全员/限时活动 |
仅看 start_time / end_time 是否在窗口内;未登录也会下发 |
new_user |
新用户首单 |
登录用户,且 now < user.created_at + params.window_hours;expires_at = user.created_at + window_hours |
inactive_user |
老用户唤回 |
登录用户,且距离最近一个订阅过期已超过 params.inactive_months 个月;expires_at = 规则 end_time |
多条促销规则命中同一 (subscribe_id, quantity) 时,按 priority DESC, id ASC 取首条,不是合并。
2.5 响应示例
2.6 价格计算建议(前端)
对每个 discount 元素:
注意:promo_price 设计为单价(与 unit_price 同级),不是总价。
命中促销时建议同时显示 rule_name("新人首单 8 折")和倒计时(基于 expires_at)。
2.7 前端易踩坑
- 必带
X-App-Id——否则 discount 数组最后一个会被砍掉。
- 促销分登录态:未登录时只能拿到
campaign;未拿到 new_user/inactive_user 时先检查是否传了 Authorization。
unit_time 是 PascalCase:Day / Month / Year,别小写匹配。
- 金额单位都是分(
unit_price、promo_price),展示时除以 100。
expires_at = 0 表示无截止,不要展示成 1970 年。
total 是当前返回的条数,不是数据库总数(接口在 logic 里强制 Size: 9999,相当于不分页)。
3. 邀请赠送记录
当前用户的"邀请赠送天数"流水。包含两类:
- 当前用户作为邀请人,被邀请的朋友下单触发的赠送;
- 当前用户作为被邀请人,自己下单触发的对应赠送(双向赠送)。
数据源:system_logs 表,type = 33 (TypeGift) 且 content.remark = "邀请赠送"。
这里只是赠送天数,不包含邀请佣金(请走 affiliate 系列接口)。
3.1 Endpoint
- Handler:
internal/handler/public/user/getInviteRecordsHandler.go
- Logic:
internal/logic/public/user/getInviteRecordsLogic.go:61
- 路由:
internal/handler/routes.go:1122
- 中间件:
AuthMiddleware + DeviceMiddleware(必须登录)
3.2 请求
Query 参数
| 字段 |
类型 |
必填 |
默认 |
说明 |
page |
int |
否 |
1 |
页码,<1 自动归一为 1 |
size |
int |
否 |
10 |
每页条数,<1 归一为 10,>100 截断为 100 |
start_time |
int64 |
否 |
0 |
起始时间(秒级 Unix),0 表示不过滤下界 |
end_time |
int64 |
否 |
0 |
截止时间(秒级 Unix),0 表示不过滤上界 |
⚠️ start_time / end_time 单位是秒(后端用 FROM_UNIXTIME(?))。传毫秒会过滤掉所有记录。
请求示例
旧 curl 模板里的 --data-urlencode 'page=1' 等对 GET 是 form body,不会被读取,请用 query string。
3.3 响应
顶层
| 字段 |
类型 |
说明 |
data.total |
int64 |
当前过滤条件下的记录总数(用于分页) |
data.list |
InviteRecord[] |
当前页列表,可能为空数组 [] |
InviteRecord 字段
| 字段 |
类型 |
说明 |
role |
string |
当前用户在该条记录中的角色:inviter 或 invitee(详见下表) |
peer_hash |
string |
对端用户的脱敏哈希(10 位定长数字字符串),用于"匿名展示朋友"。订单已删 / 对端 id 缺失时为 "" |
gift_days |
int64 |
本次赠送天数(来源 system_logs.content.amount) |
order_no |
string |
触发本次赠送的订单号 |
created_at |
int64 |
赠送时间,毫秒级 Unix(SQL 端 UNIX_TIMESTAMP(created_at) * 1000) |
⚠️ 时间戳单位不一致:请求里的 start_time/end_time 是秒,响应里的 created_at 是毫秒。前端请区分对待。
(与项目其它接口"统一秒级"约定不同,是该接口的当前实现。)
role 取值
| 值 |
含义 |
peer_hash 来源 |
inviter |
当前用户是邀请人,朋友下单触发的赠送 |
被邀请人(即订单的 user_id)的脱敏 hash |
invitee |
当前用户是被邀请人,自己下单触发的赠送 |
邀请人(user.referer_id)的脱敏 hash |
判定规则:默认 inviter;若 order.user_id == 当前用户 id,切换为 invitee 并改用 referer_id 计算 hash。
排序与分页
- 排序:
created_at DESC, id DESC(最近一条在最前)
- 分页:
LIMIT size OFFSET (page-1)*size
total 不受 LIMIT/OFFSET 影响
3.4 响应示例
非空:
空:
3.5 错误码
| 业务码 |
触发场景 |
InvalidAccess |
未登录 / JWT 无效 |
ParamError |
参数绑定失败 |
DatabaseQueryError |
DB 查询失败(count / 日志 / 订单任一) |
3.6 前端易踩坑
- 传毫秒给
start_time/end_time → 永远拿到空集。请传秒。
- 拿到的
created_at 是毫秒,不要再 *1000,直接 new Date(created_at) 即可。
- 空列表是
[] 不是 null,可直接 .map。
peer_hash 可能为 "",UI 兜底展示"未知朋友"。
size 上限 100,传 1000 会被截断。
- 本接口只含赠送天数,不含邀请佣金(佣金 → affiliate 接口)。
附录:业务码常量速查
| 名称 |
HTTP 含义 |
出现场景 |
200 |
成功 |
{"code":200,"msg":"success",...} |
InvalidAccess |
未授权 |
未登录 / JWT 无效 / 设备未绑定 |
ParamError |
参数错误 |
请求绑定失败、缺必填项 |
InvalidParams |
参数校验不通过 |
业务规则校验失败(文件超限、Content-Type 不合法等) |
DatabaseQueryError |
DB 错 |
SQL 查询失败 |
ERROR |
通用错 |
第三方/中间件失败(S3 未启用、S3 写入失败等) |