fix(mp): 接通微信小程序登录并补齐生产配置
- 前端落地页调用 wx.login → /mp/login,保存 token,并在每次带 scene 进入时刷新登录(F-130 前端断点)
- 后端 /mp/login 的名册/任务查询放入 TenantScope,修复带 taskId 的 scene 必然 500(F-130 后端断点)
- 小程序生产 API 改为 VITE_MP_API_BASE(.env.production),不再写死 localhost
- 生产配置补全:七牛目录 shenxue、连接池/健康检查/优雅停机等;学生端只发小程序,移除 H5 段
- PC 生产 API 改到 admin.shenxue.jxit.com;新增 deploy/prod.env.example 并忽略 prod.env
- ⚠️ application-prod.yml 含生产凭据(按部署要求直接写入),仓库访问范围需收紧
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# 微信小程序生产包(`pnpm build:mp-weixin`)的环境变量
|
||||
#
|
||||
# ⚠️ 必须是 **HTTPS 正式域名**,并且要在微信公众平台
|
||||
# 「开发管理 → 开发设置 → 服务器域名」里配成 **request 合法域名**。
|
||||
# 否则真机上所有请求都会以 `request:fail url not in domain list` 失败。
|
||||
#
|
||||
# ⚠️ 这里指向后端 API 的地址(PC 管理端 .env.production 的 VITE_API_BASE_URL 同源),
|
||||
# 不是 mini program 的 appid/secret —— 那两项在后端 application-prod.yml 里。
|
||||
VITE_MP_API_BASE = 'https://admin.shenxue.jxit.com'
|
||||
@@ -43,7 +43,18 @@ export const API_BASE_URL =
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
export const API_BASE_URL = 'http://localhost:8080'
|
||||
/**
|
||||
* 小程序:没有"同域"这个概念,必须写绝对地址。
|
||||
*
|
||||
* 正式包用构建变量 `VITE_MP_API_BASE` 指定(见 `.env.production`):
|
||||
* 必须是 **HTTPS 正式域名**,并且要在微信公众平台
|
||||
* 「开发管理 → 开发设置 → 服务器域名」里配成 **request 合法域名**。
|
||||
*
|
||||
* 不指定时回退到本机 8080 —— 只用于微信开发者工具调试
|
||||
* (需要在「详情 → 本地设置」里勾"不校验合法域名")。
|
||||
*/
|
||||
const MP_API_BASE = (import.meta.env?.VITE_MP_API_BASE ?? '') as string
|
||||
export const API_BASE_URL = MP_API_BASE !== '' ? MP_API_BASE : 'http://localhost:8080'
|
||||
// #endif
|
||||
|
||||
/** 主题色(与 PC 管理端同一套腾讯云 TDesign 色板,两端视觉要一致)。 */
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { request } from '@/utils/request'
|
||||
import { getToken, loginByWechat } from '@/utils/auth'
|
||||
|
||||
/**
|
||||
* 扫码落地页。
|
||||
@@ -89,15 +90,6 @@ const isError = ref(false)
|
||||
const loading = ref(false)
|
||||
const title = ref('材料填报')
|
||||
|
||||
/** 取登录态(授权回调把 token 放在 URL fragment 里,落地页存下来的)。 */
|
||||
function token(): string {
|
||||
try {
|
||||
return uni.getStorageSync('token') || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前 URL 里的 scene(授权回调回来时在 hash 路由的 query 里)。
|
||||
*
|
||||
@@ -158,7 +150,7 @@ async function load() {
|
||||
// ★ 没有 scene 但有登录态 → 说明他是**回来看进度的**,不是扫码进来的。
|
||||
// 以前这种情况一律报错"请扫描二维码进入",于是学生一旦找不到老师发的那条消息
|
||||
// 就再也进不来了(通知渠道还没做,他只能靠历史消息)。现在直接送去「我的填报」。
|
||||
if (token()) {
|
||||
if (getToken()) {
|
||||
uni.redirectTo({ url: '/pages/tasks/tasks' })
|
||||
return
|
||||
}
|
||||
@@ -188,7 +180,7 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
async function start() {
|
||||
const s = scene.value || sceneFromUrl()
|
||||
if (!s) {
|
||||
uni.showModal({
|
||||
@@ -198,10 +190,69 @@ function start() {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 扫码进来时 bootstrap 已经登录过;这里再兜一次:网络抖动、第一次登录失败后,
|
||||
// 学生点"开始填报"时还有一次机会,而不是带着空 token 去核验页吃 40101。
|
||||
if (!getToken()) {
|
||||
try {
|
||||
await loginByWechat(s)
|
||||
} catch (e) {
|
||||
uni.showModal({
|
||||
title: '登录失败',
|
||||
content: e instanceof Error ? e.message : '微信登录失败,请稍后重试',
|
||||
showCancel: false
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 带上 scene —— 见 sceneFromUrl 的说明(H5 跳转不保留 URL 参数)
|
||||
uni.navigateTo({ url: `/pages/verify/verify?scene=${encodeURIComponent(s)}` })
|
||||
}
|
||||
|
||||
/**
|
||||
* 落地页启动。
|
||||
*
|
||||
* - 小程序:带 scene 进来就先 `wx.login` 换一次 token(token 7 天有效,
|
||||
* 旧 token 过期后直接去核验页只会得到 40101);
|
||||
* - 再读 scene-context,把"哪个学校 / 什么任务 / 能不能填"显示出来。
|
||||
*
|
||||
* 登录放在读任务信息**之前**:登录只需要一次 code 交换,很快;
|
||||
* 这样页面渲染出「开始填报」时 token 一定已经落盘,不会出现
|
||||
* "学生点得比登录快"导致同一页里发两次 `wx.login`。
|
||||
* 登录失败也不挡任务信息展示 —— 点「开始填报」时会再试一次并明确报错。
|
||||
*/
|
||||
async function bootstrap() {
|
||||
// #ifdef MP-WEIXIN
|
||||
let loginError = ''
|
||||
if (scene.value) {
|
||||
loading.value = true
|
||||
try {
|
||||
// 每次带 scene 进来都换一次 token:小程序 token 有效期 7 天,
|
||||
// 拿过期 token 去核验页只会得到 40101。登录本身很便宜(一次 code 换 openid)。
|
||||
await loginByWechat(scene.value)
|
||||
} catch (e) {
|
||||
// 已有 token 时登录失败不致命(可能只是网络抖动),继续用旧 token;
|
||||
// 连 token 都没有才把错误显示出来。
|
||||
if (!getToken()) {
|
||||
loginError = e instanceof Error ? e.message : '微信登录失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
await load()
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (loginError) {
|
||||
isError.value = true
|
||||
message.value = loginError
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
rawScene.value = options?.scene || options?.q || ''
|
||||
scene.value = pickScene(options)
|
||||
@@ -225,7 +276,7 @@ onLoad((options) => {
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
void bootstrap()
|
||||
})
|
||||
|
||||
// 下拉刷新:任务被截止/恢复后,学生不必杀进程重进
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 微信小程序登录(契约 §5.3 的 `/mp/login`)。
|
||||
*
|
||||
* <h3>为什么必须由前端调一次</h3>
|
||||
*
|
||||
* `wx.login()` 拿到的 `code` **一次有效、几分钟过期**,只能由小程序端现取。
|
||||
* 后端拿它去 `jscode2session` 换 openid,再按 scene 里的租户找/建学生账号。
|
||||
* 没有这一步就没有 token —— 后面所有接口都会 40101。
|
||||
*
|
||||
* <h3>与 H5 网页授权的区别</h3>
|
||||
*
|
||||
* H5 的 token 是后端 302 回来时放在 URL 里的(见 pages/index/index.vue 的
|
||||
* `options.token`);小程序没有这个回调,必须主动调接口换。
|
||||
* 生产已不部署 H5,学生端只走这一条路。
|
||||
*/
|
||||
|
||||
/** 与后端 MpLoginService.LoginVO 对齐(字段名不要改,后端直接按 JSON 返回)。 */
|
||||
export interface MpLoginResult {
|
||||
token: string
|
||||
expireSeconds: number
|
||||
needBindPhone: boolean
|
||||
user: {
|
||||
id: number
|
||||
realName: string | null
|
||||
userType: number
|
||||
}
|
||||
/** 扫的是任务码、且这个学生已经绑定名册时才有。 */
|
||||
boundRoster?: {
|
||||
taskId: number
|
||||
taskName: string | null
|
||||
rosterId: number
|
||||
} | null
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'token'
|
||||
const NEED_BIND_PHONE_KEY = 'needBindPhone'
|
||||
const USER_KEY = 'mpUser'
|
||||
const BOUND_ROSTER_KEY = 'boundRoster'
|
||||
|
||||
/** 登录态(H5 与小程序共用同一个 storage key)。 */
|
||||
export function getToken(): string {
|
||||
try {
|
||||
return uni.getStorageSync(TOKEN_KEY) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 清掉登录态(切换学生 / 排查登录问题时用)。 */
|
||||
export function clearToken(): void {
|
||||
try {
|
||||
uni.removeStorageSync(TOKEN_KEY)
|
||||
uni.removeStorageSync(NEED_BIND_PHONE_KEY)
|
||||
uni.removeStorageSync(USER_KEY)
|
||||
uni.removeStorageSync(BOUND_ROSTER_KEY)
|
||||
} catch {
|
||||
// 存储不可用时本来也没有登录态可清
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端提示"手机号还没绑"。页面上没有绑定入口时先存下来,不影响登录。 */
|
||||
export function getNeedBindPhone(): boolean {
|
||||
try {
|
||||
return uni.getStorageSync(NEED_BIND_PHONE_KEY) === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function saveLoginResult(r: MpLoginResult): void {
|
||||
try {
|
||||
uni.setStorageSync(TOKEN_KEY, r.token)
|
||||
uni.setStorageSync(NEED_BIND_PHONE_KEY, r.needBindPhone === true)
|
||||
if (r.user) {
|
||||
uni.setStorageSync(USER_KEY, r.user)
|
||||
}
|
||||
if (r.boundRoster) {
|
||||
uni.setStorageSync(BOUND_ROSTER_KEY, r.boundRoster)
|
||||
}
|
||||
} catch {
|
||||
// ⚠️ 存不进去也不该让登录失败:本次页面还能用返回的 token,
|
||||
// 只是下次请求会重新走一次登录。
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序登录:`wx.login` 拿 code → `POST /api/v1/mp/login` 换 token。
|
||||
*
|
||||
* ⚠️ `scene` 不能丢:它决定"你属于哪个学校"(统一应用 + scene 识别租户)。
|
||||
* 缺失时后端明确报错,不会去猜一个租户 —— 猜错就是 A 校学生登进 B 校。
|
||||
*/
|
||||
export function loginByWechat(scene: string): Promise<MpLoginResult> {
|
||||
// #ifdef MP-WEIXIN
|
||||
return new Promise<MpLoginResult>((resolve, reject) => {
|
||||
if (!scene) {
|
||||
reject(new Error('缺少 scene,无法确定学校;请重新扫描班主任发的二维码'))
|
||||
return
|
||||
}
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => {
|
||||
if (!res.code) {
|
||||
reject(new Error('微信登录失败:没有拿到 code'))
|
||||
return
|
||||
}
|
||||
request<MpLoginResult>({
|
||||
url: '/api/v1/mp/login',
|
||||
method: 'POST',
|
||||
data: { code: res.code, scene }
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data || !data.token) {
|
||||
reject(new Error('登录失败:后端没有返回 token'))
|
||||
return
|
||||
}
|
||||
saveLoginResult(data)
|
||||
resolve(data)
|
||||
})
|
||||
.catch(reject)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(`微信登录失败:${err.errMsg || '未知原因'}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return Promise.reject(new Error('当前环境不是微信小程序,无法使用微信登录'))
|
||||
// #endif
|
||||
}
|
||||
|
||||
/** 已有 token 就直接用;没有就走一次微信登录。 */
|
||||
export async function ensureLogin(scene: string): Promise<string> {
|
||||
const existing = getToken()
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
const result = await loginByWechat(scene)
|
||||
return result.token
|
||||
}
|
||||
Reference in New Issue
Block a user