diff --git a/common/fishMarketApi.js b/common/fishMarketApi.js new file mode 100644 index 0000000..791768f --- /dev/null +++ b/common/fishMarketApi.js @@ -0,0 +1,132 @@ +import tui from './httpRequest' + +function unwrap(res) { + if (!res) return null + if (res.success === false) { + uni.showToast({ title: res.message || '请求失败', icon: 'none', duration: 3200 }) + return null + } + return res.result !== undefined ? res.result : res +} + +function extractUploadUrl(fileObj) { + if (!fileObj) return '' + if (typeof fileObj === 'string') return fileObj + return fileObj.url || fileObj.fileUrl || fileObj.path || fileObj.fullPath || '' +} + +function buildQuery(data = {}) { + const query = Object.keys(data) + .filter(key => data[key] !== undefined && data[key] !== null && data[key] !== '') + .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(data[key])) + .join('&') + return query ? '?' + query : '' +} + +export function uploadFishImage(path) { + return tui.uploadFile('/upload/file', path).then(extractUploadUrl) +} + +export function pageFishGoods(data = {}) { + return tui.request('/app/fishmarket/goods/page', 'POST', data, false, false, true).then(unwrap) +} + +export function pageFishWants(data = {}) { + return tui.request('/app/fishmarket/wants/page', 'POST', data, false, false, true).then(unwrap) +} + +export function getFishGoodsDetail(id) { + return tui.request(`/app/fishmarket/goods/${id}`, 'GET', {}, false, true, true).then(unwrap) +} + +export function publishFishGoods(data) { + return tui.request('/app/fishmarket/goods', 'POST', data, false, false, false).then(unwrap) +} + +export function publishFishWant(data) { + return tui.request('/app/fishmarket/wants', 'POST', data, false, false, false).then(unwrap) +} + +export function listFishCategories(regionId = '') { + return tui.request('/app/fishmarket/categories', 'GET', { regionId }, false, true, true).then(unwrap) +} + +export function toggleFishFavorite(goodsId) { + return tui.request(`/app/fishmarket/goods/${goodsId}/favorite`, 'POST', {}, false, false, true).then(unwrap) +} + +export function toggleFishLike(data) { + return tui.request('/app/fishmarket/like' + buildQuery(data), 'POST', {}, false, false, true).then(unwrap) +} + +export function addFishComment(data) { + return tui.request('/app/fishmarket/comments', 'POST', data, false, false, false).then(unwrap) +} + +export function pageFishComments(goodsId, pageNum = 1, pageSize = 10) { + return tui.request('/app/fishmarket/comments/page', 'GET', { goodsId, pageNum, pageSize }, false, true, true).then(unwrap) +} + +export function deleteFishComment(id) { + return tui.request(`/app/fishmarket/comments/${id}`, 'DELETE', {}, false, false, true).then(unwrap) +} + +export function submitFishComplaint(data) { + return tui.request('/app/fishmarket/complaints', 'POST', data, false, false, false).then(unwrap) +} + +export function createFishChatSession(goodsId) { + return tui.request(`/app/fishmarket/goods/${goodsId}/chat`, 'POST', {}, false, false, false).then(unwrap) +} + +export function getFishChatSession(sessionId) { + return tui.request(`/app/fishmarket/chat/sessions/${sessionId}`, 'GET', {}, false, true, true).then(unwrap) +} + +export function pageFishChatMessages(sessionId, pageNum = 1, pageSize = 20) { + return tui.request(`/app/fishmarket/chat/sessions/${sessionId}/messages/page`, 'GET', { pageNum, pageSize }, false, true, true).then(unwrap) +} + +export function sendFishChatMessage(data) { + return tui.request('/app/fishmarket/chat/messages', 'POST', data, false, false, false).then(unwrap) +} + +export function pageMyFishGoods(data = {}) { + return tui.request('/app/fishmarket/my/goods', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishWants(data = {}) { + return tui.request('/app/fishmarket/my/wants', 'POST', data, false, false, true).then(unwrap) +} + +export function updateMyFishGoodsStatus(data) { + return tui.request('/app/fishmarket/my/goods/status', 'POST', data, false, false, false).then(unwrap) +} + +export function updateMyFishWantStatus(data) { + return tui.request('/app/fishmarket/my/wants/status', 'POST', data, false, false, false).then(unwrap) +} + +export function pageMyFishFavorites(data = {}) { + return tui.request('/app/fishmarket/my/favorites', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishBrowse(data = {}) { + return tui.request('/app/fishmarket/my/browse', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishSessions(data = {}) { + return tui.request('/app/fishmarket/my/sessions', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishComments(data = {}) { + return tui.request('/app/fishmarket/my/comments', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishLikes(data = {}) { + return tui.request('/app/fishmarket/my/likes', 'POST', data, false, false, true).then(unwrap) +} + +export function pageMyFishComplaints(data = {}) { + return tui.request('/app/fishmarket/my/complaints', 'POST', data, false, false, true).then(unwrap) +} diff --git a/common/fishMarketSocket.js b/common/fishMarketSocket.js new file mode 100644 index 0000000..eeb5d5a --- /dev/null +++ b/common/fishMarketSocket.js @@ -0,0 +1,11 @@ +import ieSocket from './ieSocket' + +export default { + connect() { return ieSocket.connect() }, + close() { return ieSocket.close() }, + resetHandlers() { return ieSocket.resetHandlers() }, + subscribeSession(sessionId) { return ieSocket.subscribeFishSession(sessionId) }, + unsubscribeSession(sessionId) { return ieSocket.unsubscribeFishSession(sessionId) }, + sendMessage(data) { return ieSocket.sendFishMessage(data) }, + onMessage(handler) { return ieSocket.onFishMessage(handler) } +} diff --git a/common/httpRequest.js b/common/httpRequest.js index efe3c4d..a95039b 100644 --- a/common/httpRequest.js +++ b/common/httpRequest.js @@ -38,7 +38,7 @@ const tui = { //接口地址 interfaceUrl: function() { return 'https://hbkuaishi.com/hiver' //正式 - // return 'http://192.168.100.67:8888/hiver' //测试 + // return 'http://192.168.100.34:8888/hiver' //测试 }, toast: function(text, duration, success) { uni.showToast({ diff --git a/common/ieApi.js b/common/ieApi.js index aa07041..da5f468 100644 --- a/common/ieApi.js +++ b/common/ieApi.js @@ -9,6 +9,18 @@ function unwrap(res) { return res.result !== undefined ? res.result : res } +function unwrapIeRealName(res) { + if (!res) return null + if (res.success === false) { + return { + realNameVerified: false, + verified: false, + message: res.message || '实名认证未通过,请稍后重试' + } + } + return res.result !== undefined ? res.result : res +} + function unwrapIeHome(res) { if (!res) return null if (res.success === false) { @@ -51,7 +63,7 @@ export function saveIeProfile(data) { } export function verifyIeRealName(data) { - return tui.request('/app/ie/real-name/verify', 'POST', data, false, false, true).then(unwrap) + return tui.request('/app/ie/real-name/verify', 'POST', data, false, false, true).then(unwrapIeRealName) } export function verifyIeStudentCard(data) { diff --git a/common/ieRealNameAuth.js b/common/ieRealNameAuth.js index 4dcf2cf..d082fa5 100644 --- a/common/ieRealNameAuth.js +++ b/common/ieRealNameAuth.js @@ -2,7 +2,17 @@ import tui from './httpRequest' import { verifyIeRealName, verifyIeStudentCard, getIeRealNameAuditSetting } from './ieApi.js' function showLongToast(title, duration = 3200) { - uni.showToast({ title, icon: 'none', duration }) + const content = String(title || '操作未完成') + if (content.length > 18) { + uni.showModal({ + title: '提示', + content, + showCancel: false, + confirmText: '知道了' + }) + return + } + uni.showToast({ title: content, icon: 'none', duration }) } function showAuthDialog(options = {}) { @@ -108,6 +118,38 @@ function promptText(title, placeholder) { }) } +function isValidMainlandName(name) { + const value = String(name || '').trim() + if (value.length < 2 || value.length > 30) return false + if (value.indexOf('·') === 0 || value.indexOf('·') === value.length - 1 || value.indexOf('··') !== -1) return false + return /^[\u4e00-\u9fa5]+(·[\u4e00-\u9fa5]+)*$/.test(value) +} + +function isValidMainlandIdCard(idCardNo) { + const id = String(idCardNo || '').trim().toUpperCase() + if (!/^\d{17}[\dX]$/.test(id)) return false + if (id.substring(0, 6) === '000000') return false + if (!isValidIdCardBirthday(id.substring(6, 14))) return false + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + let sum = 0 + for (let i = 0; i < 17; i += 1) { + sum += Number(id.charAt(i)) * weights[i] + } + return checks[sum % 11] === id.charAt(17) +} + +function isValidIdCardBirthday(birthday) { + if (!/^\d{8}$/.test(birthday)) return false + const year = Number(birthday.substring(0, 4)) + const month = Number(birthday.substring(4, 6)) + const day = Number(birthday.substring(6, 8)) + const date = new Date(year, month - 1, day) + if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return false + const today = new Date() + return year >= 1900 && date <= today +} + function compressImage(filePath, option) { return new Promise(resolve => { if (!uni.compressImage) { @@ -186,14 +228,14 @@ async function chooseFacePictureBase64() { return lastBase64 } -function confirmAuth(actionText, cancelText) { +function confirmAuth(actionText, cancelText, dialogOptions = {}) { return showAuthDialog({ - badge: 'i/e 安全认证', - title: '进阶实名认证', - content: `为满足未成年人保护与防沉迷要求,${actionText || '继续使用'}前需要完成身份证三要素核验。`, + badge: dialogOptions.badge || 'i/e 安全认证', + title: dialogOptions.title || '进阶实名认证', + content: dialogOptions.content || `为满足未成年人保护与防沉迷要求,${actionText || '继续使用'}前需要完成身份证三要素核验。`, confirmText: '去认证', cancelText: cancelText || '暂不继续', - steps: [ + steps: dialogOptions.steps || [ '填写身份证上的姓名与号码', '拍摄本人清晰正脸照', '通过后继续完善 i/e 身份资料' @@ -243,22 +285,35 @@ async function ensureStudentCardVerified(profile, options = {}) { async function ensureAliyunRealNameVerified(profile) { if (profile && profile.realNameVerified) return true + const regionId = getCurrentRegionId() + if (!regionId) { + showLongToast('请先选择校区后再进行实名认证', 3200) + return false + } const certName = await promptText('填写真实姓名', '请输入身份证上的姓名') if (!certName) { showLongToast('未填写真实姓名,无法完成实名认证', 3200) return false } - const certNo = await promptText('填写身份证号', '请输入身份证号码') + if (!isValidMainlandName(certName)) { + showLongToast('真实姓名格式不正确,请填写中国大陆身份证上的中文姓名', 3600) + return false + } + const certNo = (await promptText('填写身份证号', '请输入18位中国大陆居民身份证号')).toUpperCase() if (!certNo) { showLongToast('未填写身份证号,无法完成实名认证', 3200) return false } + if (!isValidMainlandIdCard(certNo)) { + showLongToast('身份证号格式或校验位不正确,请核对后重试', 3600) + return false + } const certPicture = await chooseFacePictureBase64() if (!certPicture) return false uni.showLoading({ title: '实名认证中\n正在安全核验', mask: true }) let result = null try { - result = await verifyIeRealName({ certName, certNo, certPicture }) + result = await verifyIeRealName({ certName, certNo, certPicture, regionId }) } finally { uni.hideLoading() } @@ -270,6 +325,20 @@ async function ensureAliyunRealNameVerified(profile) { return true } +function getCurrentRegionId() { + const area = uni.getStorageSync('area') + if (!area) return '' + if (typeof area === 'string') { + try { + const parsed = JSON.parse(area) + return parsed && parsed.id ? parsed.id : '' + } catch (e) { + return '' + } + } + return area.id || '' +} + function needsProfileSetup(profile) { return !profile || profile.exists === false || profile.profileCompleted !== 1 } @@ -301,7 +370,7 @@ export async function ensureIeVerifiedBeforeAction(options = {}) { const realNameAuditEnabled = !setting || setting.realNameAuditEnabled !== false if (!realNameAuditEnabled) return true const confirmed = realNameAuditEnabled - ? await confirmAuth(options.actionText, options.cancelText) + ? await confirmAuth(options.actionText, options.cancelText, options.authDialog || {}) : await confirmStudentCardOnly(options.actionText, options.cancelText) if (!confirmed) return false try { @@ -326,7 +395,7 @@ export async function ensureIeVerifiedBeforeAction(options = {}) { } return true } catch (e) { - showLongToast('认证未完成,请稍后重试', 3200) + showLongToast((e && e.message) || '认证未完成,请稍后重试', 3200) return false } } diff --git a/common/ieSocket.js b/common/ieSocket.js index 06d5f47..601490e 100644 --- a/common/ieSocket.js +++ b/common/ieSocket.js @@ -10,11 +10,14 @@ const socket = { reconnectTimer: null, reconnectAttempts: 0, roomIds: [], + fishSessionIds: [], messageHandlers: [], + fishMessageHandlers: [], ackHandlers: [], presenceHandlers: [], roomEndHandlers: [], - offlineHandlers: [] + offlineHandlers: [], + fishEnabled: false } function wsUrl() { @@ -86,6 +89,24 @@ function unsubscribeRoom(roomId) { delete socket.subscriptions['ie-room-' + roomId] } +function subscribeFishSession(sessionId) { + if (!sessionId) return false + socket.fishEnabled = true + if (socket.fishSessionIds.indexOf(sessionId) < 0) socket.fishSessionIds.push(sessionId) + if (!socket.connected || !socket.task) return false + const id = 'fish-session-' + sessionId + if (socket.subscriptions[id]) return true + socket.subscriptions[id] = true + subscribe('/topic/fishmarket/session/' + sessionId, id) + return true +} + +function unsubscribeFishSession(sessionId) { + const index = socket.fishSessionIds.indexOf(sessionId) + if (index > -1) socket.fishSessionIds.splice(index, 1) + delete socket.subscriptions['fish-session-' + sessionId] +} + function bindSubscriptions() { subscribe('/user/queue/ie/ack', 'ie-ack') subscribe('/user/queue/ie/message', 'ie-message') @@ -101,6 +122,17 @@ function bindSubscriptions() { subscribe('/topic/ie/room/' + roomId, id) } }) + if (socket.fishEnabled) { + subscribe('/user/queue/fishmarket/message', 'fish-message') + send('/app/fishmarket/connect') + socket.fishSessionIds.forEach(sessionId => { + const id = 'fish-session-' + sessionId + if (!socket.subscriptions[id]) { + socket.subscriptions[id] = true + subscribe('/topic/fishmarket/session/' + sessionId, id) + } + }) + } } function scheduleReconnect() { @@ -134,6 +166,7 @@ function handleMessage(raw) { if (dest.indexOf('/queue/ie/presence') > -1) emit(socket.presenceHandlers, payload) if (dest.indexOf('/queue/ie/room-end') > -1) emit(socket.roomEndHandlers, payload) if (dest.indexOf('/queue/ie/offline') > -1) emit(socket.offlineHandlers, payload) + if (dest.indexOf('/queue/fishmarket/message') > -1 || dest.indexOf('/topic/fishmarket/session/') > -1) emit(socket.fishMessageHandlers, payload) } function startHeartbeat() { @@ -188,6 +221,7 @@ function close() { socket.reconnectTimer = null socket.reconnectAttempts = 0 socket.roomIds = [] + socket.fishSessionIds = [] socket.subscriptions = {} clearInterval(socket.heartbeatTimer) if (socket.task) socket.task.close() @@ -197,6 +231,7 @@ function close() { function resetHandlers() { socket.messageHandlers = [] + socket.fishMessageHandlers = [] socket.ackHandlers = [] socket.presenceHandlers = [] socket.roomEndHandlers = [] @@ -209,10 +244,20 @@ export default { resetHandlers, subscribeRoom, unsubscribeRoom, + subscribeFishSession, + unsubscribeFishSession, sendMessage(data) { return send('/app/ie/message', data) }, + sendFishMessage(data) { + socket.fishEnabled = true + return send('/app/fishmarket/message', data) + }, sendPresence(data) { return send('/app/ie/presence', data) }, onAck(handler) { socket.ackHandlers.push(handler) }, onMessage(handler) { socket.messageHandlers.push(handler) }, + onFishMessage(handler) { + socket.fishEnabled = true + socket.fishMessageHandlers.push(handler) + }, onPresence(handler) { socket.presenceHandlers.push(handler) }, onRoomEnd(handler) { socket.roomEndHandlers.push(handler) }, onOffline(handler) { socket.offlineHandlers.push(handler) }