- api.js 的 normalizeUserId 之前是内部函数,apply.vue 具名导入导致 HBuilderX 编译报错,现改为 export - 新增 miniapp-imports.spec.ts:静态校验小程序所有具名导入都存在对应导出,避免再出现只能靠人工编译才发现的问题(108 套件/711 用例全绿)
1508 行
45 KiB
JavaScript
1508 行
45 KiB
JavaScript
// pwuniapp/common/api.js
|
||
// 同城生活陪伴业务API接口层与数据适配器
|
||
|
||
import http from './request.js';
|
||
|
||
/**
|
||
* 格式化活动数据,使其在 MySQL 与前端展示组件间无缝互通
|
||
*/
|
||
export function formatActivityData(raw) {
|
||
if (!raw) return null;
|
||
|
||
const payTypeStr = raw.payType === 2 || raw.payType === 'PROJECT' ? 'PROJECT' : 'AA';
|
||
const totalFee = raw.totalFeeCents != null ? raw.totalFeeCents / 100 : (raw.totalFee || 0);
|
||
const perPerson = raw.perPersonFeeCents != null ? raw.perPersonFeeCents / 100 : (raw.perPersonFee || (payTypeStr === 'AA' ? totalFee / (raw.maxParticipants || 4) : 500));
|
||
const participants = (raw.participants || []).map((p) => ({
|
||
id: p.id,
|
||
userId: p.userId,
|
||
nickname: p.nickname || `搭子_${p.userId || p.id}`,
|
||
avatar: p.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100',
|
||
verifyCode: p.verifyCode || '',
|
||
checkedIn: Boolean(p.checkinStatus || p.checkedIn),
|
||
payAmount: p.payAmountCents != null ? p.payAmountCents / 100 : (p.payAmount || 0),
|
||
}));
|
||
|
||
const joinedCount = raw.joinedCount != null ? raw.joinedCount : participants.length;
|
||
|
||
return {
|
||
id: String(raw.id),
|
||
title: raw.title || '同城生活组局',
|
||
game: raw.gameCategory || raw.game || '美食陪吃',
|
||
track: raw.track || 'OFFLINE_MEAL',
|
||
payType: payTypeStr,
|
||
totalFee,
|
||
perPersonFee: perPerson,
|
||
aaFee: raw.aaFee != null ? raw.aaFee : perPerson,
|
||
maxParticipants: raw.maxParticipants || 4,
|
||
currentParticipants: joinedCount || 1,
|
||
startTime: raw.startTime || '今天 19:30',
|
||
location: raw.location || '北京市朝阳区公开商业街区',
|
||
creatorId: raw.creatorId != null ? Number(raw.creatorId) : (raw.creator_id != null ? Number(raw.creator_id) : 1001),
|
||
creatorNickname: raw.creatorNickname || '同城局长',
|
||
creatorAvatar: raw.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
|
||
status: raw.status || (joinedCount >= (raw.maxParticipants || 4) ? 'FULL' : 'RECRUITING'),
|
||
allowDynamicPrice: Boolean(raw.allowDynamicPrice),
|
||
priceAdjustRule: raw.priceAdjustRule || 'UNIFIED_HIGH',
|
||
participants,
|
||
priceAdjustHistory: raw.priceAdjustHistory || [],
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取组局活动列表 (真实请求 MySQL,异常时返回空列表)
|
||
*/
|
||
export async function getActivities() {
|
||
try {
|
||
const list = await http.get('/activities');
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
return list.map(formatActivityData);
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取后端活动列表异常:', err.message);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 获取指定活动详情 (真实请求 MySQL,不存在或异常时返回 null)
|
||
*/
|
||
export async function getActivityDetail(id) {
|
||
try {
|
||
const detail = await http.get(`/activities/${id}?userId=${normalizeUserId()}`);
|
||
if (detail) {
|
||
return formatActivityData(detail);
|
||
}
|
||
} catch (err) {
|
||
console.warn(`[API] 获取活动 [${id}] 详情异常:`, err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 发起新组局活动 (实时写入 MySQL)
|
||
*/
|
||
export async function createActivity(payload) {
|
||
const body = {
|
||
creatorId: normalizeUserId(payload.creatorId),
|
||
title: payload.title,
|
||
gameCategory: payload.game || payload.gameCategory || '美食陪吃',
|
||
location: payload.location,
|
||
startTime: payload.startTime,
|
||
payType: payload.payType === 'AA' ? 1 : 2,
|
||
totalFeeCents: Math.round((Number(payload.totalFee) || 0) * 100),
|
||
perPersonFeeCents: Math.round((Number(payload.perPersonFee || payload.aaFee) || 0) * 100),
|
||
maxParticipants: Number(payload.maxParticipants) || 4,
|
||
allowDynamicPrice: payload.allowDynamicPrice !== undefined ? (payload.allowDynamicPrice ? 1 : 0) : 1,
|
||
priceAdjustRule: payload.priceAdjustRule || 'UNIFIED_HIGH',
|
||
};
|
||
|
||
try {
|
||
const res = await http.post('/activities', body);
|
||
return formatActivityData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 后端创建活动接口未响应:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 报名活动并获取 4 位唯一核销码
|
||
*/
|
||
export async function joinActivity(activityId, params = {}) {
|
||
const body = {
|
||
userId: normalizeUserId(params.userId),
|
||
payAmountCents: Math.round((Number(params.payAmount) || 50) * 100),
|
||
};
|
||
|
||
try {
|
||
const res = await http.post(`/activities/${activityId}/join`, body);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 报名接口异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 房主/管理员核销 4 位核销码
|
||
*/
|
||
export async function verifyActivityCheckin(activityId, verifyCode, operatorId) {
|
||
return http.post(`/activities/${activityId}/verify`, {
|
||
operatorId: normalizeUserId(operatorId),
|
||
verifyCode,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 房主调整出价/追加金额
|
||
*/
|
||
export async function adjustActivityPrice(activityId, newPriceYuan, operatorId) {
|
||
const newPriceCents = Math.round(Number(newPriceYuan) * 100);
|
||
try {
|
||
const res = await http.post(`/activities/${activityId}/adjust-price`, {
|
||
operatorId: normalizeUserId(operatorId),
|
||
newPriceCents,
|
||
});
|
||
return formatActivityData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 调价请求异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 房主取消/解散组局 (原路退款已报名成员)
|
||
*/
|
||
export async function cancelActivity(activityId, hostId = 1001, reason = '房主主动解散') {
|
||
return http.post(`/activities/${activityId}/cancel`, {
|
||
hostId: normalizeUserId(hostId),
|
||
reason,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 成员主动退出组局 (原路退款)
|
||
*/
|
||
export async function leaveActivity(activityId, userId = 2001) {
|
||
return http.post(`/activities/${activityId}/leave`, {
|
||
userId: normalizeUserId(userId),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取“我的组局”(发起的 + 报名的)
|
||
*/
|
||
export async function getMyActivities(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/users/${uid}/my-activities`);
|
||
if (res) {
|
||
return {
|
||
created: (res.created || []).map(formatActivityData),
|
||
joined: (res.joined || []).map(formatActivityData),
|
||
};
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取我的组局异常:', err.message);
|
||
}
|
||
return { created: [], joined: [] };
|
||
}
|
||
|
||
/**
|
||
* 获取同城大神列表
|
||
*/
|
||
export async function getCompanions(keyword) {
|
||
try {
|
||
const kw = String(keyword == null ? '' : keyword).trim();
|
||
const query = kw ? `?keyword=${encodeURIComponent(kw)}` : '';
|
||
const list = await http.get(`/companions${query}`);
|
||
if (Array.isArray(list)) {
|
||
return list
|
||
.filter((c) => !c.auditStatus || c.auditStatus === '已通过')
|
||
.map((c) => ({
|
||
id: String(c.id),
|
||
nickname: c.name || c.nickname || '同城大神',
|
||
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||
gender: c.gender === '男' ? 'male' : 'female',
|
||
age: Number(c.age) || 22,
|
||
game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.game || '同城陪伴'))),
|
||
track: c.track || 'OFFLINE_MEAL',
|
||
city: '北京市 · 朝阳/海淀',
|
||
rankTag: c.rank || '同城认证陪伴师',
|
||
levelTag: '认证大神',
|
||
price: Number(c.hourlyRate) || 68,
|
||
rating: Number(c.rating) || 5.0,
|
||
orderCount: Number(c.orderCount) || 120,
|
||
isOnline: c.status === '接单中',
|
||
voiceDuration: Number(c.voiceDuration) || 15,
|
||
voiceType: 'voice',
|
||
voiceTitle: c.voiceTitle || '语音招呼',
|
||
bio: '专业同城生活陪伴,阳光健康公开。'
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取大神列表异常:', err.message);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 获取单个大神详情 (真实从 MySQL 读取)
|
||
*/
|
||
export async function getCompanionDetail(id) {
|
||
try {
|
||
const res = await http.get(`/companions/${id}`);
|
||
if (res && res.id) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn(`[API] 获取大神 [${id}] 详情异常:`, err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 格式化订单数据,使 MySQL 表字段与移动端视图无缝映射
|
||
*/
|
||
export function formatOrderData(raw) {
|
||
if (!raw) return null;
|
||
|
||
let statusKey = raw.status || 'IN_SERVICE';
|
||
if (statusKey === '待支付' || statusKey === '待付款') statusKey = 'PENDING_PAY';
|
||
else if (
|
||
statusKey === '待接单' ||
|
||
statusKey === '已接单' ||
|
||
statusKey === '已到场核销' ||
|
||
statusKey === '履约中' ||
|
||
statusKey === '服务中' ||
|
||
statusKey === '待确认'
|
||
) statusKey = 'IN_SERVICE';
|
||
else if (statusKey === '已完成') statusKey = 'COMPLETED';
|
||
else if (
|
||
statusKey === '争议仲裁中' ||
|
||
statusKey === '退款中' ||
|
||
statusKey === '已退款' ||
|
||
statusKey === '已全额退款' ||
|
||
statusKey === '已部分退款'
|
||
) statusKey = 'REFUNDING';
|
||
else if (statusKey === '已取消') statusKey = 'CANCELLED';
|
||
|
||
const companionAvatar = raw.expertAvatar || raw.expert_avatar || raw.avatar ||
|
||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120';
|
||
|
||
const companionNickname = raw.companionName || raw.companion_name || raw.expertNickname || (raw.expert && raw.expert.nickname) || '同城大神';
|
||
const game = raw.track || (raw.expert && raw.expert.game) || '同城陪伴';
|
||
|
||
return {
|
||
id: String(raw.id),
|
||
orderSn: raw.orderNo || raw.order_no || raw.orderSn || String(raw.id),
|
||
expert: {
|
||
id: String(raw.expertId || raw.expert_id || (raw.expert && raw.expert.id) || 'exp_101'),
|
||
nickname: companionNickname,
|
||
avatar: companionAvatar,
|
||
game: game,
|
||
},
|
||
serviceTitle: raw.serviceTitle || raw.service_title || `${game} · 专业陪伴服务`,
|
||
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 2),
|
||
totalAmount: Number(raw.amount || raw.totalAmount || 0),
|
||
refundAmount: Number(raw.refundAmount || raw.refund_amount || 0),
|
||
status: statusKey,
|
||
rawStatus: raw.status || '履约中',
|
||
meetVenue: raw.location || raw.meetVenue || '北京市公开商圈',
|
||
expenseBurden: raw.expenseBurden || raw.expense_burden || 'AA制各自付',
|
||
payType: raw.payType || raw.pay_type || 'WECHAT',
|
||
couponDiscount: Number(raw.couponDiscount || raw.coupon_discount || 0),
|
||
verifyCode: raw.verifyCode || raw.verify_code || '',
|
||
remark: raw.remark || '',
|
||
createdAt: raw.createdAt || raw.created_at || '刚刚'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取订单列表 (真实读取 MySQL)
|
||
*/
|
||
export async function getOrders() {
|
||
try {
|
||
const list = await http.get(`/orders?userId=${normalizeUserId()}`);
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
return list.map(formatOrderData);
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取订单列表异常:', err.message);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 创建新订单 (真实写入 MySQL orders 表)
|
||
*/
|
||
export async function createOrder(payload) {
|
||
try {
|
||
const uid = normalizeUserId(payload && (payload.buyerId || payload.userId));
|
||
const res = await http.post('/orders', { ...payload, buyerId: uid });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 后端下单未响应:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消订单
|
||
*/
|
||
export async function cancelOrder(orderId) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/cancel`, { userId: normalizeUserId() });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 取消订单网络异常:', err.message);
|
||
// 写操作失败必须抛出,供调用方提示并阻止"假成功"
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 支付订单
|
||
*/
|
||
export async function payOrder(orderId, payload = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/pay`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 支付订单网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 大神拒单自动退款 (待接单 -> 已退款)
|
||
*/
|
||
export async function rejectOrder(orderId, payload = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/reject`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 拒单接口网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 大神点击申请完成服务 (服务中 -> 待确认)
|
||
*/
|
||
export async function applyCompleteOrder(orderId, payload = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/apply-complete`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 申请完成服务网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 买家确认验收订单 (待确认 -> 已完成)
|
||
*/
|
||
export async function completeOrder(orderId) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/complete`, { userId: normalizeUserId() });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 确认完成网络异常:', err.message);
|
||
// 写操作失败必须抛出,供调用方提示并阻止"假成功"
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 申请退款/售后仲裁工单 (支持理由、详细说明与图片凭证)
|
||
*/
|
||
export async function refundOrder(orderId, data = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/refund`, { ...data, userId: normalizeUserId(data.userId) });
|
||
return formatOrderData(res);
|
||
} catch (err) {
|
||
console.warn('[API] 申请退款网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提交不良陪玩举报工单 (涉黄、涉赌、线下违规等)
|
||
*/
|
||
export async function submitReport(payload = {}) {
|
||
try {
|
||
const res = await http.post('/reports', { ...payload, reporterId: normalizeUserId(payload.reporterId || payload.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 提交举报网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用4位核销码反查信息 (支持组局活动与同城线下订单)
|
||
*/
|
||
export async function queryCheckinCode(code) {
|
||
try {
|
||
const res = await http.get(`/verify/query/${code}?userId=${normalizeUserId()}`);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 反查核销码失败:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用4位核销码现场核销 (支持组局活动与同城线下订单)
|
||
*/
|
||
export async function universalVerifyCheckin(code, operatorId) {
|
||
try {
|
||
const res = await http.post('/verify/universal', { verifyCode: code, operatorId: normalizeUserId(operatorId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 通用核销接口异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取友圈广场动态列表 (真实读取 MySQL posts 表)
|
||
*/
|
||
export async function getPosts(category, userId) {
|
||
try {
|
||
const uid = userId || uni.getStorageSync('userId') || 1001;
|
||
let query = `?userId=${uid}`;
|
||
if (category && category !== 'all') {
|
||
query += `&category=${encodeURIComponent(category)}`;
|
||
}
|
||
const res = await http.get(`/posts${query}`);
|
||
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
|
||
if (!Array.isArray(localLikedIds)) localLikedIds = [];
|
||
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
|
||
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
|
||
|
||
if (Array.isArray(res) && res.length > 0) {
|
||
return res.map(p => {
|
||
const strId = String(p.id);
|
||
const isLocallyLiked = localLikedIds.includes(strId);
|
||
const isServerLiked = p.liked === true || p.liked === 1;
|
||
const finalLiked = p.liked !== undefined ? isServerLiked : isLocallyLiked;
|
||
const finalViews = Math.max(Number(p.views) || 1, Number(viewsMap[strId]) || 0);
|
||
|
||
return {
|
||
id: p.id,
|
||
userId: p.userId || p.user_id || 0,
|
||
author: p.author || '神秘同好',
|
||
avatar: p.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||
tag: p.tag || '同城认证',
|
||
tagType: p.tagType || 'warm-tag',
|
||
location: p.location || '同城热门商圈',
|
||
distance: p.distance || '1km内',
|
||
time: p.createdAt ? '刚刚' : '近期',
|
||
content: p.content || '',
|
||
topics: Array.isArray(p.topics) ? p.topics : [],
|
||
images: Array.isArray(p.images) ? p.images : [],
|
||
actionText: p.actionText || '聊一聊',
|
||
likes: Number(p.likes) || 0,
|
||
liked: finalLiked || isLocallyLiked,
|
||
comments: Number(p.comments) || 0,
|
||
views: finalViews,
|
||
category: p.category || 'partner',
|
||
applyCount: Number(p.applyCount) || 0,
|
||
pendingApplyCount: Number(p.pendingApplyCount) || 0,
|
||
myApplyStatus: p.myApplyStatus || null
|
||
};
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取友圈动态列表异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 发布新动态 (真实写入 MySQL posts 表)
|
||
*/
|
||
export async function createPost(payload) {
|
||
try {
|
||
const res = await http.post('/posts', { ...(payload || {}), userId: normalizeUserId(payload && payload.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 发布动态异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 动态点赞 (落盘 MySQL)
|
||
*/
|
||
export async function likePost(id, action, userId) {
|
||
try {
|
||
const uid = userId || uni.getStorageSync('userId') || 1001;
|
||
const payload = { userId: uid };
|
||
if (action) {
|
||
payload.action = action;
|
||
}
|
||
const res = await http.post(`/posts/${id}/like`, payload);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 点赞动态异常:', err.message);
|
||
// 点赞是写操作,失败必须向上抛出,避免调用方误以为已落盘
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 动态浏览次数累加 (落盘 MySQL 并同步本地缓存)
|
||
*/
|
||
export async function viewPost(id) {
|
||
try {
|
||
const strId = String(id);
|
||
const res = await http.post(`/posts/${id}/view`, {});
|
||
if (res && res.views !== undefined) {
|
||
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
|
||
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
|
||
viewsMap[strId] = Math.max(Number(viewsMap[strId] || 0), Number(res.views));
|
||
uni.setStorageSync('COMMUNITY_POST_VIEWS_MAP', viewsMap);
|
||
}
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 累加动态浏览次数异常:', err.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 规范化用户ID:登录态以服务端 token 为准(本地缺失时返回 0)
|
||
*/
|
||
export function normalizeUserId(uid) {
|
||
let target = uid;
|
||
if (!target && target !== 0) {
|
||
try {
|
||
target = uni.getStorageSync('userId');
|
||
} catch (_) {}
|
||
}
|
||
const n = Number(target);
|
||
const valid = !isNaN(n) && n > 0;
|
||
// 已登录(存在会话 token)时身份以服务端 token 为准:若本地 userId 缺失则传 0,
|
||
// 避免"旧 token 属主 + 声明的 userId"不一致被服务端判定为以他人身份操作(403)。
|
||
try {
|
||
const token = uni.getStorageSync('userToken');
|
||
if (token) return valid ? n : 0;
|
||
} catch (_) {}
|
||
return valid ? n : 1001;
|
||
}
|
||
|
||
/**
|
||
* 获取用户个人资料与钱包资产 (真实读取 MySQL users 表)
|
||
*/
|
||
export async function getUserProfile(userId = 1001) {
|
||
const safeId = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/users/${safeId}`);
|
||
if (res && res.id) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取用户信息异常,降级读取本地默认配置:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 修改用户个人资料 (昵称、头像、手机号)
|
||
*/
|
||
export async function updateUserProfile(userId = 1001, data = {}) {
|
||
const safeId = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.post(`/users/${safeId}/profile`, data);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 修改个人资料网络异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 钱包充值 (落盘 MySQL 并记录财务入账流水)
|
||
*/
|
||
export async function rechargeBalance(userId = 1001, amount, bonusAmount = 0) {
|
||
const safeId = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.post(`/users/${safeId}/recharge`, { amount, bonusAmount });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 钱包充值接口异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 佣金提现申请 (支持多渠道: 微信 WECHAT / 支付宝 ALIPAY / 银行卡 BANK)
|
||
* 支持 (userId, amount, options) 与 (amount, channel, userId) 两种调用签名
|
||
*/
|
||
export async function withdrawCommission(arg1, arg2, arg3 = {}) {
|
||
try {
|
||
let uid = 1001;
|
||
let amt = 0;
|
||
let opt = {};
|
||
|
||
if (typeof arg1 === 'number' && (typeof arg2 === 'number' || !isNaN(Number(arg2)))) {
|
||
uid = arg1;
|
||
amt = Number(arg2);
|
||
opt = typeof arg3 === 'object' ? arg3 : { channel: arg3 || 'WECHAT' };
|
||
} else {
|
||
amt = Number(arg1) || 0;
|
||
opt = typeof arg2 === 'object' ? (arg2 || {}) : { channel: arg2 || 'WECHAT' };
|
||
uid = Number(arg3) || 1001;
|
||
}
|
||
|
||
const safeId = normalizeUserId(uid);
|
||
const payload = typeof opt === 'string'
|
||
? { amount: amt, channel: opt }
|
||
: { amount: amt, ...opt };
|
||
const res = await http.post(`/users/${safeId}/withdraw`, payload);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 提现申请接口异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询用户提现记录与审批状态 (待审核/处理中/已打款/已驳回)
|
||
*/
|
||
export async function getUserWithdrawals(userId = 1001) {
|
||
const safeId = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/users/${safeId}/withdrawals`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取提现记录异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询用户财务收支流水
|
||
*/
|
||
export async function getUserFinanceRecords(userId = 1001) {
|
||
const safeId = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/users/${safeId}/finance-records`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取财务流水异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取可抢订单池 (真实读取 MySQL orders 待接单列表)
|
||
*/
|
||
export async function getOrdersPool() {
|
||
try {
|
||
const res = await http.get('/orders/pool');
|
||
if (Array.isArray(res) && res.length > 0) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取可抢订单池异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 大神抢单认领
|
||
*/
|
||
export async function claimOrder(orderId, expertData = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/claim`, { ...expertData, userId: normalizeUserId(expertData.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 大神抢单异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取大神今日接单数据汇总 (落盘 MySQL)
|
||
*/
|
||
export async function getExpertStats(expertId = 101) {
|
||
try {
|
||
const res = await http.get(`/expert/stats/${expertId}`);
|
||
if (res && typeof res.income !== 'undefined') {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取大神今日统计异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取首页运营 Banner 列表 (配置化接口)
|
||
*/
|
||
export async function getBanners() {
|
||
try {
|
||
const res = await http.get('/banners');
|
||
if (Array.isArray(res) && res.length > 0) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取Banner异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取首页金刚区活动品类导航列表 (配置化接口)
|
||
*/
|
||
export async function getNavCategories() {
|
||
try {
|
||
const res = await http.get('/nav-categories');
|
||
if (Array.isArray(res) && res.length > 0) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取金刚区品类导航异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取钱包充值阶梯与赠送配置列表 (真实从 MySQL 读取)
|
||
*/
|
||
export async function getRechargeTiers() {
|
||
try {
|
||
const res = await http.get('/marketing/recharge-tiers');
|
||
if (Array.isArray(res) && res.length > 0) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取充值阶梯异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取可用优惠券列表 (支持品类过滤;带 userId 时会返回 claimed 标识)
|
||
*/
|
||
export async function getMarketingCoupons(category) {
|
||
try {
|
||
const uid = normalizeUserId();
|
||
const qs = [];
|
||
if (category) qs.push(`category=${category}`);
|
||
if (uid) qs.push(`userId=${uid}`);
|
||
const query = qs.length ? `?${qs.join('&')}` : '';
|
||
const res = await http.get(`/marketing/coupons${query}`);
|
||
if (Array.isArray(res)) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取营销优惠券异常:', err.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 领取优惠券
|
||
*/
|
||
export async function claimCoupon(couponId, userId) {
|
||
const uid = normalizeUserId(userId);
|
||
const res = await http.post(`/marketing/coupons/${Number(couponId)}/claim`, { userId: uid });
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 我的优惠券(status: UNUSED | USED | EXPIRED,缺省返回全部)
|
||
*/
|
||
export async function getMyCoupons(userId, status) {
|
||
const uid = normalizeUserId(userId);
|
||
const query = status ? `?userId=${uid}&status=${status}` : `?userId=${uid}`;
|
||
const res = await http.get(`/marketing/my-coupons${query}`);
|
||
return res || { list: [], usable: 0 };
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 提交订单服务评价与星级打分 (落盘 MySQL order_reviews)
|
||
*/
|
||
export async function submitOrderReview(orderId, reviewData = {}) {
|
||
try {
|
||
const res = await http.post(`/orders/${orderId}/review`, { ...reviewData, userId: normalizeUserId(reviewData.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 提交订单评价异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取大神真实历史评价列表
|
||
*/
|
||
export async function getExpertReviews(expertId) {
|
||
try {
|
||
const res = await http.get(`/companions/${expertId}/reviews`);
|
||
if (Array.isArray(res)) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取大神评价列表异常:', err.message);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 获取友圈动态评论区列表 (落盘 MySQL post_comments)
|
||
*/
|
||
export async function getPostComments(postId) {
|
||
try {
|
||
const res = await http.get(`/posts/${postId}/comments`);
|
||
if (Array.isArray(res)) {
|
||
return res;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[API] 获取动态评论异常:', err.message);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 发表友圈动态评论
|
||
*/
|
||
export async function addPostComment(postId, commentData = {}) {
|
||
try {
|
||
const res = await http.post(`/posts/${postId}/comments`, { ...commentData, userId: normalizeUserId(commentData.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 发表动态评论异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 申请友圈搭子互动上车
|
||
*/
|
||
export async function applyPostParty(postId, applyData = {}) {
|
||
try {
|
||
const res = await http.post(`/posts/${postId}/apply`, { ...applyData, applicantId: normalizeUserId(applyData.applicantId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 申请搭子上车异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户会话 token 请求头(上传类接口需要)
|
||
*/
|
||
function authHeader() {
|
||
try {
|
||
const t = uni.getStorageSync('userToken');
|
||
return t ? { Authorization: `Bearer ${t}` } : {};
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 真实上传本地图片至服务端持久化静态存储
|
||
* @param {string} tempFilePath 本地图片临时路径 (如 wxfile://tmp_xxx 或 blob:xxx)
|
||
* @returns {Promise<string>} 返回持久化公网图片访问路径 (形如 /uploads/img_xxx.png)
|
||
*/
|
||
export function uploadImage(tempFilePath) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!tempFilePath) return reject(new Error('图片路径为空'));
|
||
|
||
// 微信开发者工具临时路径形如 http://tmp/ 或 http://usr/
|
||
// 微信真机临时路径形如 wxfile://tmp_xxx 或 wxfile://clientdata/xxx
|
||
// H5 临时路径形如 blob:xxx
|
||
const isLocalTemp =
|
||
tempFilePath.startsWith('http://tmp/') ||
|
||
tempFilePath.startsWith('http://usr/') ||
|
||
tempFilePath.startsWith('wxfile://') ||
|
||
tempFilePath.startsWith('blob:');
|
||
|
||
// 只有真正的公网远程地址或相对静态路径,才无需上传直接返回
|
||
if (
|
||
!isLocalTemp &&
|
||
(tempFilePath.startsWith('http://') ||
|
||
tempFilePath.startsWith('https://') ||
|
||
tempFilePath.startsWith('/uploads/'))
|
||
) {
|
||
// 若是小程序端且为 /uploads/ 开头,补全完整公网域名
|
||
// #ifndef H5
|
||
if (tempFilePath.startsWith('/uploads/')) {
|
||
return resolve(`https://peiww.gynhc.com${tempFilePath}`);
|
||
}
|
||
// #endif
|
||
return resolve(tempFilePath);
|
||
}
|
||
|
||
// #ifdef H5
|
||
const uploadUrl = '/api/upload';
|
||
// #endif
|
||
// #ifndef H5
|
||
const uploadUrl = 'https://peiww.gynhc.com/api/upload';
|
||
// #endif
|
||
|
||
uni.uploadFile({
|
||
url: uploadUrl,
|
||
filePath: tempFilePath,
|
||
name: 'file',
|
||
header: authHeader(),
|
||
success: (uploadRes) => {
|
||
try {
|
||
const data = JSON.parse(uploadRes.data);
|
||
if (data && data.code === 20000 && data.data && data.data.url) {
|
||
let finalUrl = data.data.url;
|
||
// 非H5平台(小程序端)图片访问需补全公网域名
|
||
// #ifndef H5
|
||
if (finalUrl.startsWith('/uploads/')) {
|
||
finalUrl = `https://peiww.gynhc.com${finalUrl}`;
|
||
}
|
||
// #endif
|
||
resolve(finalUrl);
|
||
} else {
|
||
reject(new Error((data && data.message) || '上传解析异常'));
|
||
}
|
||
} catch (e) {
|
||
reject(new Error('上传数据返回格式错误'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
reject(new Error(err.errMsg || '图片上传网络异常'));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 客户端前端前置检测文本安全 (对接 security.msgSecCheck)
|
||
*/
|
||
export async function checkTextSecurity(content) {
|
||
try {
|
||
const res = await http.post('/security/msg-sec-check', { content });
|
||
return res;
|
||
} catch (err) {
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取平台通用配置(官方客服、履约公告指引、平台规范协议等)
|
||
*/
|
||
export async function getPlatformConfigs() {
|
||
try {
|
||
const res = await http.get('/system/configs');
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 获取平台通用配置降级:', err.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取平台合规商圈场所白名单库
|
||
*/
|
||
export async function getSafetyPlaces() {
|
||
try {
|
||
const res = await http.get('/safety/places');
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 获取合规商圈场所降级:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提交大神实名资质入驻认证申请
|
||
*/
|
||
export async function applyExpertCert(data) {
|
||
try {
|
||
const res = await http.post('/companions/apply', { ...data, userId: normalizeUserId(data && data.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取大神多维收益走势统计分析 (近7天/近4周/近6个月)
|
||
*/
|
||
export async function getExpertRevenueTrends(expertId = 101) {
|
||
try {
|
||
const res = await http.get(`/expert/stats/${expertId}/trends`);
|
||
return res || { daily: [], weekly: [], monthly: [] };
|
||
} catch (err) {
|
||
console.warn('[API] 获取大神收益走势异常:', err.message);
|
||
return { daily: [], weekly: [], monthly: [] };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 大神感谢回复买家评价
|
||
*/
|
||
export async function replyOrderReview(reviewId, replyContent) {
|
||
try {
|
||
const res = await http.post(`/reviews/${reviewId}/reply`, { replyContent, userId: normalizeUserId() });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 回复评价异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 友圈本人动态删除
|
||
*/
|
||
export async function deletePost(postId, userId) {
|
||
try {
|
||
const uid = normalizeUserId(userId);
|
||
const res = await http.delete(`/posts/${postId}?userId=${uid}`);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 删除动态异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取大神专属细分服务包列表
|
||
*/
|
||
export async function getExpertServices(expertId = 101) {
|
||
try {
|
||
const res = await http.get(`/companions/${expertId}/services`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取服务包异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换大神单个服务包上下架状态
|
||
*/
|
||
export async function updateExpertServiceStatus(expertId = 101, serviceId, status) {
|
||
try {
|
||
const res = await http.put(`/companions/${expertId}/services/${serviceId}/status`, { status, userId: normalizeUserId() });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 切换服务包状态异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前登录用户的大神档案(大神工作台使用)
|
||
*/
|
||
export async function getMyExpertProfile(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/expert/me?userId=${uid}`);
|
||
return res || null;
|
||
} catch (err) {
|
||
console.warn('[API] 获取大神档案异常:', err.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 新增服务包(仅本人大神)
|
||
*/
|
||
export async function createExpertService(expertId, payload = {}) {
|
||
const res = await http.post(`/companions/${Number(expertId)}/services`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 编辑服务包(整体覆盖,仅本人大神)
|
||
*/
|
||
export async function updateExpertService(expertId, serviceId, payload = {}) {
|
||
const res = await http.put(`/companions/${Number(expertId)}/services/${serviceId}`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 删除服务包(仅本人大神)
|
||
*/
|
||
export async function deleteExpertService(expertId, serviceId) {
|
||
const res = await http.delete(`/companions/${Number(expertId)}/services/${serviceId}`);
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 上传大神语音介绍(录音文件),返回可直接播放的公网地址
|
||
* @param {number} expertId 大神ID
|
||
* @param {string} tempFilePath 录音临时文件路径
|
||
* @param {number} duration 时长(秒,≤60)
|
||
*/
|
||
export function uploadVoiceIntro(expertId, tempFilePath, duration) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!tempFilePath) return reject(new Error('语音文件路径为空'));
|
||
// #ifdef H5
|
||
const uploadUrl = `/api/companions/${Number(expertId)}/voice`;
|
||
// #endif
|
||
// #ifndef H5
|
||
const uploadUrl = `https://peiww.gynhc.com/api/companions/${Number(expertId)}/voice`;
|
||
// #endif
|
||
uni.uploadFile({
|
||
url: uploadUrl,
|
||
filePath: tempFilePath,
|
||
name: 'file',
|
||
header: authHeader(),
|
||
formData: {
|
||
duration: String(Math.min(60, Math.max(1, Math.round(Number(duration) || 15)))),
|
||
userId: String(normalizeUserId()),
|
||
},
|
||
success: (uploadRes) => {
|
||
try {
|
||
const data = JSON.parse(uploadRes.data);
|
||
if (data && data.code === 20000 && data.data && data.data.voiceIntroUrl) {
|
||
let finalUrl = data.data.voiceIntroUrl;
|
||
// #ifndef H5
|
||
if (String(finalUrl).startsWith('/uploads/')) finalUrl = `https://peiww.gynhc.com${finalUrl}`;
|
||
// #endif
|
||
resolve({ url: finalUrl, duration: data.data.voiceDuration });
|
||
} else {
|
||
reject(new Error((data && data.message) || '语音上传失败'));
|
||
}
|
||
} catch (e) {
|
||
reject(new Error('语音上传返回格式错误'));
|
||
}
|
||
},
|
||
fail: (err) => reject(new Error(err.errMsg || '语音上传网络异常')),
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取用户收藏的陪玩列表
|
||
*/
|
||
export async function getFavorites(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/favorites?userId=${uid}`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取收藏列表异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 探测是否已收藏特定陪玩
|
||
*/
|
||
export async function checkFavorite(expertId, userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/favorites/check?userId=${uid}&expertId=${expertId}`);
|
||
return res && res.isFavorite;
|
||
} catch (err) {
|
||
console.warn('[API] 探测收藏状态异常:', err.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加收藏
|
||
*/
|
||
export async function addFavorite(expertId, userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.post('/favorites', { userId: uid, expertId });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 添加收藏异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消收藏
|
||
*/
|
||
export async function removeFavorite(expertId, userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.delete(`/favorites/${expertId}?userId=${uid}`);
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 取消收藏异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取分销团队列表 (一级与二级成员)
|
||
*/
|
||
export async function getDistributionTeam(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/distribution/team?userId=${uid}`);
|
||
return res || { directCount: 0, indirectCount: 0, totalTeamCount: 0, directList: [], indirectList: [] };
|
||
} catch (err) {
|
||
console.warn('[API] 获取分销团队异常:', err.message);
|
||
return { directCount: 0, indirectCount: 0, totalTeamCount: 0, directList: [], indirectList: [] };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取分销返佣订单记录
|
||
*/
|
||
export async function getDistributionRecords(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/distribution/records?userId=${uid}`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取分销返佣记录异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 绑定上级推荐人邀请码 (如 PW1001)
|
||
*/
|
||
export async function bindInviteCode(userId, inviteCode) {
|
||
const uid = normalizeUserId(userId);
|
||
const res = await http.post('/distribution/bind', { userId: uid, inviteCode });
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 获取指定友圈动态的搭子上车申请列表 (供动态房主查看与审批)
|
||
*/
|
||
export async function getPostApplies(postId) {
|
||
try {
|
||
const res = await http.get(`/posts/${postId}/applies?userId=${normalizeUserId()}`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取搭子上车申请列表异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 房主或管理员处置搭子上车申请 (ACCEPTED | REJECTED)
|
||
*/
|
||
export async function handlePostApply(applyId, status) {
|
||
const res = await http.post(`/post-applies/${applyId}/handle`, { status, userId: normalizeUserId() });
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户提交的违规举报工单与处理结果反馈
|
||
*/
|
||
export async function getMyReports(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/reports?reporterId=${uid}`);
|
||
return res || [];
|
||
} catch (err) {
|
||
console.warn('[API] 获取我的举报记录异常:', err.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新大神资料与接单状态 (如 status: '接单中' | '休息中')
|
||
*/
|
||
export async function updateCompanionProfile(expertId = 101, profileData = {}) {
|
||
try {
|
||
const res = await http.put(`/companions/${expertId}`, { ...profileData, userId: normalizeUserId(profileData.userId) });
|
||
return res;
|
||
} catch (err) {
|
||
console.warn('[API] 更新大神资料/接单状态异常:', err.message);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户隐私与通知订阅设置 (支持服务端持久化与本地缓存同步)
|
||
*/
|
||
export async function getUserSettings(userId) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/users/${uid}/settings`);
|
||
if (res && typeof res === 'object') {
|
||
if (typeof res.privacyHidePhone === 'boolean') {
|
||
uni.setStorageSync('privacy_hide_phone', res.privacyHidePhone ? '1' : '0');
|
||
}
|
||
if (typeof res.noticeSubscribe === 'boolean') {
|
||
uni.setStorageSync('notice_subscribe', res.noticeSubscribe ? '1' : '0');
|
||
}
|
||
return res;
|
||
}
|
||
} catch (_) {}
|
||
return {
|
||
userId: uid,
|
||
privacyHidePhone: uni.getStorageSync('privacy_hide_phone') === '1',
|
||
noticeSubscribe: uni.getStorageSync('notice_subscribe') !== '0'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取微信订阅消息模板ID (用于 wx.requestSubscribeMessage 请求用户授权;模板ID非敏感)
|
||
*/
|
||
export async function getSubscribeTemplates() {
|
||
try {
|
||
const res = await http.get('/message/subscribe-templates');
|
||
return res || { enablePush: false };
|
||
} catch (err) {
|
||
console.warn('[API] 获取订阅消息模板异常:', err.message);
|
||
return { enablePush: false };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取站内消息通知列表与未读数
|
||
*/
|
||
export async function getNotifications(userId, limit = 50) {
|
||
const uid = normalizeUserId(userId);
|
||
try {
|
||
const res = await http.get(`/notifications?userId=${uid}&limit=${Number(limit) || 50}`);
|
||
return res || { list: [], unread: 0 };
|
||
} catch (err) {
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 标记站内消息为已读(不传 ids 表示全部已读)
|
||
*/
|
||
export async function markNotificationsRead(userId, ids) {
|
||
const uid = normalizeUserId(userId);
|
||
const payload = { userId: uid };
|
||
if (Array.isArray(ids) && ids.length > 0) payload.ids = ids;
|
||
const res = await http.post('/notifications/read', payload);
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* 保存用户隐私与通知订阅设置 (同步落盘服务端与本地存储)
|
||
*/
|
||
export async function updateUserSettings(userId, settings = {}) {
|
||
const uid = normalizeUserId(userId);
|
||
if (typeof settings.privacyHidePhone === 'boolean') {
|
||
uni.setStorageSync('privacy_hide_phone', settings.privacyHidePhone ? '1' : '0');
|
||
}
|
||
if (typeof settings.noticeSubscribe === 'boolean') {
|
||
uni.setStorageSync('notice_subscribe', settings.noticeSubscribe ? '1' : '0');
|
||
}
|
||
try {
|
||
const res = await http.post(`/users/${uid}/settings`, settings);
|
||
return res;
|
||
} catch (err) {
|
||
// 写入失败必须向上抛出,避免调用方误以为已保存
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据隐私开关格式化手机号/联系方式脱敏展示
|
||
*/
|
||
export function formatPrivacyPhone(phone, forceHide) {
|
||
if (!phone) return '';
|
||
const str = String(phone).trim();
|
||
const shouldHide = typeof forceHide === 'boolean'
|
||
? forceHide
|
||
: uni.getStorageSync('privacy_hide_phone') === '1';
|
||
if (!shouldHide) return str;
|
||
if (/^\d{11}$/.test(str)) {
|
||
return str.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
||
}
|
||
if (str.length >= 7) {
|
||
const head = str.slice(0, 3);
|
||
const tail = str.slice(-2);
|
||
return `${head}****${tail}`;
|
||
}
|
||
return str.slice(0, 2) + '****';
|
||
}
|
||
|
||
/**
|
||
* 统一调起微信小程序支付 (对接 /api/pay/wechat/prepay,支持 LIVE 原生 requestPayment 与 SANDBOX 模拟收银台)
|
||
*/
|
||
export async function requestWechatPayment({ amount, title = '同城陪伴担保服务', orderId = '', scene = 'ORDER', userId = 1001 } = {}) {
|
||
const uid = normalizeUserId(userId);
|
||
const numAmount = Number(amount || 0);
|
||
let prepayData = null;
|
||
|
||
let wxCode = '';
|
||
try {
|
||
if (typeof uni !== 'undefined' && typeof uni.login === 'function') {
|
||
wxCode = await new Promise((resolve) => {
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (res) => resolve((res && res.code) || ''),
|
||
fail: () => resolve('')
|
||
});
|
||
});
|
||
}
|
||
} catch (_) {}
|
||
|
||
try {
|
||
prepayData = await http.post('/pay/wechat/prepay', {
|
||
orderId,
|
||
amount: numAmount,
|
||
title,
|
||
scene,
|
||
userId: uid,
|
||
wxCode
|
||
});
|
||
} catch (_) {
|
||
prepayData = {
|
||
mode: 'SANDBOX',
|
||
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||
nonceStr: `pw_${Date.now()}`,
|
||
package: `prepay_id=wx${Date.now()}`,
|
||
signType: 'RSA',
|
||
paySign: 'SANDBOX_FALLBACK',
|
||
outTradeNo: orderId || `WX_${Date.now()}`
|
||
};
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
if (prepayData && prepayData.mode === 'LIVE' && typeof uni.requestPayment === 'function') {
|
||
uni.requestPayment({
|
||
provider: 'wxpay',
|
||
timeStamp: String(prepayData.timeStamp),
|
||
nonceStr: String(prepayData.nonceStr),
|
||
package: String(prepayData.package),
|
||
signType: prepayData.signType || 'RSA',
|
||
paySign: String(prepayData.paySign),
|
||
success: (res) => {
|
||
resolve({ paid: true, mode: 'LIVE', raw: res, outTradeNo: prepayData.outTradeNo });
|
||
},
|
||
fail: (err) => {
|
||
const isCancel = err && err.errMsg && err.errMsg.includes('cancel');
|
||
const errorObj = new Error(isCancel ? '已取消微信支付' : (err?.errMsg || '微信支付未完成'));
|
||
errorObj.cancelled = isCancel;
|
||
reject(errorObj);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 沙盒/开发联调收银台确认弹窗(未配置正式商户证书时自动接管,支持模拟确认支付与取消支付)
|
||
uni.showModal({
|
||
title: '微信支付收银台',
|
||
content: `收款商户:同城陪伴官方担保托管\n商品名称:${title}\n支付金额:¥${numAmount.toFixed(2)}\n\n是否确认完成本次微信支付?`,
|
||
confirmText: '确认支付',
|
||
cancelText: '取消支付',
|
||
confirmColor: '#10B981',
|
||
success: (modalRes) => {
|
||
if (modalRes.confirm) {
|
||
resolve({
|
||
paid: true,
|
||
mode: 'SANDBOX',
|
||
outTradeNo: (prepayData && prepayData.outTradeNo) || orderId
|
||
});
|
||
} else {
|
||
const cancelErr = new Error('已取消微信支付');
|
||
cancelErr.cancelled = true;
|
||
reject(cancelErr);
|
||
}
|
||
},
|
||
fail: () => {
|
||
const cancelErr = new Error('已取消微信支付');
|
||
cancelErr.cancelled = true;
|
||
reject(cancelErr);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
* 微信小程序真实手机号一键授权登录 (调用后端换取真实手机号并登录/注册)
|
||
*/
|
||
export async function loginWithWechatPhone({ phoneCode, wxCode } = {}) {
|
||
return http.post('/auth/wechat-phone', {
|
||
phoneCode,
|
||
wxCode
|
||
});
|
||
}
|