wlzboy
2025-11-09 fa5ea853099e88be253fca4fb2b0c2b7af5f396e
feat:微信登录
6个文件已添加
14个文件已修改
1246 ■■■■■ 已修改文件
app/api/login.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/login.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/create-emergency.vue 105 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-dev.yml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-prod.yml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/security/WechatAuthenticationProvider.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/security/WechatAuthenticationToken.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/WechatLoginService.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatLoginService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/wechat_login_fields.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/login.js
@@ -57,3 +57,47 @@
    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
    }
  })
}
app/pages/login.vue
@@ -37,13 +37,33 @@
      
      <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() {
@@ -60,11 +80,17 @@
          password: "",
          code: "",
          uuid: ''
        }
        },
        // 微信一键登录相关
        isWechat: false, // 是否为微信小程序环境
        wechatOpenId: '', // 微信OpenID
        wechatUnionId: '', // 微信UnionID
      }
    },
    created() {
      this.getCode()
      this.checkWechatEnv()
      this.tryAutoLogin()
    },
    methods: {
      // 用户注册
@@ -128,6 +154,137 @@
          // 触发登录成功事件,启动消息轮询
          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('登录失败,请重试')
        })
      }
    }
@@ -246,6 +403,14 @@
          height: 90rpx;
          font-size: 32rpx;
        }
        .wechat-login-btn {
          height: 90rpx;
          font-size: 32rpx;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
      .reg {
app/pages/task/create-emergency.vue
@@ -189,7 +189,10 @@
              :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>
@@ -198,12 +201,21 @@
      
      <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">
@@ -259,7 +271,10 @@
              :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>
@@ -268,12 +283,21 @@
      
      <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">
@@ -917,22 +941,30 @@
      })
    },
    
    // 按地域排序医院:本地区域优先
    // 按地域排序医院:本地区域优先,"家中"始终在最前面
    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)
@@ -941,8 +973,8 @@
        }
      })
      
      // 本地医院在前,其他医院在后
      return [...localHospitals, ...otherHospitals]
      // "家中"在最前,本地医院其次,其他医院在后
      return [...homeHospital, ...localHospitals, ...otherHospitals]
    },
    
    // 转出医院输入框获得焦点
@@ -958,23 +990,28 @@
        // 否则重新加载常用医院
        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 = []
@@ -994,10 +1031,12 @@
        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 = []
@@ -1016,7 +1055,9 @@
    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 => {
@@ -1032,10 +1073,18 @@
      // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址
      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
@@ -1115,7 +1164,7 @@
      if (!keyword || keyword.trim() === '') {
        searchHospitals('', '').then(response => {
          const allHospitals = response.data || []
          // 按地域排序:本地区域优先
          // 按地域排序:"家中"最前,本地区域优先
          this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
        }).catch(error => {
          console.error('加载转入医院失败:', error)
@@ -1136,7 +1185,7 @@
      // 传入关键词,不传地域过滤(搜索所有区域)
      searchHospitals(keyword, '').then(response => {
        const allHospitals = response.data || []
        // 按地域排序:本地区域优先
        // 按地域排序:"家中"最前,本地区域优先
        this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
        this.showHospitalInResults = true
        console.log('搜索转入医院:', keyword, '结果数:', this.hospitalInResults.length)
@@ -1153,10 +1202,18 @@
      // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址
      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
@@ -2006,6 +2063,14 @@
              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 {
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java
@@ -36,6 +36,11 @@
            @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);
    }
    
@@ -62,12 +67,10 @@
        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);
    }
    
@@ -86,11 +89,14 @@
            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);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -2,6 +2,8 @@
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;
@@ -18,8 +20,10 @@
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;
/**
 * 登录验证
@@ -33,6 +37,8 @@
    private SysLoginService loginService;
    @Autowired
    private ISysUserService userService;
    @Autowired
    private ISysMenuService menuService;
    @Autowired
@@ -43,6 +49,87 @@
    @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());
        }
    }
    /**
     * 登录方法
ruoyi-admin/src/main/resources/application-dev.yml
@@ -91,6 +91,17 @@
# 二维码配置
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:
ruoyi-admin/src/main/resources/application-prod.yml
@@ -89,9 +89,18 @@
  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:
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -94,6 +94,15 @@
    /** OA系统的订单编码列表(如:BF,AB,SA) */
    private String oaOrderClass;
    /** 微信OpenID */
    private String openId;
    /** 微信UnionID */
    private String unionId;
    /** 微信昵称 */
    private String wechatNickname;
    public SysUser()
    {
@@ -322,6 +331,36 @@
    {
        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() {
@@ -347,6 +386,9 @@
            .append("dept", getDept())
            .append("oaUserId", getOaUserId())
            .append("oaOrderClass", getOaOrderClass())
            .append("openId", getOpenId())
            .append("unionId", getUnionId())
            .append("wechatNickname", getWechatNickname())
            .toString();
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -20,6 +20,7 @@
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;
@@ -78,6 +79,12 @@
    private RequestMappingHandlerMapping requestMappingHandlerMapping;
    /**
     * 微信认证提供者
     */
    @Autowired
    private WechatAuthenticationProvider wechatAuthenticationProvider;
    /**
     * 获取所有标注了@Anonymous的URL
     */
    private Set<String> getAnonymousUrls() {
@@ -98,14 +105,18 @@
    /**
     * 身份验证实现
     * 支持用户名密码认证和微信认证
     */
    @Bean
    public AuthenticationManager authenticationManager()
    {
        // 用户名密码认证提供者
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
        return new ProviderManager(daoAuthenticationProvider);
        // 返回ProviderManager,支持多种认证方式
        return new ProviderManager(daoAuthenticationProvider, wechatAuthenticationProvider);
    }
    /**
ruoyi-framework/src/main/java/com/ruoyi/framework/security/WechatAuthenticationProvider.java
New file
@@ -0,0 +1,85 @@
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);
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/security/WechatAuthenticationToken.java
New file
@@ -0,0 +1,86 @@
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;
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
@@ -37,11 +37,19 @@
    @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);
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/WechatLoginService.java
New file
@@ -0,0 +1,175 @@
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());
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
@@ -139,4 +139,12 @@
     * @return 用户信息
     */
    public SysUser selectUserByOaUserId(@Param("oaUserId") Integer oaUserId);
    /**
     * 通过微信OpenID查询用户
     *
     * @param openId 微信OpenID
     * @return 用户对象信息
     */
    public SysUser selectUserByOpenId(@Param("openId") String openId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
@@ -49,6 +49,14 @@
     * @return 用户对象信息
     */
    public SysUser selectUserByPhonenumber(String phonenumber);
    /**
     * 通过微信OpenID查询用户
     *
     * @param openId 微信OpenID
     * @return 用户对象信息
     */
    public SysUser selectUserByOpenId(String openId);
    /**
     * 通过用户ID查询用户
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatLoginService.java
New file
@@ -0,0 +1,36 @@
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);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
@@ -139,6 +139,18 @@
    {
        return userMapper.selectUserById(userId);
    }
    /**
     * 通过微信OpenID查询用户
     *
     * @param openId 微信OpenID
     * @return 用户对象信息
     */
    @Override
    public SysUser selectUserByOpenId(String openId)
    {
        return userMapper.selectUserByOpenId(openId);
    }
    /**
     * 查询用户所属角色组
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java
New file
@@ -0,0 +1,291 @@
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;
        }
    }
}
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -234,6 +234,9 @@
             <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>
@@ -274,4 +277,10 @@
        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> 
sql/wechat_login_fields.sql
New file
@@ -0,0 +1,22 @@
-- 微信一键登录功能 - 添加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: 微信昵称,用于显示用户信息