| | |
| | | timeout: 20000 |
| | | }) |
| | | } |
| | | |
| | | // 微信一键登录 - 通过OpenID和UnionID登录 |
| | | export function loginByOpenId(openId, unionId) { |
| | | const data = { openId } |
| | | // 如果unionId存在,一并传入进行双重验证 |
| | | if (unionId) { |
| | | data.unionId = unionId |
| | | } |
| | | return request({ |
| | | url: '/wechat/login/openid', |
| | | headers: { |
| | | isToken: false |
| | | }, |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 微信一键登录 - 绑定手机号 |
| | | export function bindWechatAndLogin(data) { |
| | | return request({ |
| | | url: '/wechat/login/bind', |
| | | headers: { |
| | | isToken: false |
| | | }, |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 微信手机号登录(推荐使用) |
| | | export function loginByWechatPhone(loginCode, phoneCode) { |
| | | return request({ |
| | | url: '/wechat/login/phone', |
| | | headers: { |
| | | isToken: false |
| | | }, |
| | | method: 'post', |
| | | data: { |
| | | loginCode: loginCode, |
| | | phoneCode: phoneCode |
| | | } |
| | | }) |
| | | } |
| | |
| | | |
| | | <view class="action-btn"> |
| | | <button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button> |
| | | <!-- 微信一键登录按钮(仅在微信小程序环境显示) --> |
| | | <!-- #ifdef MP-WEIXIN --> |
| | | <button |
| | | v-if="isWechat && wechatOpenId" |
| | | @click="loginByOpenId" |
| | | class="wechat-login-btn cu-btn block bg-green lg round" |
| | | style="margin-top: 20rpx;"> |
| | | <text class="cuIcon-wechat" style="margin-right: 10rpx;"></text> |
| | | 微信一键登录 |
| | | </button> |
| | | <button |
| | | v-else-if="isWechat" |
| | | open-type="getPhoneNumber" |
| | | @getphonenumber="onGetPhoneNumber" |
| | | class="wechat-login-btn cu-btn block bg-green lg round" |
| | | style="margin-top: 20rpx;"> |
| | | <text class="cuIcon-wechat" style="margin-right: 10rpx;"></text> |
| | | 微信一键登录 |
| | | </button> |
| | | <!-- #endif --> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </template> |
| | | |
| | | <script> |
| | | import { getCodeImg } from '@/api/login' |
| | | import { getCodeImg, loginByOpenId, loginByWechatPhone } from '@/api/login' |
| | | |
| | | export default { |
| | | data() { |
| | |
| | | password: "", |
| | | code: "", |
| | | uuid: '' |
| | | } |
| | | }, |
| | | // 微信一键登录相关 |
| | | isWechat: false, // 是否为微信小程序环境 |
| | | wechatOpenId: '', // 微信OpenID |
| | | wechatUnionId: '', // 微信UnionID |
| | | } |
| | | }, |
| | | created() { |
| | | this.getCode() |
| | | this.checkWechatEnv() |
| | | this.tryAutoLogin() |
| | | }, |
| | | methods: { |
| | | // 用户注册 |
| | |
| | | // 触发登录成功事件,启动消息轮询 |
| | | uni.$emit('user-login') |
| | | this.$tab.reLaunch('/pages/index') |
| | | }) |
| | | }, |
| | | |
| | | // ==================== 微信一键登录相关方法 ==================== |
| | | |
| | | // 检查是否在微信小程序环境 |
| | | checkWechatEnv() { |
| | | // #ifdef MP-WEIXIN |
| | | this.isWechat = true |
| | | console.log('当前环境:微信小程序') |
| | | // #endif |
| | | |
| | | // #ifndef MP-WEIXIN |
| | | this.isWechat = false |
| | | console.log('当前环境:非微信小程序') |
| | | // #endif |
| | | }, |
| | | |
| | | // 尝试自动登录(检查本地是否有保存的OpenID) |
| | | tryAutoLogin() { |
| | | if (!this.isWechat) { |
| | | return |
| | | } |
| | | |
| | | // 从本地存储中获取OpenID和UnionID |
| | | const savedOpenId = uni.getStorageSync('wechat_openid') |
| | | const savedUnionId = uni.getStorageSync('wechat_unionid') |
| | | |
| | | if (savedOpenId) { |
| | | console.log('检测到已保存的OpenID,尝试自动登录') |
| | | this.wechatOpenId = savedOpenId |
| | | this.wechatUnionId = savedUnionId // 可能为null |
| | | this.loginByOpenId() |
| | | } |
| | | }, |
| | | |
| | | // 处理获取手机号的回调 |
| | | onGetPhoneNumber(e) { |
| | | console.log('获取手机号回调:', e) |
| | | |
| | | if (!this.agreedToPolicy) { |
| | | this.$modal.msgError("请先阅读并同意用户协议和隐私政策") |
| | | return |
| | | } |
| | | |
| | | if (e.detail.errMsg === 'getPhoneNumber:ok') { |
| | | // 用户同意授权 |
| | | const { code } = e.detail |
| | | |
| | | this.$modal.loading("正在获取手机号...") |
| | | |
| | | // 先获取微信登录code |
| | | uni.login({ |
| | | provider: 'weixin', |
| | | success: (loginRes) => { |
| | | console.log('微信登录code:', loginRes.code) |
| | | console.log('手机号code:', code) |
| | | |
| | | // 调用后端接口,传入loginCode和phoneCode |
| | | loginByWechatPhone(loginRes.code, code).then(response => { |
| | | this.$modal.closeLoading() |
| | | |
| | | if (response.code === 200) { |
| | | // 登录成功,保存OpenID和UnionID |
| | | const openId = response.openId |
| | | const unionId = response.unionId |
| | | |
| | | uni.setStorageSync('wechat_openid', openId) |
| | | this.wechatOpenId = openId |
| | | |
| | | if (unionId) { |
| | | uni.setStorageSync('wechat_unionid', unionId) |
| | | this.wechatUnionId = unionId |
| | | } |
| | | |
| | | // 保存token到本地存储和Vuex |
| | | const token = response.token |
| | | this.$store.commit('SET_TOKEN', token) |
| | | // 必须调用setToken保存到本地存储 |
| | | const { setToken } = require('@/utils/auth') |
| | | setToken(token) |
| | | |
| | | this.loginSuccess() |
| | | } else { |
| | | this.$modal.msgError(response.msg || '登录失败') |
| | | } |
| | | }).catch(error => { |
| | | this.$modal.closeLoading() |
| | | console.error('登录失败:', error) |
| | | this.$modal.msgError('登录失败,请重试') |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | this.$modal.closeLoading() |
| | | console.error('获取微信登录code失败:', err) |
| | | this.$modal.msgError('获取微信信息失败') |
| | | } |
| | | }) |
| | | } else { |
| | | // 用户拒绝授权 |
| | | this.$modal.msgError('需要您的手机号才能登录') |
| | | } |
| | | }, |
| | | |
| | | // 通过OpenID和UnionID登录 |
| | | loginByOpenId() { |
| | | this.$modal.loading("正在登录...") |
| | | |
| | | // 同时传入openId和unionId进行双重验证 |
| | | loginByOpenId(this.wechatOpenId, this.wechatUnionId).then(response => { |
| | | this.$modal.closeLoading() |
| | | |
| | | if (response.code === 200) { |
| | | // 登录成功,保存token到本地存储和Vuex |
| | | const token = response.token |
| | | this.$store.commit('SET_TOKEN', token) |
| | | // 必须调用setToken保存到本地存储 |
| | | const { setToken } = require('@/utils/auth') |
| | | setToken(token) |
| | | |
| | | this.loginSuccess() |
| | | } else { |
| | | // OpenID未绑定或验证失败,需要获取手机号绑定 |
| | | console.log('该OpenID尚未绑定或验证失败,需要获取手机号') |
| | | this.$modal.closeLoading() |
| | | this.$modal.msgError(response.msg || '该微信账号尚未绑定,请点击微信一键登录按钮获取手机号授权') |
| | | } |
| | | }).catch(error => { |
| | | this.$modal.closeLoading() |
| | | console.error('登录失败:', error) |
| | | this.$modal.msgError('登录失败,请重试') |
| | | }) |
| | | } |
| | | } |
| | |
| | | height: 90rpx; |
| | | font-size: 32rpx; |
| | | } |
| | | |
| | | .wechat-login-btn { |
| | | height: 90rpx; |
| | | font-size: 32rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | } |
| | | |
| | | .reg { |
| | |
| | | :key="hospital.hospId" |
| | | @click="selectHospitalOut(hospital)" |
| | | > |
| | | <view class="hospital-name">{{ hospital.hospName }}</view> |
| | | <view class="hospital-name"> |
| | | {{ hospital.hospName }} |
| | | <text class="hospital-short" v-if="hospital.hospShort">{{ hospital.hospShort }}</text> |
| | | </view> |
| | | <view class="hospital-address">{{ buildFullAddress(hospital) }}</view> |
| | | </view> |
| | | </view> |
| | |
| | | |
| | | <view class="form-item"> |
| | | <view class="form-label required">科室</view> |
| | | <picker mode="selector" :range="departmentOptions" range-key="text" @change="onHospitalOutDepartmentChange"> |
| | | <picker |
| | | v-if="taskForm.hospitalOut.name !== '家中'" |
| | | mode="selector" |
| | | :range="departmentOptions" |
| | | range-key="text" |
| | | @change="onHospitalOutDepartmentChange" |
| | | > |
| | | <view class="form-input picker-input"> |
| | | {{ taskForm.hospitalOut.department || '请选择科室' }} |
| | | <uni-icons type="arrowright" size="16" color="#999"></uni-icons> |
| | | </view> |
| | | </picker> |
| | | <view v-else class="form-input picker-input disabled"> |
| | | 其它 |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="form-item"> |
| | |
| | | :key="hospital.hospId" |
| | | @click="selectHospitalIn(hospital)" |
| | | > |
| | | <view class="hospital-name">{{ hospital.hospName }}</view> |
| | | <view class="hospital-name"> |
| | | {{ hospital.hospName }} |
| | | <text class="hospital-short" v-if="hospital.hospShort">{{ hospital.hospShort }}</text> |
| | | </view> |
| | | <view class="hospital-address">{{ buildFullAddress(hospital) }}</view> |
| | | </view> |
| | | </view> |
| | |
| | | |
| | | <view class="form-item"> |
| | | <view class="form-label required">科室</view> |
| | | <picker mode="selector" :range="departmentOptions" range-key="text" @change="onHospitalInDepartmentChange"> |
| | | <picker |
| | | v-if="taskForm.hospitalIn.name !== '家中'" |
| | | mode="selector" |
| | | :range="departmentOptions" |
| | | range-key="text" |
| | | @change="onHospitalInDepartmentChange" |
| | | > |
| | | <view class="form-input picker-input"> |
| | | {{ taskForm.hospitalIn.department || '请选择科室' }} |
| | | <uni-icons type="arrowright" size="16" color="#999"></uni-icons> |
| | | </view> |
| | | </picker> |
| | | <view v-else class="form-input picker-input disabled"> |
| | | 其它 |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="form-item"> |
| | |
| | | }) |
| | | }, |
| | | |
| | | // 按地域排序医院:本地区域优先 |
| | | // 按地域排序医院:本地区域优先,"家中"始终在最前面 |
| | | sortHospitalsByRegion(hospitals) { |
| | | if (!this.selectedRegion || !hospitals || hospitals.length === 0) { |
| | | if (!hospitals || hospitals.length === 0) { |
| | | return hospitals |
| | | } |
| | | |
| | | const region = this.selectedRegion |
| | | const localHospitals = [] |
| | | const otherHospitals = [] |
| | | const homeHospital = [] // "家中" |
| | | const localHospitals = [] // 本地医院 |
| | | const otherHospitals = [] // 其他医院 |
| | | |
| | | hospitals.forEach(hospital => { |
| | | // "家中"优先处理,放在最前面 |
| | | if (hospital.hospName === '家中') { |
| | | homeHospital.push(hospital) |
| | | return |
| | | } |
| | | |
| | | // 判断医院是否在本地区域(省、市、区任一包含地域关键词) |
| | | const isLocal = |
| | | const isLocal = region && ( |
| | | (hospital.hopsProvince && hospital.hopsProvince.includes(region)) || |
| | | (hospital.hopsCity && hospital.hopsCity.includes(region)) || |
| | | (hospital.hopsArea && hospital.hopsArea.includes(region)) |
| | | ) |
| | | |
| | | if (isLocal) { |
| | | localHospitals.push(hospital) |
| | |
| | | } |
| | | }) |
| | | |
| | | // 本地医院在前,其他医院在后 |
| | | return [...localHospitals, ...otherHospitals] |
| | | // "家中"在最前,本地医院其次,其他医院在后 |
| | | return [...homeHospital, ...localHospitals, ...otherHospitals] |
| | | }, |
| | | |
| | | // 转出医院输入框获得焦点 |
| | |
| | | // 否则重新加载常用医院 |
| | | if (this.selectedOrganizationServiceOrderClass) { |
| | | getFrequentOutHospitals(this.selectedOrganizationServiceOrderClass, this.selectedRegion).then(response => { |
| | | this.hospitalOutResults = response.data || [] |
| | | const hospitals = response.data || [] |
| | | // 确保"家中"在最前面 |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | // 如果没有常用医院,降级为普通搜索 |
| | | if (this.hospitalOutResults.length === 0) { |
| | | searchHospitals('', this.selectedRegion).then(res => { |
| | | this.hospitalOutResults = res.data || [] |
| | | const hospitals = res.data || [] |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | }) |
| | | } |
| | | }).catch(error => { |
| | | console.error('加载常用转出医院失败:', error) |
| | | searchHospitals('', this.selectedRegion).then(res => { |
| | | this.hospitalOutResults = res.data || [] |
| | | const hospitals = res.data || [] |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | }) |
| | | }) |
| | | } else { |
| | | // 没有服务单编码,使用普通搜索 |
| | | searchHospitals('', this.selectedRegion).then(response => { |
| | | this.hospitalOutResults = response.data || [] |
| | | const hospitals = response.data || [] |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | }).catch(error => { |
| | | console.error('加载转出医院失败:', error) |
| | | this.hospitalOutResults = [] |
| | |
| | | clearTimeout(this.searchTimer) |
| | | } |
| | | |
| | | // 如果关键词为空,只显示当前区域的医院 |
| | | // 如果关键词为空,显示当前区域的医院 |
| | | if (!keyword || keyword.trim() === '') { |
| | | searchHospitals('', this.selectedRegion).then(response => { |
| | | this.hospitalOutResults = response.data || [] |
| | | const hospitals = response.data || [] |
| | | // 确保"家中"在最前面 |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | }).catch(error => { |
| | | console.error('加载转出医院失败:', error) |
| | | this.hospitalOutResults = [] |
| | |
| | | searchHospitalOut(keyword) { |
| | | // 传入关键词和地域过滤,只搜索当前区域的医院 |
| | | searchHospitals(keyword, this.selectedRegion).then(response => { |
| | | this.hospitalOutResults = response.data || [] |
| | | const hospitals = response.data || [] |
| | | // 确保"家中"在最前面 |
| | | this.hospitalOutResults = this.sortHospitalsByRegion(hospitals) |
| | | this.showHospitalOutResults = true |
| | | console.log('搜索转出医院:', keyword, '区域:', this.selectedRegion, '结果数:', this.hospitalOutResults.length) |
| | | }).catch(error => { |
| | |
| | | // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址 |
| | | if (hospital.hospName === '家中') { |
| | | this.taskForm.hospitalOut.address = '' |
| | | // 科室自动设置为"其它" |
| | | this.taskForm.hospitalOut.department = '其它' |
| | | this.taskForm.hospitalOut.departmentId = null |
| | | } else { |
| | | // 合并省市区 + 详细地址 |
| | | const fullAddress = this.buildFullAddress(hospital) |
| | | this.taskForm.hospitalOut.address = fullAddress |
| | | // 清空科室,让用户重新选择 |
| | | if (this.taskForm.hospitalOut.department === '其它') { |
| | | this.taskForm.hospitalOut.department = '' |
| | | this.taskForm.hospitalOut.departmentId = null |
| | | } |
| | | } |
| | | this.hospitalOutSearchKeyword = hospital.hospName |
| | | this.showHospitalOutResults = false |
| | |
| | | if (!keyword || keyword.trim() === '') { |
| | | searchHospitals('', '').then(response => { |
| | | const allHospitals = response.data || [] |
| | | // 按地域排序:本地区域优先 |
| | | // 按地域排序:"家中"最前,本地区域优先 |
| | | this.hospitalInResults = this.sortHospitalsByRegion(allHospitals) |
| | | }).catch(error => { |
| | | console.error('加载转入医院失败:', error) |
| | |
| | | // 传入关键词,不传地域过滤(搜索所有区域) |
| | | searchHospitals(keyword, '').then(response => { |
| | | const allHospitals = response.data || [] |
| | | // 按地域排序:本地区域优先 |
| | | // 按地域排序:"家中"最前,本地区域优先 |
| | | this.hospitalInResults = this.sortHospitalsByRegion(allHospitals) |
| | | this.showHospitalInResults = true |
| | | console.log('搜索转入医院:', keyword, '结果数:', this.hospitalInResults.length) |
| | |
| | | // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址 |
| | | if (hospital.hospName === '家中') { |
| | | this.taskForm.hospitalIn.address = '' |
| | | // 科室自动设置为"其它" |
| | | this.taskForm.hospitalIn.department = '其它' |
| | | this.taskForm.hospitalIn.departmentId = null |
| | | } else { |
| | | // 合并省市区 + 详细地址 |
| | | const fullAddress = this.buildFullAddress(hospital) |
| | | this.taskForm.hospitalIn.address = fullAddress |
| | | // 清空科室,让用户重新选择 |
| | | if (this.taskForm.hospitalIn.department === '其它') { |
| | | this.taskForm.hospitalIn.department = '' |
| | | this.taskForm.hospitalIn.departmentId = null |
| | | } |
| | | } |
| | | this.hospitalInSearchKeyword = hospital.hospName |
| | | this.showHospitalInResults = false |
| | |
| | | color: #333; |
| | | font-weight: bold; |
| | | margin-bottom: 8rpx; |
| | | |
| | | .hospital-short { |
| | | display: block; |
| | | font-size: 22rpx; |
| | | color: #999; |
| | | font-weight: normal; |
| | | margin-top: 6rpx; |
| | | } |
| | | } |
| | | |
| | | .hospital-address { |
| | |
| | | @RequestParam(value = "keyword", required = false) String keyword, |
| | | @RequestParam(value = "region", required = false) String region) { |
| | | List<HospData> list = hospDataMapper.searchHospitals(keyword, region); |
| | | Integer homeHospId=hospDataMapper.getHomeHospId(); |
| | | if(homeHospId>0 && list.stream().count()>0 && list.stream().filter(hospData -> hospData.getHospId().equals(homeHospId)).count()<=0) { |
| | | HospData hospData= hospDataMapper.selectHospDataById(homeHospId); |
| | | list.add(0,hospData); |
| | | } |
| | | return success(list); |
| | | } |
| | | |
| | |
| | | if (hospIds == null || hospIds.isEmpty()) { |
| | | return success(); |
| | | } |
| | | Integer homeHospId=hospDataMapper.getHomeHospId(); |
| | | if (!hospIds.contains(homeHospId)) { |
| | | hospIds.add(0,homeHospId); |
| | | } |
| | | |
| | | // 根据ID列表查询医院详情 |
| | | List<HospData> hospitals = hospDataMapper.selectHospDataByIds(hospIds, region); |
| | | |
| | | return success(hospitals); |
| | | } |
| | | |
| | |
| | | return success(); |
| | | } |
| | | Integer homeHospId=hospDataMapper.getHomeHospId(); |
| | | if (!hospIds.contains(homeHospId)) { |
| | | hospIds.add(0,homeHospId); |
| | | } |
| | | |
| | | // 根据ID列表查询医院详情 |
| | | List<HospData> hospitals = hospDataMapper.selectHospDataByIds(hospIds, region); |
| | | if(homeHospId>0) { |
| | | |
| | | HospData hospData= hospDataMapper.selectHospDataById(homeHospId); |
| | | hospitals.add(0,hospData); |
| | | } |
| | | return success(hospitals); |
| | | } |
| | | } |
| | |
| | | |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import com.ruoyi.system.service.ISysUserService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | |
| | | import com.ruoyi.framework.web.service.SysLoginService; |
| | | import com.ruoyi.framework.web.service.SysPermissionService; |
| | | import com.ruoyi.framework.web.service.TokenService; |
| | | import com.ruoyi.framework.web.service.WechatLoginService; |
| | | import com.ruoyi.system.service.ISysDeptService; |
| | | import com.ruoyi.system.service.ISysMenuService; |
| | | import com.ruoyi.common.annotation.Anonymous; |
| | | |
| | | /** |
| | | * 登录验证 |
| | |
| | | private SysLoginService loginService; |
| | | |
| | | @Autowired |
| | | private ISysUserService userService; |
| | | @Autowired |
| | | private ISysMenuService menuService; |
| | | |
| | | @Autowired |
| | |
| | | |
| | | @Autowired |
| | | private ISysDeptService deptService; |
| | | |
| | | @Autowired |
| | | private WechatLoginService wechatLogin; |
| | | |
| | | /** |
| | | * 微信一键登录 - 通过OpenID和UnionID登录 |
| | | * 使用WechatLoginService进行认证 |
| | | * |
| | | * @param requestBody 包含openId和unionId的请求体 |
| | | * @return 结果 |
| | | */ |
| | | @Anonymous |
| | | @PostMapping("/wechat/login/openid") |
| | | public AjaxResult loginByOpenId(@RequestBody java.util.Map<String, Object> requestBody) |
| | | { |
| | | String openId = (String) requestBody.get("openId"); |
| | | String unionId = (String) requestBody.get("unionId"); |
| | | |
| | | if (com.ruoyi.common.utils.StringUtils.isEmpty(openId)) |
| | | { |
| | | return AjaxResult.error("缺少openId参数"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | // 调用WechatLoginService进行认证 |
| | | String token = wechatLogin.loginByOpenId(openId, unionId); |
| | | |
| | | AjaxResult ajax = AjaxResult.success("登录成功"); |
| | | ajax.put(Constants.TOKEN, token); |
| | | return ajax; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | return AjaxResult.error(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 微信手机号登录(推荐使用) |
| | | * |
| | | * @param requestBody 包含loginCode(微信登录code)和phoneCode(手机号授权code) |
| | | * @return 结果 |
| | | */ |
| | | @Anonymous |
| | | @PostMapping("/wechat/login/phone") |
| | | public AjaxResult loginByWechatPhone(@RequestBody java.util.Map<String, Object> requestBody) |
| | | { |
| | | String loginCode = (String) requestBody.get("loginCode"); |
| | | String phoneCode = (String) requestBody.get("phoneCode"); |
| | | |
| | | if (com.ruoyi.common.utils.StringUtils.isEmpty(loginCode)) |
| | | { |
| | | return AjaxResult.error("缺少微信登录code"); |
| | | } |
| | | |
| | | if (com.ruoyi.common.utils.StringUtils.isEmpty(phoneCode)) |
| | | { |
| | | return AjaxResult.error("缺少手机号授权code"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | // 调用WechatLoginService进行认证 |
| | | java.util.Map<String, Object> loginResult = wechatLogin.loginByWechatPhone(loginCode, phoneCode); |
| | | |
| | | AjaxResult ajax = AjaxResult.success("登录成功"); |
| | | ajax.put(Constants.TOKEN, loginResult.get("token")); |
| | | ajax.put("openId", loginResult.get("openId")); |
| | | if (loginResult.containsKey("unionId")) |
| | | { |
| | | ajax.put("unionId", loginResult.get("unionId")); |
| | | } |
| | | return ajax; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | return AjaxResult.error(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 登录方法 |
| | |
| | | # 二维码配置 |
| | | qrcode: |
| | | defaultUrl: http://localhost:81/evaluation?vehicle={vehicleNo} # 默认二维码URL模板,{vehicleNo}会被替换为实际车牌号 |
| | | |
| | | |
| | | # 微信配置 |
| | | evaluationWechat: |
| | | appId: wx70f6a7346ee842c0 |
| | | appSecret: 2d6c59de85e876b7eadebeba62e5417a |
| | | redirectUri: http://yourdomain.com/evaluation |
| | | # 调度用的weixin配置 |
| | | transferConfigWeixin: |
| | | appId: wx40692cc44953a8cb |
| | | appSecret: 9638b7d8bb988e4daaac7ac35457f296 |
| | | # 旧系统配置 |
| | | legacy: |
| | | system: |
| | |
| | | apiUrl: https://api.966120.com.cn/v1/ #测试环境:localhost:8011 |
| | | qrcode: |
| | | defaultUrl: https://gzgj.966120.com.cn/evaluation?vehicle={vehicleNo} |
| | | wechat: |
| | | |
| | | |
| | | |
| | | # 微信配置 |
| | | evaluationWechat: |
| | | appId: wx70f6a7346ee842c0 |
| | | appSecret: 2d6c59de85e876b7eadebeba62e5417a |
| | | redirectUri: http://yourdomain.com/evaluation |
| | | # 调度用的weixin配置 |
| | | transferConfigWeixin: |
| | | appId: wx40692cc44953a8cb |
| | | appSecret: 9638b7d8bb988e4daaac7ac35457f296 |
| | | # 腾讯地图配置 |
| | | tencent: |
| | | map: |
| | |
| | | |
| | | /** OA系统的订单编码列表(如:BF,AB,SA) */ |
| | | private String oaOrderClass; |
| | | |
| | | /** 微信OpenID */ |
| | | private String openId; |
| | | |
| | | /** 微信UnionID */ |
| | | private String unionId; |
| | | |
| | | /** 微信昵称 */ |
| | | private String wechatNickname; |
| | | |
| | | public SysUser() |
| | | { |
| | |
| | | { |
| | | this.oaOrderClass = oaOrderClass; |
| | | } |
| | | |
| | | public String getOpenId() |
| | | { |
| | | return openId; |
| | | } |
| | | |
| | | public void setOpenId(String openId) |
| | | { |
| | | this.openId = openId; |
| | | } |
| | | |
| | | public String getUnionId() |
| | | { |
| | | return unionId; |
| | | } |
| | | |
| | | public void setUnionId(String unionId) |
| | | { |
| | | this.unionId = unionId; |
| | | } |
| | | |
| | | public String getWechatNickname() |
| | | { |
| | | return wechatNickname; |
| | | } |
| | | |
| | | public void setWechatNickname(String wechatNickname) |
| | | { |
| | | this.wechatNickname = wechatNickname; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | |
| | | .append("dept", getDept()) |
| | | .append("oaUserId", getOaUserId()) |
| | | .append("oaOrderClass", getOaOrderClass()) |
| | | .append("openId", getOpenId()) |
| | | .append("unionId", getUnionId()) |
| | | .append("wechatNickname", getWechatNickname()) |
| | | .toString(); |
| | | } |
| | | } |
| | |
| | | import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; |
| | | import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; |
| | | import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; |
| | | import com.ruoyi.framework.security.WechatAuthenticationProvider; |
| | | import com.ruoyi.common.annotation.Anonymous; |
| | | import org.springframework.security.web.util.matcher.RequestMatcher; |
| | | import org.springframework.web.method.HandlerMethod; |
| | |
| | | private RequestMappingHandlerMapping requestMappingHandlerMapping; |
| | | |
| | | /** |
| | | * 微信认证提供者 |
| | | */ |
| | | @Autowired |
| | | private WechatAuthenticationProvider wechatAuthenticationProvider; |
| | | |
| | | /** |
| | | * 获取所有标注了@Anonymous的URL |
| | | */ |
| | | private Set<String> getAnonymousUrls() { |
| | |
| | | |
| | | /** |
| | | * 身份验证实现 |
| | | * 支持用户名密码认证和微信认证 |
| | | */ |
| | | @Bean |
| | | public AuthenticationManager authenticationManager() |
| | | { |
| | | // 用户名密码认证提供者 |
| | | DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); |
| | | daoAuthenticationProvider.setUserDetailsService(userDetailsService); |
| | | daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder()); |
| | | return new ProviderManager(daoAuthenticationProvider); |
| | | |
| | | // 返回ProviderManager,支持多种认证方式 |
| | | return new ProviderManager(daoAuthenticationProvider, wechatAuthenticationProvider); |
| | | } |
| | | |
| | | /** |
| New file |
| | |
| | | package com.ruoyi.framework.security; |
| | | |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.core.domain.model.LoginUser; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.web.service.SysPermissionService; |
| | | import com.ruoyi.system.service.ISysUserService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.authentication.AuthenticationProvider; |
| | | import org.springframework.security.authentication.BadCredentialsException; |
| | | import org.springframework.security.core.Authentication; |
| | | import org.springframework.security.core.AuthenticationException; |
| | | import org.springframework.stereotype.Component; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 微信登录认证提供者 |
| | | * 类似于DaoAuthenticationProvider |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Component |
| | | public class WechatAuthenticationProvider implements AuthenticationProvider |
| | | { |
| | | @Autowired |
| | | private ISysUserService userService; |
| | | |
| | | @Autowired |
| | | private SysPermissionService permissionService; |
| | | |
| | | @Override |
| | | public Authentication authenticate(Authentication authentication) throws AuthenticationException |
| | | { |
| | | WechatAuthenticationToken wechatToken = (WechatAuthenticationToken) authentication; |
| | | |
| | | String openId = (String) wechatToken.getPrincipal(); |
| | | String unionId = (String) wechatToken.getCredentials(); |
| | | |
| | | // 根据OpenID查询用户 |
| | | SysUser user = userService.selectUserByOpenId(openId); |
| | | |
| | | if (user == null) |
| | | { |
| | | throw new BadCredentialsException("该微信账号尚未绑定系统用户"); |
| | | } |
| | | |
| | | // 如果传入了unionId,进行额外验证 |
| | | if (StringUtils.isNotEmpty(unionId)) |
| | | { |
| | | if (StringUtils.isNotEmpty(user.getUnionId())) |
| | | { |
| | | if (!unionId.equals(user.getUnionId())) |
| | | { |
| | | throw new BadCredentialsException("微信账号验证失败"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 检查用户状态 |
| | | if ("1".equals(user.getStatus())) |
| | | { |
| | | throw new BadCredentialsException("用户已被停用,请联系管理员"); |
| | | } |
| | | |
| | | if ("1".equals(user.getDelFlag())) |
| | | { |
| | | throw new BadCredentialsException("用户已被删除,请联系管理员"); |
| | | } |
| | | |
| | | // 获取用户权限 |
| | | Set<String> permissions = permissionService.getMenuPermission(user); |
| | | |
| | | // 创建LoginUser对象 |
| | | LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissions); |
| | | |
| | | // 返回已认证的Token |
| | | return new WechatAuthenticationToken(loginUser, unionId, loginUser.getAuthorities()); |
| | | } |
| | | |
| | | @Override |
| | | public boolean supports(Class<?> authentication) |
| | | { |
| | | return WechatAuthenticationToken.class.isAssignableFrom(authentication); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.framework.security; |
| | | |
| | | import org.springframework.security.authentication.AbstractAuthenticationToken; |
| | | import org.springframework.security.core.GrantedAuthority; |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * 微信登录认证Token |
| | | * 类似于UsernamePasswordAuthenticationToken |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | public class WechatAuthenticationToken extends AbstractAuthenticationToken |
| | | { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** |
| | | * 认证主体(登录前为openId,登录后为LoginUser) |
| | | */ |
| | | private final Object principal; |
| | | |
| | | /** |
| | | * 认证凭证(可选的unionId) |
| | | */ |
| | | private Object credentials; |
| | | |
| | | /** |
| | | * 创建未认证的Token(登录前) |
| | | * |
| | | * @param openId 微信OpenID |
| | | * @param unionId 微信UnionID(可选) |
| | | */ |
| | | public WechatAuthenticationToken(String openId, String unionId) |
| | | { |
| | | super(null); |
| | | this.principal = openId; |
| | | this.credentials = unionId; |
| | | setAuthenticated(false); |
| | | } |
| | | |
| | | /** |
| | | * 创建已认证的Token(登录后) |
| | | * |
| | | * @param principal 登录用户信息 |
| | | * @param credentials 凭证 |
| | | * @param authorities 权限列表 |
| | | */ |
| | | public WechatAuthenticationToken(Object principal, Object credentials, |
| | | Collection<? extends GrantedAuthority> authorities) |
| | | { |
| | | super(authorities); |
| | | this.principal = principal; |
| | | this.credentials = credentials; |
| | | super.setAuthenticated(true); |
| | | } |
| | | |
| | | @Override |
| | | public Object getCredentials() |
| | | { |
| | | return this.credentials; |
| | | } |
| | | |
| | | @Override |
| | | public Object getPrincipal() |
| | | { |
| | | return this.principal; |
| | | } |
| | | |
| | | @Override |
| | | public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException |
| | | { |
| | | if (isAuthenticated) |
| | | { |
| | | throw new IllegalArgumentException( |
| | | "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); |
| | | } |
| | | super.setAuthenticated(false); |
| | | } |
| | | |
| | | @Override |
| | | public void eraseCredentials() |
| | | { |
| | | super.eraseCredentials(); |
| | | credentials = null; |
| | | } |
| | | } |
| | |
| | | @Override |
| | | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException |
| | | { |
| | | // 尝试判断是手机号还是用户名 |
| | | |
| | | // 尝试判断是手机号、openId还是用户名 |
| | | SysUser user = null; |
| | | |
| | | // 判断是否为微信OpenID(通常以"o"开头,28位字符) |
| | | if (username.startsWith("o") && username.length() == 28) |
| | | { |
| | | // 微信OpenID登录 |
| | | log.info("尝试使用微信OpenID登录:{}", username); |
| | | user = userService.selectUserByOpenId(username); |
| | | } |
| | | // 判断是否为手机号(简单判断:全是数字且11位) |
| | | if (username.matches("^1[3-9]\\d{9}$")) |
| | | else if (username.matches("^1[3-9]\\d{9}$")) |
| | | { |
| | | // 手机号登录 |
| | | log.info("尝试使用手机号登录:{}", username); |
| New file |
| | |
| | | package com.ruoyi.framework.web.service; |
| | | |
| | | import com.ruoyi.common.constant.Constants; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.core.domain.model.LoginUser; |
| | | import com.ruoyi.common.utils.MessageUtils; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.manager.AsyncManager; |
| | | import com.ruoyi.framework.manager.factory.AsyncFactory; |
| | | import com.ruoyi.framework.security.WechatAuthenticationToken; |
| | | import com.ruoyi.system.service.ISysUserService; |
| | | import com.ruoyi.system.service.IWechatLoginService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.authentication.AuthenticationManager; |
| | | import org.springframework.security.authentication.BadCredentialsException; |
| | | import org.springframework.security.core.Authentication; |
| | | import org.springframework.stereotype.Component; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 微信登录校验方法 |
| | | * 类似于SysLoginService |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Component |
| | | public class WechatLoginService |
| | | { |
| | | @Autowired |
| | | private TokenService tokenService; |
| | | |
| | | @Autowired |
| | | private AuthenticationManager authenticationManager; |
| | | |
| | | @Autowired |
| | | private SysLoginService sysLoginService; |
| | | |
| | | @Autowired |
| | | private ISysUserService userService; |
| | | |
| | | @Autowired |
| | | private IWechatLoginService wechatApiService; |
| | | |
| | | /** |
| | | * 微信OpenID登录验证 |
| | | * |
| | | * @param openId 微信OpenID |
| | | * @param unionId 微信UnionID(可选) |
| | | * @return token |
| | | */ |
| | | public String loginByOpenId(String openId, String unionId) |
| | | { |
| | | try |
| | | { |
| | | // 创建微信认证Token |
| | | WechatAuthenticationToken authenticationToken = new WechatAuthenticationToken(openId, unionId); |
| | | |
| | | // 使用AuthenticationManager进行认证 |
| | | Authentication authentication = authenticationManager.authenticate(authenticationToken); |
| | | |
| | | // 认证成功,获取LoginUser |
| | | LoginUser loginUser = (LoginUser) authentication.getPrincipal(); |
| | | |
| | | // 记录登录成功日志 |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | loginUser.getUsername(), |
| | | Constants.LOGIN_SUCCESS, |
| | | "微信OpenID登录成功")); |
| | | |
| | | // 记录登录信息(IP和时间) |
| | | sysLoginService.recordLoginInfo(loginUser.getUserId()); |
| | | |
| | | // 生成token |
| | | return tokenService.createToken(loginUser); |
| | | } |
| | | catch (BadCredentialsException e) |
| | | { |
| | | // 记录登录失败日志 |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | openId, |
| | | Constants.LOGIN_FAIL, |
| | | e.getMessage())); |
| | | throw e; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | // 记录登录失败日志 |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | openId, |
| | | Constants.LOGIN_FAIL, |
| | | e.getMessage())); |
| | | throw new BadCredentialsException(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信手机号登录验证 |
| | | * 通过微信API获取手机号后进行登录 |
| | | * |
| | | * @param loginCode 微信登录code |
| | | * @param phoneCode 手机号授权code |
| | | * @return 包含token、openId、unionId的Map |
| | | */ |
| | | public Map<String, Object> loginByWechatPhone(String loginCode, String phoneCode) |
| | | { |
| | | Map<String, Object> loginResult = new HashMap<>(); |
| | | |
| | | try |
| | | { |
| | | // 1. 调用微信API服务获取用户信息 |
| | | Map<String, Object> result = wechatApiService.loginByWechatPhone(loginCode, phoneCode); |
| | | |
| | | if (!(Boolean)result.get("success")) |
| | | { |
| | | String errorMsg = (String)result.get("message"); |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | "微信用户", |
| | | Constants.LOGIN_FAIL, |
| | | errorMsg)); |
| | | throw new BadCredentialsException(errorMsg); |
| | | } |
| | | |
| | | // 2. 获取用户信息和openId |
| | | SysUser user = (SysUser) result.get("user"); |
| | | String openId = (String) result.get("openId"); |
| | | String unionId = (String) result.get("unionId"); |
| | | |
| | | // 3. 使用openId和unionId进行认证 |
| | | WechatAuthenticationToken authenticationToken = new WechatAuthenticationToken(openId, unionId); |
| | | Authentication authentication = authenticationManager.authenticate(authenticationToken); |
| | | |
| | | // 4. 认证成功,获取LoginUser |
| | | LoginUser loginUser = (LoginUser) authentication.getPrincipal(); |
| | | |
| | | // 5. 记录登录成功日志 |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | loginUser.getUsername(), |
| | | Constants.LOGIN_SUCCESS, |
| | | "微信手机号登录成功")); |
| | | |
| | | // 6. 记录登录信息(IP和时间) |
| | | sysLoginService.recordLoginInfo(loginUser.getUserId()); |
| | | |
| | | // 7. 生成token |
| | | String token = tokenService.createToken(loginUser); |
| | | |
| | | // 8. 返回结果(包含token、openId、unionId) |
| | | loginResult.put("token", token); |
| | | loginResult.put("openId", openId); |
| | | if (StringUtils.isNotEmpty(unionId)) |
| | | { |
| | | loginResult.put("unionId", unionId); |
| | | } |
| | | |
| | | return loginResult; |
| | | } |
| | | catch (BadCredentialsException e) |
| | | { |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | "微信用户", |
| | | Constants.LOGIN_FAIL, |
| | | e.getMessage())); |
| | | throw e; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | AsyncManager.me().execute(AsyncFactory.recordLogininfor( |
| | | "微信用户", |
| | | Constants.LOGIN_FAIL, |
| | | e.getMessage())); |
| | | throw new BadCredentialsException(e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | * @return 用户信息 |
| | | */ |
| | | public SysUser selectUserByOaUserId(@Param("oaUserId") Integer oaUserId); |
| | | |
| | | /** |
| | | * 通过微信OpenID查询用户 |
| | | * |
| | | * @param openId 微信OpenID |
| | | * @return 用户对象信息 |
| | | */ |
| | | public SysUser selectUserByOpenId(@Param("openId") String openId); |
| | | } |
| | |
| | | * @return 用户对象信息 |
| | | */ |
| | | public SysUser selectUserByPhonenumber(String phonenumber); |
| | | |
| | | /** |
| | | * 通过微信OpenID查询用户 |
| | | * |
| | | * @param openId 微信OpenID |
| | | * @return 用户对象信息 |
| | | */ |
| | | public SysUser selectUserByOpenId(String openId); |
| | | |
| | | /** |
| | | * 通过用户ID查询用户 |
| New file |
| | |
| | | package com.ruoyi.system.service; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 微信登录服务接口 |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | public interface IWechatLoginService |
| | | { |
| | | /** |
| | | * 通过微信code获取openid和session_key |
| | | * |
| | | * @param code 微信登录code |
| | | * @return 包含openid、unionid、session_key的Map |
| | | */ |
| | | Map<String, Object> getWechatSession(String code); |
| | | |
| | | /** |
| | | * 获取微信用户手机号 |
| | | * |
| | | * @param code 手机号授权code |
| | | * @return 包含手机号信息的Map |
| | | */ |
| | | Map<String, Object> getPhoneNumber(String code); |
| | | |
| | | /** |
| | | * 微信手机号登录 |
| | | * |
| | | * @param loginCode 微信登录code |
| | | * @param phoneCode 手机号授权code |
| | | * @return 登录结果 |
| | | */ |
| | | Map<String, Object> loginByWechatPhone(String loginCode, String phoneCode); |
| | | } |
| | |
| | | { |
| | | return userMapper.selectUserById(userId); |
| | | } |
| | | |
| | | /** |
| | | * 通过微信OpenID查询用户 |
| | | * |
| | | * @param openId 微信OpenID |
| | | * @return 用户对象信息 |
| | | */ |
| | | @Override |
| | | public SysUser selectUserByOpenId(String openId) |
| | | { |
| | | return userMapper.selectUserByOpenId(openId); |
| | | } |
| | | |
| | | /** |
| | | * 查询用户所属角色组 |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.ruoyi.common.config.WechatConfig; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.http.HttpUtils; |
| | | import com.ruoyi.system.service.IWechatLoginService; |
| | | import com.ruoyi.system.service.ISysUserService; |
| | | |
| | | /** |
| | | * 微信登录服务实现 |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Service |
| | | public class WechatLoginServiceImpl implements IWechatLoginService |
| | | { |
| | | private static final Logger log = LoggerFactory.getLogger(WechatLoginServiceImpl.class); |
| | | |
| | | @Autowired |
| | | private WechatConfig wechatConfig; |
| | | |
| | | @Autowired |
| | | private ISysUserService userService; |
| | | |
| | | /** |
| | | * 微信API - code2Session |
| | | */ |
| | | private static final String JSCODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session"; |
| | | |
| | | /** |
| | | * 微信API - 获取手机号 |
| | | */ |
| | | private static final String GET_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; |
| | | |
| | | /** |
| | | * 微信API - 获取access_token |
| | | */ |
| | | private static final String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"; |
| | | |
| | | /** |
| | | * 通过微信code获取openid和session_key |
| | | * |
| | | * @param code 微信登录code |
| | | * @return 包含openid、unionid、session_key的Map |
| | | */ |
| | | @Override |
| | | public Map<String, Object> getWechatSession(String code) |
| | | { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | |
| | | try |
| | | { |
| | | // 构建请求参数 |
| | | String params = "appid=" + wechatConfig.getAppId() + |
| | | "&secret=" + wechatConfig.getAppSecret() + |
| | | "&js_code=" + code + |
| | | "&grant_type=authorization_code"; |
| | | |
| | | log.info("调用微信jscode2session接口, code: {}", code); |
| | | |
| | | // 发送请求 |
| | | String response = HttpUtils.sendGet(JSCODE_2_SESSION_URL, params); |
| | | |
| | | log.info("微信jscode2session响应: {}", response); |
| | | |
| | | // 解析响应 |
| | | JSONObject jsonResponse = JSONObject.parseObject(response); |
| | | |
| | | if (jsonResponse.containsKey("errcode") && jsonResponse.getInteger("errcode") != 0) |
| | | { |
| | | log.error("微信jscode2session失败: {}", response); |
| | | result.put("success", false); |
| | | result.put("message", "获取微信会话失败: " + jsonResponse.getString("errmsg")); |
| | | return result; |
| | | } |
| | | |
| | | result.put("success", true); |
| | | result.put("openid", jsonResponse.getString("openid")); |
| | | result.put("session_key", jsonResponse.getString("session_key")); |
| | | |
| | | // unionid可能为空(需要小程序绑定微信开放平台) |
| | | if (jsonResponse.containsKey("unionid")) |
| | | { |
| | | result.put("unionid", jsonResponse.getString("unionid")); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | log.error("调用微信jscode2session接口异常", e); |
| | | result.put("success", false); |
| | | result.put("message", "获取微信会话异常: " + e.getMessage()); |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取微信access_token |
| | | */ |
| | | private String getAccessToken() |
| | | { |
| | | try |
| | | { |
| | | String params = "grant_type=client_credential" + |
| | | "&appid=" + wechatConfig.getAppId() + |
| | | "&secret=" + wechatConfig.getAppSecret(); |
| | | |
| | | String response = HttpUtils.sendGet(GET_ACCESS_TOKEN_URL, params); |
| | | JSONObject jsonResponse = JSONObject.parseObject(response); |
| | | |
| | | if (jsonResponse.containsKey("errcode")) |
| | | { |
| | | log.error("获取access_token失败: {}", response); |
| | | return null; |
| | | } |
| | | |
| | | return jsonResponse.getString("access_token"); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | log.error("获取access_token异常", e); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取微信用户手机号 |
| | | * |
| | | * @param code 手机号授权code |
| | | * @return 包含手机号信息的Map |
| | | */ |
| | | @Override |
| | | public Map<String, Object> getPhoneNumber(String code) |
| | | { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | |
| | | try |
| | | { |
| | | // 获取access_token |
| | | String accessToken = getAccessToken(); |
| | | if (StringUtils.isEmpty(accessToken)) |
| | | { |
| | | result.put("success", false); |
| | | result.put("message", "获取access_token失败"); |
| | | return result; |
| | | } |
| | | |
| | | // 构建请求URL |
| | | String url = GET_PHONE_NUMBER_URL + "?access_token=" + accessToken; |
| | | |
| | | // 构建请求体 |
| | | JSONObject requestBody = new JSONObject(); |
| | | requestBody.put("code", code); |
| | | |
| | | log.info("调用微信getPhoneNumber接口, code: {}", code); |
| | | |
| | | // 发送POST请求 |
| | | String response = HttpUtils.sendPost(url, requestBody.toJSONString()); |
| | | |
| | | log.info("微信getPhoneNumber响应: {}", response); |
| | | |
| | | // 解析响应 |
| | | JSONObject jsonResponse = JSONObject.parseObject(response); |
| | | |
| | | if (jsonResponse.getInteger("errcode") != 0) |
| | | { |
| | | log.error("微信getPhoneNumber失败: {}", response); |
| | | result.put("success", false); |
| | | result.put("message", "获取手机号失败: " + jsonResponse.getString("errmsg")); |
| | | return result; |
| | | } |
| | | |
| | | // 获取手机号信息 |
| | | JSONObject phoneInfo = jsonResponse.getJSONObject("phone_info"); |
| | | |
| | | result.put("success", true); |
| | | result.put("phoneNumber", phoneInfo.getString("phoneNumber")); |
| | | result.put("purePhoneNumber", phoneInfo.getString("purePhoneNumber")); |
| | | result.put("countryCode", phoneInfo.getString("countryCode")); |
| | | |
| | | return result; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | log.error("调用微信getPhoneNumber接口异常", e); |
| | | result.put("success", false); |
| | | result.put("message", "获取手机号异常: " + e.getMessage()); |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信手机号登录 |
| | | * |
| | | * @param loginCode 微信登录code |
| | | * @param phoneCode 手机号授权code |
| | | * @return 登录结果 |
| | | */ |
| | | @Override |
| | | public Map<String, Object> loginByWechatPhone(String loginCode, String phoneCode) |
| | | { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | |
| | | try |
| | | { |
| | | // 1. 获取微信session(openid、unionid) |
| | | Map<String, Object> sessionResult = getWechatSession(loginCode); |
| | | if (!(Boolean)sessionResult.get("success")) |
| | | { |
| | | return sessionResult; |
| | | } |
| | | |
| | | String openId = (String) sessionResult.get("openid"); |
| | | String unionId = (String) sessionResult.get("unionid"); |
| | | |
| | | log.info("获取到openid: {}, unionid: {}", openId, unionId); |
| | | |
| | | // 2. 获取手机号 |
| | | Map<String, Object> phoneResult = getPhoneNumber(phoneCode); |
| | | if (!(Boolean)phoneResult.get("success")) |
| | | { |
| | | return phoneResult; |
| | | } |
| | | |
| | | String phoneNumber = (String) phoneResult.get("purePhoneNumber"); |
| | | |
| | | log.info("获取到手机号: {}", phoneNumber); |
| | | |
| | | // 3. 根据手机号查找用户 |
| | | SysUser user = userService.selectUserByPhonenumber(phoneNumber); |
| | | |
| | | if (user == null) |
| | | { |
| | | result.put("success", false); |
| | | result.put("message", "该手机号尚未注册,请先注册账号"); |
| | | return result; |
| | | } |
| | | |
| | | // 4. 检查用户状态 |
| | | if ("1".equals(user.getStatus())) |
| | | { |
| | | result.put("success", false); |
| | | result.put("message", "用户已被停用,请联系管理员"); |
| | | return result; |
| | | } |
| | | |
| | | if ("1".equals(user.getDelFlag())) |
| | | { |
| | | result.put("success", false); |
| | | result.put("message", "用户已被删除,请联系管理员"); |
| | | return result; |
| | | } |
| | | |
| | | // 5. 更新用户的微信信息 |
| | | SysUser updateUser = new SysUser(); |
| | | updateUser.setUserId(user.getUserId()); |
| | | updateUser.setOpenId(openId); |
| | | if (StringUtils.isNotEmpty(unionId)) |
| | | { |
| | | updateUser.setUnionId(unionId); |
| | | } |
| | | userService.updateUser(updateUser); |
| | | |
| | | log.info("用户{}微信信息更新成功", user.getUserName()); |
| | | |
| | | // 6. 返回成功结果 |
| | | result.put("success", true); |
| | | result.put("user", user); |
| | | result.put("openId", openId); |
| | | result.put("unionId", unionId); |
| | | |
| | | return result; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | log.error("微信手机号登录异常", e); |
| | | result.put("success", false); |
| | | result.put("message", "登录异常: " + e.getMessage()); |
| | | return result; |
| | | } |
| | | } |
| | | } |
| | |
| | | <if test="status != null and status != ''">status = #{status},</if> |
| | | <if test="oaUserId != null">oa_user_id = #{oaUserId},</if> |
| | | <if test="oaOrderClass != null">oa_order_class = #{oaOrderClass},</if> |
| | | <if test="openId != null and openId != ''">open_id = #{openId},</if> |
| | | <if test="unionId != null and unionId != ''">union_id = #{unionId},</if> |
| | | <if test="wechatNickname != null and wechatNickname != ''">wechat_nickname = #{wechatNickname},</if> |
| | | <if test="loginIp != null and loginIp != ''">login_ip = #{loginIp},</if> |
| | | <if test="loginDate != null">login_date = #{loginDate},</if> |
| | | <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> |
| | |
| | | limit 1 |
| | | </select> |
| | | |
| | | <!-- 通过微信OpenID查询用户 --> |
| | | <select id="selectUserByOpenId" parameterType="String" resultMap="SysUserResult"> |
| | | <include refid="selectUserVo"/> |
| | | where u.open_id = #{openId} and u.del_flag = '0' |
| | | </select> |
| | | |
| | | </mapper> |
| New file |
| | |
| | | -- 微信一键登录功能 - 添加sys_user表微信相关字段 |
| | | -- 执行时间: 2025-11-09 |
| | | |
| | | -- 添加微信OpenID字段 |
| | | ALTER TABLE `sys_user` ADD COLUMN `open_id` VARCHAR(128) NULL COMMENT '微信OpenID' AFTER `oa_order_class`; |
| | | |
| | | -- 添加微信UnionID字段 |
| | | ALTER TABLE `sys_user` ADD COLUMN `union_id` VARCHAR(128) NULL COMMENT '微信UnionID' AFTER `open_id`; |
| | | |
| | | -- 添加微信昵称字段 |
| | | ALTER TABLE `sys_user` ADD COLUMN `wechat_nickname` VARCHAR(100) NULL COMMENT '微信昵称' AFTER `union_id`; |
| | | |
| | | -- 为OpenID添加索引,提高查询效率 |
| | | CREATE INDEX `idx_open_id` ON `sys_user` (`open_id`); |
| | | |
| | | -- 为UnionID添加索引,提高查询效率 |
| | | CREATE INDEX `idx_union_id` ON `sys_user` (`union_id`); |
| | | |
| | | -- 说明 |
| | | -- 1. open_id: 微信小程序的OpenID,每个小程序唯一 |
| | | -- 2. union_id: 微信开放平台的UnionID,同一主体多个应用共享(需要小程序绑定开放平台) |
| | | -- 3. wechat_nickname: 微信昵称,用于显示用户信息 |