diff --git a/app.json b/app.json
index b1fe7c8..e79f7a6 100644
--- a/app.json
+++ b/app.json
@@ -17,6 +17,14 @@
"components/chenghao-tag/chenghao-tag"
],
"subPackages": [
+ {
+ "root": "pages/dashouduan",
+ "pages": ["dashouduan"]
+ },
+ {
+ "root": "pages/guanshiduan",
+ "pages": ["guanshiduan"]
+ },
{
"root": "pages/pick-user",
"pages": ["pick-user"]
diff --git a/pages/chat/chat.js b/pages/chat/chat.js
index 47b8d07..3f3ec92 100644
--- a/pages/chat/chat.js
+++ b/pages/chat/chat.js
@@ -7,7 +7,7 @@ import {
fetchHistoryMessages,
getOldestHistoryTimestamp,
mergeHistoryMessages,
- waitImConnected,
+ waitChatImReady,
} from '../../utils/chat-history.js';
Page({
@@ -98,27 +98,27 @@ Page({
}
},
- ensureChatReady() {
+ async ensureChatReady() {
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
- const ready = () => {
+ const ready = async () => {
this.setupAllListeners();
- this.loadHistory(true);
+ await this.loadHistory(true);
};
- if (app.ensureConnection) {
- const ret = app.ensureConnection();
- if (ret && typeof ret.then === 'function') {
- ret.then(ready).catch(ready);
- } else {
- setTimeout(ready, 300);
+ try {
+ const ok = await waitChatImReady(app, 12000);
+ if (ok) {
+ await ready();
+ return;
}
- return;
+ } catch (e) {
+ console.warn('[私聊] IM 连接失败', e);
}
if (this.isConnected() && wx.goEasy.im?.userId === targetUserId) {
- ready();
+ await ready();
return;
}
this.connectGoEasy(targetUserId);
@@ -295,7 +295,7 @@ Page({
this.setData({ loadingHistory: true });
try {
- const ready = await waitImConnected(app);
+ const ready = await waitChatImReady(app, 12000);
if (!ready) {
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
return;
diff --git a/pages/cs-chat/cs-chat.js b/pages/cs-chat/cs-chat.js
index c8cbe93..1e1a4c8 100644
--- a/pages/cs-chat/cs-chat.js
+++ b/pages/cs-chat/cs-chat.js
@@ -6,8 +6,9 @@ import {
fetchHistoryMessages,
getOldestHistoryTimestamp,
mergeHistoryMessages,
- waitImConnected,
+ waitChatImReady,
} from '../../utils/chat-history.js';
+import { matchAutoReply, getWelcomeText } from '../../utils/scriptService.js';
Page({
data: {
@@ -37,6 +38,7 @@ Page({
keyboardHeight: 0,
bottomSafeHeight: 140,
defaultAvatarUrl: '',
+ welcomeText: '你好,请问有什么可以帮到您的?',
},
onLoad(options) {
@@ -58,6 +60,7 @@ Page({
this.setData({ teamId, teamName, teamAvatar: teamAvatarResolved, defaultAvatarUrl: defAvatar });
wx.setNavigationBarTitle({ title: teamName });
this.initCurrentUser();
+ this.loadWelcomeScript();
this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => {
const def = getDefaultAvatarUrl(app);
this.setData({
@@ -100,31 +103,52 @@ Page({
this.clearAllListeners();
},
- autoConnect() {
+ async loadWelcomeScript() {
+ try {
+ const text = await getWelcomeText();
+ if (!text) return;
+ this.setData({ welcomeText: text });
+ const msgs = this.data.messages;
+ const idx = msgs.findIndex((m) => m.messageId === 'welcome-cs');
+ if (idx >= 0) {
+ const updated = [...msgs];
+ updated[idx] = { ...updated[idx], payload: { ...updated[idx].payload, text } };
+ this.setData({ messages: updated });
+ }
+ } catch (e) {
+ console.warn('loadWelcomeScript failed', e);
+ }
+ },
+
+ async autoConnect() {
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
- const ready = () => {
+ const ready = async () => {
this.setupAllListeners();
- this.loadHistory(true);
+ await this.loadHistory(true);
};
- if (app.ensureConnection) {
- app.ensureConnection();
- setTimeout(ready, 400);
- return;
- }
-
- const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
- if (status === 'connected' || status === 'reconnected') {
- const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
- if (currentUserId === targetUserId) {
- ready();
+ try {
+ const ok = await waitChatImReady(app, 12000);
+ if (ok) {
+ await ready();
return;
}
- wx.goEasy.disconnect();
+ } catch (e) {
+ console.warn('[客服] IM 连接失败', e);
}
- this.connectGoEasy(targetUserId);
+
+ if (this.isConnected()) {
+ await ready();
+ } else {
+ this.connectGoEasy(targetUserId);
+ }
+ },
+
+ isConnected() {
+ const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
+ return s === 'connected' || s === 'reconnected';
},
connectGoEasy(userId) {
@@ -256,7 +280,7 @@ Page({
this.setData({ loadingHistory: true });
try {
- const ready = await waitImConnected(app);
+ const ready = await waitChatImReady(app, 12000);
if (!ready) {
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
return;
@@ -317,7 +341,7 @@ Page({
name: this.data.teamName || '客服',
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
},
- payload: { text: '你好,请问有什么可以帮到您的?' },
+ payload: { text: this.data.welcomeText || '你好,请问有什么可以帮到您的?' },
status: 'success',
read: false,
formattedTime: formatDate(Date.now()),
@@ -451,11 +475,53 @@ Page({
});
wx.goEasy.im.sendMessage({
message:msg,
- onSuccess:() => this.updateMsg(id, 'success'),
+ onSuccess:() => {
+ this.updateMsg(id, 'success');
+ this.tryAutoReply(text);
+ },
onFailed:() => this.updateMsg(id, 'failed')
});
},
+ async tryAutoReply(userText) {
+ try {
+ const result = await matchAutoReply(userText);
+ if (result && result.matched && result.content) {
+ this.insertCsAutoReply(result);
+ }
+ } catch (e) {
+ console.warn('tryAutoReply failed', e);
+ }
+ },
+
+ insertCsAutoReply(result) {
+ const replyType = (result && result.reply_type) || 'text';
+ const content = (result && result.content) || '';
+ if (!content) return;
+ const isImage = replyType === 'image';
+ const msg = {
+ messageId: 'auto-reply-' + Date.now(),
+ type: isImage ? 'image' : 'text',
+ timestamp: Date.now(),
+ senderId: this.data.teamId,
+ receiverId: this.data.currentUser?.id,
+ senderData: {
+ name: this.data.teamName,
+ avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
+ },
+ payload: isImage ? { url: content } : { text: content },
+ status: 'success',
+ read: false,
+ formattedTime: formatDate(Date.now()),
+ showTime: this.needShow(Date.now()),
+ };
+ if (isImage) msg.imageUrl = content;
+ this.setData({
+ messages: [...this.data.messages, msg],
+ scrollToView: 'msg-bottom',
+ });
+ },
+
sendImageMsg(filePath, fileObj) {
const { currentUser, teamId, teamName, teamAvatar } = this.data;
if (!teamId || !currentUser || !filePath) return;
diff --git a/pages/dashouduan/dashouduan.js b/pages/dashouduan/dashouduan.js
new file mode 100644
index 0000000..395bbe5
--- /dev/null
+++ b/pages/dashouduan/dashouduan.js
@@ -0,0 +1,14 @@
+/** 旧二维码兼容:dashouduan → 打手中心,inviteCode 自动注册逻辑不变 */
+import { parseSceneOptions } from '../../utils/base-page.js';
+
+Page({
+ onLoad(options) {
+ const parsed = parseSceneOptions(options || {});
+ const inviteCode = parsed.inviteCode || options.inviteCode || '';
+ let url = '/pages/fighter/fighter';
+ if (inviteCode) {
+ url += `?inviteCode=${encodeURIComponent(inviteCode)}`;
+ }
+ wx.reLaunch({ url });
+ },
+});
diff --git a/pages/dashouduan/dashouduan.json b/pages/dashouduan/dashouduan.json
new file mode 100644
index 0000000..52bc9cd
--- /dev/null
+++ b/pages/dashouduan/dashouduan.json
@@ -0,0 +1 @@
+{ "navigationBarTitleText": "加载中", "usingComponents": {} }
diff --git a/pages/dashouduan/dashouduan.wxml b/pages/dashouduan/dashouduan.wxml
new file mode 100644
index 0000000..3dee721
--- /dev/null
+++ b/pages/dashouduan/dashouduan.wxml
@@ -0,0 +1 @@
+正在进入打手端…
diff --git a/pages/dashouduan/dashouduan.wxss b/pages/dashouduan/dashouduan.wxss
new file mode 100644
index 0000000..8082289
--- /dev/null
+++ b/pages/dashouduan/dashouduan.wxss
@@ -0,0 +1 @@
+.tip { display:flex; align-items:center; justify-content:center; min-height:100vh; color:#999; font-size:28rpx; }
diff --git a/pages/group-chat/group-chat.js b/pages/group-chat/group-chat.js
index 6930909..2a73f76 100644
--- a/pages/group-chat/group-chat.js
+++ b/pages/group-chat/group-chat.js
@@ -9,7 +9,7 @@ import {
fetchHistoryMessages,
getOldestHistoryTimestamp,
mergeHistoryMessages,
- waitImConnected,
+ waitChatImReady,
} from '../../utils/chat-history.js';
Page({
@@ -280,29 +280,32 @@ Page({
this.stopOrderStatusPoll();
},
- autoConnect() {
+ async autoConnect() {
const targetUserId = this.data.currentUser?.id || getLocalImUserId(app);
if (!targetUserId) return;
- const afterReady = () => {
- this.subscribeGroupIfNeeded().then(() => {
- this.setupAllListeners();
- this.loadHistory(true);
- this.markGroupMessageAsRead();
- });
+ const afterReady = async () => {
+ await this.subscribeGroupIfNeeded();
+ this.setupAllListeners();
+ await this.loadHistory(true);
+ this.markGroupMessageAsRead();
};
- if (app.ensureConnection) {
- app.ensureConnection();
- setTimeout(afterReady, 400);
- return;
+ try {
+ const ok = await waitChatImReady(app, 12000);
+ if (ok) {
+ await afterReady();
+ return;
+ }
+ } catch (e) {
+ console.warn('[群聊] IM 连接失败', e);
}
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (status === 'connected' || status === 'reconnected') {
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
if (currentUserId === targetUserId) {
- afterReady();
+ await afterReady();
return;
}
wx.goEasy.disconnect();
@@ -413,7 +416,7 @@ Page({
this.setData({ loadingHistory: true });
try {
- const ready = await waitImConnected(app);
+ const ready = await waitChatImReady(app, 12000);
if (!ready) {
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
return;
diff --git a/pages/guanshiduan/guanshiduan.js b/pages/guanshiduan/guanshiduan.js
new file mode 100644
index 0000000..9674848
--- /dev/null
+++ b/pages/guanshiduan/guanshiduan.js
@@ -0,0 +1,14 @@
+/** 旧二维码兼容:guanshiduan → 打手中心管事注册,逻辑不变 */
+import { parseSceneOptions } from '../../utils/base-page.js';
+
+Page({
+ onLoad(options) {
+ const parsed = parseSceneOptions(options || {});
+ const inviteCode = parsed.inviteCode || options.inviteCode || '';
+ let url = '/pages/fighter/fighter?registerType=guanshi';
+ if (inviteCode) {
+ url += `&inviteCode=${encodeURIComponent(inviteCode)}`;
+ }
+ wx.reLaunch({ url });
+ },
+});
diff --git a/pages/guanshiduan/guanshiduan.json b/pages/guanshiduan/guanshiduan.json
new file mode 100644
index 0000000..52bc9cd
--- /dev/null
+++ b/pages/guanshiduan/guanshiduan.json
@@ -0,0 +1 @@
+{ "navigationBarTitleText": "加载中", "usingComponents": {} }
diff --git a/pages/guanshiduan/guanshiduan.wxml b/pages/guanshiduan/guanshiduan.wxml
new file mode 100644
index 0000000..4a671a9
--- /dev/null
+++ b/pages/guanshiduan/guanshiduan.wxml
@@ -0,0 +1 @@
+正在进入管事注册…
diff --git a/pages/guanshiduan/guanshiduan.wxss b/pages/guanshiduan/guanshiduan.wxss
new file mode 100644
index 0000000..8082289
--- /dev/null
+++ b/pages/guanshiduan/guanshiduan.wxss
@@ -0,0 +1 @@
+.tip { display:flex; align-items:center; justify-content:center; min-height:100vh; color:#999; font-size:28rpx; }
diff --git a/pages/merchant/merchant.wxss b/pages/merchant/merchant.wxss
index c1c5b07..499b4c6 100644
--- a/pages/merchant/merchant.wxss
+++ b/pages/merchant/merchant.wxss
@@ -72,6 +72,21 @@ page {
padding: 10rpx 30rpx;
}
+.user-info > .flex {
+ align-items: flex-start;
+ width: 100%;
+}
+
+.user-info > .flex > .avatar {
+ flex-shrink: 0;
+}
+
+.user-info > .flex > view:not(.avatar) {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+}
+
.avatar {
width: 110rpx;
height: 110rpx;
@@ -90,7 +105,12 @@ page {
.nickname-row {
flex-wrap: wrap;
align-items: center;
- gap: 8rpx;
+ gap: 6rpx;
+ max-width: 100%;
+}
+
+.sj-identity-tag {
+ flex-shrink: 0;
}
.nickname-edit-btn {
@@ -124,7 +144,7 @@ page {
}
.badge-zone {
- padding: 0 24rpx 8rpx;
+ padding: 0 24rpx 4rpx;
}
.badge-scroll {
@@ -134,19 +154,21 @@ page {
.badge-tag-wrap {
display: inline-block;
- transform: scale(0.72);
+ transform: scale(0.65);
transform-origin: left center;
- margin-right: -8rpx;
+ margin-right: -12rpx;
}
.identity-tags-row {
flex-wrap: wrap;
- gap: 4rpx;
- margin-top: 8rpx;
+ gap: 2rpx;
+ margin-top: 6rpx;
+ max-width: 100%;
}
.identity-tags-row .badge-tag-wrap {
- transform: scale(0.68);
+ transform: scale(0.62);
+ margin-right: -14rpx;
}
.wallet-btns .sj-btn-outline {
diff --git a/utils/chat-history.js b/utils/chat-history.js
index 4946c9e..6147619 100644
--- a/utils/chat-history.js
+++ b/utils/chat-history.js
@@ -4,7 +4,7 @@
* - 统一 IM 连接等待与 history API 封装
*/
-const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-)/;
+const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-|auto-reply-)/;
export function isHistorySyntheticMessage(msg) {
return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId)));
@@ -22,6 +22,21 @@ export function getOldestHistoryTimestamp(messages, fallback = null) {
return oldest != null ? oldest : fallback;
}
+/** 等待 IM 连接就绪(优先 ensureImForRole,避免未连上就拉历史) */
+export async function waitChatImReady(app, timeoutMs = 12000) {
+ const role = app?.globalData?.currentRole || 'normal';
+ if (app?.ensureImForRole) {
+ try {
+ await app.ensureImForRole(role);
+ if (isImConnected()) return true;
+ } catch (e) {
+ console.warn('[IM] ensureImForRole failed', e);
+ }
+ }
+ if (app?.ensureConnection) app.ensureConnection();
+ return waitImConnected(app, timeoutMs);
+}
+
export function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im;
diff --git a/utils/scriptService.js b/utils/scriptService.js
new file mode 100644
index 0000000..635e649
--- /dev/null
+++ b/utils/scriptService.js
@@ -0,0 +1,127 @@
+// utils/scriptService.js — 客服话术拉取与自动回复匹配
+import request from './request.js';
+
+const DEFAULT_SCRIPTS = {
+ chat_image_confirm: {
+ title: '温馨提示',
+ content: '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
+ confirm_text: '确认',
+ cancel_text: '取消',
+ },
+ cs_welcome: {
+ content: '你好,请问有什么可以帮到您的?',
+ },
+};
+
+const textCache = {};
+const TEXT_CACHE_TTL = 5 * 60 * 1000;
+
+function fitModalBtn(text, fallback) {
+ const t = (text || fallback || '确定').trim();
+ if (t && t.length <= 4) return t;
+ return fallback || '确定';
+}
+
+function openConfirmModal(script, onConfirm, onCancel) {
+ wx.showModal({
+ title: ((script.title || '提示').trim()).slice(0, 32),
+ content: script.content || '',
+ confirmText: fitModalBtn(script.confirm_text, '确认'),
+ cancelText: fitModalBtn(script.cancel_text, '取消'),
+ success: (res) => {
+ if (res.confirm) onConfirm && onConfirm();
+ else onCancel && onCancel();
+ },
+ fail: () => onCancel && onCancel(),
+ });
+}
+
+function mergeScript(sceneKey, remote) {
+ const base = DEFAULT_SCRIPTS[sceneKey] || {};
+ if (!remote) return { ...base };
+ return { ...base, ...remote };
+}
+
+export async function fetchScripts(sceneKeys) {
+ if (!sceneKeys || !sceneKeys.length) return {};
+
+ const now = Date.now();
+ const cached = {};
+ const needFetch = [];
+
+ sceneKeys.forEach((key) => {
+ const entry = textCache[key];
+ if (entry && now - entry.time < TEXT_CACHE_TTL) {
+ cached[key] = entry.data;
+ } else {
+ needFetch.push(key);
+ }
+ });
+
+ if (needFetch.length) {
+ try {
+ const res = await request({
+ url: '/peizhi/huashuhq',
+ method: 'POST',
+ data: { scene_keys: needFetch },
+ });
+ const payload = (res && res.data && res.data.code === 200) ? res.data.data || {} : {};
+ needFetch.forEach((key) => {
+ textCache[key] = { time: now, data: payload[key] };
+ cached[key] = payload[key];
+ });
+ } catch (e) {
+ console.warn('fetchScripts failed', e);
+ }
+ }
+
+ const result = {};
+ sceneKeys.forEach((key) => {
+ result[key] = mergeScript(key, cached[key]);
+ });
+ return result;
+}
+
+export async function showConfirmByScene(sceneKey, onConfirm, onCancel) {
+ try {
+ const data = await fetchScripts([sceneKey]);
+ openConfirmModal(data[sceneKey] || mergeScript(sceneKey, null), onConfirm, onCancel);
+ } catch (e) {
+ openConfirmModal(mergeScript(sceneKey, null), onConfirm, onCancel);
+ }
+}
+
+export async function matchAutoReply(userText) {
+ if (!userText || !userText.trim()) {
+ return { matched: false, content: '', reply_type: 'text' };
+ }
+ try {
+ const res = await request({
+ url: '/peizhi/huashu_match',
+ method: 'POST',
+ data: {
+ scene_key: 'cs_auto_reply',
+ user_text: userText.trim(),
+ },
+ });
+ if (res && res.data && res.data.code === 200 && res.data.data) {
+ return res.data.data;
+ }
+ } catch (e) {
+ console.warn('matchAutoReply failed', e);
+ }
+ return { matched: false, content: '', reply_type: 'text' };
+}
+
+export async function getWelcomeText() {
+ const data = await fetchScripts(['cs_welcome']);
+ const script = data.cs_welcome || DEFAULT_SCRIPTS.cs_welcome;
+ return script.content || DEFAULT_SCRIPTS.cs_welcome.content;
+}
+
+export default {
+ fetchScripts,
+ showConfirmByScene,
+ matchAutoReply,
+ getWelcomeText,
+};