You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3676 lines
130 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const cloud = require("wx-server-sdk");
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});
const axios = require('axios');
const crypto = require('crypto');
const db = cloud.database();
const _ = db.command; // 数据库命令引用
// 获取openid
const getOpenId = async () => {
// 获取基础信息
const wxContext = cloud.getWXContext();
return {
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
};
};
const sendChatMessage = async (event) => {
try {
const wxContext = cloud.getWXContext();
const fromOpenId = wxContext.OPENID;
const toUserIdParam = String(event?.toUserId || '').trim();
const toOpenIdParam = String(event?.toOpenId || '').trim();
const contentType = String(event?.contentType || '').trim();
const content = String(event?.content || '').trim();
const imageUrl = String(event?.imageUrl || '');
const productId = String(event?.productId || '');
if (!contentType) return { success: false, error: 'contentType required' };
if (contentType === 'text' && !content) return { success: false, error: 'content required' };
if (contentType === 'image' && !imageUrl) return { success: false, error: 'imageUrl required' };
let toUserId = '';
let toOpenId = '';
if (toUserIdParam) {
const uById = await db.collection('T_user').doc(toUserIdParam).get();
const uDoc = uById.data || null;
toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam;
toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam;
} else if (toOpenIdParam) {
const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get();
const uDoc = (uByOpen.data && uByOpen.data[0]) || null;
toOpenId = toOpenIdParam;
toUserId = uDoc ? (uDoc._id || '') : '';
}
if (!toOpenId && !toUserId) return { success: false, error: 'toUserId or toOpenId required' };
if (!toOpenId && toUserId) {
const uById2 = await db.collection('T_user').doc(toUserId).get();
const uDoc2 = uById2.data || null;
toOpenId = uDoc2 ? (uDoc2._openid || '') : '';
}
if (!toOpenId || toOpenId === fromOpenId) return { success: false, error: 'invalid recipient' };
const meRes = await db.collection('T_user').where({ _openid: fromOpenId }).limit(1).get();
const meDoc = (meRes.data && meRes.data[0]) || null;
const fromUserId = meDoc ? (meDoc._id || '') : '';
const sessionKey = [fromOpenId, toOpenId].sort().join('|');
const sessionKeyUser = (fromUserId && toUserId) ? [fromUserId, toUserId].sort().join('|') : '';
const nowTs = Date.now();
const isSystem = Boolean(event?.isSystem);
const orderId = String(event?.orderId || '');
const chatDoc = {
sessionKey,
sessionKeyUser,
productId,
fromOpenId,
toOpenId,
fromUserId,
toUserId,
contentType,
content,
imageUrl,
isSystem,
orderId,
timestamp: nowTs,
createTime: new Date()
};
const addRes = await db.collection('T_chat').add({ data: chatDoc });
return { success: true, id: addRes._id || '', data: { ...chatDoc, _id: addRes._id || '' } };
} catch (e) {
return { success: false, error: e.message };
}
};
const getChatMessages = async (event) => {
try {
const wxContext = cloud.getWXContext();
const myOpenId = wxContext.OPENID;
const toUserIdParam = String(event?.toUserId || '').trim();
const toOpenIdParam = String(event?.toOpenId || '').trim();
const limit = Math.max(1, Math.min(200, Number(event?.limit) || 100));
const before = Number(event?.before) || 0;
let toOpenId = '';
let toUserId = '';
if (toUserIdParam) {
const uById = await db.collection('T_user').doc(toUserIdParam).get();
const uDoc = uById.data || null;
toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam;
toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam;
} else if (toOpenIdParam) {
const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get();
const uDoc = (uByOpen.data && uByOpen.data[0]) || null;
toOpenId = toOpenIdParam;
toUserId = uDoc ? (uDoc._id || '') : '';
}
if (!toOpenId && !toUserId) return { success: false, error: 'toUserId or toOpenId required' };
if (!toOpenId && toUserId) {
const uById2 = await db.collection('T_user').doc(toUserId).get();
const uDoc2 = uById2.data || null;
toOpenId = uDoc2 ? (uDoc2._openid || '') : '';
}
const sessionKey = [myOpenId, toOpenId].sort().join('|');
let query = null;
if (toUserId) {
const meRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get();
const meDoc = (meRes.data && meRes.data[0]) || null;
const myUserId = meDoc ? (meDoc._id || '') : '';
const sessionKeyUser = (myUserId && toUserId) ? [myUserId, toUserId].sort().join('|') : '';
query = sessionKeyUser ? db.collection('T_chat').where({ sessionKeyUser }) : db.collection('T_chat').where({ sessionKey });
} else {
query = db.collection('T_chat').where({ sessionKey });
}
if (before > 0) {
query = query.where({ timestamp: _.lt(before) });
}
const ret = await query.orderBy('timestamp', 'asc').limit(limit).get();
const docs = (ret.data || []).filter(d => d && (d.fromOpenId === myOpenId || d.toOpenId === myOpenId));
return { success: true, data: docs };
} catch (e) {
return { success: false, error: e.message };
}
};
const revokeChatMessage = async (event) => {
try {
const wxContext = cloud.getWXContext();
const myOpenId = wxContext.OPENID;
const id = String(event?.id || '').trim();
if (!id) return { success: false, error: 'id required' };
const doc = await db.collection('T_chat').doc(id).get();
const m = doc.data || null;
if (!m) return { success: false, error: 'not found' };
if (m.fromOpenId !== myOpenId) return { success: false, error: 'not sender' };
const now = Date.now();
if ((now - (m.timestamp || 0)) > 2 * 60 * 1000) return { success: false, error: 'timeout' };
await db.collection('T_chat').doc(id).update({ data: { isRevoked: true, contentType: 'revoke', content: '' } });
return { success: true };
} catch (e) {
return { success: false, error: e.message };
}
};
const listChatSessions = async () => {
try {
const wxContext = cloud.getWXContext();
const myOpenId = wxContext.OPENID;
const uRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get();
const me = (uRes.data && uRes.data[0]) || null;
const myUserId = me ? (me._id || '') : '';
let msgs = [];
if (myUserId) {
const retUser = await db.collection('T_chat')
.where(_.or([{ fromUserId: myUserId }, { toUserId: myUserId }]))
.orderBy('timestamp', 'desc')
.limit(200)
.get();
msgs = retUser.data || [];
}
if (!msgs.length) {
const retOpen = await db.collection('T_chat')
.where(_.or([{ fromOpenId: myOpenId }, { toOpenId: myOpenId }]))
.orderBy('timestamp', 'desc')
.limit(200)
.get();
msgs = retOpen.data || [];
}
const bySession = {};
msgs.forEach(m => {
const sk = m.sessionKeyUser || (m.sessionKey || [m.fromOpenId, m.toOpenId].sort().join('|'));
const ts = m.timestamp || 0;
if (!bySession[sk] || ts > (bySession[sk].timestamp || 0)) bySession[sk] = m;
});
let sessions = Object.values(bySession).map(m => {
const peerUserId = (m.fromUserId === myUserId ? m.toUserId : m.fromUserId) || '';
const peerOpenId = (m.fromOpenId === myOpenId ? m.toOpenId : m.fromOpenId) || '';
return {
sessionKey: m.sessionKey || [m.fromOpenId, m.toOpenId].sort().join('|'),
sessionKeyUser: m.sessionKeyUser || ((myUserId && peerUserId) ? [myUserId, peerUserId].sort().join('|') : ''),
peerUserId,
peerOpenId,
lastContentType: m.contentType,
lastContent: m.contentType === 'text' ? m.content : '[图片]',
lastTime: m.timestamp || Date.now(),
peerName: '',
peerAvatar: '',
unreadCount: 0
};
});
const peerUserIds = sessions.map(s => s.peerUserId).filter(Boolean);
if (peerUserIds.length) {
const uresById = await db.collection('T_user').where({ _id: _.in(peerUserIds) }).get();
const umap = {};
(uresById.data || []).forEach(u => { umap[u._id] = u; });
sessions = sessions.map(s => {
const u = umap[s.peerUserId] || {};
return { ...s, peerName: u.sname || u.nickName || '用户', peerAvatar: u.avatar || '' };
});
} else {
const peerOpenIds = sessions.map(s => s.peerOpenId).filter(Boolean);
if (peerOpenIds.length) {
const uresByOpen = await db.collection('T_user').where({ _openid: _.in(peerOpenIds) }).get();
const umap2 = {};
(uresByOpen.data || []).forEach(u => { umap2[u._openid] = u; });
sessions = sessions.map(s => {
const u = umap2[s.peerOpenId] || {};
return { ...s, peerName: u.sname || u.nickName || '用户', peerAvatar: u.avatar || '' };
});
}
}
sessions.sort((a, b) => b.lastTime - a.lastTime);
// 计算未读数
const statesRes = await db.collection('T_chat_read_state').where({ myOpenId }).get();
const states = statesRes.data || [];
const stateMap = new Map();
states.forEach(s => {
const key = s.sessionKeyUser || s.sessionKey;
stateMap.set(key, s.lastReadTime || 0);
});
for (let i = 0; i < sessions.length; i++) {
const s = sessions[i];
const key = s.sessionKeyUser || s.sessionKey;
const lastRead = stateMap.get(key) || 0;
let q = null;
if (s.sessionKeyUser) {
q = db.collection('T_chat').where({ sessionKeyUser: s.sessionKeyUser, toOpenId: myOpenId, timestamp: _.gt(lastRead) });
} else {
q = db.collection('T_chat').where({ sessionKey: s.sessionKey, toOpenId: myOpenId, timestamp: _.gt(lastRead) });
}
try {
const cnt = await q.count();
s.unreadCount = cnt.total || 0;
} catch (e) {
s.unreadCount = 0;
}
}
return { success: true, data: sessions };
} catch (e) {
return { success: false, error: e.message };
}
};
const updateChatReadState = async (event) => {
try {
const wxContext = cloud.getWXContext();
const myOpenId = wxContext.OPENID;
const toUserIdParam = String(event?.toUserId || '').trim();
const toOpenIdParam = String(event?.toOpenId || '').trim();
let toOpenId = '';
let toUserId = '';
if (toUserIdParam) {
const uById = await db.collection('T_user').doc(toUserIdParam).get();
const uDoc = uById.data || null;
toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam;
toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam;
} else if (toOpenIdParam) {
const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get();
const uDoc = (uByOpen.data && uByOpen.data[0]) || null;
toOpenId = toOpenIdParam;
toUserId = uDoc ? (uDoc._id || '') : '';
}
if (!toOpenId && toUserId) {
const uById2 = await db.collection('T_user').doc(toUserId).get();
const uDoc2 = uById2.data || null;
toOpenId = uDoc2 ? (uDoc2._openid || '') : '';
}
const meRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get();
const meDoc = (meRes.data && meRes.data[0]) || null;
const myUserId = meDoc ? (meDoc._id || '') : '';
const sessionKey = [myOpenId, toOpenId].sort().join('|');
const sessionKeyUser = (myUserId && toUserId) ? [myUserId, toUserId].sort().join('|') : '';
const now = Date.now();
const col = db.collection('T_chat_read_state');
const where = sessionKeyUser ? { sessionKeyUser, myOpenId } : { sessionKey, myOpenId };
const exist = await col.where(where).limit(1).get();
if (exist.data && exist.data[0]) {
await col.doc(exist.data[0]._id).update({ data: { lastReadTime: now } });
} else {
await col.add({ data: { sessionKey, sessionKeyUser, myOpenId, myUserId, lastReadTime: now, createTime: new Date() } });
}
return { success: true };
} catch (e) {
return { success: false, error: e.message };
}
};
// 获取小程序二维码
const getMiniProgramCode = async () => {
// 获取小程序二维码的buffer
const resp = await cloud.openapi.wxacode.get({
path: "pages/index/index",
});
const { buffer } = resp;
// 将图片上传云存储空间
const upload = await cloud.uploadFile({
cloudPath: "code.png",
fileContent: buffer,
});
return upload.fileID;
};
// 创建集合
const createCollection = async () => {
try {
// 创建集合
await db.createCollection("sales");
await db.collection("sales").add({
// data 字段表示需新增的 JSON 数据
data: {
region: "华东",
city: "上海",
sales: 11,
},
});
await db.collection("sales").add({
// data 字段表示需新增的 JSON 数据
data: {
region: "华东",
city: "南京",
sales: 11,
},
});
await db.collection("sales").add({
// data 字段表示需新增的 JSON 数据
data: {
region: "华南",
city: "广州",
sales: 22,
},
});
await db.collection("sales").add({
// data 字段表示需新增的 JSON 数据
data: {
region: "华南",
city: "深圳",
sales: 22,
},
});
return {
success: true,
};
} catch (e) {
// 这里catch到的是该collection已经存在从业务逻辑上来说是运行成功的所以catch返回success给前端避免工具在前端抛出异常
return {
success: true,
data: "create collection success",
};
}
};
// 查询数据
const selectRecord = async () => {
// 返回数据库查询结果
return await db.collection("sales").get();
};
// 更新数据
const updateRecord = async (event) => {
try {
// 遍历修改数据库信息
for (let i = 0; i < event.data.length; i++) {
await db
.collection("sales")
.where({
_id: event.data[i]._id,
})
.update({
data: {
sales: event.data[i].sales,
},
});
}
return {
success: true,
data: event.data,
};
} catch (e) {
return {
success: false,
errMsg: e,
};
}
};
// 新增数据
const insertRecord = async (event) => {
try {
const insertRecord = event.data;
// 插入数据
await db.collection("sales").add({
data: {
region: insertRecord.region,
city: insertRecord.city,
sales: Number(insertRecord.sales),
},
});
return {
success: true,
data: event.data,
};
} catch (e) {
return {
success: false,
errMsg: e,
};
}
};
// 删除数据
const deleteRecord = async (event) => {
try {
await db
.collection("sales")
.where({
_id: event.data._id,
})
.remove();
return {
success: true,
};
} catch (e) {
return {
success: false,
errMsg: e,
};
}
};
// 通过云函数进行腾讯位置服务逆地理编码(更安全,避免前端域名/密钥限制)
const cloudReverseGeocode = async (event) => {
try {
const lat = Number(event.latitude || (event.location && event.location.latitude));
const lng = Number(event.longitude || (event.location && event.location.longitude));
// 组装可用 Keys优先单 Key其次多 Key 列表(逗号分隔)
const primaryKey = process.env.QQMAP_KEY || event.key; // 优先使用云环境变量
const multiKeysStr = process.env.QQMAP_KEYS || event.keys || '';
const extraKeys = multiKeysStr.split(',').map(k => k.trim()).filter(Boolean);
const keys = [];
if (primaryKey) keys.push(primaryKey);
for (const k of extraKeys) { if (!keys.includes(k)) keys.push(k); }
if (keys.length === 0) {
return { success: false, error: '缺少QQMAP_KEY请在云环境变量配置或传入key' };
}
if (!isFinite(lat) || !isFinite(lng)) {
return { success: false, error: '坐标参数无效' };
}
// 规范化 Referer移除所有非 URL 合法字符(含空格、反引号等),避免校验失败
const refererRaw = process.env.QQMAP_REFERER || event.referer || '';
// 规范化 Referer显式去除常见引号字符包含反引号、中文引号并移除空白
const normalizeReferer = (val) => {
let s = String(val || '')
.replace(/[`"'“”‘’]/g, '')
.replace(/\s+/g, '');
// 二次清理:仅保留 URL 常见合法字符
s = s.replace(/[^a-zA-Z0-9:\/._-]/g, '');
// 若为空或不以 http(s) 开头,兜底为微信域
if (!s || !/^https?:\/\//.test(s)) return 'https://servicewechat.com';
// 若包含 servicewechat.com则统一为基础域避免子路径差异
if (s.includes('servicewechat.com')) return 'https://servicewechat.com';
return s;
};
const referer = normalizeReferer(refererRaw);
const sk = process.env.QQMAP_SK || event.sk || '';
const urlBase = 'https://apis.map.qq.com';
const apiPathVariants = ['/ws/geocoder/v1', '/ws/geocoder/v1/'];
let lastError = null;
console.log('[Reverse] using referer:', referer);
for (const k of keys) {
// 仅使用必需参数参与签名,避免含 '=' 的值(如 poi_options造成歧义
const base = { key: k, location: `${lat},${lng}` };
const keysSorted = Object.keys(base).sort();
const qsPlain = keysSorted.map(p => `${p}=${base[p]}`).join('&');
const qsEncoded = keysSorted.map(p => `${p}=${encodeURIComponent(String(base[p]))}`).join('&');
const qsReqBase = qsEncoded;
for (const apiPath of apiPathVariants) {
let qsReq = qsReqBase;
// 先尝试带签名
if (sk) {
// 同时尝试“未编码”和“已编码”两种签名算法
const sigStrPlain = `${apiPath}?${qsPlain}${sk}`;
const sigPlain = crypto.createHash('md5').update(sigStrPlain).digest('hex');
console.log('[Reverse] try sigPlain', { apiPath, qsPlain, sigPlain });
let tried = false;
// 先尝试 plain 签名
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigPlain}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
tried = true;
if (resp.data && resp.data.status === 0) {
const r = resp.data.result || {};
return {
success: true,
data: {
address: r.address || '',
formatted_addresses: r.formatted_addresses || {},
address_component: r.address_component || {},
address_reference: r.address_reference || {},
ad_info: r.ad_info || {},
location: { lat, lng }
}
};
} else {
lastError = new Error(resp.data?.message || '逆地理编码失败');
}
} catch (e) {
lastError = e;
}
// 若失败再尝试 encoded 签名
try {
const sigStrEnc = `${apiPath}?${qsEncoded}${sk}`;
const sigEnc = crypto.createHash('md5').update(sigStrEnc).digest('hex');
console.log('[Reverse] try sigEnc', { apiPath, qsEncoded, sigEnc });
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigEnc}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0) {
const r = resp.data.result || {};
return {
success: true,
data: {
address: r.address || '',
formatted_addresses: r.formatted_addresses || {},
address_component: r.address_component || {},
address_reference: r.address_reference || {},
ad_info: r.ad_info || {},
location: { lat, lng }
}
};
} else {
lastError = new Error(resp.data?.message || '逆地理编码失败');
}
} catch (e) {
lastError = e;
}
// 如果上面两种签名都失败,将在后面尝试无签名请求
}
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0) {
const r = resp.data.result || {};
return {
success: true,
data: {
address: r.address || '',
formatted_addresses: r.formatted_addresses || {},
address_component: r.address_component || {},
address_reference: r.address_reference || {},
ad_info: r.ad_info || {},
location: { lat, lng }
}
};
} else {
lastError = new Error(resp.data?.message || '逆地理编码失败');
}
} catch (e) {
lastError = e;
}
// 若签名失败且开启了签名校验,尝试不带签名的请求(部分控制台配置允许)
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReqBase}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0) {
const r = resp.data.result || {};
return {
success: true,
data: {
address: r.address || '',
formatted_addresses: r.formatted_addresses || {},
address_component: r.address_component || {},
address_reference: r.address_reference || {},
ad_info: r.ad_info || {},
location: { lat, lng }
}
};
}
} catch (e) {
// 保留最后的错误用于返回
lastError = e;
}
}
}
return { success: false, error: lastError?.message || '逆地理编码失败' };
} catch (e) {
return { success: false, error: e.message };
}
};
// 正向地理编码:根据地址获取坐标(支持多 Key 与签名)
const cloudGeocode = async (event) => {
try {
const address = String(event.address || '').trim();
if (!address) {
return { success: false, error: '地址为空' };
}
const primaryKey = process.env.QQMAP_KEY || event.key; // 优先环境变量
const multiKeysStr = process.env.QQMAP_KEYS || event.keys || '';
const extraKeys = multiKeysStr.split(',').map(k => k.trim()).filter(Boolean);
const keys = [];
if (primaryKey) keys.push(primaryKey);
for (const k of extraKeys) { if (!keys.includes(k)) keys.push(k); }
if (keys.length === 0) {
return { success: false, error: '缺少QQMAP_KEY请在云环境变量配置或传入key' };
}
const refererRaw = process.env.QQMAP_REFERER || event.referer || '';
const normalizeReferer = (val) => {
let s = String(val || '')
.replace(/[`"'“”‘’]/g, '')
.replace(/\s+/g, '');
s = s.replace(/[^a-zA-Z0-9:\/._-]/g, '');
if (!s || !/^https?:\/\//.test(s)) return 'https://servicewechat.com';
if (s.includes('servicewechat.com')) return 'https://servicewechat.com';
return s;
};
const referer = normalizeReferer(refererRaw);
const sk = process.env.QQMAP_SK || event.sk || '';
const urlBase = 'https://apis.map.qq.com';
const apiPathVariants = ['/ws/geocoder/v1', '/ws/geocoder/v1/'];
let lastError = null;
console.log('[Forward] using referer:', referer);
for (const k of keys) {
// 仅使用必需参数参与签名
const base = { address, key: k };
const keysSorted = Object.keys(base).sort();
const qsPlain = keysSorted.map(p => `${p}=${base[p]}`).join('&');
const qsEncoded = keysSorted.map(p => `${p}=${encodeURIComponent(String(base[p]))}`).join('&');
const qsReqBase = qsEncoded;
for (const apiPath of apiPathVariants) {
let qsReq = qsReqBase;
// 先尝试带签名
if (sk) {
// 同时尝试“未编码”和“已编码”两种签名算法
const sigStrPlain = `${apiPath}?${qsPlain}${sk}`;
const sigPlain = crypto.createHash('md5').update(sigStrPlain).digest('hex');
console.log('[Forward] try sigPlain', { apiPath, qsPlain, sigPlain });
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigPlain}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) {
const loc = resp.data.result.location;
return { success: true, data: { latitude: loc.lat, longitude: loc.lng } };
} else {
lastError = new Error(resp.data?.message || '正向地理编码失败');
}
} catch (e) {
lastError = e;
}
try {
const sigStrEnc = `${apiPath}?${qsEncoded}${sk}`;
const sigEnc = crypto.createHash('md5').update(sigStrEnc).digest('hex');
console.log('[Forward] try sigEnc', { apiPath, qsEncoded, sigEnc });
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigEnc}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) {
const loc = resp.data.result.location;
return { success: true, data: { latitude: loc.lat, longitude: loc.lng } };
} else {
lastError = new Error(resp.data?.message || '正向地理编码失败');
}
} catch (e) {
lastError = e;
}
}
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) {
const loc = resp.data.result.location;
return { success: true, data: { latitude: loc.lat, longitude: loc.lng } };
} else {
lastError = new Error(resp.data?.message || '正向地理编码失败');
}
} catch (e) {
lastError = e;
}
// 若签名失败,尝试不带签名
try {
const resp = await axios.get(`${urlBase}${apiPath}?${qsReqBase}`, {
timeout: 5000,
headers: referer ? { Referer: referer } : {}
});
if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) {
const loc = resp.data.result.location;
return { success: true, data: { latitude: loc.lat, longitude: loc.lng } };
}
} catch (e) {
lastError = e;
}
}
}
return { success: false, error: lastError?.message || '正向地理编码失败' };
} catch (e) {
return { success: false, error: e.message };
}
};
// 创建通知集合 T_notify
const createNotifyCollection = async () => {
try {
await db.createCollection('T_notify');
return { success: true };
} catch (e) {
// 集合可能已存在,视为成功
return { success: true, data: 'T_notify exists' };
}
};
// 创建聊天集合 T_chat并为 sessionKey/sessionKeyUser + timestamp 建索引(若环境支持)
const createChatCollection = async () => {
try {
await db.createCollection('T_chat');
try {
// 索引创建在部分环境可能不支持,失败不影响主要功能
await db.collection('T_chat').createIndex({
fields: {
sessionKey: 1,
timestamp: -1
},
name: 'idx_session_time'
});
await db.collection('T_chat').createIndex({
fields: {
sessionKeyUser: 1,
timestamp: -1
},
name: 'idx_session_user_time'
});
} catch (e) {
// 忽略索引创建失败
}
return { success: true };
} catch (e) {
// 集合可能已存在,视为成功
return { success: true, data: 'T_chat exists' };
}
};
const createMessageCollection = async () => {
try {
await db.createCollection('T_message');
try {
await db.collection('T_message').createIndex({
fields: { toUserId: 1, fromUserId: 1 },
name: 'idx_to_from'
});
} catch (e) {}
return { success: true };
} catch (e) {
return { success: true, data: 'T_message exists' };
}
};
// 批量为 T_product 补齐缺失的 sellerUserId根据 sellerOpenId 关联 T_user
const backfillSellerUserId = async (event) => {
try {
const batchSize = Math.max(10, Math.min(500, Number(event?.batchSize) || 100));
const dryRun = Boolean(event?.dryRun);
const colProduct = db.collection('T_product');
const colUser = db.collection('T_user');
// 统计总数
const countRes = await colProduct.count();
const total = countRes.total || 0;
let processed = 0;
let corrected = 0;
let skipped = 0;
const openIdToUserId = new Map();
for (let offset = 0; offset < total; offset += batchSize) {
const page = await colProduct.skip(offset).limit(batchSize).get();
const items = page.data || [];
for (const p of items) {
processed += 1;
const hasSellerUserId = !!(p.sellerUserId && String(p.sellerUserId).trim() !== '');
if (hasSellerUserId) {
skipped += 1;
continue;
}
const sellerOpenId = p.sellerOpenId || '';
if (!sellerOpenId) {
skipped += 1;
continue;
}
// 缓存查询,避免重复命中数据库
let userId = openIdToUserId.get(sellerOpenId);
if (!userId) {
const uRes = await colUser.where({ _openid: sellerOpenId }).limit(1).get();
const u = (uRes.data && uRes.data[0]) || null;
userId = u ? u._id : undefined;
if (userId) openIdToUserId.set(sellerOpenId, userId);
}
if (!userId) {
skipped += 1;
continue;
}
if (!dryRun) {
try {
await colProduct.doc(p._id).update({
data: {
sellerUserId: userId
}
});
corrected += 1;
} catch (e) {
// 单条失败不影响整体流程
console.error('更新商品 sellerUserId 失败:', p._id, e?.message);
}
} else {
corrected += 1; // 试运行只统计可修复数
}
}
}
return {
success: true,
message: dryRun ? '试运行完成(未实际写库)' : '回填完成',
total,
processed,
corrected,
skipped,
batchSize,
dryRun
};
} catch (e) {
return {
success: false,
error: e?.message || 'backfillSellerUserId 执行失败'
};
}
};
// 创建用户地址集合 T_address用于存储默认地址等
const createAddressCollection = async () => {
try {
await db.createCollection('T_address');
return { success: true };
} catch (e) {
// 已存在也视为成功
return { success: true, data: 'T_address exists' };
}
};
// 便捷初始化:为当前用户写入一个默认地址(校园地址),便于测试“地址附近交易点”
const seedDefaultAddress = async (event) => {
try {
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
if (!openid) {
return { success: false, error: '未获取openid' };
}
const col = db.collection('T_address');
// 若已有默认地址则直接返回成功
const existing = await col.where({ _openid: openid, isDefault: true }).limit(1).get();
if (existing.data && existing.data.length > 0) {
return { success: true, data: '已有默认地址' };
}
const now = new Date();
await col.add({
data: {
name: event?.name || '默认地址',
phone: event?.phone || '',
province: event?.province || '天津市',
city: event?.city || '东丽区',
district: event?.district || '',
detail: event?.detail || '中国民航大学',
isDefault: true,
createTime: now
}
});
return { success: true };
} catch (e) {
return { success: false, error: e.message };
}
};
// 批量修复商品缺失坐标:优先地标匹配,其次地址正向地理编码
// 用法wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'fixProductCoords', limit: 200, dryRun: false } })
const fixProductCoords = async (event) => {
try {
const limit = Number(event?.limit) > 0 ? Math.min(Number(event.limit), 500) : 200;
const dryRun = !!event?.dryRun;
const col = db.collection('T_product');
// QQMap 配置(若云环境变量未设,可通过 event 传入)
const qqKey = process.env.QQMAP_KEY || event?.key || '';
const qqKeys = process.env.QQMAP_KEYS || event?.keys || '';
const qqSk = process.env.QQMAP_SK || event?.sk || '';
const qqReferer = process.env.QQMAP_REFERER || event?.referer || 'https://servicewechat.com';
// 内置校园地标(云端版,可加入更细的别名以提高匹配率)
const campusLandmarksCloud = [
{ name: '图书馆', latitude: 39.1234, longitude: 117.3365 },
{ name: '体育馆', latitude: 39.1216, longitude: 117.3394 },
{ name: '第一食堂', latitude: 39.1242, longitude: 117.3396 },
{ name: '第二食堂', latitude: 39.1211, longitude: 117.3358 },
{ name: '西门快递点', latitude: 39.1209, longitude: 117.3349 },
{ name: '南门', latitude: 39.1198, longitude: 117.3382 },
{ name: '北门', latitude: 39.1255, longitude: 117.3361 },
// 常见别称与扩展(示例)
{ name: '礼堂', latitude: 39.112869, longitude: 117.349220 },
{ name: '中国民航大学北校区礼堂', latitude: 39.112869, longitude: 117.349220 },
{ name: '北校区礼堂', latitude: 39.112869, longitude: 117.349220 }
];
const normalizeNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const extractCoordsFromString = (s) => {
if (!s || typeof s !== 'string') return null;
const m = s.match(/(-?\d+\.?\d*)[\s,]+(-?\d+\.?\d*)/);
if (m) {
const lat = normalizeNum(m[1]);
const lng = normalizeNum(m[2]);
if (lat != null && lng != null) return { lat, lng };
}
return null;
};
const simplify = (s) => String(s || '')
.replace(/[()]/g, '')
.replace(/附近$/g, '')
.trim();
const matchLandmark = (names) => {
const cands = (names || []).filter(Boolean).map(v => simplify(v));
if (cands.length === 0) return null;
for (const candidate of cands) {
const m = campusLandmarksCloud.find(l => candidate.includes(l.name) || l.name.includes(candidate));
if (m && Number.isFinite(m.latitude) && Number.isFinite(m.longitude)) {
return { lat: m.latitude, lng: m.longitude };
}
}
return null;
};
// 分页获取,筛选出缺坐标的记录
const countRes = await col.count();
const total = (countRes && countRes.total) || 0;
const pageSize = 100;
const toFix = [];
for (let skip = 0; skip < Math.max(total, pageSize) && toFix.length < limit; skip += pageSize) {
const page = await col.skip(skip).limit(pageSize).get();
const batch = (page && page.data) || [];
for (const p of batch) {
const lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude);
const lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude);
if (!(lat != null && lng != null)) {
toFix.push(p);
}
if (toFix.length >= limit) break;
}
if (!page || !page.data || page.data.length < pageSize) break;
}
const results = [];
let fixedCount = 0;
for (const p of toFix) {
let latLng = null;
// 1) 解析字符串坐标
latLng = latLng || extractCoordsFromString(String(p.tradeLocation || ''));
// 2) 地标匹配
latLng = latLng || matchLandmark([p.tradeLandmarkName, p.tradeLocationName, p.tradeAddress]);
// 3) 地址正向地理编码
if (!latLng) {
const addr = simplify(p.tradeAddress || '') || `${simplify(p.tradeLandmarkName || p.tradeLocationName || '')} 中国民航大学 东丽校区`;
if (addr) {
try {
const r = await cloudGeocode({ address: addr, key: qqKey, keys: qqKeys, sk: qqSk, referer: qqReferer });
if (r && r.success && r.data && Number.isFinite(r.data.latitude) && Number.isFinite(r.data.longitude)) {
latLng = { lat: r.data.latitude, lng: r.data.longitude };
}
} catch (e) {
// 忽略该条错误,继续下一个
}
}
}
if (latLng) {
if (!dryRun) {
try {
await col.doc(p._id).update({ data: { tradeLocationLat: latLng.lat, tradeLocationLng: latLng.lng } });
fixedCount += 1;
results.push({ id: p._id, ok: true, lat: latLng.lat, lng: latLng.lng });
} catch (e) {
results.push({ id: p._id, ok: false, error: e.message });
}
} else {
results.push({ id: p._id, ok: true, lat: latLng.lat, lng: latLng.lng, dryRun: true });
}
} else {
results.push({ id: p._id, ok: false, error: '未能解析或编码地址' });
}
}
return { success: true, fixedCount, totalCandidates: toFix.length, dryRun, results };
} catch (e) {
return { success: false, error: e.message };
}
};
// 创建地标集合 T_campus_landmarks若不存在
const createCampusLandmarksCollection = async () => {
try {
await db.createCollection('T_campus_landmarks');
return { success: true };
} catch (e) {
// 已存在也视为成功
return { success: true, data: 'T_campus_landmarks exists' };
}
};
// 规范化/去噪地标名称(用于去重与匹配)
const normalizeLandmarkName = (s) => String(s || '')
.replace(/[()]/g, '')
.replace(/附近$/g, '')
.replace(/\s+/g, '')
.trim();
// 上报/同步单条地标:按规范化名称去重,存在则更新,不存在则新增
// 用法wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'upsertCampusLandmark', name, latitude, longitude, address, tags } })
const upsertCampusLandmark = async (event) => {
try {
const name = String(event?.name || '').trim();
const latitude = Number(event?.latitude);
const longitude = Number(event?.longitude);
const address = String(event?.address || '').trim();
const tags = Array.isArray(event?.tags) ? event.tags : [];
const source = event?.source || 'product';
const productId = String(event?.productId || '').trim();
const selling = typeof event?.selling === 'boolean' ? event.selling : undefined;
const productCategory = String(event?.productCategory || '').trim();
const thumbUrl = String(event?.thumbUrl || '').trim();
if (!name) return { success: false, error: 'name不能为空' };
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
return { success: false, error: '坐标无效' };
}
const col = db.collection('T_campus_landmarks');
const normalizedName = normalizeLandmarkName(name);
// 若集合不存在,尝试创建
try { await db.collection('T_campus_landmarks').limit(1).get(); } catch (e) {
const msg = String(e?.message || e?.errMsg || '');
if (/DATABASE_COLLECTION_NOT_EXIST|ResourceNotFound|Db or Table not exist/i.test(msg)) {
await createCampusLandmarksCollection();
}
}
const existing = await col.where({ normalizedName }).limit(1).get();
const now = new Date();
if (existing.data && existing.data.length > 0) {
const id = existing.data[0]._id;
const prev = existing.data[0] || {};
const prevIds = Array.isArray(prev.productIds) ? prev.productIds : [];
const nextIds = productId && !prevIds.includes(productId) ? [...prevIds, productId] : prevIds;
const data = {
name,
normalizedName,
latitude,
longitude,
address,
tags,
source,
productIds: nextIds,
selling: selling === undefined ? prev.selling : selling,
productCategory: productCategory ? productCategory : prev.productCategory,
thumbUrl: thumbUrl ? thumbUrl : prev.thumbUrl,
updateTime: now
};
await col.doc(id).update({ data });
return { success: true, updated: 1, id };
} else {
const addRes = await col.add({
data: {
name,
normalizedName,
latitude,
longitude,
address,
tags,
source,
productIds: productId ? [productId] : [],
selling: selling === undefined ? true : selling,
productCategory,
thumbUrl,
createTime: now,
updateTime: now
}
});
return { success: true, created: 1, id: addRes._id };
}
} catch (e) {
return { success: false, error: e.message };
}
};
// 批量从商品集合同步可用地标到 T_campus_landmarks去重保留最新坐标
// 支持:旧字段、字符串坐标解析;若无坐标但有 tradeAddress则云端正向地理编码补齐
// 用法wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'syncProductLandmarks', limit: 500, dryRun: false, key: '<QQMAP_KEY>', referer: '<Referer>' } })
const syncProductLandmarks = async (event) => {
try {
const limit = Number(event?.limit) > 0 ? Math.min(Number(event.limit), 1000) : 500;
const dryRun = !!event?.dryRun;
const col = db.collection('T_product');
// 工具:数值规范化与字符串坐标解析、名称简化
const normalizeNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const parseStringCoords = (s) => {
if (!s || typeof s !== 'string') return null;
const m = s.match(/(-?\d+\.?\d*)[\s,]+(-?\d+\.?\d*)/);
if (!m) return null;
const lat = normalizeNum(m[1]);
const lng = normalizeNum(m[2]);
return lat != null && lng != null ? { lat, lng } : null;
};
const simplify = (s) => String(s || '')
.replace(/[()]/g, '')
.replace(/附近$/g, '')
.trim();
const countRes = await col.count();
const total = (countRes && countRes.total) || 0;
const pageSize = 100;
const candidates = [];
for (let skip = 0; skip < Math.max(total, pageSize) && candidates.length < limit; skip += pageSize) {
const page = await col.skip(skip).limit(pageSize).get();
const batch = (page && page.data) || [];
for (const p of batch) {
// 解析坐标:优先数值字段,其次字符串坐标
let lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude);
let lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude);
if (lat == null || lng == null) {
const sc = parseStringCoords(String(p.tradeLocation || p.location || p.coords || ''));
if (sc) { lat = sc.lat; lng = sc.lng; }
}
// 名称:支持嵌套字段与地址,并做简化
const rawName = p.tradeLandmarkName || p.tradeLocationName || p?.tradeLocation?.landmarkName || p.tradeAddress || '';
const name = simplify(rawName);
if (name && lat != null && lng != null) {
candidates.push({ name, lat, lng, address: String(p.tradeAddress || ''), source: 'product', productId: String(p._id || '') });
}
if (candidates.length >= limit) break;
}
if (!page || !page.data || page.data.length < pageSize) break;
}
const results = [];
let created = 0;
let updated = 0;
let geocoded = 0;
// 若存在缺坐标但有地址的潜在项,尝试地理编码补齐
const potentials = [];
{
// 重新扫描一次(避免漏掉仅有地址的商品),但不超过 limit
const countRes2 = await col.count();
const total2 = (countRes2 && countRes2.total) || 0;
const pageSize2 = 100;
for (let skip = 0; skip < Math.max(total2, pageSize2) && potentials.length < limit; skip += pageSize2) {
const page = await col.skip(skip).limit(pageSize2).get();
const batch = (page && page.data) || [];
for (const p of batch) {
// 名称与地址
const rawName = p.tradeLandmarkName || p.tradeLocationName || p?.tradeLocation?.landmarkName || p.tradeAddress || '';
const name = simplify(rawName);
const address = String(p.tradeAddress || '').trim();
// 坐标(含字符串解析)
let lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude);
let lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude);
if (lat == null || lng == null) {
const sc = parseStringCoords(String(p.tradeLocation || p.location || p.coords || ''));
if (sc) { lat = sc.lat; lng = sc.lng; }
}
if ((name || address) && (lat == null || lng == null) && address) {
potentials.push({ name, address, lat, lng, productId: String(p._id || '') });
}
if (potentials.length >= limit) break;
}
if (!page || !page.data || page.data.length < pageSize2) break;
}
}
// 并发地理编码补齐
if (potentials.length > 0) {
const concurrency = 6;
let idx = 0;
const worker = async () => {
while (idx < potentials.length) {
const item = potentials[idx++];
try {
const r = await cloudGeocode({ address: item.address, key: event?.key, keys: event?.keys, referer: event?.referer, sk: event?.sk });
if (r.success && r.data && typeof r.data.latitude === 'number' && typeof r.data.longitude === 'number') {
item.lat = r.data.latitude;
item.lng = r.data.longitude;
geocoded += 1;
// 若名称有效,直接纳入候选进行 upsert
if (item.name && item.lat != null && item.lng != null) {
candidates.push({ name: item.name, lat: item.lat, lng: item.lng, address: item.address, source: 'product', productId: String(item.productId || '') });
}
}
} catch (e) {
// 忽略失败,继续
}
}
};
const workers = Array.from({ length: Math.min(concurrency, potentials.length) }, () => worker());
await Promise.allSettled(workers);
}
for (const c of candidates) {
const normalizedName = normalizeLandmarkName(c.name);
if (dryRun) {
results.push({ name: c.name, normalizedName, latitude: c.lat, longitude: c.lng, dryRun: true });
continue;
}
try {
// 直接调用内部 upsert 逻辑
const r = await upsertCampusLandmark({ name: c.name, latitude: c.lat, longitude: c.lng, address: c.address, source: c.source, productId: c.productId });
if (r.success && r.created) created += r.created;
if (r.success && r.updated) updated += r.updated;
results.push({ name: c.name, normalizedName, latitude: c.lat, longitude: c.lng, ok: r.success, id: r.id });
} catch (e) {
results.push({ name: c.name, normalizedName, error: e.message });
}
}
return { success: true, totalCandidates: candidates.length, created, updated, geocoded, dryRun, results };
} catch (e) {
return { success: false, error: e.message };
}
};
// const getOpenId = require('./getOpenId/index');
// const getMiniProgramCode = require('./getMiniProgramCode/index');
// const createCollection = require('./createCollection/index');
// const selectRecord = require('./selectRecord/index');
// const updateRecord = require('./updateRecord/index');
// const sumRecord = require('./sumRecord/index');
// const fetchGoodsList = require('./fetchGoodsList/index');
// const genMpQrcode = require('./genMpQrcode/index');
// 查询T_user表数据
const queryTUser = async () => {
try {
const result = await db.collection("T_user").get();
return {
success: true,
data: result.data,
count: result.data.length
};
} catch (e) {
return {
success: false,
error: e.message
};
}
};
// 根据openid查询用户信息用于表关联
const getUserByOpenId = async (event) => {
try {
const wxContext = cloud.getWXContext();
const openid = event.openid || wxContext.OPENID;
console.log('查询用户信息openid:', openid);
if (!openid) {
return {
success: false,
error: 'openid不能为空'
};
}
// 先尝试通过_openid查询微信云开发自动添加的字段
let userResult = await db.collection("T_user").where({
_openid: openid
}).get();
// 如果没找到尝试通过其他方式查询如果T_user表中有openid字段
if (userResult.data.length === 0) {
// 这里可以根据实际需求添加其他查询方式
// 例如如果有存储openid字段可以查询
// userResult = await db.collection("T_user").where({
// openid: openid
// }).get();
}
if (userResult.data.length > 0) {
const userData = userResult.data[0];
return {
success: true,
data: {
userId: userData._id,
sno: userData.sno || '',
sname: userData.sname || '',
phone: userData.phone || '',
major: userData.major || '',
sushe: userData.sushe || '',
grade: userData.年级 || '',
avatar: userData.avatar || '',
openid: openid
}
};
} else {
return {
success: false,
error: '未找到用户信息',
openid: openid
};
}
} catch (e) {
console.error('查询用户信息失败:', e);
return {
success: false,
error: e.message
};
}
};
// 添加测试用户到T_user表
const addTestUser = async (event) => {
try {
const userData = event.userData || {
sno: "230340151",
sname: "测试用户",
phone: "13800138000",
password: "100997@mkg",
major: "计算机科学",
sushe: "1号楼 101",
年级: "大三",
avatar: "https://via.placeholder.com/100x100/4285F4/ffffff?text=T"
};
const result = await db.collection("T_user").add({
data: userData
});
return {
success: true,
data: result,
message: "测试用户添加成功"
};
} catch (e) {
return {
success: false,
error: e.message
};
}
};
// AI定价功能 - 调用Coze工作流分析商品图片
const analyzeProductPrice = async (event) => {
const axios = require('axios');
const FormData = require('form-data');
const COZE_API_TOKEN = 'pat_EB0pYMqiuAjIEnGK3i1e12RWwBjF5iZzrzMgdLQE8jNgZWVykJo6ZPGZ2YESYamq';
const COZE_UPLOAD_URL = 'https://api.coze.cn/v1/files/upload';
const COZE_WORKFLOW_URL = 'https://api.coze.cn/v1/workflow/run';
const WORKFLOW_ID = '7567021771821105167';
try {
console.log('========== AI定价功能开始 ==========');
console.log('接收到的参数:', JSON.stringify(event, null, 2));
// 获取参数
const { fileID, originalPrice, imageUrl } = event;
// 确保originalPrice是数字类型
const priceNum = originalPrice ? parseFloat(originalPrice) : null;
if (priceNum && isNaN(priceNum)) {
throw new Error('原价必须是有效的数字');
}
console.log('接收到的参数详情:');
console.log(' fileID:', fileID);
console.log(' originalPrice:', originalPrice, '类型:', typeof originalPrice);
console.log(' imageUrl:', imageUrl);
// 第一步:获取图片文件内容
let imageBuffer = null;
let fileName = 'image.jpg';
if (fileID) {
// 从云存储下载文件
console.log('步骤1: 从云存储下载文件, fileID:', fileID);
try {
if (!fileID || typeof fileID !== 'string') {
throw new Error('fileID参数无效');
}
const result = await cloud.downloadFile({
fileID: fileID
});
if (!result || !result.fileContent) {
throw new Error('下载文件结果为空');
}
imageBuffer = result.fileContent;
fileName = fileID.split('/').pop() || 'image.jpg';
// 确保imageBuffer是Buffer类型
if (!Buffer.isBuffer(imageBuffer)) {
imageBuffer = Buffer.from(imageBuffer);
}
console.log('✅ 文件下载成功,文件名:', fileName, '文件大小:', imageBuffer.length, 'bytes');
} catch (downloadError) {
console.error('❌ 下载云存储文件失败:', downloadError);
console.error('下载错误详情:', JSON.stringify(downloadError, Object.getOwnPropertyNames(downloadError)));
throw new Error('下载图片文件失败: ' + (downloadError.message || downloadError));
}
} else if (imageUrl) {
// 从URL下载图片
console.log('步骤1: 从URL下载图片, imageUrl:', imageUrl);
try {
if (!imageUrl || typeof imageUrl !== 'string') {
throw new Error('imageUrl参数无效');
}
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer',
timeout: 30000 // 30秒超时
});
imageBuffer = Buffer.from(response.data);
fileName = imageUrl.split('/').pop().split('?')[0] || 'image.jpg';
console.log('✅ 图片下载成功,文件名:', fileName, '文件大小:', imageBuffer.length, 'bytes');
} catch (downloadError) {
console.error('❌ 下载图片失败:', downloadError);
console.error('下载错误详情:', downloadError.response?.data || downloadError.message);
throw new Error('下载图片失败: ' + (downloadError.message || downloadError));
}
} else {
throw new Error('缺少必要参数需要提供fileID或imageUrl');
}
// 验证图片缓冲区
if (!imageBuffer || imageBuffer.length === 0) {
throw new Error('图片数据为空');
}
// 第二步上传图片到Coze
console.log('步骤2: 上传图片到Coze');
let cozeFileId = null;
let uploadResponse = null; // 在外部声明,确保在返回时能访问
try {
if (!imageBuffer || imageBuffer.length === 0) {
throw new Error('图片数据为空,无法上传');
}
const formData = new FormData();
formData.append('file', imageBuffer, {
filename: fileName,
contentType: 'image/jpeg'
});
console.log('准备上传到Coze文件大小:', imageBuffer.length, 'bytes');
uploadResponse = await axios.post(COZE_UPLOAD_URL, formData, {
headers: {
'Authorization': `Bearer ${COZE_API_TOKEN}`,
...formData.getHeaders()
},
timeout: 60000, // 60秒超时
maxContentLength: Infinity,
maxBodyLength: Infinity
});
console.log('✅ 图片上传到Coze成功');
console.log('上传响应数据:', JSON.stringify(uploadResponse.data, null, 2));
if (uploadResponse.data.code === 0 && uploadResponse.data.data && uploadResponse.data.data.id) {
cozeFileId = uploadResponse.data.data.id;
console.log('✅ 获取到Coze file_id:', cozeFileId);
} else {
throw new Error('上传响应格式错误: ' + JSON.stringify(uploadResponse.data));
}
} catch (uploadError) {
console.error('❌ 上传图片到Coze失败:', uploadError.response?.data || uploadError.message);
throw new Error('上传图片到Coze失败: ' + (uploadError.response?.data?.msg || uploadError.message));
}
// 第三步调用Coze工作流
console.log('步骤3: 调用Coze工作流');
console.log('workflow_id:', WORKFLOW_ID);
console.log('file_id:', cozeFileId);
try {
// 根据curl示例input参数应该是数字类型原价
// curl示例: "input": 30
// 不再需要inputText直接使用priceNum
// 验证file_id
if (!cozeFileId || typeof cozeFileId !== 'string') {
throw new Error('Coze file_id无效: ' + cozeFileId);
}
// 根据API文档image参数应该是字符串内容是JSON格式的字符串
// 格式: "{\"file_id\":\"...\"}"
// 注意需要手动构建JSON字符串确保格式正确
const imageParam = JSON.stringify({ file_id: String(cozeFileId) });
// 验证image参数格式
try {
const imageObj = JSON.parse(imageParam);
if (!imageObj.file_id || typeof imageObj.file_id !== 'string') {
throw new Error('image参数格式错误: file_id无效');
}
console.log('✅ image参数格式验证通过:', imageObj);
} catch (parseError) {
console.error('❌ image参数格式验证失败:', parseError);
throw new Error('image参数格式错误: ' + parseError.message);
}
// 构建请求对象
const workflowRequest = {
workflow_id: String(WORKFLOW_ID),
parameters: {
image: imageParam, // JSON字符串: "{\"file_id\":\"7567222758887850038\"}"
input: Number(priceNum || 0) // 数字类型原价根据curl示例
}
};
// 验证最终请求格式
const testSerialize = JSON.stringify(workflowRequest);
console.log('测试序列化结果:', testSerialize);
const testParse = JSON.parse(testSerialize);
console.log('测试解析结果:', JSON.stringify(testParse, null, 2));
// 验证解析后的image参数格式
const parsedImage = JSON.parse(testParse.parameters.image);
console.log('解析后的image对象:', parsedImage);
if (!parsedImage.file_id) {
throw new Error('序列化后image参数格式错误');
}
// 打印最终发送的请求(序列化后的格式)
const requestBody = JSON.stringify(workflowRequest);
console.log('========== 请求参数详情 ==========');
console.log('工作流请求数据(序列化前):', JSON.stringify(workflowRequest, null, 2));
console.log('工作流请求数据序列化后将发送给API:', requestBody);
console.log('参数类型检查:');
console.log(' workflow_id类型:', typeof workflowRequest.workflow_id);
console.log(' workflow_id值:', workflowRequest.workflow_id);
console.log(' image类型:', typeof workflowRequest.parameters.image);
console.log(' image值:', workflowRequest.parameters.image);
console.log(' image值长度:', workflowRequest.parameters.image.length);
console.log(' image值解析:', JSON.parse(workflowRequest.parameters.image));
console.log(' input类型:', typeof workflowRequest.parameters.input);
console.log(' input值:', workflowRequest.parameters.input);
console.log('===================================');
// 验证input是数字类型
if (typeof workflowRequest.parameters.input !== 'number') {
throw new Error('input参数必须是数字类型当前类型: ' + typeof workflowRequest.parameters.input);
}
// 模拟curl命令格式用于对比
const curlFormat = JSON.stringify({
workflow_id: workflowRequest.workflow_id,
parameters: {
image: workflowRequest.parameters.image,
input: workflowRequest.parameters.input
}
}, null, 2);
console.log('预期格式类似curl:', curlFormat);
console.log('✅ 参数格式验证通过符合curl示例格式');
const workflowResponse = await axios.post(COZE_WORKFLOW_URL, workflowRequest, {
headers: {
'Authorization': `Bearer ${COZE_API_TOKEN}`,
'Content-Type': 'application/json'
},
timeout: 120000 // 120秒超时因为AI分析可能需要较长时间
});
console.log('✅ Coze工作流HTTP请求成功');
console.log('HTTP状态码:', workflowResponse.status);
console.log('工作流响应数据:', JSON.stringify(workflowResponse.data, null, 2));
// 检查响应状态
if (workflowResponse.data.code !== 0) {
const errorData = workflowResponse.data;
console.error('❌ Coze API返回错误');
console.error('错误代码:', errorData.code);
console.error('错误消息:', errorData.msg);
console.error('错误详情:', errorData.detail);
console.error('调试URL:', errorData.debug_url);
// 如果是参数错误,提供更详细的诊断信息
if (errorData.code === 4000) {
console.error('参数错误诊断:');
console.error(' 发送的请求数据:', JSON.stringify(workflowRequest, null, 2));
console.error(' image参数原始值:', imageParam);
console.error(' input参数原始值:', inputText);
}
throw new Error(`Coze API错误 (${errorData.code}): ${errorData.msg || '未知错误'}`);
}
// 解析返回结果
let parsedResult = {};
if (workflowResponse.data.data) {
try {
// data字段是字符串需要解析
const dataStr = typeof workflowResponse.data.data === 'string'
? workflowResponse.data.data
: JSON.stringify(workflowResponse.data.data);
const parsedData = JSON.parse(dataStr);
// 解析output字段
if (parsedData.output) {
let outputStr = typeof parsedData.output === 'string'
? parsedData.output
: JSON.stringify(parsedData.output);
console.log('步骤3.1: 原始output字符串:', outputStr);
// 尝试解析output中的JSON字符串
try {
// 处理多重转义的情况
// 如果output是被双重转义的JSON字符串需要先解析一次
if (outputStr.startsWith('"') && outputStr.endsWith('"')) {
try {
outputStr = JSON.parse(outputStr);
console.log('步骤3.2: 解析外层引号后的output:', outputStr);
} catch (e) {
// 如果解析失败,继续使用原始字符串
console.warn('步骤3.2: 解析外层引号失败,继续使用原始字符串');
}
}
// 处理转义字符
let cleanedOutput = outputStr;
// 如果仍然是字符串,尝试多次解析转义
if (typeof cleanedOutput === 'string') {
// 尝试解析转义的JSON字符串
try {
// 如果看起来像转义的JSON尝试解析
if (cleanedOutput.includes('\\"')) {
cleanedOutput = cleanedOutput.replace(/\\"/g, '"').replace(/\\n/g, '\n');
}
} catch (e) {
// 忽略解析错误
}
}
console.log('步骤3.3: 清理后的output:', cleanedOutput);
console.log('步骤3.3: output类型:', typeof cleanedOutput);
// 使用正则表达式提取字段值
// 匹配格式: "name":"值" 或 name:"值" 或 name=值
const outputContent = typeof cleanedOutput === 'string' ? cleanedOutput : JSON.stringify(cleanedOutput);
const nameMatch = outputContent.match(/["']?name["']?\s*[:=]\s*["']([^"']+)["']/);
const priceMatch = outputContent.match(/["']?price["']?\s*[:=]\s*["']([^"']+)["']/);
const chengseMatch = outputContent.match(/["']?chengse["']?\s*[:=]\s*["']([^"']+)["']/);
const gradeMatch = outputContent.match(/["']?grade["']?\s*[:=]\s*["']([^"']+)["']/);
const suggMatch = outputContent.match(/["']?sugg["']?\s*[:=]\s*["']([^"']+)["']/);
console.log('步骤3.4: 正则匹配结果:');
console.log(' name:', nameMatch ? nameMatch[1] : '未匹配');
console.log(' price:', priceMatch ? priceMatch[1] : '未匹配');
console.log(' chengse:', chengseMatch ? chengseMatch[1] : '未匹配');
console.log(' grade:', gradeMatch ? gradeMatch[1] : '未匹配');
console.log(' sugg:', suggMatch ? (suggMatch[1].substring(0, 50) + '...') : '未匹配');
// 如果正则匹配失败尝试直接解析为JSON对象
let parsedOutput = null;
try {
// 尝试构建一个完整的JSON对象
if (outputContent.includes('name') && outputContent.includes('price')) {
// 尝试提取JSON部分
const jsonMatch = outputContent.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsedOutput = JSON.parse(jsonMatch[0]);
console.log('步骤3.5: 成功解析为JSON对象:', parsedOutput);
} else {
// 尝试手动构建JSON字符串
const jsonStr = '{' + outputContent.replace(/\n/g, ',') + '}';
try {
parsedOutput = JSON.parse(jsonStr);
console.log('步骤3.5: 成功构建并解析JSON对象:', parsedOutput);
} catch (e) {
// 忽略错误
}
}
}
} catch (e) {
console.warn('步骤3.5: 解析JSON对象失败:', e.message);
}
// 优先使用解析后的对象,否则使用正则匹配
parsedResult = {
productName: parsedOutput?.name || nameMatch?.[1] || '--',
suggestedPrice: parsedOutput?.price || priceMatch?.[1] || '0.00',
conditionLevel: parsedOutput?.chengse || chengseMatch?.[1] || '--',
aiScore: parsedOutput?.grade || gradeMatch?.[1] || '--',
analysisReport: parsedOutput?.sugg || suggMatch?.[1] || 'AI分析完成',
rawOutput: outputContent
};
console.log('✅ 步骤3.6: 最终解析结果:', JSON.stringify(parsedResult, null, 2));
} catch (parseError) {
console.warn('⚠️ 解析output失败使用原始数据:', parseError.message);
console.warn('解析错误堆栈:', parseError.stack);
parsedResult = {
productName: '--',
suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00',
conditionLevel: '--',
aiScore: '--',
analysisReport: outputStr.substring(0, 200),
rawOutput: outputStr
};
}
} else {
parsedResult = {
productName: '--',
suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00',
conditionLevel: '--',
aiScore: '--',
analysisReport: 'AI分析完成但未获取到详细信息',
rawOutput: dataStr
};
}
} catch (parseError) {
console.error('❌ 解析响应数据失败:', parseError);
parsedResult = {
productName: '--',
suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00',
conditionLevel: '--',
aiScore: '--',
analysisReport: '解析响应数据失败: ' + parseError.message,
rawResponse: workflowResponse.data
};
}
} else {
console.warn('⚠️ 工作流响应中没有data字段');
console.warn('响应内容:', JSON.stringify(workflowResponse.data, null, 2));
// 如果响应中没有data返回默认结果
parsedResult = {
productName: '--',
suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00',
conditionLevel: '--',
aiScore: '--',
analysisReport: 'AI分析完成但未获取到详细信息',
rawResponse: workflowResponse.data
};
}
console.log('========== AI定价功能完成 ==========');
return {
success: true,
data: parsedResult,
debug: {
uploadResponse: uploadResponse ? uploadResponse.data : null,
workflowResponse: workflowResponse.data
}
};
} catch (workflowError) {
console.error('❌ 调用Coze工作流失败');
console.error('错误对象:', workflowError);
console.error('错误响应:', workflowError.response?.data);
console.error('错误状态码:', workflowError.response?.status);
console.error('错误消息:', workflowError.message);
// 提取详细的错误信息
let errorMsg = '调用Coze工作流失败';
if (workflowError.response?.data) {
const errorData = workflowError.response.data;
errorMsg = errorData.msg || errorData.message || errorMsg;
if (errorData.detail) {
errorMsg += '\n详情: ' + JSON.stringify(errorData.detail);
}
if (errorData.code) {
errorMsg += '\n错误代码: ' + errorData.code;
}
} else {
errorMsg += ': ' + workflowError.message;
}
throw new Error(errorMsg);
}
} catch (error) {
console.error('========== AI定价功能出错 ==========');
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
return {
success: false,
error: error.message,
details: error.response?.data || error.stack
};
}
};
// 初始化T_category表如果不存在则创建
const initCategoryTable = async () => {
try {
// 默认类别数据
const defaultCategories = [
{ categoryName: '电子产品', description: '手机、电脑、配件', icon: '📱', iconBg: '#2196F3', sort: 1, enabled: true },
{ categoryName: '图书文具', description: '教材、小说、专业书籍、文具', icon: '📚', iconBg: '#4CAF50', sort: 2, enabled: true },
{ categoryName: '服装鞋帽', description: '衣服、鞋子、配饰', icon: '👕', iconBg: '#FF9800', sort: 3, enabled: true },
{ categoryName: '家居用品', description: '桌椅、小家电、生活用品', icon: '🛋️', iconBg: '#795548', sort: 4, enabled: true },
{ categoryName: '运动户外', description: '健身器材、球类运动、户外用品', icon: '⚽', iconBg: '#9C27B0', sort: 5, enabled: true },
{ categoryName: '美妆个护', description: '美妆护肤产品', icon: '💄', iconBg: '#FF6B9D', sort: 6, enabled: true },
{ categoryName: '其他', description: '其他各类商品', icon: '📦', iconBg: '#757575', sort: 7, enabled: true }
];
// 检查T_category表是否存在数据
const existing = await db.collection("T_category").get();
if (existing.data.length === 0) {
// 初始化类别数据使用循环add避免batch兼容性问题
for (const category of defaultCategories) {
await db.collection("T_category").add({
data: {
...category,
createTime: new Date(),
updateTime: new Date()
}
});
}
console.log('T_category表初始化成功');
}
return {
success: true,
message: 'T_category表已初始化'
};
} catch (e) {
console.error('初始化T_category表失败:', e);
return {
success: false,
error: e.message
};
}
};
// 获取所有商品类别优先从T_category表获取如果没有则从T_product表提取
const getProductCategories = async () => {
try {
// 先尝试从T_category表获取
let categoriesResult = await db.collection("T_category")
.where({
enabled: true
})
.orderBy('sort', 'asc')
.get();
if (categoriesResult.data.length > 0) {
// 从T_category表获取类别
const categories = categoriesResult.data.map(item => ({
id: item.categoryName,
name: item.categoryName,
description: item.description || getCategoryDescription(item.categoryName),
icon: item.icon || getCategoryIcon(item.categoryName),
iconBg: item.iconBg || getCategoryBg(item.categoryName)
}));
return {
success: true,
data: categories,
source: 'T_category'
};
}
// 如果T_category表为空从T_product表提取兼容旧数据
console.log('T_category表为空从T_product表提取类别');
const result = await db.collection("T_product").field({
productCategory: true
}).get();
// 提取不重复的类别
const categoriesSet = new Set();
result.data.forEach(item => {
if (item.productCategory) {
categoriesSet.add(item.productCategory);
}
});
// 转换为数组并返回
const categories = Array.from(categoriesSet).map(category => ({
id: category,
name: category,
description: getCategoryDescription(category),
icon: getCategoryIcon(category),
iconBg: getCategoryBg(category)
}));
// 如果数据库中没有类别,返回默认类别
if (categories.length === 0) {
return {
success: true,
data: [
{ id: '电子产品', name: '电子产品', description: '手机、电脑、配件', icon: '📱', iconBg: '#2196F3' },
{ id: '图书文具', name: '图书文具', description: '教材、小说、专业书籍、文具', icon: '📚', iconBg: '#4CAF50' },
{ id: '服装鞋帽', name: '服装鞋帽', description: '衣服、鞋子、配饰', icon: '👕', iconBg: '#FF9800' },
{ id: '家居用品', name: '家居用品', description: '桌椅、小家电、生活用品', icon: '🛋️', iconBg: '#795548' },
{ id: '运动户外', name: '运动户外', description: '健身器材、球类运动、户外用品', icon: '⚽', iconBg: '#9C27B0' },
{ id: '美妆个护', name: '美妆个护', description: '美妆护肤产品', icon: '💄', iconBg: '#FF6B9D' },
{ id: '其他', name: '其他', description: '其他各类商品', icon: '📦', iconBg: '#757575' }
],
source: 'default'
};
}
return {
success: true,
data: categories,
source: 'T_product'
};
} catch (e) {
console.error('获取商品类别失败:', e);
return {
success: false,
error: e.message
};
}
};
// 获取类别描述
const getCategoryDescription = (category) => {
const descriptions = {
'电子产品': '手机、电脑、配件',
'图书文具': '教材、小说、专业书籍、文具',
'服装鞋帽': '衣服、鞋子、配饰',
'家居用品': '桌椅、小家电、生活用品',
'运动户外': '健身器材、球类运动、户外用品',
'美妆个护': '美妆护肤产品',
'其他': '其他各类商品'
};
return descriptions[category] || '各类商品';
};
// 获取类别图标使用emoji
const getCategoryIcon = (category) => {
const icons = {
'电子产品': '📱',
'图书文具': '📚',
'服装鞋帽': '👕',
'家居用品': '🛋️',
'运动户外': '⚽',
'美妆个护': '💄',
'其他': '📦'
};
return icons[category] || '📦';
};
// 获取类别背景色
const getCategoryBg = (category) => {
const colors = {
'电子产品': '#2196F3',
'图书文具': '#4CAF50',
'服装鞋帽': '#FF9800',
'家居用品': '#795548',
'运动户外': '#9C27B0',
'美妆个护': '#FF6B9D',
'其他': '#757575'
};
return colors[category] || '#757575';
};
// 更新用户兴趣
const updateUserInterests = async (event) => {
try {
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
const interests = event.interests || [];
console.log('更新用户兴趣openid:', openid, 'interests:', interests);
if (!openid) {
return {
success: false,
error: 'openid不能为空'
};
}
// 查询用户是否存在
const userResult = await db.collection("T_user").where({
_openid: openid
}).get();
if (userResult.data.length === 0) {
return {
success: false,
error: '用户不存在'
};
}
// 更新用户兴趣
const updateResult = await db.collection("T_user").where({
_openid: openid
}).update({
data: {
interests: interests,
updateTime: new Date()
}
});
console.log('更新用户兴趣成功:', updateResult);
return {
success: true,
data: {
interests: interests,
updateResult: updateResult
}
};
} catch (e) {
console.error('更新用户兴趣失败:', e);
return {
success: false,
error: e.message
};
}
};
// 类别名称映射(兼容旧数据)
const mapCategoryName = (category) => {
const categoryMap = {
'化妆品': '美妆个护',
'二手书': '图书文具',
'运动器材': '运动户外',
'家具家电': '家居用品',
'文具用品': '图书文具',
'其他商品': '其他'
};
return categoryMap[category] || category;
};
// 记录用户行为(浏览、收藏、购买)
const recordUserBehavior = async (event) => {
try {
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
const { productId, behaviorType, productCategory } = event; // behaviorType: 'view', 'favorite', 'purchase'
console.log('记录用户行为,参数:', { openid, productId, behaviorType, productCategory });
if (!productId || !behaviorType) {
console.error('缺少必要参数:', { productId, behaviorType });
return {
success: false,
error: '缺少必要参数'
};
}
if (!openid) {
console.error('缺少openid');
return {
success: false,
error: '用户未登录'
};
}
// 行为权重浏览1分收藏3分购买5分
const behaviorWeights = {
'view': 1,
'favorite': 3,
'purchase': 5
};
const score = behaviorWeights[behaviorType] || 1;
try {
// 检查是否已存在该行为记录
const existingRecord = await db.collection('T_user_behavior')
.where({
_openid: openid,
productId: productId,
behaviorType: behaviorType
})
.get();
if (existingRecord.data && existingRecord.data.length > 0) {
// 更新现有记录的时间
await db.collection('T_user_behavior')
.doc(existingRecord.data[0]._id)
.update({
data: {
updateTime: new Date(),
count: _.inc(1) // 增加行为次数
}
});
console.log('更新用户行为记录成功');
} else {
// 创建新记录
const result = await db.collection('T_user_behavior').add({
data: {
_openid: openid,
productId: productId,
productCategory: productCategory || '',
behaviorType: behaviorType,
score: score,
count: 1,
createTime: new Date(),
updateTime: new Date()
}
});
console.log('创建用户行为记录成功ID:', result._id);
}
return {
success: true,
message: '行为记录成功'
};
} catch (dbError) {
console.error('数据库操作失败:', dbError);
// 如果集合不存在,尝试创建
if (dbError.errMsg && (dbError.errMsg.includes('not exist') || dbError.errMsg.includes('not exists') || dbError.errCode === -502005)) {
console.log('集合不存在,尝试创建 T_user_behavior 集合');
try {
await db.createCollection('T_user_behavior');
console.log('集合创建成功,重新尝试添加记录');
// 重新尝试添加记录
await db.collection('T_user_behavior').add({
data: {
_openid: openid,
productId: productId,
productCategory: productCategory || '',
behaviorType: behaviorType,
score: score,
count: 1,
createTime: new Date(),
updateTime: new Date()
}
});
return {
success: true,
message: '行为记录成功(集合已创建)'
};
} catch (createError) {
console.error('创建集合失败:', createError);
return {
success: false,
error: '集合创建失败: ' + createError.message
};
}
} else {
return {
success: false,
error: dbError.message || '数据库操作失败'
};
}
}
} catch (e) {
console.error('记录用户行为失败:', e);
return {
success: false,
error: e.message
};
}
};
// 创建用户行为集合
const createBehaviorCollection = async () => {
try {
// 检查集合是否已存在
const collections = await db.listCollections();
const collectionNames = collections.data.map(col => col.name);
if (collectionNames.includes('T_user_behavior')) {
return {
success: true,
message: "集合已存在",
};
}
// 创建 T_user_behavior 集合
await db.createCollection("T_user_behavior");
return {
success: true,
message: "T_user_behavior 集合创建成功",
};
} catch (e) {
// 如果集合已存在,也返回成功
if (e.message && e.message.includes('already exists')) {
return {
success: true,
message: "集合已存在",
};
}
console.error('创建 T_user_behavior 集合失败:', e);
return {
success: false,
error: e.message,
};
}
};
// 管理员登录验证
const adminLogin = async (event) => {
try {
const { username, password } = event;
if (!username || !password) {
return {
success: false,
error: '缺少必要参数'
};
}
// 查询管理员表(如果不存在,使用默认管理员)
try {
const adminResult = await db.collection('T_admin')
.where({
username: username,
password: password,
status: 'active'
})
.get();
if (adminResult.data && adminResult.data.length > 0) {
const admin = adminResult.data[0];
return {
success: true,
adminId: admin._id,
username: admin.username,
name: admin.name || '管理员',
role: admin.role || 'admin'
};
}
} catch (err) {
// 如果集合不存在,使用默认管理员账号
if (err.errMsg && err.errMsg.includes('not exist')) {
// 默认管理员账号admin / admin123
if (username === 'admin' && password === 'admin123') {
return {
success: true,
adminId: 'default_admin',
username: 'admin',
name: '系统管理员',
role: 'admin'
};
}
}
}
return {
success: false,
error: '账号或密码错误'
};
} catch (e) {
console.error('管理员登录失败:', e);
return {
success: false,
error: e.message
};
}
};
// 获取管理员统计数据
const getAdminStats = async () => {
try {
console.log('开始获取管理员统计数据...');
// 获取商品总数(包括所有状态的商品)
const productsResult = await db.collection('T_product').count();
const totalProducts = productsResult.total || 0;
console.log('商品总数:', totalProducts);
// 获取用户总数
const usersResult = await db.collection('T_user').count();
const totalUsers = usersResult.total || 0;
console.log('用户总数:', totalUsers);
// 获取订单总数和总销售额
let totalOrders = 0;
let totalSales = 0;
try {
const ordersResult = await db.collection('T_order')
.where({
status: _.in(['已完成', '已支付'])
})
.get();
totalOrders = ordersResult.data.length;
totalSales = ordersResult.data.reduce((sum, order) => {
return sum + (parseFloat(order.totalPrice) || 0);
}, 0);
console.log('订单总数:', totalOrders, '总销售额:', totalSales);
} catch (err) {
console.error('获取订单数据失败:', err);
}
// 获取最近6个月的月度商品发布量
const monthlyProducts = await getMonthlyProducts();
// 获取最近6个月的月度销售额
const monthlySales = await getMonthlySales();
// 获取商品分类统计
const categoryStats = await getCategoryStats();
const result = {
success: true,
data: {
totalProducts,
totalUsers,
totalSales: Math.round(totalSales * 100) / 100,
totalOrders,
monthlyProducts,
monthlySales,
categoryStats
}
};
console.log('统计数据获取成功:', JSON.stringify(result.data, null, 2));
return result;
} catch (e) {
console.error('获取统计数据失败:', e);
return {
success: false,
error: e.message
};
}
};
// 获取月度商品发布量
const getMonthlyProducts = async () => {
try {
const now = new Date();
const months = [];
for (let i = 5; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
const monthStr = `${date.getMonth() + 1}`;
const result = await db.collection('T_product')
.where({
createTime: _.gte(date).and(_.lt(nextDate))
})
.count();
months.push({
month: monthStr,
count: result.total || 0
});
}
return months;
} catch (e) {
console.error('获取月度商品发布量失败:', e);
return [];
}
};
// 获取月度销售额
const getMonthlySales = async () => {
try {
const now = new Date();
const months = [];
for (let i = 5; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
const monthStr = `${date.getMonth() + 1}`;
try {
const result = await db.collection('T_order')
.where({
createTime: _.gte(date).and(_.lt(nextDate)),
status: _.in(['已完成', '已支付'])
})
.get();
const sales = result.data.reduce((sum, order) => {
return sum + (parseFloat(order.totalPrice) || 0);
}, 0);
months.push({
month: monthStr,
sales: Math.round(sales * 100) / 100
});
} catch (err) {
months.push({
month: monthStr,
sales: 0
});
}
}
return months;
} catch (e) {
console.error('获取月度销售额失败:', e);
return [];
}
};
// 获取商品分类统计
const getCategoryStats = async () => {
try {
const result = await db.collection('T_product')
.field({
productCategory: true
})
.get();
const categoryMap = {};
result.data.forEach(item => {
const category = item.productCategory || '其他';
categoryMap[category] = (categoryMap[category] || 0) + 1;
});
return Object.entries(categoryMap).map(([category, count]) => ({
category,
count
})).sort((a, b) => b.count - a.count).slice(0, 8);
} catch (e) {
console.error('获取分类统计失败:', e);
return [];
}
};
// 管理端聚合求购关键词词云基于T_want
const getWantedKeywordCloud = async (event) => {
try {
const limit = event && event.limit ? parseInt(event.limit) : 60;
// 拉取活跃求购数据字段productName、purchaseDescription、expectedBrand、expectedModel、productCategory
const res = await db.collection('T_want')
.field({ productName: true, description: true, purchaseDescription: true, expectedBrand: true, expectedModel: true, productCategory: true, status: true })
.get();
const stopwords = new Set(['求购','求','购买','需要','急需','求助','有没有','二手','全新','新','的','和','及','或','我','我们','联系','微信','电话','价','价格','预算','希望','求一个','一个','一台','一本','一套']);
const freq = {};
const toTokens = (text) => {
if (!text || typeof text !== 'string') return [];
const cleaned = text.replace(/[\s\n\r\t]+/g, ' ')
.replace(/[,。、“”‘’!!?:;、,/\.\-]+/g, ' ')
.trim();
const parts = cleaned.split(' ').filter(Boolean);
return parts;
};
res.data.forEach(item => {
if (item.status && item.status !== 'active') return; // 只统计活跃求购
const texts = [item.productName, item.purchaseDescription || item.description, item.expectedBrand, item.expectedModel];
const tokens = texts.flatMap(t => toTokens(t));
tokens.forEach(tok => {
const token = (tok || '').trim();
if (!token) return;
if (token.length < 2) return; // 过滤过短词
if (stopwords.has(token)) return;
// 统一大小写处理英文
const key = /[A-Za-z]/.test(token) ? token.toLowerCase() : token;
freq[key] = (freq[key] || 0) + 1;
});
});
const entries = Object.entries(freq).map(([text, count]) => ({ text, count }));
entries.sort((a, b) => b.count - a.count);
const top = entries.slice(0, limit);
const max = top.length > 0 ? top[0].count : 1;
const min = top.length > 0 ? top[top.length - 1].count : 1;
const norm = (c) => {
if (max === min) return 1;
return (c - min) / (max - min);
};
const keywords = top.map(k => ({
text: k.text,
weight: k.count,
score: norm(k.count) // 0..1
}));
return { success: true, data: { keywords, total: entries.length } };
} catch (e) {
console.error('聚合求购关键词失败:', e);
return { success: false, error: e.message };
}
};
// 计算分类供需统计与发布建议(不分校区,支持个性化权重,并返回类别指引)
const getPublishRecommendations = async (event) => {
try {
const forType = event && event.forType; // 'product' | 'wanted' | admin
const campusFilter = ''; // 不分校区
const wxContext = cloud.getWXContext();
const callerOpenId = (event && event.openid) || (wxContext && wxContext.OPENID) || '';
let interestsFromEvent = Array.isArray(event && event.interests) ? (event.interests || []) : [];
// 辅助:从用户信息推断校区
const getCampusFromUser = (user) => {
if (!user) return '未知';
if (user.campus) return user.campus;
const dorm = user.sushe || '';
if (/闵行/.test(dorm)) return '闵行校区';
if (/徐汇/.test(dorm)) return '徐汇校区';
if (/松江/.test(dorm)) return '松江校区';
return '未知';
};
// 统计供给(商品在售/交易中顺便收集sellerUserId用于校区映射
const productRes = await db.collection('T_product')
.field({ productCategory: true, status: true, transactionMethod: true, salePrice: true, suggestedPrice: true, sellerUserId: true, sellerOpenId: true })
.get();
// 批量获取卖家用户校区
const sellerIds = [...new Set(productRes.data.map(p => p.sellerUserId).filter(Boolean))];
let sellerUsers = { };
if (sellerIds.length > 0) {
try {
const sellerUserDocs = await db.collection('T_user').where({ _id: _.in(sellerIds) }).field({ _id: true, campus: true, sushe: true, interests: true, _openid: true }).get();
sellerUserDocs.data.forEach(u => { sellerUsers[u._id] = u; });
} catch (err) {
console.error('批量获取卖家用户信息失败:', err);
}
}
// 统计需求求购信息收集openids用于校区映射
let wantResData = [];
try {
const wantRes = await db.collection('T_want')
.field({ productCategory: true, _openid: true })
.get();
wantResData = wantRes.data || [];
} catch (e) {
wantResData = [];
}
const wantOpenIds = [...new Set(wantResData.map(w => w._openid).filter(Boolean))];
let wantUsersByOpenId = { };
if (wantOpenIds.length > 0) {
try {
const wantUserDocs = await db.collection('T_user').where({ _openid: _.in(wantOpenIds) }).field({ _openid: true, campus: true, sushe: true, interests: true, _id: true }).get();
wantUserDocs.data.forEach(u => { wantUsersByOpenId[u._openid] = u; });
} catch (err) {
console.error('批量获取求购用户信息失败:', err);
}
}
// 如果没有传入兴趣,尝试从当前调用者的用户信息获取
if (!interestsFromEvent || interestsFromEvent.length === 0) {
try {
const curUser = await db.collection('T_user').where({ _openid: callerOpenId }).field({ interests: true, campus: true, sushe: true }).get();
const userDoc = (curUser.data && curUser.data[0]) || null;
if (userDoc && Array.isArray(userDoc.interests)) {
interestsFromEvent = userDoc.interests;
}
} catch (err) {
// 忽略错误
}
}
// 供需统计(可选校区过滤)
const supplyMap = {};
const demandMap = {};
const campusBreakdown = {}; // { 校区: { category -> { supply, demand } } }
const supplyStatuses = new Set(['在售','交易中','已发布','待卖家确认','待发货','待收货']);
productRes.data.forEach(item => {
const cat = item.productCategory || '其他';
const status = item.status || '';
const isSupply = supplyStatuses.has(status);
const sellerUser = item.sellerUserId ? sellerUsers[item.sellerUserId] : null;
const campus = getCampusFromUser(sellerUser);
if (isSupply) {
// 总体
supplyMap[cat] = (supplyMap[cat] || 0) + 1;
// 校区分布
campusBreakdown[campus] = campusBreakdown[campus] || {};
campusBreakdown[campus][cat] = campusBreakdown[campus][cat] || { supply: 0, demand: 0 };
campusBreakdown[campus][cat].supply += 1;
}
});
wantResData.forEach(item => {
const cat = item.productCategory || '其他';
const user = wantUsersByOpenId[item._openid];
const campus = getCampusFromUser(user);
demandMap[cat] = (demandMap[cat] || 0) + 1;
campusBreakdown[campus] = campusBreakdown[campus] || {};
campusBreakdown[campus][cat] = campusBreakdown[campus][cat] || { supply: 0, demand: 0 };
campusBreakdown[campus][cat].demand += 1;
});
// 如果指定校区,则使用该校区下的供需统计替换总体视图
let effectiveSupplyMap = supplyMap;
let effectiveDemandMap = demandMap;
if (campusFilter && campusBreakdown[campusFilter]) {
const campusStats = campusBreakdown[campusFilter];
effectiveSupplyMap = {};
effectiveDemandMap = {};
Object.keys(campusStats).forEach(cat => {
effectiveSupplyMap[cat] = campusStats[cat].supply || 0;
effectiveDemandMap[cat] = campusStats[cat].demand || 0;
});
}
// 汇总所有出现过的类别
const allCats = new Set([...Object.keys(effectiveSupplyMap), ...Object.keys(effectiveDemandMap)]);
const stats = Array.from(allCats).map(cat => {
const supply = effectiveSupplyMap[cat] || 0;
const demand = effectiveDemandMap[cat] || 0;
return {
category: cat,
supply,
demand,
gap: demand - supply
};
}).sort((a, b) => Math.abs(b.gap) - Math.abs(a.gap));
// 价格与交易方式指引
const categoryGuides = {};
const methodCountByCat = {};
const priceByCat = {};
productRes.data.forEach(item => {
const status = item.status || '';
if (!supplyStatuses.has(status)) return;
const cat = item.productCategory || '其他';
const price = (typeof item.salePrice === 'number' && item.salePrice > 0) ? item.salePrice
: ((typeof item.suggestedPrice === 'number' && item.suggestedPrice > 0) ? item.suggestedPrice : null);
if (price !== null) {
priceByCat[cat] = priceByCat[cat] || [];
priceByCat[cat].push(price);
}
const method = item.transactionMethod || '面交';
methodCountByCat[cat] = methodCountByCat[cat] || {};
methodCountByCat[cat][method] = (methodCountByCat[cat][method] || 0) + 1;
});
Object.keys(allCats).forEach(() => {}); // no-op to satisfy lint
for (const cat of allCats) {
const prices = priceByCat[cat] || [];
let guide = { avgPrice: 0, minPrice: 0, maxPrice: 0, typicalPriceRange: '', commonTrade: '面交', methodsRanked: [] };
if (prices.length > 0) {
const sum = prices.reduce((a, b) => a + b, 0);
const avg = Math.round((sum / prices.length) * 100) / 100;
const min = Math.min(...prices);
const max = Math.max(...prices);
guide.avgPrice = avg;
guide.minPrice = min;
guide.maxPrice = max;
guide.typicalPriceRange = `${Math.round(min)}-${Math.round(max)}`;
}
const mc = methodCountByCat[cat] || {};
const ranked = Object.entries(mc).sort((a, b) => b[1] - a[1]).map(([m]) => m);
guide.commonTrade = ranked[0] || '面交';
guide.methodsRanked = ranked;
categoryGuides[cat] = guide;
}
// 生成建议支持forType与兴趣权重
const shortage = stats.filter(s => s.gap >= 3).sort((a, b) => b.gap - a.gap).slice(0, 5);
const oversupply = stats.filter(s => (s.supply - s.demand) >= 3).sort((a, b) => (b.supply - b.demand) - (a.supply - a.demand)).slice(0, 5);
const hotDemand = [...stats].sort((a, b) => b.demand - a.demand).slice(0, 5);
const richSupply = [...stats].sort((a, b) => b.supply - a.supply).slice(0, 5);
const interestSet = new Set((interestsFromEvent || []).map(i => i));
let candidates = [];
let baseMessage = '';
if (forType === 'product') {
candidates = (shortage.length > 0 ? shortage : hotDemand);
baseMessage = shortage.length > 0 ? '以下类别买家需求旺盛、供给偏少,优先发布:' : '以下类别近期需求较高,发布更易成交:';
} else if (forType === 'wanted') {
candidates = (oversupply.length > 0 ? oversupply : richSupply);
baseMessage = oversupply.length > 0 ? '以下类别货源充足、响应更快,建议求购:' : '以下类别当前货源较多,求购更容易被响应:';
} else {
candidates = shortage.slice(0, 3);
baseMessage = '供需缺口较大的类别:';
}
// 应用兴趣加权
const scored = candidates.map(c => {
const interestBoost = interestSet.has(c.category) ? 1 : 0;
const score = (Math.abs(c.gap) || 0) + interestBoost;
return { cat: c.category, score };
}).sort((a, b) => b.score - a.score);
const recommended = scored.map(s => s.cat);
const message = `${baseMessage}${recommended.join('、')}`;
return {
success: true,
data: {
stats,
shortage,
oversupply,
hotDemand,
richSupply,
recommended,
message,
campus: '全部校区',
interestsUsed: Array.from(interestSet),
categoryGuides,
campusBreakdown
}
};
} catch (e) {
console.error('计算发布建议失败:', e);
return {
success: false,
error: e.message
};
}
};
// 管理员:一键推送建议到用户群(不分校区)
const adminPushRecommendations = async (event) => {
try {
const categories = Array.isArray(event.categories) ? event.categories : [];
const target = event.target || 'sellers'; // sellers | buyers | all
const campus = ''; // 不分校区
const customMessage = event.message || '';
// 本地工具:从用户资料推断校区
const getCampusFromUserLocal = (user) => {
if (!user) return '未知';
if (user.campus) return user.campus;
const dorm = user.dorm || user.sushe || '';
if (/闵行/.test(dorm)) return '闵行校区';
if (/徐汇/.test(dorm)) return '徐汇校区';
if (/松江/.test(dorm)) return '松江校区';
return '未知';
};
if (categories.length === 0) {
return { success: false, error: '缺少要推送的类别列表' };
}
// 查询用户(按兴趣筛选,不分校区)
const userQuery = {};
// 拉取可能的兴趣用户这里不在where里使用数组匹配改为拉取后过滤兼容旧数据
const usersRes = await db.collection('T_user').where(userQuery).field({ _id: true, _openid: true, interests: true, campus: true, sushe: true, notificationEnabled: true, sname: true }).get();
// 可能的买家(发布过求购)
let wantUsersOpenIds = new Set();
try {
const wantRes = await db.collection('T_want').where({ productCategory: _.in(categories) }).field({ _openid: true }).get();
wantRes.data.forEach(w => { if (w._openid) wantUsersOpenIds.add(w._openid); });
} catch (err) {
// ignore
}
// 过滤用户:兴趣命中或是活跃求购用户
const getCampus = (u) => u.campus || getCampusFromUserLocal(u);
const recipients = [];
usersRes.data.forEach(u => {
const enabled = (u.notificationEnabled !== false);
if (!enabled) return;
const campusOk = true;
const interests = Array.isArray(u.interests) ? u.interests : [];
const interestHit = interests.some(i => categories.includes(i));
const wantHit = wantUsersOpenIds.has(u._openid);
if (campusOk && (interestHit || (target !== 'sellers' && wantHit) || target === 'all')) {
recipients.push(u);
}
});
// 写入通知集合
let successCount = 0;
for (const user of recipients) {
try {
const content = customMessage || `当前【${categories.join('、')}】类别存在供需变化,建议关注并发布/求购。`;
await db.collection('T_notify').add({
data: {
userId: user._id,
_openid: user._openid,
type: 'recommendation',
title: '供需发布建议',
content,
categories,
campus: '全部校区',
target,
read: false,
createTime: new Date()
}
});
successCount += 1;
} catch (err) {
console.error('写入通知失败:', err);
}
}
return { success: true, pushed: successCount, recipients: recipients.length };
} catch (e) {
console.error('管理员推送建议失败:', e);
return { success: false, error: e.message };
}
};
// 管理员更新商品
const adminUpdateProduct = async (event) => {
try {
const { productId, data } = event;
if (!productId) {
return {
success: false,
error: '缺少商品ID'
};
}
await db.collection('T_product').doc(productId).update({
data: {
...data,
updateTime: new Date()
}
});
return {
success: true,
message: '更新成功'
};
} catch (e) {
console.error('更新商品失败:', e);
return {
success: false,
error: e.message
};
}
};
// 管理员删除商品(下架)
const adminDeleteProduct = async (event) => {
try {
const { productId } = event;
if (!productId) {
return {
success: false,
error: '缺少商品ID'
};
}
// 更新商品状态为已下架
await db.collection('T_product').doc(productId).update({
data: {
status: '已下架',
updateTime: new Date()
}
});
return {
success: true,
message: '商品已下架'
};
} catch (e) {
console.error('下架商品失败:', e);
return {
success: false,
error: e.message
};
}
};
// 管理员获取用户列表
const adminGetUsers = async (event) => {
try {
const { page = 0, pageSize = 20, keyword = '' } = event;
let queryCondition = {};
if (keyword) {
// 微信小程序云数据库使用db.RegExp
const regex = db.RegExp({
regexp: keyword,
options: 'i'
});
queryCondition = _.or([
{ sno: regex },
{ sname: regex },
{ phone: regex }
]);
}
const result = await db.collection('T_user')
.where(queryCondition)
.skip(page * pageSize)
.limit(pageSize)
.orderBy('createTime', 'desc')
.get();
const countResult = await db.collection('T_user')
.where(queryCondition)
.count();
return {
success: true,
data: {
users: result.data,
total: countResult.total,
page: page,
pageSize: pageSize
}
};
} catch (e) {
console.error('获取用户列表失败:', e);
return {
success: false,
error: e.message
};
}
};
// 管理员更新用户信息
const adminUpdateUser = async (event) => {
try {
const { userId, data } = event;
console.log('管理员更新用户信息userId:', userId, 'data:', JSON.stringify(data, null, 2));
if (!userId) {
return {
success: false,
error: '缺少用户ID'
};
}
// 检查用户是否存在
const userDoc = await db.collection('T_user').doc(userId).get();
if (!userDoc.data) {
return {
success: false,
error: '用户不存在'
};
}
// 更新用户信息
const updateResult = await db.collection('T_user').doc(userId).update({
data: {
...data,
updateTime: new Date()
}
});
console.log('更新用户信息成功,结果:', updateResult);
return {
success: true,
message: '更新成功',
data: updateResult
};
} catch (e) {
console.error('更新用户失败:', e);
return {
success: false,
error: e.message || '更新失败'
};
}
};
// 安全订单状态更新(基于角色和状态流转校验)
const updateOrderStatus = async (event) => {
try {
const { orderId, action, callerUserId } = event || {};
if (!orderId || !action) {
return { success: false, error: '缺少必要参数orderId 或 action' };
}
const ctx = cloud.getWXContext();
const callerOpenId = ctx.OPENID || event.openid || '';
const orderRes = await db.collection('T_order').doc(orderId).get();
const order = orderRes.data;
if (!order) {
return { success: false, error: '订单不存在' };
}
const flows = {
buyerPay: { from: '待付款', to: '待确认付款', role: 'buyer' },
sellerConfirmPayment: { from: '待确认付款', to: '待发货', role: 'seller' },
sellerShip: { from: '待发货', to: '待收货', role: 'seller' },
buyerConfirmReceipt: { from: '待收货', to: '已完成', role: 'buyer' },
buyerCancel: { from: '待付款', to: '已取消', role: 'buyer' }
};
const flow = flows[action];
if (!flow) {
return { success: false, error: '不支持的动作' };
}
const isSeller = (
(order.sellerOpenId && order.sellerOpenId === callerOpenId) ||
(order.sellerUserId && callerUserId && order.sellerUserId === callerUserId)
);
const isBuyer = (
(order.buyerOpenId && order.buyerOpenId === callerOpenId) ||
(order.buyerUserId && callerUserId && order.buyerUserId === callerUserId)
);
if (flow.role === 'seller' && !isSeller) {
return { success: false, error: '无权执行:仅卖家可进行该操作' };
}
if (flow.role === 'buyer' && !isBuyer) {
return { success: false, error: '无权执行:仅买家可进行该操作' };
}
if (order.status !== flow.from) {
return { success: false, error: `状态不匹配:当前为${order.status},期望${flow.from}` };
}
const now = new Date();
const updates = { status: flow.to, updateTime: now };
switch (action) {
case 'buyerPay':
updates.paymentTime = now;
break;
case 'sellerConfirmPayment':
updates.sellerConfirmTime = now;
break;
case 'sellerShip':
updates.shipTime = now;
if (event.shipInfo) updates.shipInfo = event.shipInfo;
break;
case 'buyerConfirmReceipt':
updates.finishTime = now;
break;
case 'buyerCancel':
updates.cancelTime = now;
break;
}
const updateRes = await db.collection('T_order')
.where({ _id: orderId, status: flow.from })
.update({ data: updates });
if (!updateRes.stats || updateRes.stats.updated !== 1) {
return { success: false, error: '更新失败或状态已变化,请刷新重试' };
}
// 写入系统聊天消息(买家/卖家互相通知订单状态)
try {
const buyerOpenId = order.buyerOpenId || '';
const sellerOpenId = order.sellerOpenId || '';
if (buyerOpenId && sellerOpenId) {
const sessionKey = [buyerOpenId, sellerOpenId].sort().join('|');
let fromOpenId = '';
let toOpenId = '';
let content = '';
if (action === 'buyerPay') {
fromOpenId = buyerOpenId;
toOpenId = sellerOpenId;
content = `[系统] 买家已付款:订单号 ${order.orderNumber},金额 ¥${order.totalPrice}`;
} else if (action === 'sellerConfirmPayment') {
fromOpenId = sellerOpenId;
toOpenId = buyerOpenId;
content = `[系统] 卖家已确认收款:订单号 ${order.orderNumber}`;
} else if (action === 'sellerShip') {
fromOpenId = sellerOpenId;
toOpenId = buyerOpenId;
const shipText = event.shipInfo ? `,物流:${event.shipInfo}` : '';
content = `[系统] 卖家已发货:订单号 ${order.orderNumber}${shipText}`;
} else if (action === 'buyerConfirmReceipt') {
fromOpenId = buyerOpenId;
toOpenId = sellerOpenId;
content = `[系统] 买家已确认收货:订单号 ${order.orderNumber}`;
} else if (action === 'buyerCancel') {
fromOpenId = buyerOpenId;
toOpenId = sellerOpenId;
content = `[系统] 买家已取消订单:订单号 ${order.orderNumber}`;
}
if (fromOpenId && toOpenId && content) {
const buyerUserId = order.buyerUserId || '';
const sellerUserId = order.sellerUserId || '';
const sessionKeyUser = (buyerUserId && sellerUserId) ? [buyerUserId, sellerUserId].sort().join('|') : '';
let fromUserId = '';
let toUserId = '';
if (action === 'buyerPay' || action === 'buyerConfirmReceipt' || action === 'buyerCancel') {
fromUserId = buyerUserId;
toUserId = sellerUserId;
} else {
// sellerConfirmPayment / sellerShip
fromUserId = sellerUserId;
toUserId = buyerUserId;
}
const chatDoc = {
fromOpenId,
toOpenId,
fromUserId,
toUserId,
sessionKey,
sessionKeyUser,
contentType: 'text',
content,
timestamp: Date.now(),
orderId: orderId,
productId: order.productId || (Array.isArray(order.products) && order.products.length === 1 ? order.products[0].productId : ''),
isSystem: true
};
try {
await db.collection('T_chat').add({ data: chatDoc });
} catch (e) {
// 如果集合不存在,先创建再重试一次
if (String(e.errMsg || '').includes('does not exist') || e.errCode === -502005) {
await createChatCollection();
await db.collection('T_chat').add({ data: chatDoc });
} else {
throw e;
}
}
}
}
} catch (e) {
// 聊天消息写入失败不影响主流程
console.error('写入订单状态聊天消息失败:', e);
}
if (action === 'buyerConfirmReceipt' && order.productId) {
try {
await db.collection('T_product').doc(order.productId).update({
data: { status: '已售', updateTime: now }
});
} catch (e) {
console.error('更新商品为已售失败:', e);
return { success: true, warning: '订单完成成功,但商品状态更新失败', newStatus: flow.to };
}
try {
const pr = await db.collection('T_product').doc(order.productId).get();
const p = pr.data || {};
const name = String(p.tradeLandmarkName || p.tradeLocationName || p.tradeAddress || '').trim();
const lat = Number(p.tradeLocationLat);
const lng = Number(p.tradeLocationLng);
if (name && Number.isFinite(lat) && Number.isFinite(lng)) {
await upsertCampusLandmark({ name, latitude: lat, longitude: lng, address: String(p.tradeAddress || ''), source: 'product', productId: order.productId, selling: false });
}
} catch (e) {
console.error('同步地标为非在售失败:', e);
}
}
return { success: true, newStatus: flow.to };
} catch (e) {
console.error('安全更新订单状态失败:', e);
return { success: false, error: e.message };
}
};
// 创建论坛集合
const createForumCollection = async () => {
try {
const collections = await db.listCollections();
const collectionNames = collections.data.map(col => col.name);
const collectionsToCreate = ['T_forum_post', 'T_forum_comment', 'T_forum_like'];
const created = [];
for (const collectionName of collectionsToCreate) {
if (!collectionNames.includes(collectionName)) {
try {
await db.createCollection(collectionName);
created.push(collectionName);
console.log(`${collectionName} 集合创建成功`);
} catch (err) {
if (err.message && !err.message.includes('already exists')) {
console.error(`创建 ${collectionName} 失败:`, err);
}
}
}
}
return {
success: true,
message: created.length > 0 ? `成功创建 ${created.join(', ')} 集合` : "所有集合已存在",
created: created
};
} catch (e) {
console.error('创建论坛集合失败:', e);
return {
success: false,
error: e.message,
};
}
};
// 计算余弦相似度
const cosineSimilarity = (vecA, vecB) => {
let dotProduct = 0;
let normA = 0;
let normB = 0;
const keys = new Set([...Object.keys(vecA), ...Object.keys(vecB)]);
for (const key of keys) {
const a = vecA[key] || 0;
const b = vecB[key] || 0;
dotProduct += a * b;
normA += a * a;
normB += b * b;
}
if (normA === 0 || normB === 0) {
return 0;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
};
// 基于用户的协同过滤推荐
const userBasedCF = async (openid, limit = 10) => {
try {
// 获取所有用户行为数据
const allBehaviors = await db.collection('T_user_behavior').get();
if (allBehaviors.data.length === 0) {
return [];
}
// 构建用户-商品评分矩阵
const userProductMatrix = {};
allBehaviors.data.forEach(behavior => {
const userId = behavior._openid;
const productId = behavior.productId;
const score = behavior.score * (behavior.count || 1); // 考虑行为次数
if (!userProductMatrix[userId]) {
userProductMatrix[userId] = {};
}
userProductMatrix[userId][productId] = (userProductMatrix[userId][productId] || 0) + score;
});
// 获取当前用户的行为数据
const currentUserProducts = userProductMatrix[openid] || {};
if (Object.keys(currentUserProducts).length === 0) {
return []; // 新用户,没有行为数据
}
// 计算当前用户与其他用户的相似度
const userSimilarities = [];
for (const userId in userProductMatrix) {
if (userId === openid) continue;
const similarity = cosineSimilarity(currentUserProducts, userProductMatrix[userId]);
if (similarity > 0) {
userSimilarities.push({
userId: userId,
similarity: similarity
});
}
}
// 按相似度排序
userSimilarities.sort((a, b) => b.similarity - a.similarity);
// 获取相似用户喜欢的商品(当前用户未交互过的)
const recommendedProducts = {};
const topSimilarUsers = userSimilarities.slice(0, 20); // 取前20个相似用户
for (const similarUser of topSimilarUsers) {
const similarUserProducts = userProductMatrix[similarUser.userId];
for (const productId in similarUserProducts) {
if (!currentUserProducts[productId]) {
// 当前用户未交互过的商品
const score = similarUserProducts[productId] * similarUser.similarity;
recommendedProducts[productId] = (recommendedProducts[productId] || 0) + score;
}
}
}
// 转换为数组并排序
const productScores = Object.entries(recommendedProducts)
.map(([productId, score]) => ({ productId, score }))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
return productScores.map(item => item.productId);
} catch (e) {
console.error('基于用户的协同过滤失败:', e);
return [];
}
};
// 基于物品的协同过滤推荐
const itemBasedCF = async (openid, limit = 10) => {
try {
// 获取所有用户行为数据
const allBehaviors = await db.collection('T_user_behavior').get();
if (allBehaviors.data.length === 0) {
return [];
}
// 构建商品-用户评分矩阵
const productUserMatrix = {};
allBehaviors.data.forEach(behavior => {
const productId = behavior.productId;
const userId = behavior._openid;
const score = behavior.score * (behavior.count || 1);
if (!productUserMatrix[productId]) {
productUserMatrix[productId] = {};
}
productUserMatrix[productId][userId] = (productUserMatrix[productId][userId] || 0) + score;
});
// 获取当前用户交互过的商品
const currentUserBehaviors = await db.collection('T_user_behavior')
.where({
_openid: openid
})
.get();
if (currentUserBehaviors.data.length === 0) {
return []; // 新用户
}
const currentUserProducts = {};
currentUserBehaviors.data.forEach(behavior => {
currentUserProducts[behavior.productId] = behavior.score * (behavior.count || 1);
});
// 计算当前用户交互过的商品与其他商品的相似度
const productSimilarities = {};
for (const productId in currentUserProducts) {
if (!productUserMatrix[productId]) continue;
const currentProductUsers = productUserMatrix[productId];
// 计算与其他商品的相似度
for (const otherProductId in productUserMatrix) {
if (otherProductId === productId || currentUserProducts[otherProductId]) {
continue; // 跳过自己或已交互过的商品
}
const similarity = cosineSimilarity(currentProductUsers, productUserMatrix[otherProductId]);
if (similarity > 0) {
const weightedScore = currentUserProducts[productId] * similarity;
productSimilarities[otherProductId] = (productSimilarities[otherProductId] || 0) + weightedScore;
}
}
}
// 转换为数组并排序
const productScores = Object.entries(productSimilarities)
.map(([productId, score]) => ({ productId, score }))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
return productScores.map(item => item.productId);
} catch (e) {
console.error('基于物品的协同过滤失败:', e);
return [];
}
};
// 根据用户兴趣推荐商品(保留原有功能作为备用)
const getRecommendedProductsByInterest = async (userInterests, limit) => {
const mappedInterests = userInterests.map(cat => mapCategoryName(cat));
let queryCondition = {
status: '在售'
};
if (mappedInterests.length > 0) {
queryCondition.productCategory = _.in(mappedInterests);
}
const result = await db.collection("T_product")
.where(queryCondition)
.orderBy('createTime', 'desc')
.limit(limit)
.get();
return result.data.map(item => item._id);
};
// 根据用户兴趣推荐商品(增强版:结合协同过滤)
const getRecommendedProducts = async (event) => {
try {
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
const userInterests = event.interests || [];
const limit = event.limit || 8;
const useCF = event.useCF !== false; // 默认使用协同过滤
console.log('获取推荐商品openid:', openid, '使用协同过滤:', useCF);
let recommendedProductIds = [];
// 优先使用协同过滤算法
if (useCF) {
try {
// 尝试基于用户的协同过滤
const userCFIds = await userBasedCF(openid, limit);
console.log('基于用户的CF推荐数量:', userCFIds.length);
// 如果基于用户的CF结果不足尝试基于物品的CF
if (userCFIds.length < limit) {
const itemCFIds = await itemBasedCF(openid, limit - userCFIds.length);
console.log('基于物品的CF推荐数量:', itemCFIds.length);
// 合并结果,去重
const allCFIds = [...new Set([...userCFIds, ...itemCFIds])];
recommendedProductIds = allCFIds.slice(0, limit);
} else {
recommendedProductIds = userCFIds;
}
console.log('协同过滤推荐商品ID:', recommendedProductIds);
} catch (cfError) {
console.error('协同过滤推荐失败,使用兴趣推荐:', cfError);
recommendedProductIds = [];
}
}
// 如果协同过滤结果不足,使用兴趣推荐补充
if (recommendedProductIds.length < limit && userInterests.length > 0) {
const interestIds = await getRecommendedProductsByInterest(userInterests, limit - recommendedProductIds.length);
// 去重并合并
const allIds = [...new Set([...recommendedProductIds, ...interestIds])];
recommendedProductIds = allIds.slice(0, limit);
}
// 如果还是没有足够的推荐,获取热门商品
if (recommendedProductIds.length < limit) {
const hotProducts = await db.collection("T_product")
.where({
status: '在售'
})
.orderBy('createTime', 'desc')
.limit(limit - recommendedProductIds.length)
.get();
const hotIds = hotProducts.data.map(item => item._id);
const allIds = [...new Set([...recommendedProductIds, ...hotIds])];
recommendedProductIds = allIds.slice(0, limit);
}
// 根据ID获取商品详细信息
const products = [];
for (const productId of recommendedProductIds) {
try {
const productDoc = await db.collection("T_product").doc(productId).get();
if (productDoc.data && productDoc.data.status === '在售') {
const item = productDoc.data;
products.push({
id: item._id,
name: item.productName || '商品名称',
price: item.salePrice || item.suggestedPrice || item.originalPrice || 0,
image: Array.isArray(item.productImage) ? (item.productImage[0] || '') : (item.productImage || 'https://via.placeholder.com/280x200/4285F4/ffffff?text=商品'),
tag: item.productCategory || '其他',
category: item.productCategory || '其他',
description: item.productDescription || '',
createTime: item.createTime
});
}
} catch (err) {
console.error('获取商品详情失败:', productId, err);
}
}
console.log('最终推荐商品数量:', products.length);
return {
success: true,
data: products,
count: products.length,
message: products.length === 0 ? '暂无推荐商品' : '推荐商品加载成功',
algorithm: useCF && recommendedProductIds.length > 0 ? '协同过滤' : '兴趣推荐'
};
} catch (e) {
console.error('获取推荐商品失败:', e);
return {
success: false,
error: e.message
};
}
};
// 云函数入口函数
exports.main = async (event, context) => {
try {
// 获取操作类型,支持多种参数格式
const type = event.type || event.userInfo?.type || (event.tcbContext && event.tcbContext.type);
console.log('云函数被调用type:', type);
console.log('event参数:', JSON.stringify(event, null, 2));
if (!type) {
return {
success: false,
error: '缺少操作类型参数(type)',
event: event
};
}
// 添加调试日志
console.log('开始执行switchtype值:', type, '类型:', typeof type);
switch (type) {
case "getOpenId":
return await getOpenId();
case "getMiniProgramCode":
return await getMiniProgramCode();
case "createCollection":
return await createCollection();
case "selectRecord":
return await selectRecord();
case "updateRecord":
return await updateRecord(event);
case "insertRecord":
return await insertRecord(event);
case "deleteRecord":
return await deleteRecord(event);
case "queryTUser":
return await queryTUser();
case "addTestUser":
return await addTestUser(event);
case "analyzeProductPrice":
return await analyzeProductPrice(event);
case "getUserByOpenId":
return await getUserByOpenId(event);
case "getProductCategories":
return await getProductCategories();
case "updateUserInterests":
return await updateUserInterests(event);
case "getRecommendedProducts":
console.log('匹配到getRecommendedProducts case');
return await getRecommendedProducts(event);
case "getPublishRecommendations":
// 基于分类供需对比的发布建议(商品/求购)
return await getPublishRecommendations(event);
case "adminPushRecommendations":
// 管理员一键推送建议到指定用户群
return await adminPushRecommendations(event);
case "recordUserBehavior":
return await recordUserBehavior(event);
case "createBehaviorCollection":
return await createBehaviorCollection();
case "createForumCollection":
return await createForumCollection();
case "createNotifyCollection":
return await createNotifyCollection();
case "createChatCollection":
return await createChatCollection();
case "createMessageCollection":
return await createMessageCollection();
case "sendChatMessage":
return await sendChatMessage(event);
case "getChatMessages":
return await getChatMessages(event);
case "listChatSessions":
return await listChatSessions();
case "updateChatReadState":
return await updateChatReadState(event);
case "revokeChatMessage":
return await revokeChatMessage(event);
case "backfillSellerUserId":
// 批量为商品补齐缺失的卖家用户ID通过 sellerOpenId 关联 T_user
return await backfillSellerUserId(event);
case "createAddressCollection":
return await createAddressCollection();
case "createCampusLandmarksCollection":
return await createCampusLandmarksCollection();
case "seedDefaultAddress":
return await seedDefaultAddress(event);
case "adminLogin":
console.log('匹配到adminLogin case');
return await adminLogin(event);
case "getAdminStats":
return await getAdminStats();
case "getWantedKeywordCloud":
// 管理端词云:求购关键词聚合
return await getWantedKeywordCloud(event);
case "adminUpdateProduct":
return await adminUpdateProduct(event);
case "adminDeleteProduct":
return await adminDeleteProduct(event);
case "adminGetUsers":
return await adminGetUsers(event);
case "adminUpdateUser":
return await adminUpdateUser(event);
case "updateOrderStatus":
// 安全更新订单状态(带角色和状态校验)
return await updateOrderStatus(event);
case "initCategoryTable":
return await initCategoryTable();
case "cloudReverseGeocode":
return await cloudReverseGeocode(event);
case "cloudGeocode":
return await cloudGeocode(event);
case "fixProductCoords":
// 批量修复商品缺失坐标(地标匹配 + 地址地理编码)
return await fixProductCoords(event);
case "upsertCampusLandmark":
// 上报/同步单条地标
return await upsertCampusLandmark(event);
case "syncProductLandmarks":
// 批量从商品同步地标到 T_campus_landmarks
return await syncProductLandmarks(event);
default:
console.log('未匹配到任何casetype:', type, 'type类型:', typeof type);
return {
success: false,
error: '未知的操作类型: ' + type
};
}
} catch (error) {
console.error('========== 云函数入口函数错误 ==========');
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
console.error('event:', JSON.stringify(event, null, 2));
const type = event.type || event.userInfo?.type || (event.tcbContext && event.tcbContext.type);
return {
success: false,
error: error.message || '云函数执行失败',
details: error.stack,
type: type
};
}
};