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.

177 lines
6.0 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.

// utils/recommendation.js
// 行为权重与推荐算法(本地轻量级实时推荐)
const DEFAULT_WEIGHTS = {
categories: {},
keywords: {},
lastUpdated: null
};
function getStoreKey() {
return 'behaviorWeights';
}
function loadWeights() {
try {
const w = wx.getStorageSync(getStoreKey());
if (w && typeof w === 'object') return w;
} catch (e) {}
return { ...DEFAULT_WEIGHTS };
}
function saveWeights(w) {
try {
w.lastUpdated = Date.now();
wx.setStorageSync(getStoreKey(), w);
} catch (e) {}
}
function inc(map, key, delta) {
if (!key) return;
const k = String(key).trim();
if (!k) return;
map[k] = (map[k] || 0) + (delta || 1);
if (map[k] < 0) map[k] = 0; // 不允许为负,便于稳定排序
}
function tokenize(text) {
if (!text || typeof text !== 'string') return [];
const t = text.toLowerCase();
// 基础分词:空白/标点分割,保留中文词串
const rough = t.split(/[\s,;,。.!?\-_/]+/).filter(Boolean);
// 去重并限制长度
const seen = new Set();
const out = [];
for (let i = 0; i < rough.length && out.length < 20; i++) {
const token = rough[i].trim();
if (!token) continue;
if (!seen.has(token)) {
seen.add(token);
out.push(token);
}
}
return out;
}
// 记录:注册兴趣 -> 类别权重
function recordInterests(interests, weight = 5) {
const w = loadWeights();
(interests || []).forEach(cat => inc(w.categories, cat, weight));
saveWeights(w);
}
// 记录:搜索关键词(商品/求购)
function recordSearch(keyword, type = 'product') {
const w = loadWeights();
const base = type === 'wanted' ? 2 : 3; // 商品搜索更强一点
tokenize(keyword).forEach(k => inc(w.keywords, k, base));
saveWeights(w);
}
// 记录:浏览商品详情(类别 + 关键词)
function recordView(product) {
const w = loadWeights();
inc(w.categories, product?.productCategory || product?.category || '其他', 1);
const nameTokens = tokenize(product?.productName || '');
const descTokens = tokenize(product?.productDescription || product?.description || '');
[...nameTokens, ...descTokens].slice(0, 15).forEach(k => inc(w.keywords, k, 1));
saveWeights(w);
}
// 记录:点击商品卡片(比浏览稍高)
function recordClick(product) {
const w = loadWeights();
inc(w.categories, product?.productCategory || product?.category || '其他', 2);
const nameTokens = tokenize(product?.productName || '');
const descTokens = tokenize(product?.productDescription || product?.description || '');
[...nameTokens, ...descTokens].slice(0, 15).forEach(k => inc(w.keywords, k, 2));
saveWeights(w);
try {
const cat = product?.productCategory || product?.category || '其他';
const pid = product?._id || product?.id || '';
wx.setStorageSync('lastInteraction', { category: cat, productId: pid, ts: Date.now() });
} catch (e) {}
}
// 记录:收藏商品(比点击更高,表示强偏好)
function recordFavorite(product) {
const w = loadWeights();
inc(w.categories, product?.productCategory || product?.category || '其他', 4);
const nameTokens = tokenize(product?.productName || product?.name || '');
const descTokens = tokenize(product?.productDescription || product?.description || '');
[...nameTokens, ...descTokens].slice(0, 20).forEach(k => inc(w.keywords, k, 4));
saveWeights(w);
try {
const cat = product?.productCategory || product?.category || '其他';
const pid = product?._id || product?.id || '';
wx.setStorageSync('lastInteraction', { category: cat, productId: pid, ts: Date.now() });
} catch (e) {}
}
// 记录:取消收藏(轻度降低偏好)
function recordUnfavorite(product) {
const w = loadWeights();
inc(w.categories, product?.productCategory || product?.category || '其他', -2);
const nameTokens = tokenize(product?.productName || product?.name || '');
const descTokens = tokenize(product?.productDescription || product?.description || '');
[...nameTokens, ...descTokens].slice(0, 20).forEach(k => inc(w.keywords, k, -2));
saveWeights(w);
}
// 记录:发布求购(类别 + 关键词权重更高)
function recordPublishWanted(category, productName, description) {
const w = loadWeights();
inc(w.categories, category || '其他', 4);
const tokens = [...tokenize(productName), ...tokenize(description)].slice(0, 20);
tokens.forEach(k => inc(w.keywords, k, 4));
saveWeights(w);
}
function getWeights() {
return loadWeights();
}
function scoreProduct(product, weights) {
const cat = product?.productCategory || product?.category || '其他';
const catWeight = (weights.categories[cat] || 0) * 1; // 类别次要
const nameTokens = tokenize(product?.productName || product?.name || '');
const descTokens = tokenize(product?.productDescription || product?.description || '');
const kw = weights.keywords || {};
// 名称更重要,描述次之
const nameScore = nameTokens.reduce((s, k) => s + (kw[k] || 0), 0);
const descScore = descTokens.reduce((s, k) => s + Math.min((kw[k] || 0), 3), 0);
const kwScore = nameScore * 2 + descScore * 1;
// 轻度时间加成:越新越靠前
let timeBoost = 0;
const ct = product?.createTime ? new Date(product.createTime).getTime() : 0;
if (ct) {
const now = Date.now();
const days = (now - ct) / (24 * 3600 * 1000);
timeBoost = Math.max(0, 3 - Math.min(days, 7)); // 7天后加成耗尽
}
// 人气加成:浏览量等(仿电商平台,影响较轻)
const vc = Number(product?.viewCount || 0);
const popularity = Math.log(1 + Math.max(0, vc)) * 0.5; // 0~约2
return catWeight + kwScore + timeBoost + popularity;
}
function reorderProductsByWeights(products) {
const weights = getWeights();
if (!products || products.length === 0) return [];
const withScore = products.map(p => ({ p, s: scoreProduct(p, weights) }));
withScore.sort((a, b) => b.s - a.s);
return withScore.map(x => x.p);
}
module.exports = {
recordInterests,
recordSearch,
recordView,
recordClick,
recordFavorite,
recordUnfavorite,
recordPublishWanted,
getWeights,
scoreProduct,
reorderProductsByWeights,
tokenize
};