fix: 修复Tool模块相关问题 - 优化签到、社区、食物和AI服务功能

This commit is contained in:
msh-agent
2026-04-12 09:31:00 +08:00
parent b164d8ba11
commit 77632510cf
12 changed files with 1158 additions and 756 deletions

View File

@@ -119,8 +119,8 @@ export function getCheckinList(data) {
* 获取打卡记录详情
* @param {Number} id - 打卡记录ID
*/
export function getCheckinDetail(id) {
return request.get('tool/checkin/detail/' + id);
export function getCheckinDetail(id, opt) {
return request.get('tool/checkin/detail/' + id, {}, opt || {});
}
/**
@@ -196,7 +196,7 @@ export function shareCheckinToCommunity(checkinId) {
* @param {Number} data.limit - 每页数量
*/
export function searchFood(data) {
return request.get('tool/food/search', data);
return request.get('tool/food/search', data, { noAuth: true });
}
/**
@@ -205,9 +205,31 @@ export function searchFood(data) {
* @param {String} data.category - 分类all/grain/vegetable/fruit/meat/seafood
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*
* 响应 data 为分页list[] 项含 image、nutrientsJson、energy、protein、钾/磷/钠/钙、suitabilityLevelToolFoodServiceImpl
*/
export function getFoodList(data) {
return request.get('tool/food/list', data);
return request.get('tool/food/list', data, { noAuth: true });
}
/** 将列表/路由上的 id 规范为整数字符串(兼容 Long 序列化为 123、123.0、"123.0" 等,拒绝小数与非数字) */
export function normalizeFoodDetailIdString(v) {
if (v === undefined || v === null || v === '') return '';
let input = v;
if (typeof input === 'string') {
input = input.trim();
if (input === '') return '';
}
const n = Number(input);
if (Number.isFinite(n)) {
const t = Math.trunc(n);
if (Math.abs(n - t) > 1e-6) return '';
return String(t);
}
const s = String(input).trim();
if (s === '') return '';
if (/^-?\d+$/.test(s)) return s;
return '';
}
/**
@@ -215,14 +237,18 @@ export function getFoodList(data) {
* @param {Number|String} id - 食物ID必须为数字不能传名称
*/
export function getFoodDetail(id) {
const numId = typeof id === 'number' && !isNaN(id) ? id : parseInt(String(id), 10);
// 打印请求参数便于确认(后端仅接受 Long 类型 id传 name 会 400
const apiPath = 'tool/food/detail/' + numId;
console.log('[api/tool] getFoodDetail 请求参数:', { id, numId, type: typeof id, apiPath });
if (isNaN(numId)) {
const rawId = normalizeFoodDetailIdString(id);
// 严格按整型 ID 校验,避免 parseInt('123abc') 这类误判;路径用字符串拼接避免超过 JS 安全整数时 Number() 精度丢失
const isIntegerId = rawId !== '' && /^-?\d+$/.test(rawId);
const apiPath = 'tool/food/detail/' + rawId;
const numId = isIntegerId ? Number(rawId) : NaN;
// 打印请求参数便于确认(后端仅接受 Long 类型 id传 name 会导致 400 NumberFormatException
console.log('[api/tool] getFoodDetail 请求参数:', { id, rawId, numId, idType: typeof id, apiPath, isNumeric: !isNaN(numId) });
if (!isIntegerId) {
return Promise.reject(new Error('食物详情接口需要数字ID当前传入: ' + id));
}
return request.get(apiPath);
// 与食物百科列表一致:只读接口免登录,避免未带 token 时请求被拦截导致详情页空白/仅 toast
return request.get(apiPath, {}, { noAuth: true });
}
/**
@@ -244,7 +270,8 @@ export function getSimilarFoods(foodId) {
* @param {Number} data.limit - 每页数量
*/
export function getKnowledgeList(data) {
return request.get('tool/knowledge/list', data);
// 与后端 /api/front/tool/** 免登录一致,未登录用户也可浏览饮食指南/科普文章
return request.get('tool/knowledge/list', data, { noAuth: true });
}
/**
@@ -252,7 +279,7 @@ export function getKnowledgeList(data) {
* @param {Number} id - 知识ID
*/
export function getKnowledgeDetail(id) {
return request.get('tool/knowledge/detail/' + id);
return request.get('tool/knowledge/detail/' + id, {}, { noAuth: true });
}
/**

View File

@@ -21,9 +21,10 @@ export function getUserInfo(){
/**
* 获取用户信息GET /api/front/user/info用于打卡后刷新积分等
* @param {Object} [query] - 可选查询参数(如 { _: Date.now() } 避免缓存陈旧积分)
*/
export function getFrontUserInfo(){
return request.get('user/info');
export function getFrontUserInfo(query) {
return request.get('user/info', query || {});
}
/**
@@ -120,12 +121,26 @@ export function getSignGet() {
}
/**
* 用户签到
*/
* 用户签到(每日打卡领积分)
* 对应 GET /api/front/user/sign/integral服务端累加积分无 /user/checkin 时与此等价。
*/
export function setSignIntegral(){
return request.get('user/sign/integral')
}
/**
* 用户打卡(优先接口)
* 对应 /api/front/user/checkin如后端未实现可回退至 user/sign/integral
*/
export function userCheckin() {
return request.get('user/checkin');
}
/** 与 setSignIntegral 相同,语义为「每日打卡」 */
export function userCheckinDaily() {
return request.get('user/sign/integral');
}
/**
* 签到列表(年月)
* @param object data

View File

@@ -2,9 +2,9 @@
// |
// +----------------------------------------------------------------------
// 移动端商城API
let domain = 'http://127.0.0.1:20822'
// let domain = 'http://127.0.0.1:20822'
// let domain = 'https://chenyin.uj345.cc'
// let domain = 'https://sophia-shop.uj345.cc'
let domain = 'https://sophia-shop.uj345.cc'
module.exports = {
domain,

View File

@@ -113,7 +113,13 @@ export default {
data() {
return {
todaySigned: false,
currentPoints: 522,
/** 防止 loadCheckinData 与打卡后刷新并发时,旧 user/info 响应覆盖新积分 */
_userInfoFetchGen: 0,
/** 打卡流程进行中:禁止 onShow 触发的 loadCheckinData 用「打卡前」发出的 user/info 写 currentPoints */
_suppressStalePointsLoad: false,
/** 防止连点导致多次打卡请求与积分展示乱序 */
_checkinSubmitting: false,
currentPoints: 0,
streakDays: [
{ day: 1, completed: false, special: false },
{ day: 2, completed: false, special: false },
@@ -183,17 +189,25 @@ export default {
this.loadCheckinData();
},
onShow() {
// 页面显示时刷新数据
// 页面显示时刷新数据(打卡请求进行中时跳过,避免与 handleCheckin 内刷新竞态)
if (this._checkinSubmitting) {
return;
}
this.loadCheckinData();
},
methods: {
async loadCheckinData() {
async loadCheckinData(options = {}) {
const includePointsRefresh = !!options.includePointsRefresh;
const skipPoints = includePointsRefresh
? false
: (!!options.skipPoints || this._suppressStalePointsLoad);
const pointsGen = skipPoints ? null : ++this._userInfoFetchGen;
try {
const { getUserPoints, getCheckinStreak, getCheckinTasks } = await import('@/api/tool.js');
const { getSignGet } = await import('@/api/user.js');
const { getCheckinStreak, getCheckinTasks } = await import('@/api/tool.js');
const { getSignGet, getFrontUserInfo } = await import('@/api/user.js');
const [pointsRes, streakRes, tasksRes, signRes] = await Promise.all([
getUserPoints().catch(() => ({ data: { points: 0 } })),
const [userInfoRes, streakRes, tasksRes, signRes] = await Promise.all([
skipPoints ? Promise.resolve(null) : getFrontUserInfo().catch(() => null),
getCheckinStreak().catch(() => ({ data: { streakDays: 7, currentStreak: 0 } })),
getCheckinTasks().catch(() => ({ data: { tasks: [] } })),
getSignGet().catch(() => ({ data: { today: false } }))
@@ -203,8 +217,14 @@ export default {
this.todaySigned = true;
}
if (pointsRes.data) {
this.currentPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? 0;
// BUG-001-A打卡请求进行中时勿用并行的 user/info 回写积分(避免先显示旧分再跳变);显式 includePointsRefresh 时仍刷新
const allowPointsFromThisLoad =
!this._checkinSubmitting || includePointsRefresh;
if (!skipPoints && userInfoRes && pointsGen === this._userInfoFetchGen && allowPointsFromThisLoad) {
const p = this._parsePointsFromUserInfo(userInfoRes);
if (p != null && !Number.isNaN(Number(p))) {
this.currentPoints = Number(p);
}
}
if (streakRes.data) {
@@ -235,68 +255,107 @@ export default {
url: '/pages/tool/points-rules'
})
},
/**
* BUG-001-A打卡 API 成功前不得修改 currentPoints禁止本地 +30 或与跳转前的乐观更新)。
* BUG-001-B仅 GET /api/front/user/infogetFrontUserInfo解析账户 integral打卡接口返回的 integral 为当日奖励分值,绝非总积分。
*/
async handleCheckin() {
if (this._checkinSubmitting) {
return;
}
if (this.todaySigned) {
uni.showToast({ title: '今日已打卡', icon: 'none' });
return;
}
this._checkinSubmitting = true;
this._suppressStalePointsLoad = true;
const pointsBeforeCheckin = Number(this.currentPoints) || 0;
const { userCheckin, userCheckinDaily, getFrontUserInfo } = await import('@/api/user.js');
uni.showLoading({ title: '打卡中...', mask: true });
try {
const { setSignIntegral, getFrontUserInfo, getUserInfo } = await import('@/api/user.js');
const { getUserPoints } = await import('@/api/tool.js');
// 子问题 A在 API 返回成功前不得修改 currentPoints避免打卡前积分提前跳变禁止前端本地 +30 等)
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral即本项目的签到/打卡接口
await setSignIntegral();
// 子问题 B打卡成功后必须用服务端数据更新积分。发 GET /api/front/user/info 刷新用户积分,禁止硬编码 +30
const serverPoints = await this._fetchServerPoints(getFrontUserInfo, getUserInfo, getUserPoints);
if (serverPoints != null) {
this.currentPoints = Number(serverPoints);
// 1) /api/front/user/checkin失败时回退 /api/front/user/sign/integral二者均不读取 body 更新积分
await this._requestCheckin(userCheckin, userCheckinDaily);
// 2) 成功后必须 GET user/info用服务端账户积分更新展示
let refreshed = await this._refreshCurrentPointsFromUserInfo(getFrontUserInfo).catch((err) => {
console.warn('打卡后刷新积分失败:', err);
return false;
});
if (refreshed && Number(this.currentPoints) === pointsBeforeCheckin) {
await new Promise((r) => setTimeout(r, 250));
refreshed =
(await this._refreshCurrentPointsFromUserInfo(getFrontUserInfo).catch(() => false)) || refreshed;
}
if (!refreshed) {
await this.loadCheckinData({ includePointsRefresh: true });
}
this.todaySigned = true;
await this.loadCheckinData({ skipPoints: true });
uni.navigateTo({
url: '/pages/tool/checkin-publish'
});
} catch (e) {
const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败';
if (msg.includes('今日已签到') || msg.includes('不可重复')) {
this.todaySigned = true;
await this.loadCheckinData({ skipPoints: true });
} else {
await this.loadCheckinData({ includePointsRefresh: true }).catch(() => {});
}
uni.showToast({ title: msg, icon: 'none' });
return;
} finally {
this._suppressStalePointsLoad = false;
this._checkinSubmitting = false;
uni.hideLoading();
}
uni.navigateTo({
url: '/pages/tool/checkin-publish'
});
},
/**
* 从服务端拉取积分(用于打卡成功后刷新,积分值必须来自服务端,不可前端累加)
* 优先 GET /api/front/user/info失败时回退到 user 或 tool/points/info
*/
async _fetchServerPoints(getFrontUserInfo, getUserInfo, getUserPoints) {
/** 优先 GET user/checkin路由类失败时回退 user/sign/integral返回值勿用于 currentPointsdata 为签到档位配置integral 为奖励分非余额)。 */
async _requestCheckin(userCheckin, userCheckinDaily) {
try {
const userRes = await getFrontUserInfo();
if (userRes && userRes.data) {
const d = userRes.data;
const p = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints));
if (p != null) return p;
}
} catch (_) {}
try {
const userRes = await getUserInfo();
if (userRes && userRes.data) {
const d = userRes.data;
const p = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints));
if (p != null) return p;
}
} catch (_) {}
try {
const pointsRes = await getUserPoints();
if (pointsRes && pointsRes.data) {
const d = pointsRes.data;
const p = d.totalPoints ?? d.points ?? d.availablePoints;
if (p != null) return p;
}
} catch (_) {}
return null;
await userCheckin();
} catch (err) {
const msg = String((typeof err === 'string' ? err : (err && (err.message || err.msg))) || '');
const m = msg.toLowerCase();
// CRMEB 前端对 code=404 常 reject 文案「没有找到相关数据」,不含数字 404须一并识别才能回退到 sign/integral
const needFallback =
m.includes('404') ||
m.includes('405') ||
m.includes('not found') ||
m.includes('no static resource') ||
msg.includes('没有找到相关数据') ||
msg.includes('找不到');
if (!needFallback) throw err;
await userCheckinDaily();
}
},
async _refreshCurrentPointsFromUserInfo(getFrontUserInfoFn) {
const gen = ++this._userInfoFetchGen;
const getInfo =
typeof getFrontUserInfoFn === 'function'
? getFrontUserInfoFn
: (await import('@/api/user.js')).getFrontUserInfo;
const infoRes = await getInfo({ _: Date.now() });
if (gen !== this._userInfoFetchGen) {
return false;
}
const serverPoints = this._parsePointsFromUserInfo(infoRes);
if (serverPoints != null && !Number.isNaN(Number(serverPoints))) {
this.currentPoints = Number(serverPoints);
return true;
}
return false;
},
_parsePointsFromUserInfo(infoRes) {
if (!infoRes) return null;
const payload = infoRes.data;
const d = (payload && payload.data != null && typeof payload === 'object') ? payload.data : payload;
if (d == null || typeof d !== 'object') return null;
// 勿把打卡/签到接口的 SystemGroupDataSignConfigVo含 day + integral 奖励)当成个人中心 integral
if (d.day != null && d.nickname == null && d.nowMoney == null) {
return null;
}
const v = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints));
return v != null && v !== '' ? v : null;
},
getTodayIndex() {
// 根据连续打卡数据计算当前是第几天

File diff suppressed because it is too large Load Diff