Files
hhrs/pages/index/login.vue
T
jacobxu666 e836fb33c8 refactor: 登录改用 hhrs 专属接口地址,并收紧成功判定防假成功
1. http.api.js 登录/验证码地址切到 open-api/hhService/login、open-api/hhService/sendLoginVerifyCode(后端保留 /pingXiang 旧地址转发,旧包不受影响;新包需后端先部署)

2. isBizOk 收紧:无业务码时额外排除网关把 404/错误页包成 HTTP 200 的情况(实测未部署的新地址会返回 {data:{status:404,error:'Not Found'}} 且 errcode=0),避免又被判成成功

验证:7 组判定用例全通过(含 404 包 200、旧后端无 accessToken 等边界);HBuilderX 编译成功;浏览器实测新地址未部署时提示“登录失败”且不写入登录态,其余页面正常、无 JS 错误。
2026-09-15 10:24:50 +08:00

334 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- xuchao: 顶部导航(返回+页面名称) -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<u-icon name="arrow-left" color="#1a1818" size="40"></u-icon>
</view>
<text class="nav-title">手机号登录</text>
</view>
<!-- 标题区 -->
<view class="title-area">
<text class="title">手机号登录</text>
<text class="subtitle">请使用真实手机号登录</text>
</view>
<!-- 表单区 -->
<view class="form-area">
<view class="input-item">
<input class="input-field" type="number" maxlength="11" v-model="phone" placeholder="请输入手机号码" />
</view>
<view class="input-item code-row">
<input class="input-field" type="number" maxlength="4" v-model="code" placeholder="请输入验证码" />
<text class="code-btn" :class="{ disabled: countdown > 0 }" @click="sendCode">
{{ countdown > 0 ? countdown + '秒后重新获取' : '获取验证码' }}
</text>
</view>
</view>
<!-- 登录按钮 -->
<view class="login-btn" @click="onLogin">立即登录</view>
<!-- 协议区 -->
<view class="agreement-area">
<view class="checkbox" :class="{ checked: agreed }" @click="agreed = !agreed">
<text class="check-icon" v-if="agreed">✓</text>
</view>
<text class="agreement-text" @click="agreed = !agreed">我已阅读并同意</text>
<text class="agreement-link" @click.stop="goAgreement('user')">《用户协议》</text>
<text class="agreement-text" @click="agreed = !agreed">和</text>
<text class="agreement-link" @click.stop="goAgreement('privacy')">《隐私协议》</text>
</view>
</view>
</template>
<script>
import config from '@/common/config.js'
export default {
data() {
return {
phone: '',
code: '',
countdown: 0,
timer: null,
agreed: true,
redirect: ''
}
},
onLoad(options) {
this.redirect = options.redirect || ''
},
onUnload() {
if (this.timer) {
clearInterval(this.timer)
}
},
methods: {
// xuchao: open-api/hhService/login、sendLoginVerifyCode 返回 OpenapiResponse{code,msg,data},
// 被网关再包一层后外层 errcode 恒为 0,真正的业务码在 data.code 里。
// 只判 errcode 会把「验证码错误/过期、短信被限流」全部当成成功。
// 宽松兜底:后端若改成不返回 data.code,仍按 errcode 判定,不会把成功误判为失败。
isBizOk(res) {
if (!res || res.errcode !== 0) return false
const d = res.data
if (!d || d.code === undefined || d.code === null) {
// xuchao: 没有业务码时,还要排除“网关把 404/错误页包成 HTTP 200”的情况
// (实测未部署的新地址会返回 {data:{timestamp,status:404,error:'Not Found'}} 且 errcode=0)
return !d || (d.status === undefined && d.error === undefined)
}
return d.code === 200
},
// xuchao: 失败原因优先取业务 msg,其次取网关 errmsg
bizMsg(res) {
return (res && res.data && res.data.msg) || (res && res.errmsg) || ''
},
sendCode() {
if (this.countdown > 0) return
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
this.$u.api.sendLoginVerifyCode({ smsType: 'CIB_LOGIN', mobile: this.phone }).then(res => {
if (this.isBizOk(res)) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
// xuchao: 倒计时移到发送成功之后(原来无条件启动,发送失败也会被白锁 60 秒无法重试)
this.countdown = 60
this.timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.timer)
this.timer = null
}
}, 1000)
} else {
// xuchao: 透出后端真实原因,如「操作频繁,请稍后再试」
uni.showToast({ title: this.bizMsg(res) || '发送失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '发送失败', icon: 'none' })
})
},
onLogin() {
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!this.code || this.code.length < 4) {
uni.showToast({ title: '请输入4位验证码', icon: 'none' })
return
}
if (!this.agreed) {
uni.showToast({ title: '请同意用户协议和隐私协议', icon: 'none' })
return
}
// xuchao: password 为后端要求的固定旁路口令,集中取自 config.loginFixedPassword
this.$u.api.checkerLogin({ password: config.loginFixedPassword, mobile: this.phone, verifyCode: this.code }).then(res => {
if (this.isBizOk(res)) {
// xuchao: 后端当前只回 userId/userName,并未下发 accessToken(字段名也是 accessToken)。
// 取到才存、取不到存空串,避免把 undefined 写进 storage 变成 "Bearer undefined"
const payload = (res.data && res.data.data) || {}
uni.setStorageSync('token', payload.accessToken || payload.token || '')
uni.setStorageSync('phone', this.phone)
this.$u.api.getEligibility({ phone: this.phone, identityNo: '', name: '' }).then(eRes => {
if (eRes.errcode === 0) {
uni.setStorageSync('isAuth', eRes.data.isAuth)
uni.setStorageSync('hasWin', eRes.data.hasWin)
uni.setStorageSync('cardNumber', eRes.data.cardNumber)
uni.setStorageSync('expiresTime', eRes.data.expiresTime)
}
}).finally(() => {
if (this.redirect === 'rights') {
const isAuth = uni.getStorageSync('isAuth')
const hasWin = uni.getStorageSync('hasWin')
let status = 'auth'
if (isAuth) {
status = hasWin ? 'info' : 'noWin'
}
uni.redirectTo({ url: `/pages/rights/rights?status=${status}` })
} else {
uni.navigateBack()
}
})
} else {
// xuchao: 透出后端真实原因,如「验证码错误」「验证码已过期,请重新获取」;
// 失败时绝不写 phone,避免留下“假登录态”让后续页面以为已登录
uni.showToast({ title: this.bizMsg(res) || '登录失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '登录失败', icon: 'none' })
})
},
goAgreement(type) {
const url = type === 'user' ? '/pages/agreement/user' : '/pages/agreement/privacy'
uni.navigateTo({ url })
},
goBack() {
// xuchao: 直接通过链接进入(无历史栈)时回到首页,避免返回无响应
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
}
</script>
<style>
.page {
height: 100vh;
background-color: #ffffff;
padding: 0 40rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 标题区 */
.title-area {
margin-top: 160rpx;
margin-bottom: 80rpx;
}
.title {
font-size: 44rpx;
font-weight: 700;
color: #1a1a1a;
display: block;
text-align: center;
margin-bottom: 16rpx;
}
.subtitle {
font-size: 26rpx;
color: #999999;
display: block;
text-align: center;
}
/* 表单区 */
.form-area {
margin-bottom: 40rpx;
}
.input-item {
border: 1px solid #e5e5e5;
border-radius: 12rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.input-field {
height: 96rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333333;
}
.code-row {
display: flex;
align-items: center;
}
.code-row .input-field {
flex: 1;
}
.code-btn {
font-size: 26rpx;
color: #0088FF;
padding: 0 24rpx;
white-space: nowrap;
flex-shrink: 0;
}
.code-btn.disabled {
color: #cccccc;
}
/* 登录按钮 */
.login-btn {
height: 96rpx;
line-height: 96rpx;
text-align: center;
background-color: #0088FF;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
/* 协议区 */
.agreement-area {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #0088FF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
}
.checkbox.checked {
background-color: #0088FF;
}
.check-icon {
font-size: 20rpx;
color: #ffffff;
}
.agreement-text {
font-size: 24rpx;
color: #999999;
}
.agreement-link {
font-size: 24rpx;
color: #0088FF;
}
/* xuchao: 顶部返回导航条(负外边距抵消页面左右 padding,通栏展示) */
.nav-bar {
display: flex;
align-items: center;
height: 56px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
position: relative;
margin: 0 -40rpx;
}
.nav-back {
position: absolute;
left: 12px;
top: 0;
bottom: 0;
display: flex;
align-items: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 600;
color: #1a1818;
padding: 0 56px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>