feat: security hardening, wechat pay v3 platform cert integration, companion certification revocation, and audit closed-loop
这个提交包含在:
@@ -85,12 +85,15 @@ export async function createActivity(payload) {
|
||||
const body = {
|
||||
creatorId: payload.creatorId || 1001,
|
||||
title: payload.title,
|
||||
gameCategory: payload.game || '美食陪吃',
|
||||
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 {
|
||||
@@ -123,9 +126,9 @@ export async function joinActivity(activityId, params = {}) {
|
||||
/**
|
||||
* 房主/管理员核销 4 位核销码
|
||||
*/
|
||||
export async function verifyActivityCheckin(activityId, verifyCode, operatorId = 1001) {
|
||||
export async function verifyActivityCheckin(activityId, verifyCode, operatorId) {
|
||||
return http.post(`/activities/${activityId}/verify`, {
|
||||
operatorId,
|
||||
operatorId: normalizeUserId(operatorId),
|
||||
verifyCode,
|
||||
});
|
||||
}
|
||||
@@ -133,11 +136,11 @@ export async function verifyActivityCheckin(activityId, verifyCode, operatorId =
|
||||
/**
|
||||
* 房主调整出价/追加金额
|
||||
*/
|
||||
export async function adjustActivityPrice(activityId, newPriceYuan, operatorId = 1001) {
|
||||
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,
|
||||
operatorId: normalizeUserId(operatorId),
|
||||
newPriceCents,
|
||||
});
|
||||
return formatActivityData(res);
|
||||
@@ -192,26 +195,28 @@ export async function getCompanions() {
|
||||
try {
|
||||
const list = await http.get('/companions');
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return list.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: '专业同城生活陪伴,阳光健康公开。'
|
||||
}));
|
||||
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);
|
||||
@@ -311,7 +316,8 @@ export async function getOrders() {
|
||||
*/
|
||||
export async function createOrder(payload) {
|
||||
try {
|
||||
const res = await http.post('/orders', payload);
|
||||
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);
|
||||
@@ -324,7 +330,7 @@ export async function createOrder(payload) {
|
||||
*/
|
||||
export async function cancelOrder(orderId) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/cancel`);
|
||||
const res = await http.post(`/orders/${orderId}/cancel`, { userId: normalizeUserId() });
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 取消订单网络异常:', err.message);
|
||||
@@ -335,14 +341,14 @@ export async function cancelOrder(orderId) {
|
||||
/**
|
||||
* 支付订单
|
||||
*/
|
||||
export async function payOrder(orderId) {
|
||||
export async function payOrder(orderId, payload = {}) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/pay`);
|
||||
const res = await http.post(`/orders/${orderId}/pay`, payload);
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 支付订单网络异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +356,7 @@ export async function payOrder(orderId) {
|
||||
*/
|
||||
export async function rejectOrder(orderId, payload = {}) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/reject`, payload);
|
||||
const res = await http.post(`/orders/${orderId}/reject`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 拒单接口网络异常:', err.message);
|
||||
@@ -363,7 +369,7 @@ export async function rejectOrder(orderId, payload = {}) {
|
||||
*/
|
||||
export async function applyCompleteOrder(orderId, payload = {}) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/apply-complete`, payload);
|
||||
const res = await http.post(`/orders/${orderId}/apply-complete`, { ...payload, userId: normalizeUserId(payload.userId) });
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 申请完成服务网络异常:', err.message);
|
||||
@@ -376,7 +382,7 @@ export async function applyCompleteOrder(orderId, payload = {}) {
|
||||
*/
|
||||
export async function completeOrder(orderId) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/complete`);
|
||||
const res = await http.post(`/orders/${orderId}/complete`, { userId: normalizeUserId() });
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 确认完成网络异常:', err.message);
|
||||
@@ -389,7 +395,7 @@ export async function completeOrder(orderId) {
|
||||
*/
|
||||
export async function refundOrder(orderId, data = {}) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/refund`, data);
|
||||
const res = await http.post(`/orders/${orderId}/refund`, { ...data, userId: normalizeUserId(data.userId) });
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 申请退款网络异常:', err.message);
|
||||
@@ -402,7 +408,7 @@ export async function refundOrder(orderId, data = {}) {
|
||||
*/
|
||||
export async function submitReport(payload = {}) {
|
||||
try {
|
||||
const res = await http.post('/reports', payload);
|
||||
const res = await http.post('/reports', { ...payload, reporterId: normalizeUserId(payload.reporterId || payload.userId) });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 提交举报网络异常:', err.message);
|
||||
@@ -426,9 +432,9 @@ export async function queryCheckinCode(code) {
|
||||
/**
|
||||
* 通用4位核销码现场核销 (支持组局活动与同城线下订单)
|
||||
*/
|
||||
export async function universalVerifyCheckin(code, operatorId = 1001) {
|
||||
export async function universalVerifyCheckin(code, operatorId) {
|
||||
try {
|
||||
const res = await http.post('/verify/universal', { verifyCode: code, operatorId });
|
||||
const res = await http.post('/verify/universal', { verifyCode: code, operatorId: normalizeUserId(operatorId) });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 通用核销接口异常:', err.message);
|
||||
@@ -449,6 +455,8 @@ export async function getPosts(category, userId) {
|
||||
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 => {
|
||||
@@ -456,6 +464,7 @@ export async function getPosts(category, userId) {
|
||||
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,
|
||||
@@ -474,7 +483,7 @@ export async function getPosts(category, userId) {
|
||||
likes: Number(p.likes) || 0,
|
||||
liked: finalLiked || isLocallyLiked,
|
||||
comments: Number(p.comments) || 0,
|
||||
views: Number(p.views) || 1,
|
||||
views: finalViews,
|
||||
category: p.category || 'partner',
|
||||
applyCount: Number(p.applyCount) || 0,
|
||||
pendingApplyCount: Number(p.pendingApplyCount) || 0,
|
||||
@@ -519,6 +528,26 @@ export async function likePost(id, action, userId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态浏览次数累加 (落盘 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,自动兼容历史测试账号映射 (将虚拟陪伴师 101 映射至真实用户 2006)
|
||||
*/
|
||||
@@ -567,10 +596,10 @@ export async function updateUserProfile(userId = 1001, data = {}) {
|
||||
/**
|
||||
* 钱包充值 (落盘 MySQL 并记录财务入账流水)
|
||||
*/
|
||||
export async function rechargeBalance(userId = 1001, amount) {
|
||||
export async function rechargeBalance(userId = 1001, amount, bonusAmount = 0) {
|
||||
const safeId = normalizeUserId(userId);
|
||||
try {
|
||||
const res = await http.post(`/users/${safeId}/recharge`, { amount });
|
||||
const res = await http.post(`/users/${safeId}/recharge`, { amount, bonusAmount });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 钱包充值接口异常:', err.message);
|
||||
@@ -794,7 +823,7 @@ export async function getPostComments(postId) {
|
||||
*/
|
||||
export async function addPostComment(postId, commentData = {}) {
|
||||
try {
|
||||
const res = await http.post(`/posts/${postId}/comments`, commentData);
|
||||
const res = await http.post(`/posts/${postId}/comments`, { ...commentData, userId: normalizeUserId(commentData.userId) });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 发表动态评论异常:', err.message);
|
||||
@@ -929,7 +958,7 @@ export async function getSafetyPlaces() {
|
||||
*/
|
||||
export async function applyExpertCert(data) {
|
||||
try {
|
||||
const res = await http.post('/companions/apply', data);
|
||||
const res = await http.post('/companions/apply', { ...data, userId: normalizeUserId(data && data.userId) });
|
||||
return res;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
@@ -954,7 +983,7 @@ export async function getExpertRevenueTrends(expertId = 101) {
|
||||
*/
|
||||
export async function replyOrderReview(reviewId, replyContent) {
|
||||
try {
|
||||
const res = await http.post(`/reviews/${reviewId}/reply`, { replyContent });
|
||||
const res = await http.post(`/reviews/${reviewId}/reply`, { replyContent, userId: normalizeUserId() });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 回复评价异常:', err.message);
|
||||
@@ -965,9 +994,10 @@ export async function replyOrderReview(reviewId, replyContent) {
|
||||
/**
|
||||
* 友圈本人动态删除
|
||||
*/
|
||||
export async function deletePost(postId, userId = 1001) {
|
||||
export async function deletePost(postId, userId) {
|
||||
try {
|
||||
const res = await http.delete(`/posts/${postId}?userId=${userId}`);
|
||||
const uid = userId || uni.getStorageSync('userId') || 1001;
|
||||
const res = await http.delete(`/posts/${postId}?userId=${uid}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 删除动态异常:', err.message);
|
||||
@@ -993,7 +1023,7 @@ export async function getExpertServices(expertId = 101) {
|
||||
*/
|
||||
export async function updateExpertServiceStatus(expertId = 101, serviceId, status) {
|
||||
try {
|
||||
const res = await http.put(`/companions/${expertId}/services/${serviceId}/status`, { status });
|
||||
const res = await http.put(`/companions/${expertId}/services/${serviceId}/status`, { status, userId: normalizeUserId() });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 切换服务包状态异常:', err.message);
|
||||
@@ -1111,7 +1141,7 @@ export async function getPostApplies(postId) {
|
||||
* 房主或管理员处置搭子上车申请 (ACCEPTED | REJECTED)
|
||||
*/
|
||||
export async function handlePostApply(applyId, status) {
|
||||
const res = await http.post(`/post-applies/${applyId}/handle`, { status });
|
||||
const res = await http.post(`/post-applies/${applyId}/handle`, { status, userId: normalizeUserId() });
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1134,7 +1164,7 @@ export async function getMyReports(userId) {
|
||||
*/
|
||||
export async function updateCompanionProfile(expertId = 101, profileData = {}) {
|
||||
try {
|
||||
const res = await http.put(`/companions/${expertId}/profile`, profileData);
|
||||
const res = await http.put(`/companions/${expertId}`, { ...profileData, userId: normalizeUserId(profileData.userId) });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 更新大神资料/接单状态异常:', err.message);
|
||||
@@ -1142,4 +1172,172 @@ export async function updateCompanionProfile(expertId = 101, profileData = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户隐私与通知订阅设置 (支持服务端持久化与本地缓存同步)
|
||||
*/
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户隐私与通知订阅设置 (同步落盘服务端与本地存储)
|
||||
*/
|
||||
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 (_) {
|
||||
return {
|
||||
userId: uid,
|
||||
privacyHidePhone: uni.getStorageSync('privacy_hide_phone') === '1',
|
||||
noticeSubscribe: uni.getStorageSync('notice_subscribe') !== '0'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据隐私开关格式化手机号/联系方式脱敏展示
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -597,6 +597,21 @@ export const ICONS = {
|
||||
<line x1="18" y1="12" x2="22" y2="12"></line>
|
||||
<circle cx="12" cy="12" r="2" fill="#10B981"></circle>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
editPen: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
alert: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F43F5E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
`)
|
||||
};
|
||||
|
||||
|
||||
83
pwuniapp/common/location.js
普通文件
83
pwuniapp/common/location.js
普通文件
@@ -0,0 +1,83 @@
|
||||
// pwuniapp/common/location.js
|
||||
// 同城生活陪伴小程序统一地图与定位能力服务层
|
||||
|
||||
// 默认腾讯位置服务 Key(可在管理后台【安全合规-平台系统配置-地图定位服务】动态更新维护)
|
||||
// 申请地址:https://lbs.qq.com
|
||||
export const DEFAULT_TENCENT_MAP_KEY = '';
|
||||
|
||||
/**
|
||||
* 计算两个经纬度坐标之间的球面距离(千米/km)
|
||||
* @param {number} lat1 纬度1
|
||||
* @param {number} lon1 经度1
|
||||
* @param {number} lat2 纬度2
|
||||
* @param {number} lon2 经度2
|
||||
* @returns {string} 格式化后的距离(例如 "1.2km" 或 "350m")
|
||||
*/
|
||||
export function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
if (!lat1 || !lon1 || !lat2 || !lon2) return '同城';
|
||||
const toRad = (d) => (d * Math.PI) / 180;
|
||||
const R = 6371; // 地球平均半径(km)
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const d = R * c;
|
||||
|
||||
if (d < 1) {
|
||||
return `${Math.round(d * 1000)}m`;
|
||||
}
|
||||
return `${d.toFixed(1)}km`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤起地图选点 (chooseLocation)
|
||||
* 封装权限申请与平台兼容处理
|
||||
* @returns {Promise<{name: string, address: string, latitude: number, longitude: number}>}
|
||||
*/
|
||||
export function chooseMapLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
uni.chooseLocation({
|
||||
success: (res) => {
|
||||
resolve({
|
||||
name: res.name || '',
|
||||
address: res.address || '',
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN || APP-PLUS
|
||||
reject(new Error('当前运行环境不支持原生地图选点'));
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户地理位置经纬度 (getLocation)
|
||||
* @returns {Promise<{latitude: number, longitude: number}>}
|
||||
*/
|
||||
export function getUserLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02', // 国测局坐标系,腾讯地图/高德地图通用
|
||||
success: (res) => {
|
||||
resolve({
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -24,8 +24,15 @@ export function request(options = {}) {
|
||||
}
|
||||
|
||||
const method = (options.method || 'GET').toUpperCase();
|
||||
let sessionToken = '';
|
||||
try {
|
||||
sessionToken = uni.getStorageSync('userToken') || '';
|
||||
} catch (_) {
|
||||
sessionToken = '';
|
||||
}
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...(sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {}),
|
||||
...options.header,
|
||||
};
|
||||
|
||||
|
||||
@@ -122,6 +122,79 @@ export function toggleRole() {
|
||||
}
|
||||
}
|
||||
|
||||
export function isUserLoggedIn() {
|
||||
try {
|
||||
if (typeof uni === 'undefined') return false;
|
||||
const uid = uni.getStorageSync('userId');
|
||||
const hasLoggedOut = uni.getStorageSync('user_has_logged_out');
|
||||
if (hasLoggedOut === '1') return false;
|
||||
return Boolean(uid && String(uid).trim() !== '' && String(uid) !== '0');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isUserBanned() {
|
||||
try {
|
||||
if (typeof uni === 'undefined') return false;
|
||||
const uInfo = uni.getStorageSync('userInfo') || uni.getStorageSync('PW_USER_INFO') || uni.getStorageSync('USER_INFO');
|
||||
return Boolean(uInfo && (uInfo.status === '已封禁' || uInfo.status === '封禁'));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 必须登录才能访问的受保护业务页面清单 (未登录游客访问自动拦截并跳登录)
|
||||
export const AUTH_GUARD_PAGES = [
|
||||
'/pages/activity/create', // 发单/发起组队
|
||||
'/pages/order/create', // 确认下单/预约
|
||||
'/pages/order/list', // 我的订单中心
|
||||
'/pages/activity/my-ticket', // 我的活动门票 (4位核销码)
|
||||
'/pages/user/recharge', // 钱包充值
|
||||
'/pages/user/withdraw', // 收益提现
|
||||
'/pages/user/withdraw-records', // 提现审批明细
|
||||
'/pages/user/bill-records', // 账单明细
|
||||
'/pages/user/promote', // 推广赚钱
|
||||
'/pages/user/favorites', // 我的收藏
|
||||
'/pages/user/reports', // 我的举报与维权
|
||||
'/pages/user/settings', // 系统与隐私设置
|
||||
'/pages/expert/apply', // 大神入驻认证
|
||||
'/pages/expert/workspace', // 大神接单工作台
|
||||
'/pages/expert/radar', // 我要接单 (附近5km雷达)
|
||||
'/pages/expert/hall' // 接单大厅
|
||||
];
|
||||
|
||||
export function ensureLoginOrRedirect(redirectUrl = '') {
|
||||
if (!isUserLoggedIn()) {
|
||||
uni.showToast({
|
||||
title: '请先登录账号',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
setTimeout(() => {
|
||||
const target = redirectUrl ? `/pages/auth/login?redirect=${encodeURIComponent(redirectUrl)}` : '/pages/auth/login';
|
||||
uni.navigateTo({
|
||||
url: target,
|
||||
fail: () => {
|
||||
uni.switchTab({ url: '/pages/user/index' });
|
||||
}
|
||||
});
|
||||
}, 350);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isUserBanned()) {
|
||||
uni.showToast({
|
||||
title: '账号已被封禁',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 非大神禁止访问的受保护页面清单
|
||||
export const EXPERT_GUARD_PAGES = [
|
||||
'/pages/expert/workspace',
|
||||
@@ -132,6 +205,20 @@ export const EXPERT_GUARD_PAGES = [
|
||||
export function checkRoleGuard(pageUrl) {
|
||||
if (!pageUrl) return { allowed: true };
|
||||
const pureUrl = pageUrl.split('?')[0];
|
||||
|
||||
// 1. 登录守卫拦截 (针对未登录游客)
|
||||
if (AUTH_GUARD_PAGES.includes(pureUrl)) {
|
||||
if (!isUserLoggedIn()) {
|
||||
return {
|
||||
allowed: false,
|
||||
code: 'UNAUTHORIZED',
|
||||
redirectUrl: `/pages/auth/login?redirect=${encodeURIComponent(pageUrl)}`,
|
||||
message: '该功能需要登录账号,正在为您跳转登录...',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 大神身份守卫拦截 (针对已登录但非认证大神的普通玩家)
|
||||
if (EXPERT_GUARD_PAGES.includes(pureUrl)) {
|
||||
if (!isExpertCertified()) {
|
||||
return {
|
||||
@@ -159,10 +246,20 @@ export function installRoleGuard() {
|
||||
const check = checkRoleGuard(args.url);
|
||||
if (!check.allowed) {
|
||||
uni.showToast({
|
||||
title: check.message || '非大神禁止访问接单页面',
|
||||
title: check.message || '请先登录账号',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
if (check.redirectUrl) {
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: check.redirectUrl,
|
||||
fail: () => {
|
||||
uni.switchTab({ url: '/pages/user/index' });
|
||||
}
|
||||
});
|
||||
}, 350);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -173,3 +270,23 @@ export function installRoleGuard() {
|
||||
});
|
||||
}
|
||||
|
||||
let isRedirectingToExpert = false;
|
||||
export function safeRedirectToExpertHall(target = '/pages/expert/hall') {
|
||||
if (isRedirectingToExpert) {
|
||||
return true;
|
||||
}
|
||||
if (getCurrentRole() === ROLE_TYPES.EXPERT && isExpertCertified()) {
|
||||
isRedirectingToExpert = true;
|
||||
uni.reLaunch({
|
||||
url: target || '/pages/expert/hall',
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
isRedirectingToExpert = false;
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户