topfans/frontend/utils/api.js
2026-07-17 16:53:57 +08:00

1430 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// API 基础配置 — 通过 Vite 注入的环境变量区分开发/生产环境
// .env.development → VITE_API_BASE_URL开发
// .env.production → VITE_API_BASE_URL生产
// HBuilderX「运行」= dev「发行」= prod自动加载对应 .env
// CLIvite --mode development|production
import { getDeviceFingerprint } from './deviceFingerprint.js'
const API_BASE_URL = String(
import.meta.env.VITE_API_BASE_URL
).replace(/\/+$/, '')
const USE_MOCK_API = String(import.meta.env.VITE_USE_MOCK_API || 'false').toLowerCase() === 'true'
// 导出供 composable / 其他模块按需走 mock 短路(避免再读一次 env
export const IS_MOCK_API = USE_MOCK_API
const baseURL = API_BASE_URL
/**
* WebSocket 基础地址
* 优先读取 VITE_WS_BASE_URL独立部署 WS 时使用),未配置则按 API 地址自动推导 http→ws / https→wss
*/
const WS_BASE_URL = (() => {
const configured = import.meta.env.VITE_WS_BASE_URL
if (configured && String(configured).trim()) {
return String(configured).replace(/\/+$/, '')
}
return baseURL.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:')
})()
console.log('[API] env:', 'baseURL:', baseURL, 'ws:', WS_BASE_URL, 'mock:', USE_MOCK_API)
/** 获取 WebSocket 基础地址(环境变量 VITE_WS_BASE_URL未配置时由 API 地址自动推导) */
export async function getWebSocketBaseUrl() {
return WS_BASE_URL
}
/** 抠图专用 Gateway业务走团队库segment 走本机;如需分离可再加 VITE_SEGMENT_BASE_URL */
export function getSegmentApiBaseUrl() {
return baseURL
}
/** 镭射专用 GatewayAI 生成 + compositor 走本地,其他业务走远程;如需分离可再加 VITE_LASER_BASE_URL */
export function getLaserApiBaseUrl() {
return baseURL
}
// 模拟网络延迟
function mockDelay(ms = 800) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// 请求封装
export function request(options) {
let _aborted = false
let requestTask = null
const p = new Promise((resolve, reject) => {
// 构建请求头
const headers = {
'Content-Type': 'application/json',
// 风控限流spec §9.1 v2.3):设备指纹维度
// 详见 utils/deviceFingerprint.js —— 跨重启/跨账号稳定
'X-Device-Fingerprint': getDeviceFingerprint(),
...options.header
}
// 判断是否为登录或注册接口
const isAuthRequest = options.url.includes('/api/v1/auth/login') || options.url.includes(
'/api/v1/auth/register') || options.url.includes('/api/v1/auth/send-code') || options.url.includes('/api/v1/auth/verify-code')
// 如果不是登录/注册接口则自动添加JWT token
if (!isAuthRequest) {
const token = uni.getStorageSync('access_token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
requestTask = uni.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: headers,
timeout: 60000,
success: (res) => {
// 处理 token 过期HTTP 401 — transport error,网关层真鉴权失败)
if (res.statusCode === 401) {
// 清除本地存储的token和用户信息
uni.removeStorageSync('access_token')
uni.removeStorageSync('user')
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/portal'
})
reject(new Error('登录已过期,请重新登录'))
return
}
// 可以在这里统一处理响应
if (res.statusCode === 200 || res.statusCode === 202) {
// 检查业务状态码
if (res.data && res.data.code !== undefined) {
if (res.data.code === 0) {
resolve(res.data)
} else if (res.data.code === 16 || res.data.code === 7) {
// 业务状态码 401(token 失效) / 403(账号被封)→ 清缓存 + 跳登录页
// 注:400 是业务校验错误(密码错、参数错、verify_token 错),不登出,reject 让调用方 toast
uni.removeStorageSync('access_token')
uni.removeStorageSync('user')
// 保留错误消息用于显示
const errorMsg = res.data.message || '登录已过期,请重新登录'
uni.reLaunch({
url: '/pages/login/portal?error=' + encodeURIComponent(
errorMsg)
})
const authErr = new Error(errorMsg)
authErr.code = res.data.code
reject(authErr)
return
} else {
// 其他业务错误,返回包含 message 的错误
const bizErr = new Error(res.data.message || '请求失败')
bizErr.code = res.data.code
reject(bizErr)
}
} else {
resolve(res.data)
}
} else {
// HTTP 错误,尝试从响应中获取 message
const errorMessage = res.data?.message || `请求失败 (${res.statusCode})`
const httpErr = new Error(errorMessage)
if (res.data?.code !== undefined) httpErr.code = res.data.code
reject(httpErr)
}
},
fail: (err) => {
if (_aborted) return
reject(new Error(err.errMsg || '网络请求失败'))
}
})
})
p.abort = () => {
_aborted = true
if (requestTask) requestTask.abort()
}
return p
}
// 登录接口
export function loginApi(mobile, password) {
return request({
url: '/api/v1/auth/login',
method: 'POST',
data: {
mobile,
password
}
})
}
// 检查昵称是否已被注册
export function checkNicknameApi(nickname) {
return request({
url: '/api/v1/auth/check-nickname',
method: 'POST',
data: {
nickname
}
})
}
// 检查手机号是否已被注册
export function checkmobileApi(mobile) {
return request({
url: '/api/v1/auth/check-mobile',
method: 'POST',
data: {
mobile
}
})
}
// 注册接口
export function registerApi(mobile, password, star_id, nickname, verify_token = '', avatar_url = '') {
return request({
url: '/api/v1/auth/register',
method: 'POST',
data: {
mobile,
password,
star_id,
nickname,
verify_token,
avatar_url
}
})
}
// 发送验证码
export function sendCodeApi(mobile, scene = 'register') {
return request({
url: '/api/v1/auth/send-code',
method: 'POST',
data: {
mobile,
scene
}
});
}
// 验证验证码
export function verifyCodeApi(mobile, code, scene = 'register') {
return request({
url: '/api/v1/auth/verify-code',
method: 'POST',
data: {
mobile,
code,
scene
}
});
}
// 更新用户信息接口
export function updateUserInfoApi(nickname) {
return request({
url: '/api/user/update',
method: 'POST',
data: {
nickname
}
})
}
// 获取随机用户列表(广场页面使用)
export function getRandomUsersApi(page = 1, pageSize) {
return request({
url: `/api/v1/social/users?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 好友列表接口
export function friendListApi(page = 1, pageSize = 10) {
return request({
url: `/api/v1/social/friends?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取用户信息接口从缓存或API
export function getUserProfileApi() {
return request({
url: '/api/v1/auth/me',
method: 'GET'
})
}
// 修改昵称接口
export function updateNicknameApi(nickname) {
return request({
url: '/api/v1/me/nickname',
method: 'PUT',
data: {
nickname
}
})
}
// 模拟注销账号接口
async function mockDeleteAccountApi() {
await mockDelay(600) // 模拟网络延迟
return {
code: 0,
message: '账号注销成功',
data: null
}
}
// 注销账号接口
export function deleteAccountApi() {
if (USE_MOCK_API) {
return mockDeleteAccountApi()
}
return request({
url: '/api/v1/user/delete-account',
method: 'POST'
})
}
// 修改密码接口(Plan A: 需带 verify_token,scene=password 下发的一次性 token)
export function updatePasswordApi(oldPassword, newPassword, verifyToken) {
return request({
url: '/api/v1/account/password',
method: 'POST',
data: {
old_password: oldPassword,
new_password: newPassword,
verify_token: verifyToken
}
})
}
// 忘记密码场景 - 匿名重置密码接口
// 参数:mobile 手机号 / newPassword 新密码 / verifyToken scene=password 下发的一次性 token
// 注意:无需 access_token(后端不挂 AuthMiddleware)
export function resetPasswordApi(mobile, newPassword, verifyToken) {
return request({
url: '/api/v1/auth/reset-password',
method: 'POST',
data: {
mobile,
new_password: newPassword,
verify_token: verifyToken
}
})
}
// 搜索用户
export function searchUserApi(friendUserId) {
return request({
url: `/api/v1/social/search-user?friend_fan_profile_id=${friendUserId}`,
method: 'GET'
})
}
// 获取已发送的好友请求
export function getSentFriendRequestsApi(page = 1, pageSize = 10) {
return request({
url: `/api/v1/social/friend-requests?type=sent&status=pending&page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 发送好友请求(添加好友)
export function sendFriendRequestApi(friendUserId) {
return request({
url: '/api/v1/social/friend-requests',
method: 'POST',
data: {
friend_user_id: friendUserId
}
})
}
// 获取收到的好友请求
export function getReceivedFriendRequestsApi(page = 1, pageSize = 10) {
return request({
url: `/api/v1/social/friend-requests?type=received&status=pending&page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 处理好友请求(接受/拒绝)
export function handleFriendRequestApi(requestId, action) {
return request({
url: '/api/v1/social/friend-requests/handle',
method: 'POST',
data: {
request_id: requestId,
action: action
}
})
}
// 删除好友
export function deleteFriendApi(friendUserId) {
return request({
url: '/api/v1/social/friends',
method: 'DELETE',
data: {
friend_user_id: friendUserId
}
})
}
// 获取可选的粉丝身份列表
export function getFanIdentitiesApi() {
return request({
url: '/api/v1/fan-identities',
method: 'GET'
})
}
// 添加新的粉丝身份
export function addFanIdentityApi(starId, nickname) {
return request({
url: '/api/v1/my/fan-identities',
method: 'POST',
data: {
star_id: starId,
nickname: nickname
}
})
}
// 获取用户当前拥有的所有粉丝身份
export function getMyFanIdentitiesApi() {
return request({
url: '/api/v1/my/fan-identities',
method: 'GET'
})
}
// 切换粉丝身份
export function switchFanIdentityApi(newStarId) {
return request({
url: '/api/v1/my/fan-identities/switch',
method: 'POST',
data: {
new_star_id: newStarId
}
})
}
// 获取用户藏品列表
export function getMyAssetsApi(page = 1, pageSize = 20) {
return request({
url: `/api/v1/assets/me/items?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取藏品详情
export function getAssetDetailApi(assetId) {
return request({
url: `/api/v1/assets/${assetId}`,
method: 'GET'
})
}
// 点赞藏品
export function likeAssetApi(assetId) {
return request({
url: `/api/v1/social/assets/${assetId}/like`,
method: 'POST'
})
}
// 取消点赞藏品
export function unlikeAssetApi(assetId) {
return request({
url: `/api/v1/social/assets/${assetId}/like`,
method: 'DELETE'
})
}
// 获取资产点赞用户列表
export function getAssetLikersApi(assetId, pageSize = 6, cursor = 0) {
return request({
url: `/api/v1/social/assets/${assetId}/likers?page_size=${pageSize}&cursor=${cursor}`,
method: 'GET'
})
}
// 获取我的展馆槽位信息
export function getMyGalleriesApi() {
return request({
url: '/api/v1/mygalleries',
method: 'GET'
})
}
// 获取指定用户的展馆槽位信息
export function getUserGalleriesApi(targetUid) {
return request({
url: `/api/v1/galleries/${targetUid}`,
method: 'GET'
})
}
// 获取随机用户的展馆槽位信息
export function getRandomGalleryApi() {
return request({
url: '/api/v1/galleries/random',
method: 'GET'
})
}
// 放置藏品到展馆
export function placeAssetToGalleryApi(assetId, galleryOwnerId, slotId) {
return request({
url: '/api/v1/galleries/place',
method: 'POST',
data: {
asset_id: assetId,
gallery_owner_id: galleryOwnerId,
slot_id: slotId
}
})
}
// 下架藏品
export function removeAssetFromGalleryApi(slotId) {
return request({
url: `/api/v1/galleries/slots/${slotId}/asset`,
method: 'DELETE'
})
}
/**
* 获取 OSS 浏览器直传凭证PostObject 表单字段)
* 对应网关GET /api/v1/assets/oss/upload-signature与 /oss/signature 同一处理逻辑,此处走阶段一推荐路径)
* @param {'avatar'|'asset'|'report'|'feedback'} [type='asset'] 上传目录类型
* - avatar → avatar/(头像注册/编辑场景)
* - asset → asset/<uid>/<sid>/(铸造素材)
* - report → report/(举报证据命名空间,与 asset 隔离)
* - feedback → feedback/(反馈截图命名空间,与 asset 隔离)
* @param {string} [orderId] 可选;仅 asset 类型会用于铸造,后续 POST /api/v1/assets/mints 使用同一 order_id后端 InitMintOrder
*/
export function getOssSignatureApi(type = 'asset', orderId = '') {
let url = `/api/v1/assets/oss/upload-signature?type=${encodeURIComponent(type)}`
if (orderId) {
url += `&order_id=${encodeURIComponent(orderId)}`
}
return request({
url,
method: 'GET'
})
}
/** 兼容旧调用名:与 getOssSignatureApi 相同 */
export const getOssUploadSignatureApi = getOssSignatureApi
/**
* 获取公开 OSS 上传签名(注册等未登录场景用)
* 对应网关GET /api/v1/public/oss/upload-signature
* @param {string} scene 场景,目前固定为 'register'
* @param {string} key 命名空间,注册场景下传 mobile用于服务端限定上传目录
*/
export function getPublicOssSignatureApi(scene, key) {
return request({
url: `/api/v1/public/oss/upload-signature?scene=${encodeURIComponent(scene)}&key=${encodeURIComponent(key)}`,
method: 'GET'
})
}
/**
* 获取 OSS 预签名 GET URL私有桶读图
* 对应网关GET /api/v1/assets/oss/presigned-url
* @param {string} fileName 文件名 / OSS key / 完整 OSS URL
* @param {number} [expires=3600] 过期时间(秒),最大 86400
* @param {'avatar'|'asset'|'report'|'feedback'} [type='avatar'] 命名空间类型
*/
export function getOssPresignedUrlApi(fileName, expires = 3600, type = 'avatar') {
return request({
url: `/api/v1/assets/oss/presigned-url?file_name=${encodeURIComponent(fileName)}&expires=${expires}&type=${type}`,
method: 'GET'
})
}
/**
* 批量预签名读 URL
* 对应网关GET /api/v1/assets/oss/batch-presigned-urls
*/
export function getBatchOssPresignedUrlsApi(files, expires = 3600, type = 'asset') {
return request({
url: `/api/v1/assets/oss/batch-presigned-urls?files=${encodeURIComponent(JSON.stringify(files))}&expires=${expires}&type=${type}`,
method: 'GET'
})
}
/**
* 使用网关下发的 Post 策略将本地临时文件直传 OSSApp / 小程序等)
* @param {string} filePath uni.chooseImage 等返回的 tempFilePaths 项
* @param {{ type?: 'avatar'|'asset'|'report'|'feedback', orderId?: string, fileName?: string }} [options]
* type 与 getOssSignatureApi 一致report/feedback 用于举报证据 / 反馈截图
* @returns {Promise<{ imageUrl: string, orderId?: string, data: object }>}
*/
export function uploadLocalFileToOss(filePath, options = {}) {
const {
type = 'asset', orderId = '', fileName
} = options
const objectName = fileName || `${Date.now()}.jpg`
return new Promise((resolve, reject) => {
getOssSignatureApi(type, orderId)
.then((signRes) => {
if (!signRes || signRes.code !== 0 || !signRes.data) {
reject(new Error((signRes && signRes.message) || '获取 OSS 签名失败'))
return
}
const d = signRes.data
const host = d.host
const dir = d.dir || ''
if (!host) {
reject(new Error('签名数据缺少 host'))
return
}
uni.uploadFile({
url: host,
filePath,
name: 'file',
formData: {
key: dir + objectName,
policy: d.policy,
success_action_status: '200',
'x-oss-credential': d.x_oss_credential,
'x-oss-date': d.x_oss_date,
'x-oss-security-token': d.security_token,
'x-oss-signature': d.signature,
'x-oss-signature-version': d.x_oss_signature_version
},
success: (uploadRes) => {
const ok = uploadRes.statusCode === 200 || uploadRes.statusCode === 204
if (!ok) {
reject(new Error(`OSS 上传失败 HTTP ${uploadRes.statusCode}`))
return
}
const imageUrl = `${host.replace(/\/+$/, '')}/${dir}${objectName}`
resolve({
imageUrl,
orderId: d.order_id,
data: d
})
},
fail: (e) => {
reject(new Error(e.errMsg || 'OSS 上传失败'))
}
})
})
.catch(reject)
})
}
// 更新用户头像
export function updateAvatarApi(avatarUrl) {
return request({
url: '/api/v1/me/avatar',
method: 'PUT',
data: {
avatar_url: avatarUrl
}
})
}
// 创建铸造订单
export function createMintOrderApi(orderData) {
return request({
url: '/api/v1/assets/mints',
method: 'POST',
data: orderData
})
}
// 删除铸造订单
export function deleteMintOrderApi(orderId) {
return request({
url: `/api/v1/assets/mints/${orderId}`,
method: 'DELETE'
})
}
// 获取铸造订单详情
export function getMintOrderDetailApi(orderId) {
return request({
url: `/api/v1/assets/mints/${orderId}`,
method: 'GET'
})
}
// 估算铸造费用(显示消耗和余额)
export function estimateMintCostApi() {
return request({
url: '/api/v1/assets/mints/cost-estimate',
method: 'POST'
})
}
// 上传素材(返回 material_id
export function uploadMaterialApi(data) {
return request({
url: '/api/v1/assets/materials/upload',
method: 'POST',
data
})
}
// 绑定资产素材关联
export function bindAssetMaterialsApi(assetId, materials) {
return request({
url: `/api/v1/assets/${assetId}/materials`,
method: 'POST',
data: { materials }
})
}
// 获取资产素材列表
export function getAssetMaterialsApi(assetId) {
return request({
url: `/api/v1/assets/${assetId}/materials`,
method: 'GET'
})
}
/**
* 解析抠图接口 JSON 响应
*/
function parseSegmentPortraitResponse(statusCode, rawBody) {
let body = rawBody
try {
body = typeof body === 'string' ? JSON.parse(body) : body
} catch (e) {
if (statusCode === 404) {
throw new Error(
'抠图接口 404团队 Gateway 暂无 /segment请在本机执行 start-all.ps1 并确认 SEGMENT_BASE 指向 8081'
)
}
throw new Error('抠图接口返回非 JSON')
}
if (statusCode === 401) {
throw new Error('登录已过期')
}
if (statusCode < 200 || statusCode >= 300) {
const msg = body?.message || `抠图失败 HTTP ${statusCode}`
throw new Error(msg)
}
if (body?.code !== undefined && body.code !== 0) {
throw new Error(body.message || '抠图失败')
}
return body
}
// #ifdef H5
function isH5SpecialImagePath(path) {
return /^blob:/i.test(path) || /^data:image\//i.test(path)
}
/**
* H5blob/data URL 无法用 uni.uploadFile改 fetch + FormData
* @param {string} filePath
* @param {string} [imageBase64]
*/
async function segmentPortraitApiH5(filePath, imageBase64) {
const token = uni.getStorageSync('access_token')
const source = (imageBase64 && String(imageBase64).startsWith('data:'))
? String(imageBase64)
: filePath
const res = await fetch(source)
const blob = await res.blob()
const fd = new FormData()
const ext = (blob.type || '').includes('png') ? 'png' : 'jpg'
fd.append('image', blob, `upload.${ext}`)
fd.append('scene', 'portrait')
const headers = {}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const uploadRes = await fetch(`${getSegmentApiBaseUrl()}/api/v1/segment`, {
method: 'POST',
headers,
body: fd,
})
const text = await uploadRes.text()
return parseSegmentPortraitResponse(uploadRes.status, text)
}
// #endif
/**
* 人像抠图multipart— POST /api/v1/segment?scene=portrait
* @param {string} filePath 本地图片路径
* @param {{ imageBase64?: string }} [options] H5 可传 data URL 作兜底
* @returns {Promise<{ code: number, data: { success: boolean, cutout_oss_key?: string, cutout_url_signed?: string, error_code?: string, message?: string } }>}
*/
export function segmentPortraitApi(filePath, options = {}) {
const path = String(filePath || '').trim()
const imageBase64 = options.imageBase64 || ''
// #ifdef H5
if (isH5SpecialImagePath(path) || (imageBase64 && imageBase64.startsWith('data:'))) {
return segmentPortraitApiH5(path, imageBase64)
}
// #endif
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('access_token')
uni.uploadFile({
url: `${getSegmentApiBaseUrl()}/api/v1/segment`,
filePath: path,
name: 'image',
formData: { scene: 'portrait' },
timeout: 120000,
header: token ? { Authorization: `Bearer ${token}` } : {},
success: (res) => {
try {
resolve(parseSegmentPortraitResponse(res.statusCode, res.data))
} catch (e) {
reject(e)
}
},
fail: (err) => reject(new Error(err?.errMsg || '抠图请求失败')),
})
})
}
// 图生图
export function imageGenerationApi(params) {
return request({
url: '/api/v1/assets/mints/image/generation',
method: 'POST',
data: params
})
}
// 获取热度排行榜
export function getHotRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) {
let url = `/api/v1/rankings/hot?dimension=${dimension}&page=${page}&page_size=${pageSize}`
if (starId) {
url += `&star_id=${starId}`
}
return request({
url: url,
method: 'GET'
})
}
// 获取自制排行榜
export function getOriginalRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) {
let url = `/api/v1/rankings/original?dimension=${dimension}&page=${page}&page_size=${pageSize}`
if (starId) {
url += `&star_id=${starId}`
}
return request({
url: url,
method: 'GET'
})
}
// ==================== 活动相关接口 ====================
// 获取活动列表
export function getActivityListApi(starId, page = 1, pageSize = 10) {
let url = `/api/v1/activities?star_id=${starId}&page=${page}&page_size=${pageSize}`
// if (status) {
// url += `&status=${status}`
// }
return request({
url: url,
method: 'GET'
})
}
// 获取活动详情
export function getActivityDetailApi(activityId) {
return request({
url: `/api/v1/activities/${activityId}`,
method: 'GET'
})
}
// 获取活动道具列表
export function getActivityItemsApi(activityId) {
return request({
url: `/api/v1/activities/${activityId}/items`,
method: 'GET'
})
}
// 获取活动进度
export function getActivityProgressApi(activityId) {
return request({
url: `/api/v1/activities/${activityId}/progress`,
method: 'GET'
})
}
// 购买活动道具
export function purchaseActivityItemApi(activityId, itemType, quantity = 1) {
return request({
url: `/api/v1/activities/${activityId}/purchase`,
method: 'POST',
data: {
item_type: itemType,
quantity: quantity
}
})
}
// 批量购买活动道具
export function batchPurchaseActivityItemsApi(activityId, items) {
return request({
url: `/api/v1/activities/${activityId}/batch-purchase`,
method: 'POST',
data: {
items: items
}
})
}
// 获取活动最新贡献记录(实时轮询用)
export function getActivityContributionsLatestApi(activityId, sinceTimestamp = 0, sinceId = 0, limit = 5) {
return request({
url: `/api/v1/activities/${activityId}/contributions/latest?since_timestamp=${sinceTimestamp}&since_id=${sinceId}&limit=${limit}`,
method: 'GET'
})
}
// 列出活动留言(首次/下拉加载)
export function listActivityMessagesApi(activityId, page = 1, pageSize = 20) {
return request({
url: `/api/v1/activities/${activityId}/messages?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 发送一条活动留言
export function createActivityMessageApi(activityId, content) {
return request({
url: `/api/v1/activities/${activityId}/messages`,
method: 'POST',
data: { content }
})
}
// 获取活动贡献点排名
export function getActivityRankingApi(activityId, starId = null, page = 1, pageSize = 10) {
let url = `/api/v1/activities/${activityId}/ranking?page=${page}&page_size=${pageSize}`
if (starId) {
url += `&star_id=${starId}`
}
return request({
url: url,
method: 'GET'
})
}
// 获取活动 TOP3 + 我的排名专用轻量接口TopRanking.vue 使用)
export function getActivityTopRankingApi(activityId, starId = null) {
let url = `/api/v1/activities/${activityId}/top-ranking`
if (starId) {
url += `?star_id=${starId}`
}
return request({
url: url,
method: 'GET'
})
}
// ==================== 星册相关接口 ====================
// 获取星册首页数据
export function getStarbookHomeApi() {
return request({
url: '/api/v1/starbook/home',
method: 'GET'
})
}
// 获取星册藏品列表(分页)
export function getStarbookItemsApi(type, category, grade = null, page = 1, pageSize = 20) {
let url = `/api/v1/starbook/items?type=${type}&category=${category}&page=${page}&page_size=${pageSize}`
if (grade !== null) {
url += `&grade=${grade}`
}
return request({
url: url,
method: 'GET'
})
}
// ==================== 他人作品统计接口 ====================
// 获取他人点赞的作品列表
export function getUserLikedAssetsApi(userId, page = 1, pageSize = 20) {
return request({
url: `/api/v1/users/${userId}/liked-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取他人展出的作品列表
export function getUserExhibitedAssetsApi(userId, page = 1, pageSize = 20) {
return request({
url: `/api/v1/users/${userId}/exhibited-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// ==================== 我的作品统计接口 ====================
// 获取我展出的作品列表
export function getMyExhibitedAssetsApi(page = 1, pageSize = 20) {
return request({
url: `/api/v1/me/exhibited-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取我点赞的作品列表
export function getMyLikedAssetsApi(page = 1, pageSize = 20) {
return request({
url: `/api/v1/me/liked-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取我今日点赞的作品列表
export function getMyTodayLikedAssetsApi(page = 1, pageSize = 20) {
return request({
url: `/api/v1/me/today-liked-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// 获取我本周点赞的作品列表
export function getMyWeekLikedAssetsApi(page = 1, pageSize = 20) {
return request({
url: `/api/v1/me/week-liked-assets?page=${page}&page_size=${pageSize}`,
method: 'GET'
})
}
// ==================== 点赞押注收益 ====================
/**
* 获取点赞押注收益列表GET /api/v1/tasks/like-bet-revenue
* @param {number} starId - 粉丝身份ID
* @param {string} [status] - 状态筛选claimable/claimed可选
* @param {number} [page=1] - 页码
* @param {number} [pageSize=10] - 每页条数
* @returns {Promise}
*/
export const getLikeBetRevenue = (starId, status, page = 1, pageSize = 10) =>
request({
url: '/api/v1/tasks/like-bet-revenue',
data: { star_id: starId, status, page, page_size: pageSize }
})
/**
* 领取单条点赞押注收益POST /api/v1/tasks/like-bet-revenue/claim
* @param {number} revenueId - 收益记录ID
* @param {number} starId - 粉丝身份ID
* @returns {Promise}
*/
export const claimLikeBetRevenue = (revenueId, starId) =>
request({
url: '/api/v1/tasks/like-bet-revenue/claim',
method: 'POST',
data: { revenue_id: revenueId, star_id: starId }
})
/**
* 一键领取所有可领取点赞押注收益POST /api/v1/tasks/like-bet-revenue/claim-all
* @param {number} starId - 粉丝身份ID
* @returns {Promise}
*/
export const claimAllLikeBetRevenue = (starId) =>
request({
url: '/api/v1/tasks/like-bet-revenue/claim-all',
method: 'POST',
data: { star_id: starId }
})
// ==================== 灵感瀑布相关接口 ====================
// 获取灵感瀑布藏品列表
export function getInspirationFlowApi(params) {
return request({
url: '/api/v1/inspiration-flow',
method: 'GET',
data: params
})
}
// ==================== 铸造活动相关接口运营banner====================
// 获取铸造活动列表
export function getMintingActivitiesApi(starId = null, page = 1, pageSize = 10) {
let url = `/api/v1/minting-activities?page=${page}&page_size=${pageSize}`
if (starId) {
url += `&star_id=${starId}`
}
return request({
url: url,
method: 'GET'
})
}
// 获取收益汇总接口
export function getEarningsSummaryApi() {
return request({
url: '/api/v1/assets/me/earnings-summary',
method: 'GET'
})
}
// ==================== 数据看板 ====================
import { mockRouter } from './mock/dashboard'
const DASHBOARD_PREFIX = '/api/v1/dashboard'
// mock 触发器:当 USE_MOCK_API 为 true 时短路返回 mock 数据
// 后端就绪后将 USE_MOCK_API 改为 false 即可
async function dashboardRequest(endpoint, params = {}) {
if (USE_MOCK_API) {
const factory = mockRouter[`${DASHBOARD_PREFIX}${endpoint}`]
if (factory) return factory(params)
}
return request({ url: `${DASHBOARD_PREFIX}${endpoint}`, method: 'GET', data: params })
}
export const dashboardApi = {
getTodayOverview: (starId) => dashboardRequest('/today-overview', { star_id: starId }),
get7DayIncomeCurve: (starId) => dashboardRequest('/income-curve', { star_id: starId }),
getExhibitionSummary: (starId) => dashboardRequest('/exhibition-summary', { star_id: starId }),
getLikeIncomeByLevel: (starId) => dashboardRequest('/like-income-by-level', { star_id: starId }),
getTopAssets: (starId) => dashboardRequest('/top-assets', { star_id: starId }),
getLevelDistribution: (starId) => dashboardRequest('/level-distribution', { star_id: starId }),
getUpgradeProgress: (starId) => dashboardRequest('/upgrade-progress', { star_id: starId }),
}
// ==================== 铸爱工艺配置 ====================
// 鉴权:由 gateway AuthMiddleware 强制 JWT
// 响应:{ categories: [{ id, name, type_key, sort_order, crafts: [{ id, name, image_url, route_path, route_params, sort_order }] }], version }
// 缓存:后端 5s 进程内缓存,运营改完最长 5s 端上生效
export function getCastloveConfigApi() {
return request({
url: '/api/v1/castlove/config',
method: 'GET'
})
}
// ==================== 推送设备注册 ====================
// App 端启动后调用:把 uni.getPushClientId() 拿到的 cid 上报给后端。
// 后端写入 user_devices 表(按 cid upsert),后续 CreateNotification 会按 user_id 查这张表推送。
//
// registerDeviceApi:
// - cid:必填,uni.getPushClientId success 的 res.cid
// - platform:'ios' | 'android' | 'harmony',来自 plus.os.name / uni.getSystemInfoSync()
// - app_version / device_model:可选,便于排查
//
// unregisterDeviceApi:
// - cid:可选;传空字符串表示注销当前用户全部设备(主动登出场景)
export function registerDeviceApi(data) {
return request({
url: '/api/v1/notifications/devices',
method: 'POST',
data: {
cid: data.cid,
platform: data.platform || '',
app_version: data.appVersion || '',
device_model: data.deviceModel || ''
}
})
}
export function unregisterDeviceApi(cid = '') {
return request({
url: '/api/v1/notifications/devices/unregister',
method: 'POST',
data: { cid: cid || '' }
})
}
// ==================== 通知查询/操作spec §1.3 + 收件箱聚合 §5.4====================
// 本节 6 个 wrapper 为本期新增。前端原本只暴露了 registerDeviceApi /
// unregisterDeviceApi 两个推送设备接口;通知本身 list/unread/markRead/markAllRead/
// delete/clear 在本期补齐(详见 docs/superpowers/specs/2026-07-13-mailbox-inbox-design.md §5.4)。
//
// 鉴权由 gateway AuthMiddleware 强制 JWT,前端无需再校验。
// 响应统一: { code, message, data },data 字段才是真正的 payload。
// getNotificationsApi —— 拉一页通知(列表页 onShow 并发 4 个 type / 详情页单补拉)。
//
// 参数:
// type :必填,枚举 activity|feedback_replied|report_resolved|target_reported|like|system
// tab :可选,二级过滤(本期不传)。
// page :默认 1,后端兜底。
// pageSize:默认 20,后端上限 100。
//
// 业务流:列表页 onShow Promise.all(4 type) + 详情页 fallback 补拉 50 条找 nid。
//
// 注意:GET 必须走查询串(对齐 utils/api.js 现有 GET 约定);
// gateway controller 用 g.Query("type") 读 type,data:{} 不会被解。
export function getNotificationsApi(params = {}) {
const { type, tab = '', page = 1, pageSize = 20 } = params || {}
if (!type) {
// 必须在 useMailboxCenter loadAll 调用前 assert (无 catch 也显眼)
throw new Error('[getNotificationsApi] type is required')
}
// ⚠️ V1.2.5 修正:不用 URLSearchParams (uni-app dev runtime 在 template literal
// 隐式 toString 抛错,见 .superpowers/sdd/inbox-runtime-fix-report.md)
// 用模板字符串 + encodeURIComponent 直接拼。
let url = `/api/v1/notifications?type=${encodeURIComponent(type)}` +
`&page=${page}&page_size=${pageSize}`
if (tab) url += `&tab=${encodeURIComponent(tab)}`
return request({ url, method: 'GET' })
}
// getUnreadCountApi —— 拉全局未读计数。
// 主路径不调用(push receive 事件覆盖),仅作 list onShow 兜底对齐用。
//
// 返回:Promise<{ data: { like, system, activity, total, feedback_replied, report_resolved, target_reported } }>
export function getUnreadCountApi() {
return request({ url: '/api/v1/notifications/unread-count', method: 'GET' })
}
// markAsReadApi —— 单条标已读(列表项点击 / 详情页 [✓标已读])。
//
// 参数:id 必填,notifications.id。
//
// 后端事务:① UPDATE notifications SET is_read=TRUE WHERE is_read=FALSE(幂等)。
//
// 业务流:列表项点击后 dispatch 再 navigateTo 详情页;详情页标已读后留在原页(可能再看一眼)。
export function markAsReadApi(id) {
return request({ url: `/api/v1/notifications/${id}/read`, method: 'POST' })
}
// markAllAsReadApi —— 按 type 全标已读(列表页顶部 [+全部已读] 并发 4 次)。
//
// 参数:type 必填,枚举同 getNotificationsApi。
//
// 后端 controller 用 g.Query("type") 提取,走查询串;data:{type} 不行会 400。
export function markAllAsReadApi(type) {
return request({ url: `/api/v1/notifications/read-all?type=${encodeURIComponent(type)}`, method: 'POST' })
}
// deleteNotificationApi —— 单条软删(列表项长按 / 明信片页 [🗑删除])。
//
// 业务流:列表项长按 → 本地 store REMOVE_ITEM;明信片页 → 软删 + setTimeout 500ms navigateBack。
//
// 注意:后端 is_deleted=TRUE 软删,redDot 不受影响(只 decr 标已读数)。
export function deleteNotificationApi(id) {
return request({ url: `/api/v1/notifications/${id}`, method: 'DELETE' })
}
// clearNotificationsApi —— 按 type 软删(本期仅传 'all',清 4 类)。
//
// 参数:type 枚举 activity|feedback_replied|report_resolved|target_reported|all。
// 后端:DELETE /api/v1/notifications/clear?type=<x>(spec §4 新加接口)。
export function clearNotificationsApi(type = 'all') {
return request({ url: `/api/v1/notifications/clear?type=${encodeURIComponent(type)}`, method: 'DELETE' })
}
// ==================== 举报反馈spec §5.1 客户端 API====================
export function getReportCategoriesApi() {
return request({
url: '/api/v1/moderation/report-categories',
method: 'GET'
})
}
// spec §7 举报业务错误码50001-50019→ 友好提示
// 集中维护,新增/调整时只改这里即可
const REPORT_ERROR_MSG = {
50001: '举报分类不存在或已停用',
50002: '反馈分类不存在或已停用',
50003: '目标不存在或已下架',
50004: '您已举报过该对象',
50005: '描述过长(最多 500 字)',
50006: '证据图超限(最多 5 张)',
50007: '工单不存在',
50008: '工单已被他人认领',
50009: '工单已结案,无法再操作',
50011: '不能举报自己',
50012: '今日提交过于频繁,请稍后再试',
50014: '工单状态不允许此操作',
50016: '举报对象类型不支持',
50017: '请填写处理原因',
50018: '证据图格式或大小不合法',
50019: '当前状态不可编辑该举报'
}
export async function submitReportApi(payload) {
try {
return await request({
url: '/api/v1/moderation/reports',
method: 'POST',
data: {
target_type: payload.target_type,
target_id: payload.target_id,
category_code: payload.category_code,
description: payload.description || '',
is_anonymous: !!payload.is_anonymous,
evidence_keys: payload.evidence_keys || []
}
})
} catch (err) {
if (err && err.code && REPORT_ERROR_MSG[err.code]) {
err.message = REPORT_ERROR_MSG[err.code]
}
throw err
}
}
export async function submitFeedbackApi(payload) {
try {
return await request({
url: '/api/v1/moderation/feedbacks',
method: 'POST',
data: {
star_id: payload.star_id || 0,
category_code: payload.category_code,
title: payload.title,
content: payload.content,
contact: payload.contact || '',
is_anonymous: !!payload.is_anonymous,
evidence_keys: payload.evidence_keys || []
}
})
} catch (err) {
if (err && err.code && REPORT_ERROR_MSG[err.code]) {
err.message = REPORT_ERROR_MSG[err.code]
}
throw err
}
}
export function getMyReportsApi(params = {}) {
return request({
url: '/api/v1/moderation/reports',
method: 'GET',
data: { page: params.page || 1, page_size: params.page_size || 20, status: params.status || '' }
})
}
export function getMyReportDetailApi(id) {
return request({
url: `/api/v1/moderation/reports/${id}`,
method: 'GET'
})
}
export function getFeedbackCategoriesApi() {
return request({
url: '/api/v1/moderation/feedback-categories',
method: 'GET'
})
}
export function getMyFeedbacksApi(params = {}) {
return request({
url: '/api/v1/moderation/feedbacks',
method: 'GET',
data: { page: params.page || 1, page_size: params.page_size || 20, status: params.status || '' }
})
}
export function getMyFeedbackDetailApi(id) {
return request({
url: `/api/v1/moderation/feedbacks/${id}`,
method: 'GET'
})
}
// ==================== 分享相关接口 ====================
// spec: docs/superpowers/specs/2026-06-11-share-modal-redesign-design.md § 3
/**
* 获取分享二维码 URL(GET /api/v1/share/asset-qrcode/:assetId)
* @param {number|string} assetId
* @param {number} sharerUserId 已登录用户 ID(从 storage 取,前端不传 0)
* @param {string} systemType android / ios / mp-weixin / h5 等(uni.getSystemInfo().platform)
* @returns {Promise<{qrcode_url: string, expires_at: number}>}
*/
export function getShareQrcodeApi(assetId, sharerUserId, systemType) {
// 后端期望 int64;前端 props 可能传 string,显式转 number 避免反序列化报错
return request({
url: `/api/v1/share/asset-qrcode/${Number(assetId)}?sharer_user_id=${Number(sharerUserId)}&system_type=${systemType}`,
method: 'GET'
}).then(res => res?.data || {})
}
/**
* 上报分享归因事件(POST /api/v1/share/track)
* @param {Object} payload
* @param {number} payload.asset_id
* @param {number} payload.sharer_user_id
* @param {string} payload.system_type
* @param {string} payload.share_target
* @param {string} payload.result
* @param {number} payload.client_ts
* @param {Object} [payload.extra]
* @returns {Promise<{share_event_id: number}>}
*/
export function trackShareApi(payload) {
return request({
url: '/api/v1/share/track',
method: 'POST',
data: payload
}).then(res => res?.data || {})
}
// ==================== 周边扫描验真 + 加入我的藏品spec: 周边扫描功能)====================
/**
* 按加密 code 验真(stage 1 新版,详见 design §5.1.3/§5.1.5)
*
* URL 形如 /verify/{encrypted_code}?sign={sign},encrypted_code + sign
* 都从扫码 URL 中提取,传给本接口查询验真信息。
*
* @param {string} encryptedCode 32 hex(160 bit),HMAC-SHA256(code)[:32]
* @param {string} sign 32 hex,HMAC-SHA256(encrypted_code)[:32](第二道防线)
* @returns {Promise<VerificationData>}
*/
export function getPeripheralByHashApi(encryptedCode, sign) {
return request({
url: `/api/v1/peripheral-info/by-hash/${encodeURIComponent(encryptedCode)}?sign=${encodeURIComponent(sign)}`,
method: 'GET'
})
}
/**
* 加入我的藏品(stage 1 新版,POST /api/v1/peripheral-info/mint-by-hash/:hash)
*
* @param {string} encryptedCode 32 hex
* @param {string} sign 32 hex
* @returns {Promise<MintData>}
*/
export function mintToMyCollectionByHashApi(encryptedCode, sign) {
return request({
url: `/api/v1/peripheral-info/mint-by-hash/${encodeURIComponent(encryptedCode)}?sign=${encodeURIComponent(sign)}`,
method: 'POST'
})
}