feat: Implement default store retrieval for new users and enhance login flow

- Added a new method to fetch the default store for users without a login, allowing them to browse store rankings.
- Updated the wxLogin function to streamline the login process for users with existing phone numbers, enabling direct token retrieval.
- Refactored various page components to utilize the new getDefaultStore method for better user experience when accessing store information.
- Enhanced error handling and data synchronization for user and store information across multiple pages.
This commit is contained in:
ethanfly 2026-02-07 14:04:31 +08:00
parent 459fa02eb9
commit 1a530ebf02
9 changed files with 213 additions and 63 deletions

View File

@ -26,6 +26,7 @@ App({
}, },
// 微信登录第一步获取openid和session_key // 微信登录第一步获取openid和session_key
// 已有手机号用户会直接返回 token完成登录无手机号用户需后续授权手机号
wxLogin() { wxLogin() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
wx.login({ wx.login({
@ -34,10 +35,9 @@ App({
url: `${this.globalData.baseUrl}/api/user/login`, url: `${this.globalData.baseUrl}/api/user/login`,
method: "POST", method: "POST",
data: { code: res.code }, data: { code: res.code },
success: (loginRes) => { success: async (loginRes) => {
if (loginRes.data.code === 0) { if (loginRes.data.code === 0) {
const data = loginRes.data.data; const data = loginRes.data.data;
// 保存微信登录信息(用于后续手机号授权)
this.globalData.wxLoginInfo = { this.globalData.wxLoginInfo = {
openid: data.openid, openid: data.openid,
unionid: data.unionid, unionid: data.unionid,
@ -46,9 +46,20 @@ App({
hasPhone: data.hasPhone, hasPhone: data.hasPhone,
}; };
// 如果已有token老用户直接使用 if (data.token && data.userInfo && data.hasPhone) {
if (data.userInfo && data.hasPhone) { // 已有手机号的老用户:直接登录,无需授权手机号
// 老用户已绑定手机号生成token并登录 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; 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));
},
});
});
},
/** /**
* 优先使用上次选择的门店没有或失败时再请求最近门店新用户逻辑 * 优先使用上次选择的门店没有或失败时再请求最近门店新用户逻辑
*/ */

View File

@ -52,23 +52,23 @@ Page({
}, },
async initData() { async initData() {
// 检查是否已登录(有 token // 获取当前门店(已登录用 ensureCurrentStore未登录用 getDefaultStore 可浏览排名)
if (!app.globalData.token) {
// 未登录,跳转到用户页面进行登录
wx.switchTab({ url: "/pages/user/index" });
return;
}
// 获取当前门店
try { try {
if (app.globalData.token) {
const store = await app.ensureCurrentStore(); const store = await app.ensureCurrentStore();
this.setData({ currentStore: store }); this.setData({ currentStore: store });
} else {
const store = await app.getDefaultStore();
this.setData({ currentStore: store || { storeName: "请选择门店" } });
}
this.fetchData(); this.fetchData();
} catch (e) { } catch (e) {
console.error("获取门店失败:", e); console.error("获取门店失败:", e);
// 如果是认证失败,跳转到登录页
if (e.code === 401) { 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();
} }
} }
}, },

View File

@ -28,20 +28,16 @@ Page({
}, },
async initData() { async initData() {
// 检查是否已登录(有 token // 获取门店(已登录用 ensureCurrentStore未登录用 getDefaultStore 可浏览)
if (!app.globalData.token) {
// 未登录,跳转到用户页面进行登录
wx.switchTab({ url: "/pages/user/index" });
return;
}
// 每次显示页面时重新获取门店和天梯信息
try { try {
if (app.globalData.token) {
await app.ensureCurrentStore(); await app.ensureCurrentStore();
// 如果有门店,获取该门店的天梯信息 if (app.globalData.currentStore?.storeId) {
if (app.globalData.currentStore && app.globalData.currentStore.storeId) {
await app.getLadderUser(app.globalData.currentStore.storeId); await app.getLadderUser(app.globalData.currentStore.storeId);
} }
} else {
await app.getDefaultStore();
}
} catch (e) { } catch (e) {
console.error("获取门店/天梯信息失败:", e); console.error("获取门店/天梯信息失败:", e);
} }

View File

@ -56,9 +56,12 @@ Page({
async initData() { async initData() {
if (!app.globalData.token) { if (!app.globalData.token) {
try { try {
await app.login(); await app.wxLogin();
} catch (e) { } catch (e) {
console.error("登录失败:", e); console.error("微信登录失败:", e);
}
if (!app.globalData.currentStore) {
await app.getDefaultStore();
} }
} }
const store = app.globalData.currentStore; const store = app.globalData.currentStore;

View File

@ -158,15 +158,15 @@ Page({
storeAddress: store.address storeAddress: store.address
}) })
// 清空旧的天梯用户信息
app.globalData.ladderUser = null app.globalData.ladderUser = null
// 获取该门店的天梯用户信息 if (app.globalData.token) {
try { try {
await app.getLadderUser(store.id) await app.getLadderUser(store.id)
} catch (e) { } catch (e) {
console.error('获取天梯信息失败:', e) console.error('获取天梯信息失败:', e)
} }
}
// 标记需要刷新数据 // 标记需要刷新数据
app.globalData.storeChanged = true app.globalData.storeChanged = true

View File

@ -68,7 +68,7 @@ Page({
}, },
async initData() { async initData() {
// 先进行微信登录获取openid // 微信登录:已有手机号用户会直接完成登录,无手机号用户需授权手机号
if (!app.globalData.wxLoginInfo) { if (!app.globalData.wxLoginInfo) {
try { try {
await app.wxLogin(); await app.wxLogin();
@ -79,6 +79,13 @@ Page({
if (app.globalData.token) { if (app.globalData.token) {
await this.refreshData(); await this.refreshData();
} else {
// 无 token 时同步当前状态(如 wxLoginInfo到页面用于判断是否显示手机号登录
this.setData({
userInfo: app.globalData.userInfo,
ladderUser: null,
currentStore: app.globalData.currentStore,
});
} }
}, },

View File

@ -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) { async getNearby(req, res) {
try { try {

View File

@ -47,26 +47,52 @@ class UserController {
// 查找用户 // 查找用户
let user = await User.findOne({ where: { openid } }); let user = await User.findOne({ where: { openid } });
let isNewUser = false; let isNewUser = !user;
const hasPhone = !!(user?.phone);
if (!user) {
isNewUser = true;
}
// 返回登录信息包含session_key用于后续手机号解密 // 返回登录信息包含session_key用于后续手机号解密
// 注意实际生产环境中session_key不应该直接返回给前端
// 这里为了简化流程使用加密后的session_key
const encryptedSessionKey = this.encryptSessionKey(session_key, openid); const encryptedSessionKey = this.encryptSessionKey(session_key, openid);
res.json( const responseData = {
success(
{
openid, openid,
unionid, unionid,
sessionKey: encryptedSessionKey, sessionKey: encryptedSessionKey,
isNewUser, isNewUser,
hasPhone: user?.phone ? true : false, hasPhone,
userInfo: user };
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, id: user.id,
nickname: user.nickname, nickname: user.nickname,
@ -76,8 +102,12 @@ class UserController {
memberCode: user.member_code, memberCode: user.member_code,
totalPoints: user.total_points, totalPoints: user.total_points,
} }
: null, : null;
}, }
res.json(
success(
responseData,
isNewUser ? "请授权手机号完成注册" : "登录成功", isNewUser ? "请授权手机号完成注册" : "登录成功",
), ),
); );

View File

@ -6,6 +6,8 @@ const { authUser } = require('../middlewares/auth');
// 获取门店列表 // 获取门店列表
router.get('/list', storeController.getList); router.get('/list', storeController.getList);
// 获取默认/最近门店(无需登录,供新用户浏览使用)
router.get('/default', storeController.getDefaultStore);
// 获取附近门店(必须放在 /:id 之前,否则 "nearby" 会被当作 id 匹配) // 获取附近门店(必须放在 /:id 之前,否则 "nearby" 会被当作 id 匹配)
router.get('/nearby', authUser, storeController.getNearby); router.get('/nearby', authUser, storeController.getNearby);