diff --git a/miniprogram/app.js b/miniprogram/app.js index ce76aeb9..e626b034 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -26,6 +26,7 @@ App({ }, // 微信登录(第一步:获取openid和session_key) + // 已有手机号用户会直接返回 token,完成登录;无手机号用户需后续授权手机号 wxLogin() { return new Promise((resolve, reject) => { wx.login({ @@ -34,10 +35,9 @@ App({ url: `${this.globalData.baseUrl}/api/user/login`, method: "POST", data: { code: res.code }, - success: (loginRes) => { + success: async (loginRes) => { if (loginRes.data.code === 0) { const data = loginRes.data.data; - // 保存微信登录信息(用于后续手机号授权) this.globalData.wxLoginInfo = { openid: data.openid, unionid: data.unionid, @@ -46,9 +46,20 @@ App({ hasPhone: data.hasPhone, }; - // 如果已有token(老用户),直接使用 - if (data.userInfo && data.hasPhone) { - // 老用户已绑定手机号,生成token并登录 + if (data.token && data.userInfo && data.hasPhone) { + // 已有手机号的老用户:直接登录,无需授权手机号 + this.globalData.token = data.token; + this.globalData.userInfo = data.userInfo; + wx.setStorageSync("token", data.token); + this.connectWebSocket(); + try { + await this.ensureCurrentStore(); + await this.getUserInfo(this.globalData.currentStore?.storeId); + } catch (e) { + console.error("初始化门店/用户信息失败:", e); + } + } else if (data.userInfo && data.hasPhone) { + // 兼容:若后端返回 userInfo 但未带 token(旧版) this.globalData.userInfo = data.userInfo; } @@ -168,6 +179,51 @@ App({ }); }, + /** + * 获取默认门店(无需登录,供新用户浏览) + */ + getDefaultStore() { + return new Promise((resolve) => { + const lastStore = wx.getStorageSync("last_store"); + const params = {}; + if (lastStore?.storeId) params.last_store_id = lastStore.storeId; + + wx.getLocation({ + type: "gcj02", + success: (loc) => { + params.latitude = loc.latitude; + params.longitude = loc.longitude; + this.request("/api/store/default", params) + .then((res) => { + if (res.data) { + this.globalData.currentStore = { + storeId: res.data.storeId, + storeName: res.data.storeName || "", + storeAddress: res.data.storeAddress || "", + }; + } + resolve(this.globalData.currentStore); + }) + .catch(() => resolve(null)); + }, + fail: () => { + this.request("/api/store/default", params) + .then((res) => { + if (res.data) { + this.globalData.currentStore = { + storeId: res.data.storeId, + storeName: res.data.storeName || "", + storeAddress: res.data.storeAddress || "", + }; + } + resolve(this.globalData.currentStore); + }) + .catch(() => resolve(null)); + }, + }); + }); + }, + /** * 优先使用上次选择的门店,没有或失败时再请求最近门店(新用户逻辑) */ diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index f1b511fe..43d35a43 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -52,23 +52,23 @@ Page({ }, async initData() { - // 检查是否已登录(有 token) - if (!app.globalData.token) { - // 未登录,跳转到用户页面进行登录 - wx.switchTab({ url: "/pages/user/index" }); - return; - } - - // 获取当前门店 + // 获取当前门店(已登录用 ensureCurrentStore,未登录用 getDefaultStore 可浏览排名) try { - const store = await app.ensureCurrentStore(); - this.setData({ currentStore: store }); + if (app.globalData.token) { + const store = await app.ensureCurrentStore(); + this.setData({ currentStore: store }); + } else { + const store = await app.getDefaultStore(); + this.setData({ currentStore: store || { storeName: "请选择门店" } }); + } this.fetchData(); } catch (e) { console.error("获取门店失败:", e); - // 如果是认证失败,跳转到登录页 if (e.code === 401) { - wx.switchTab({ url: "/pages/user/index" }); + app.globalData.token = null; + const store = await app.getDefaultStore(); + this.setData({ currentStore: store || { storeName: "请选择门店" } }); + this.fetchData(); } } }, diff --git a/miniprogram/pages/match/challenge/index.js b/miniprogram/pages/match/challenge/index.js index c01dbece..9c933224 100644 --- a/miniprogram/pages/match/challenge/index.js +++ b/miniprogram/pages/match/challenge/index.js @@ -28,19 +28,15 @@ Page({ }, async initData() { - // 检查是否已登录(有 token) - if (!app.globalData.token) { - // 未登录,跳转到用户页面进行登录 - wx.switchTab({ url: "/pages/user/index" }); - return; - } - - // 每次显示页面时重新获取门店和天梯信息 + // 获取门店(已登录用 ensureCurrentStore,未登录用 getDefaultStore 可浏览) try { - await app.ensureCurrentStore(); - // 如果有门店,获取该门店的天梯信息 - if (app.globalData.currentStore && app.globalData.currentStore.storeId) { - await app.getLadderUser(app.globalData.currentStore.storeId); + if (app.globalData.token) { + await app.ensureCurrentStore(); + if (app.globalData.currentStore?.storeId) { + await app.getLadderUser(app.globalData.currentStore.storeId); + } + } else { + await app.getDefaultStore(); } } catch (e) { console.error("获取门店/天梯信息失败:", e); diff --git a/miniprogram/pages/points/mall/index.js b/miniprogram/pages/points/mall/index.js index 1763b1f7..3be1f12d 100644 --- a/miniprogram/pages/points/mall/index.js +++ b/miniprogram/pages/points/mall/index.js @@ -56,9 +56,12 @@ Page({ async initData() { if (!app.globalData.token) { try { - await app.login(); + await app.wxLogin(); } catch (e) { - console.error("登录失败:", e); + console.error("微信登录失败:", e); + } + if (!app.globalData.currentStore) { + await app.getDefaultStore(); } } const store = app.globalData.currentStore; diff --git a/miniprogram/pages/store/index.js b/miniprogram/pages/store/index.js index dd6527fe..09e7b20c 100644 --- a/miniprogram/pages/store/index.js +++ b/miniprogram/pages/store/index.js @@ -158,14 +158,14 @@ Page({ storeAddress: store.address }) - // 清空旧的天梯用户信息 app.globalData.ladderUser = null - // 获取该门店的天梯用户信息 - try { - await app.getLadderUser(store.id) - } catch (e) { - console.error('获取天梯信息失败:', e) + if (app.globalData.token) { + try { + await app.getLadderUser(store.id) + } catch (e) { + console.error('获取天梯信息失败:', e) + } } // 标记需要刷新数据 diff --git a/miniprogram/pages/user/index.js b/miniprogram/pages/user/index.js index 0ed1164e..a81dc9fe 100644 --- a/miniprogram/pages/user/index.js +++ b/miniprogram/pages/user/index.js @@ -68,7 +68,7 @@ Page({ }, async initData() { - // 先进行微信登录获取openid + // 微信登录:已有手机号用户会直接完成登录,无手机号用户需授权手机号 if (!app.globalData.wxLoginInfo) { try { await app.wxLogin(); @@ -79,6 +79,13 @@ Page({ if (app.globalData.token) { await this.refreshData(); + } else { + // 无 token 时同步当前状态(如 wxLoginInfo)到页面,用于判断是否显示手机号登录 + this.setData({ + userInfo: app.globalData.userInfo, + ladderUser: null, + currentStore: app.globalData.currentStore, + }); } }, diff --git a/server/src/controllers/storeController.js b/server/src/controllers/storeController.js index 8f8095ee..0d83fd1b 100644 --- a/server/src/controllers/storeController.js +++ b/server/src/controllers/storeController.js @@ -74,6 +74,62 @@ class StoreController { } } + // 获取默认门店(无需登录,供新用户浏览排名等) + async getDefaultStore(req, res) { + try { + const { latitude, longitude } = req.query; + const lastStore = req.query.last_store_id; // 可选:上次选择的门店 ID + + if (lastStore) { + const store = await Store.findByPk(lastStore); + if (store && store.status === 1) { + return res.json(success({ + storeId: store.id, + storeName: store.name, + storeAddress: store.address, + })); + } + } + + if (latitude && longitude) { + const stores = await Store.findAll({ where: { status: 1 } }); + let nearest = stores[0]; + let minDist = Infinity; + for (const s of stores) { + if (s.latitude && s.longitude) { + const d = calculateDistance( + parseFloat(latitude), + parseFloat(longitude), + parseFloat(s.latitude), + parseFloat(s.longitude) + ); + if (d < minDist) { + minDist = d; + nearest = s; + } + } + } + if (nearest) { + return res.json(success({ + storeId: nearest.id, + storeName: nearest.name, + storeAddress: nearest.address, + })); + } + } + + const first = await Store.findOne({ where: { status: 1 } }); + res.json(success(first ? { + storeId: first.id, + storeName: first.name, + storeAddress: first.address, + } : null)); + } catch (err) { + console.error('获取默认门店失败:', err); + res.status(500).json(error('获取失败')); + } + } + // 获取附近门店(默认显示所有门店,按距离由近到远排序) async getNearby(req, res) { try { diff --git a/server/src/controllers/userController.js b/server/src/controllers/userController.js index 847bcd3b..db27eb1c 100644 --- a/server/src/controllers/userController.js +++ b/server/src/controllers/userController.js @@ -47,37 +47,67 @@ class UserController { // 查找用户 let user = await User.findOne({ where: { openid } }); - let isNewUser = false; - - if (!user) { - isNewUser = true; - } + let isNewUser = !user; + const hasPhone = !!(user?.phone); // 返回登录信息(包含session_key用于后续手机号解密) - // 注意:实际生产环境中session_key不应该直接返回给前端 - // 这里为了简化流程,使用加密后的session_key const encryptedSessionKey = this.encryptSessionKey(session_key, openid); + const responseData = { + openid, + unionid, + sessionKey: encryptedSessionKey, + isNewUser, + hasPhone, + }; + + if (hasPhone && user) { + // 已有手机号的老用户:直接生成 token 登录,无需再次授权手机号 + const token = jwt.sign( + { userId: user.id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || "7d" } + ); + const ladderUsers = await LadderUser.findAll({ + where: { user_id: user.id, status: 1 }, + include: [{ model: Store, as: "store", attributes: ["id", "name"] }], + }); + responseData.token = token; + responseData.userInfo = { + id: user.id, + nickname: user.nickname, + avatar: getFullUrl(user.avatar, req), + phone: user.phone, + gender: user.gender, + memberCode: user.member_code, + totalPoints: user.total_points, + ladderUsers: ladderUsers.map((lu) => ({ + id: lu.id, + storeId: lu.store_id, + storeName: lu.store?.name, + realName: lu.real_name, + level: lu.level, + levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`, + powerScore: lu.power_score, + })), + }; + } else { + responseData.userInfo = user + ? { + id: user.id, + nickname: user.nickname, + avatar: user.avatar, + phone: user.phone, + gender: user.gender, + memberCode: user.member_code, + totalPoints: user.total_points, + } + : null; + } + res.json( success( - { - openid, - unionid, - sessionKey: encryptedSessionKey, - isNewUser, - hasPhone: user?.phone ? true : false, - userInfo: user - ? { - id: user.id, - nickname: user.nickname, - avatar: user.avatar, - phone: user.phone, - gender: user.gender, - memberCode: user.member_code, - totalPoints: user.total_points, - } - : null, - }, + responseData, isNewUser ? "请授权手机号完成注册" : "登录成功", ), ); diff --git a/server/src/routes/store.js b/server/src/routes/store.js index dbdbcfe2..1239e2da 100644 --- a/server/src/routes/store.js +++ b/server/src/routes/store.js @@ -6,6 +6,8 @@ const { authUser } = require('../middlewares/auth'); // 获取门店列表 router.get('/list', storeController.getList); +// 获取默认/最近门店(无需登录,供新用户浏览使用) +router.get('/default', storeController.getDefaultStore); // 获取附近门店(必须放在 /:id 之前,否则 "nearby" 会被当作 id 匹配) router.get('/nearby', authUser, storeController.getNearby);