This commit is contained in:
2026-04-17 16:31:32 +08:00
parent adadb3bf1d
commit 2476655b28
116 changed files with 3875 additions and 583 deletions

View File

@@ -1,4 +1,11 @@
const { request } = require("../utils/request");
/**
* @description 小程序 auth 模块 API 封装;login/refreshToken/getAuthCurrentUser 与后端 /api/auth/* 路径一一对应
* @filename auth.js
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
const { request } = require("/utils/request");
function login(data) {
return request({
@@ -8,6 +15,23 @@ function login(data) {
});
}
function refreshToken(refreshToken) {
return request({
url: "/api/auth/tokens/refresh",
method: "POST",
data: { refreshToken }
});
}
function getAuthCurrentUser() {
return request({
url: "/api/auth/users/current",
method: "GET"
});
}
module.exports = {
login
login,
refreshToken,
getAuthCurrentUser
};

View File

@@ -1,4 +1,4 @@
const { request } = require("../utils/request");
const { request } = require("/utils/request");
function getRouteMeta() {
return request({

View File

@@ -1,5 +1,6 @@
{
"pages": [
"pages/login/index",
"pages/home/index",
"pages/profile/index"
],

View File

@@ -1,6 +1,64 @@
/**
* @description 学生端首页 Page;并发拉取 upms + auth 当前用户,客户端二次校验 roleCodes 必含 STUDENT 防御服务端异常放行
* @filename index.js
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
const { getCurrentUser } = require("/api/upms");
const { getAuthCurrentUser } = require("/api/auth");
const { clearTokens, getAccessToken } = require("/utils/session");
Page({
data: {
title: "K12Study 小程序骨架",
description: "这里先放首页占位,后续可扩展为家长端或学生端入口。"
loading: false,
error: "",
currentUser: null
},
onShow() {
const accessToken = getAccessToken();
if (!accessToken) {
wx.reLaunch({ url: "/pages/login/index" });
return;
}
this.loadCurrentUser();
},
async loadCurrentUser() {
this.setData({ loading: true, error: "" });
try {
const [upmsResp, authResp] = await Promise.all([
getCurrentUser(),
getAuthCurrentUser()
]);
const upmsUser = upmsResp.data || {};
const authUser = authResp.data || {};
const roleCodes = Array.isArray(authUser.roleCodes) ? authUser.roleCodes : [];
const isStudent = roleCodes.some((code) => String(code).toUpperCase() === "STUDENT");
if (!isStudent) {
clearTokens();
this.setData({ error: "小程序仅允许学生账号登录" });
wx.reLaunch({ url: "/pages/login/index" });
return;
}
this.setData({
currentUser: {
username: upmsUser.username || authUser.username || "-",
displayName: upmsUser.displayName || authUser.displayName || "-",
tenantId: upmsUser.tenantId || authUser.tenantId || "-",
deptId: upmsUser.deptId || authUser.deptId || "-",
roleCodesText: roleCodes.join(", ") || "-"
}
});
} catch (error) {
this.setData({
error: error && error.message ? error.message : "加载用户信息失败"
});
} finally {
this.setData({ loading: false });
}
},
handleLogout() {
clearTokens();
wx.reLaunch({ url: "/pages/login/index" });
}
});

View File

@@ -1,6 +1,15 @@
<view class="page">
<view class="card">
<view>{{title}}</view>
<view style="margin-top: 16rpx; color: #64748b;">{{description}}</view>
<view class="title">学生端首页</view>
<view wx:if="{{loading}}" class="hint">加载用户信息中...</view>
<view wx:elif="{{error}}" class="error">{{error}}</view>
<view wx:elif="{{currentUser}}" class="profile">
<view class="item">账号:{{currentUser.username}}</view>
<view class="item">姓名:{{currentUser.displayName}}</view>
<view class="item">租户:{{currentUser.tenantId}}</view>
<view class="item">部门:{{currentUser.deptId}}</view>
<view class="item">角色:{{currentUser.roleCodesText}}</view>
</view>
<button class="logout-btn" bindtap="handleLogout">退出登录</button>
</view>
</view>

View File

@@ -1,3 +1,25 @@
view {
.title {
font-size: 34rpx;
font-weight: 600;
margin-bottom: 16rpx;
}
.hint {
color: #64748b;
margin-bottom: 16rpx;
}
.error {
color: #dc2626;
margin-bottom: 16rpx;
}
.profile {
display: grid;
gap: 10rpx;
margin-bottom: 20rpx;
}
.item {
line-height: 1.6;
}

View File

@@ -0,0 +1,50 @@
const { login } = require("/api/auth");
const { getAccessToken, setTokens } = require("/utils/session");
Page({
data: {
username: "student01",
password: "stud123",
tenantId: "SCH-HQ",
loading: false,
error: ""
},
onShow() {
if (getAccessToken()) {
wx.reLaunch({ url: "/pages/home/index" });
}
},
onUsernameInput(event) {
this.setData({ username: event.detail.value });
},
onPasswordInput(event) {
this.setData({ password: event.detail.value });
},
onTenantInput(event) {
this.setData({ tenantId: event.detail.value });
},
async handleLogin() {
if (this.data.loading) {
return;
}
this.setData({ loading: true, error: "" });
try {
const response = await login({
username: this.data.username,
password: this.data.password,
provinceCode: "330000",
areaCode: "330100",
tenantId: this.data.tenantId,
clientType: "MINI"
});
setTokens(response.data.accessToken, response.data.refreshToken);
wx.reLaunch({ url: "/pages/home/index" });
} catch (error) {
this.setData({
error: error && error.message ? error.message : "登录失败,请稍后重试"
});
} finally {
this.setData({ loading: false });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "学生登录"
}

View File

@@ -0,0 +1,12 @@
<view class="page">
<view class="card login-card">
<view class="login-title">K12Study 学生端登录</view>
<input class="login-input" placeholder="用户名" value="{{username}}" bindinput="onUsernameInput" />
<input class="login-input" password placeholder="密码" value="{{password}}" bindinput="onPasswordInput" />
<input class="login-input" placeholder="租户ID" value="{{tenantId}}" bindinput="onTenantInput" />
<view wx:if="{{error}}" class="login-error">{{error}}</view>
<button class="login-btn" loading="{{loading}}" disabled="{{loading}}" bindtap="handleLogin">
{{loading ? '登录中...' : '登录'}}
</button>
</view>
</view>

View File

@@ -0,0 +1,27 @@
.login-card {
display: grid;
gap: 16rpx;
}
.login-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.login-input {
border: 1px solid #dbe3f0;
border-radius: 12rpx;
height: 72rpx;
padding: 0 20rpx;
background: #fff;
}
.login-btn {
margin-top: 6rpx;
}
.login-error {
color: #dc2626;
font-size: 24rpx;
}

View File

@@ -1,6 +1,13 @@
const { getAccessToken } = require("/utils/session");
Page({
data: {
title: "我的",
description: "这里预留账号中心、学校切换、消息入口等能力。"
},
onShow() {
if (!getAccessToken()) {
wx.reLaunch({ url: "/pages/login/index" });
}
}
});

View File

@@ -1,4 +1,7 @@
const BASE_URL = "http://localhost:8088";
const { clearTokens, getAccessToken, getRefreshToken, setTokens } = require("./session");
let refreshPromise = null;
function isApiResponse(payload) {
return (
@@ -12,16 +15,29 @@ function isApiResponse(payload) {
function request(options) {
return new Promise((resolve, reject) => {
const accessToken = getAccessToken();
wx.request({
url: `${BASE_URL}${options.url}`,
method: options.method || "GET",
data: options.data,
header: {
"Content-Type": "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
...(options.header || {})
},
success: (response) => {
success: async (response) => {
const payload = response.data;
if (response.statusCode === 401 && !options.skipAuthRefresh && !isAuthTokenPath(options.url)) {
const nextAccessToken = await tryRefreshAccessToken();
if (nextAccessToken) {
request({ ...options, skipAuthRefresh: true }).then(resolve).catch(reject);
return;
}
clearTokens();
redirectToLogin();
reject(new Error("登录已过期,请重新登录"));
return;
}
if (response.statusCode < 200 || response.statusCode >= 300) {
const message =
payload && typeof payload === "object" && payload.message
@@ -47,6 +63,61 @@ function request(options) {
});
}
function isAuthTokenPath(url) {
return url === "/api/auth/tokens" || url === "/api/auth/tokens/refresh";
}
function redirectToLogin() {
const pages = getCurrentPages();
const currentRoute = pages.length > 0 ? pages[pages.length - 1].route : "";
if (currentRoute !== "pages/login/index") {
wx.reLaunch({ url: "/pages/login/index" });
}
}
function tryRefreshAccessToken() {
if (refreshPromise) {
return refreshPromise;
}
const refreshToken = getRefreshToken();
if (!refreshToken) {
return Promise.resolve(null);
}
refreshPromise = new Promise((resolve) => {
wx.request({
url: `${BASE_URL}/api/auth/tokens/refresh`,
method: "POST",
data: { refreshToken },
header: { "Content-Type": "application/json" },
success: (response) => {
const payload = response.data;
if (
response.statusCode >= 200 &&
response.statusCode < 300 &&
isApiResponse(payload) &&
payload.code === 0 &&
payload.data &&
payload.data.accessToken &&
payload.data.refreshToken
) {
setTokens(payload.data.accessToken, payload.data.refreshToken);
resolve(payload.data.accessToken);
return;
}
resolve(null);
},
fail: () => resolve(null),
complete: () => {
refreshPromise = null;
}
});
});
return refreshPromise;
}
module.exports = {
request
};

27
app/src/utils/session.js Normal file
View File

@@ -0,0 +1,27 @@
const ACCESS_TOKEN_KEY = "k12study.access-token";
const REFRESH_TOKEN_KEY = "k12study.refresh-token";
function getAccessToken() {
return wx.getStorageSync(ACCESS_TOKEN_KEY);
}
function getRefreshToken() {
return wx.getStorageSync(REFRESH_TOKEN_KEY);
}
function setTokens(accessToken, refreshToken) {
wx.setStorageSync(ACCESS_TOKEN_KEY, accessToken);
wx.setStorageSync(REFRESH_TOKEN_KEY, refreshToken);
}
function clearTokens() {
wx.removeStorageSync(ACCESS_TOKEN_KEY);
wx.removeStorageSync(REFRESH_TOKEN_KEY);
}
module.exports = {
getAccessToken,
getRefreshToken,
setTokens,
clearTokens
};