无关风月
2025-02-13 9c2b29176050996de04c2e3fa67ff77295934202
小程序登录
9个文件已修改
25个文件已添加
2654 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TBillController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TContractController.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskUtil.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxLoginController.java 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/exception/ServiceException.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/utils/Constants.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/utils/HttpUtils.java 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/redis/configure/FastJson2JsonRedisSerializer.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/redis/service/RedisService.java 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/pom.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TTenant.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/TTenantService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTenantServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/AccessTokenRespBody.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/Code2SessionRespBody.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/RespBody.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resq/Code2SessionResqBody.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/model/WeixinProperties.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletPhoneEncrypteData.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletUserDecodeData.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletUserEncrypteData.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/Watermark.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/SHA1.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WebUtils.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxAppletTools.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxCache.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxCacheTemplate.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxException.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxJsonUtils.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TBillController.java
@@ -34,6 +34,9 @@
    @Autowired
    TBillService tBillService;
    @PreAuthorize("@ss.hasPermi('system:bill:list')")
    @PostMapping("list")
    @ApiOperation("分页查询账单列表")
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TContractController.java
@@ -47,7 +47,9 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
/**
@@ -72,6 +74,115 @@
    private TBillService billService;
    @Autowired
    private TCheckAcceptRecordService checkAcceptRecordService;
    @ApiOperation(value = "测试生成账单")
    @PostMapping(value = "/testBill")
    public R testBill(String id) {
        TContract contract = contractService.getById(id);
        // 查询所有已签订的合同并且未生成第一笔账单的
        List<TBill> bills = new ArrayList<>();
        List<TContractRentType> contractRentTypes = contractRentTypeService.list();
            contract.setFirstPayTime(contract.getStartTime().plusDays(10));
            // 第一次应缴费日期
            LocalDateTime firstPayTime = contract.getStartTime().plusDays(10).withHour(0).withMinute(0).withSecond(0);
            LocalDate localDate = contract.getStartTime().plusDays(10).toLocalDate();
            LocalDate now = LocalDate.now();
            TBill rentBill = new TBill();
            rentBill.setContractId(contract.getId());
            rentBill.setContractNumber(contract.getContractNumber());
            LocalDateTime startPayTime = contract.getStartPayTime();
            LocalDateTime endTime1 = contract.getEndTime();
//                // 计算两个时间相差多少天
//                // 如果时间小于30天 需要计算每日租金
//                if (days<30){
//                    rentBill.setPayableFeesMoney(contract.getMonthRent().divide(new BigDecimal("30"),2,BigDecimal.ROUND_DOWN).multiply(new BigDecimal(days)));
//                }else{
//                    rentBill.setPayableFeesMoney(contract.getPayType().equals("1")?contract.getMonthRent():
//                            contract.getPayType().equals("2")?contract.getMonthRent().multiply(new BigDecimal("3")):contract.getMonthRent().multiply(new BigDecimal("12")).setScale(2,BigDecimal.ROUND_DOWN));
//                }
            rentBill.setPayableFeesTime(firstPayTime);
            rentBill.setPayFeesStatus("1");
            rentBill.setBillType("1");
            rentBill.setStartTime(contract.getStartPayTime());
            TContractRentType tContractRentType = contractRentTypes.stream().filter(e -> e.getContractId().equals(contract.getId())).findFirst().orElse(null);
            if (tContractRentType!=null && contract.getStartPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12).isAfter(tContractRentType.getChangeTime())){
                // 计算租金变动的天数
                long moneyDays = ChronoUnit.DAYS.between(tContractRentType.getChangeTime(), contract.getStartPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12))+1L;
                contract.setChangeTime(LocalDateTime.now());
                // 递增递减的租金
                BigDecimal contractRentTypeMoney = new BigDecimal("0");
                // 不递增递减的租金
                BigDecimal originalMoney = new BigDecimal("0");
                // 原租金
                switch (tContractRentType.getIncreasingDecreasingType()){
                    case 1:
                        switch (tContractRentType.getIncreasingDecreasing()){
                            case 1:
                                contractRentTypeMoney =contractRentTypeMoney.add(contract.getChangeRent().multiply(new BigDecimal(100).add(tContractRentType.getNumericalValue()).divide(new BigDecimal(100),2,BigDecimal.ROUND_DOWN)).divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN).multiply(new BigDecimal(moneyDays)));
                                contract.setChangeRent(contractRentTypeMoney.add(contract.getChangeRent().multiply(new BigDecimal(100).add(tContractRentType.getNumericalValue()).divide(new BigDecimal(100),2,BigDecimal.ROUND_DOWN))));
                                break;
                            case 2:
                                contractRentTypeMoney = contractRentTypeMoney.add(contract.getChangeRent().multiply(new BigDecimal(100).subtract(tContractRentType.getNumericalValue()).divide(new BigDecimal(100),2,BigDecimal.ROUND_DOWN)).divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN).multiply(new BigDecimal(moneyDays)));
                                contract.setChangeRent(contractRentTypeMoney.add(contract.getChangeRent().multiply(new BigDecimal(100).subtract(tContractRentType.getNumericalValue()).divide(new BigDecimal(100),2,BigDecimal.ROUND_DOWN))));
                                break;
                        }
                        break;
                    case 2:
                        switch (tContractRentType.getIncreasingDecreasing()){
                            case 1:
                                contractRentTypeMoney =contractRentTypeMoney.add(contract.getChangeRent().add(tContractRentType.getNumericalValue())).divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN).multiply(new BigDecimal(moneyDays));
                                contract.setChangeRent(contractRentTypeMoney.add(contract.getChangeRent().add(tContractRentType.getNumericalValue())));
                                break;
                            case 2:
                                contractRentTypeMoney = contractRentTypeMoney.add(contract.getChangeRent().subtract(tContractRentType.getNumericalValue())).divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN).multiply(new BigDecimal(moneyDays));
                                contract.setChangeRent(contractRentTypeMoney.add(contract.getChangeRent().subtract(tContractRentType.getNumericalValue())));
                                break;
                        }
                        break;
                }
                // 不需要涨租金的时间段
                long originalDays = ChronoUnit.DAYS.between(contract.getFirstPayTime(), tContractRentType.getChangeTime());
                originalMoney=originalMoney.add(contract.getMonthRent().divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN))
                        .multiply(new BigDecimal(originalDays));
                rentBill.setPayableFeesMoney(contractRentTypeMoney.add(originalMoney));
                rentBill.setOutstandingMoney(rentBill.getPayableFeesMoney());
                if (contract.getFirstPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12).isAfter(contract.getEndTime())){
                    rentBill.setEndTime(contract.getFirstPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12));
                }else{
                    rentBill.setEndTime(contract.getEndTime());
                }
            }else{
                if (contract.getFirstPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12).isAfter(contract.getEndTime())){
                    rentBill.setEndTime(contract.getFirstPayTime().plusMonths(contract.getPayType().equals("1")? 1:contract.getPayType().equals("2")? 3:12));
                }else{
                    rentBill.setEndTime(contract.getEndTime());
                }
                // 不走递增递减
                long allDays = ChronoUnit.DAYS.between(contract.getFirstPayTime(), rentBill.getEndTime());
                rentBill.setPayableFeesMoney(contract.getMonthRent().divide(new BigDecimal(30), 2, BigDecimal.ROUND_DOWN).multiply(new BigDecimal(allDays)));
                rentBill.setOutstandingMoney(rentBill.getPayableFeesMoney());
            }
            // 租金账单
            bills.add(rentBill);
            // 押金账单
            TBill depositBill = new TBill();
            depositBill.setContractId(contract.getId());
            depositBill.setContractNumber(contract.getContractNumber());
            depositBill.setPayableFeesMoney(contract.getDeposit());
            depositBill.setOutstandingMoney(depositBill.getPayableFeesMoney());
            depositBill.setPayableFeesTime(firstPayTime);
            depositBill.setPayFeesStatus("1");
            depositBill.setBillType("2");
        contractService.updateById(contract);
        billService.save(rentBill);
        billService.save(depositBill);
        return R.ok();
    }
    @ApiOperation(value = "获取合同分页列表")
    @PostMapping(value = "/contractList")
    @PreAuthorize("@ss.hasPermi('system:contract:list')")
@@ -100,6 +211,8 @@
    @Log(title = "合同管理-编辑合同", businessType =  BusinessType.UPDATE)
    @ApiOperation(value = "编辑合同")
    @PostMapping(value = "/updateContract")
    @PreAuthorize("@ss.hasPermi('system:contract:update')")
    public R<Boolean> updateContract(@Validated @RequestBody TContractDTO dto) {
        contractService.updateById(dto);
        contractRentTypeService.remove(new LambdaQueryWrapper<TContractRentType>()
@@ -118,6 +231,8 @@
    }
    @Log(title = "合同管理-批量删除合同", businessType = BusinessType.DELETE)
    @ApiOperation(value = "批量删除合同")
    @PreAuthorize("@ss.hasPermi('system:contract:delete')")
    @DeleteMapping(value = "/deleteContractByIds")
    public R<Boolean> deleteContractByIds
            (@RequestParam String ids) {
@@ -129,6 +244,8 @@
    @ApiOperation(value = "查询合同信息信息")
    @GetMapping(value = "/getContractById")
    @PreAuthorize("@ss.hasPermi('system:contract:detail')")
    public R<TContractVO> getContractById(@RequestParam String id) {
        TContractVO res = new TContractVO();
        TContract contract = contractService.getById(id);
@@ -165,6 +282,8 @@
    }
    @Log(title = "合同管理-撤销审批", businessType =  BusinessType.UPDATE)
    @ApiOperation(value = "撤销审批")
    @PreAuthorize("@ss.hasPermi('system:contract:revoke')")
    @GetMapping(value = "/updateContractStatus")
    public R<Boolean> updateContractStatus(String id) {
        TContract contract = contractService.getById(id);
@@ -172,9 +291,12 @@
        contractService.updateById(contract);
        return R.ok();
    }
    @PreAuthorize("@ss.hasPermi('system:contract:confirm')")
    @Log(title = "合同管理-确认结算", businessType =  BusinessType.UPDATE)
    @ApiOperation(value = "确认结算")
    @PostMapping(value = "/confirmSettlement")
    public R<Boolean> confirmSettlement(String id) {
        TContract contract = contractService.getById(id);
        contract.setStatus("8");
@@ -183,17 +305,20 @@
    }
    @ApiOperation(value = "终止合同剩余未缴费账单列表")
    @PostMapping(value = "/contractBillList")
    @PreAuthorize("@ss.hasPermi('system:contract:billList')")
    public R<PageInfo<BillVO>> contractBillList(@RequestBody TContractBillQuery query) {
        return R.ok(contractService.contractBillList(query));
    }
    @ApiOperation(value = "终止合同")
    @PostMapping(value = "/terminateContract")
    @PreAuthorize("@ss.hasPermi('system:contract:terminate')")
    public R terminateContract(@RequestBody TerminateContractDTO dto) {
        contractService.terminateContract(dto);
        return R.ok();
    }
    @ApiOperation(value = "根据合同id查看验收记录")
    @GetMapping(value = "/getCheckByContractId")
    @PreAuthorize("@ss.hasPermi('system:contract:checkDetail')")
    public R<CheckAcceptRecordVO> getCheckByContractId(String id) {
        return R.ok(contractService.getCheckByContractId(id));
    }
@@ -201,6 +326,8 @@
    private WordUtil wordUtil;
    @ApiOperation(value = "生成合同附件")
    @PostMapping("/set")
    @Log(title = "生成合同附件", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:contract:set')")
    public R<List<String>> set(@RequestBody SetContractDto dto,HttpServletResponse response){
        List<TContract> list = contractService.lambdaQuery().in(TContract::getId, dto.getIds()).list();
        List<String> res = new ArrayList<>();
@@ -238,6 +365,7 @@
     * 导出
     */
    @ApiOperation(value = "导出")
    @PreAuthorize("@ss.hasPermi('system:contract:export')")
    @Log(title = "导出", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void exportOpticalInspection(@RequestBody TContractQuery query)
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskUtil.java
@@ -51,19 +51,16 @@
                long hours = ChronoUnit.HOURS.between(payableFeesTime, now);
                long l = hours / 72;
                if (l>0){
                    // 计算每天租金
                    long days = ChronoUnit.DAYS.between(tBill.getStartTime(),tBill.getEndTime());
                    BigDecimal everyDayMoney = tBill.getPayableFeesMoney().divide(new BigDecimal(days), 2, BigDecimal.ROUND_DOWN);
                    // 违约金比例
                    BigDecimal proportion = contract.getProportion();
                    // 预期x天后的违约金
                    BigDecimal money = everyDayMoney.multiply(proportion).multiply(new BigDecimal(l));
                    tBill.setPayableFeesPenalty(money);
                    tBill.setOutstandingMoney(money);
                    // 应缴违约金
                    BigDecimal money = tBill.getOutstandingMoney().multiply(proportion);
                    TBill changeBill = new TBill();
                    changeBill.setId(tBill.getId());
                    changeBill.setPayableFeesPenalty(money);
                    billService.lockAndUpdateInfo(changeBill,2);
                }
            }
            billService.updateBatchById(list);
        } catch (Exception e) {
            e.printStackTrace();
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxLoginController.java
New file
@@ -0,0 +1,247 @@
package com.ruoyi.web.controller.api;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.model.LoginUserApplet;
import com.ruoyi.common.core.utils.HttpUtils;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.model.TTenant;
import com.ruoyi.system.service.TTenantService;
import com.ruoyi.system.utils.wx.body.resp.Code2SessionRespBody;
import com.ruoyi.system.utils.wx.body.resq.Code2SessionResqBody;
import com.ruoyi.system.utils.wx.model.WeixinProperties;
import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import com.ruoyi.system.utils.wx.pojo.AppletUserEncrypteData;
import com.ruoyi.system.utils.wx.tools.WxAppletTools;
import com.ruoyi.system.utils.wx.tools.WxUtils;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * <p>
 *  微信小程序登录 前端控制器
 * </p>
 *
 * @author xiaochen
 * @since 2024-08-06
 */
@Slf4j
@RestController
@RequestMapping("/wxLogin")
public class WxLoginController {
    @Autowired
    private WeixinProperties wxConfig;
    @Autowired
    private RestTemplate wxRestTemplate;
    @Resource
    private RedisService redisService;
    @Resource
    private TTenantService tTenantService;
    /**
     * 上传文件存储在本地的根路径
     */
//    @Value("${file.upload.location}")
//    private String localFilePath;
    @Resource
    private TokenService tokenService;
    @Autowired
    private SysLoginService loginService;
    /**
     * 账号密码登录
     *
     * @param loginBody 登录信息
     * @return 结果
     */
    @ApiOperation(value = "账号密码登录",notes = "管理员账号密码登录")
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        LoginUser loginUser = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, tokenService.createToken(loginUser));
        List<SysRole> roles = loginUser.getUser().getRoles();
        if(CollectionUtils.isEmpty(roles)){
            return AjaxResult.error("请关联角色!");
        }
        if(roles.get(0).getStatus() == 1){
            return AjaxResult.error("该账号角色已被禁用!");
        }
        return ajax;
    }
    @ApiOperation(value = "通过code获得openid,获取用户信息",tags = {"微信小程序登录"})
    @PostMapping("/openIdByJsCode")
    public R<Map<String, Object>> openIdByJsCode(@RequestBody AppletUserEncrypteData data) {
        log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", data.getCode());
        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
        Code2SessionRespBody body = appletTools.getOpenIdByJscode2session(new Code2SessionResqBody().build(data.getCode()));
        String openid = body.getOpenid();
        String sessionKey = body.getSessionKey();
        // 用户信息解密 数据验签
//        if (StringUtils.isNotBlank(data.getSignature())) {
//            WxUtils.verifySignature(data.getRawData(), sessionKey, data.getSignature());
//        }
        if(StringUtils.isEmpty(data.getEncryptedData()) || StringUtils.isEmpty(data.getIv())){
            return R.fail("已拒绝授权");
        }
        AppletUserDecodeData appletUserDecodeData = WxUtils.encryptedData(data.getEncryptedData(), sessionKey,  data.getIv());
        appletUserDecodeData.setOpenId(openid);
        // 先使用openId和当前手机号进行查询
        TTenant tenant = tTenantService.getOne(Wrappers.lambdaQuery(TTenant.class)
                .eq(TTenant::getOpenId, appletUserDecodeData.getOpenId())
                .eq(TTenant::getPhone, appletUserDecodeData.getPhoneNumber()));
        if (tenant==null){
//            appUser.setTenantAttributes();
//            appUser.setTenantType();
            tenant.setPhone(appletUserDecodeData.getPhoneNumber());
            tenant.setAccount(appletUserDecodeData.getPhoneNumber());
            tenant.setPassword(SecurityUtils.encryptPassword(appletUserDecodeData.getPhoneNumber().substring(5)));
            tenant.setOpenId(appletUserDecodeData.getOpenId());
            tTenantService.save(tenant);
        }
        LoginUserApplet loginUserApplet = new LoginUserApplet();
        loginUserApplet.setUserId(Long.valueOf(tenant.getId()));
        Map<String, Object> tokenInfos = new HashMap<>();
        tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
        tokenInfos.put("info",loginUserApplet);
        return R.ok(tokenInfos);
    }
//    @ApiOperation(value = "获取微信小程序二维码",tags = {"获取微信小程序二维码"})
//    @PostMapping("/getQRCode")
//    public AjaxResult getQRCode() {
//        InputStream inputStream = null;
//        OutputStream outputStream = null;
//        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
//        String accessToken = appletTools.getAccessToken("");
//        try {
//            String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
//            Map<String, Object> param = new HashMap<>();
////            param.put("page", "pageA/houseDetail");
//            param.put("check_path", false);
//            param.put("env_version", "trial");
//            param.put("width", 200); //二维码尺寸
//            param.put("is_hyaline", true); // 是否需要透明底色, is_hyaline 为true时,生成透明底色的小程序码 参数仅对小程序码生效
//            param.put("auto_color", true); // 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 参数仅对小程序码生效
//            Map<String, Object> line_color = new HashMap<>();
//            line_color.put("r", 0);
//            line_color.put("g", 0);
//            line_color.put("b", 0);
//            param.put("line_color", line_color);
//            System.err.println("调用生成微信URL接口传参:" + param);
//            MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
//            HttpEntity requestEntity = new HttpEntity(param, headers);
//            ResponseEntity<byte[]> entity = wxRestTemplate.exchange(url, HttpMethod.POST, requestEntity, byte[].class, new Object[0]);
//            System.err.println("调用小程序生成微信永久小程序码URL接口返回结果:" + entity.getBody());
//            byte[] result = entity.getBody();
//            System.err.println(Base64.encodeBase64String(result));
//            inputStream = new ByteArrayInputStream(result);
//            String finalFileName = System.currentTimeMillis() + "" + new SecureRandom().nextInt(0x0400) + ".jpeg";
////            MultipartFile multipartFile = convertInputStreamToMultipartFile(inputStream, finalFileName, "image/jpeg");
////            String name = FileUploadUtils.upload(localFilePath, multipartFile);
////            System.err.println(name);
//            return AjaxResult.success(null);
//        } catch (Exception e) {
//            System.err.println("调用小程序生成微信永久小程序码URL接口异常" + e);
//        } finally {
//            if (inputStream != null) {
//                try {
//                    inputStream.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//            if (outputStream != null) {
//                try {
//                    outputStream.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//        return AjaxResult.success();
//    }
    /**
     * 获取微信token
     * @return
     */
    @PostMapping("/getWXToken")
    public R<String> getWXToken(){
        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
        String accessToken = appletTools.getAccessToken("");
        return R.ok(accessToken);
    }
    /**
     * 敏感词检测
     * @param content
     * @param openid
     * @return
     */
    @PostMapping("/sensitiveWordDetection")
    public R<Boolean> sensitiveWordDetection (@RequestParam("content") String content, @RequestParam("openid") String openid){
        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
        String accessToken = appletTools.getAccessToken("");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("content", content);
        jsonObject.put("version", 2);
        jsonObject.put("scene", 2);
        jsonObject.put("openid", openid);
        String post = HttpUtils.post("https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken, jsonObject.toString());
        JSONObject object = JSONObject.parseObject(post);
        Integer errcode = object.getInteger("errcode");
        if(0 != errcode){
            throw new RuntimeException(object.getString("errmsg"));
        }
        JSONArray detail = object.getJSONArray("detail");
        for (int i = 0; i < detail.size(); i++) {
            JSONObject jsonObject1 = detail.getJSONObject(i);
            Integer errcode1 = jsonObject1.getInteger("errcode");
            if(0 == errcode1){
                String suggest = jsonObject1.getString("suggest");
                Integer label = jsonObject1.getInteger("label");
                String keyword = jsonObject1.getString("keyword");
                Integer prob = jsonObject1.getInteger("prob");
                if(("risky".equals(suggest) || "review".equals(suggest)) && 100 != label && com.ruoyi.common.utils.StringUtils.isNotEmpty(keyword) && 80 <= prob){
                    return R.ok(true);
                }
            }
        }
        return R.ok(false);
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java
@@ -83,6 +83,10 @@
     * 令牌前缀
     */
    public static final String LOGIN_USER_KEY = "login_user_key";
    /**
     * 小程序
     */
    public static final String LOGIN_USER_APPLET_KEY = "login_user_applet_key";
    /**
     * 用户ID
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
New file
@@ -0,0 +1,267 @@
package com.ruoyi.common.core.domain.model;
import com.alibaba.fastjson2.annotation.JSONField;
import com.ruoyi.common.core.domain.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
/**
 * 登录用户身份权限
 *
 * @author ruoyi
 */
public class LoginUserApplet implements UserDetails
{
    private static final long serialVersionUID = 1L;
    /**
     * 用户ID
     */
    private Long userId;
    /**
     * 部门ID
     */
    private Long deptId;
    /**
     * 用户唯一标识
     */
    private String token;
    /**
     * 登录时间
     */
    private Long loginTime;
    /**
     * 过期时间
     */
    private Long expireTime;
    /**
     * 登录IP地址
     */
    private String ipaddr;
    /**
     * 登录地点
     */
    private String loginLocation;
    /**
     * 浏览器类型
     */
    private String browser;
    /**
     * 操作系统
     */
    private String os;
    /**
     * 权限列表
     */
    private Set<String> permissions;
    /**
     * 用户信息
     */
    private SysUser user;
    public LoginUserApplet()
    {
    }
    public LoginUserApplet(SysUser user, Set<String> permissions)
    {
        this.user = user;
        this.permissions = permissions;
    }
    public LoginUserApplet(Long userId, Long deptId, SysUser user, Set<String> permissions)
    {
        this.userId = userId;
        this.deptId = deptId;
        this.user = user;
        this.permissions = permissions;
    }
    public Long getUserId()
    {
        return userId;
    }
    public void setUserId(Long userId)
    {
        this.userId = userId;
    }
    public Long getDeptId()
    {
        return deptId;
    }
    public void setDeptId(Long deptId)
    {
        this.deptId = deptId;
    }
    public String getToken()
    {
        return token;
    }
    public void setToken(String token)
    {
        this.token = token;
    }
    @JSONField(serialize = false)
    @Override
    public String getPassword()
    {
        return user.getPassword();
    }
    @Override
    public String getUsername()
    {
        return user.getUserName();
    }
    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }
    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }
    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }
    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled()
    {
        return true;
    }
    public Long getLoginTime()
    {
        return loginTime;
    }
    public void setLoginTime(Long loginTime)
    {
        this.loginTime = loginTime;
    }
    public String getIpaddr()
    {
        return ipaddr;
    }
    public void setIpaddr(String ipaddr)
    {
        this.ipaddr = ipaddr;
    }
    public String getLoginLocation()
    {
        return loginLocation;
    }
    public void setLoginLocation(String loginLocation)
    {
        this.loginLocation = loginLocation;
    }
    public String getBrowser()
    {
        return browser;
    }
    public void setBrowser(String browser)
    {
        this.browser = browser;
    }
    public String getOs()
    {
        return os;
    }
    public void setOs(String os)
    {
        this.os = os;
    }
    public Long getExpireTime()
    {
        return expireTime;
    }
    public void setExpireTime(Long expireTime)
    {
        this.expireTime = expireTime;
    }
    public Set<String> getPermissions()
    {
        return permissions;
    }
    public void setPermissions(Set<String> permissions)
    {
        this.permissions = permissions;
    }
    public SysUser getUser()
    {
        return user;
    }
    public void setUser(SysUser user)
    {
        this.user = user;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return null;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/core/exception/ServiceException.java
New file
@@ -0,0 +1,74 @@
package com.ruoyi.common.core.exception;
/**
 * 业务异常
 *
 * @author ruoyi
 */
public final class ServiceException extends RuntimeException
{
    private static final long serialVersionUID = 1L;
    /**
     * 错误码
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;
    /**
     * 错误明细,内部调试错误
     *
     * 和 {@link CommonResult#getDetailMessage()} 一致的设计
     */
    private String detailMessage;
    /**
     * 空构造方法,避免反序列化问题
     */
    public ServiceException()
    {
    }
    public ServiceException(String message)
    {
        this.message = message;
    }
    public ServiceException(String message, Integer code)
    {
        this.message = message;
        this.code = code;
    }
    public String getDetailMessage()
    {
        return detailMessage;
    }
    @Override
    public String getMessage()
    {
        return message;
    }
    public Integer getCode()
    {
        return code;
    }
    public ServiceException setMessage(String message)
    {
        this.message = message;
        return this;
    }
    public ServiceException setDetailMessage(String detailMessage)
    {
        this.detailMessage = detailMessage;
        return this;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/core/utils/Constants.java
New file
@@ -0,0 +1,143 @@
package com.ruoyi.common.core.utils;
/**
 * 通用常量信息
 *
 * @author ruoyi
 */
public class Constants
{
    /**
     * UTF-8 字符集
     */
    public static final String UTF8 = "UTF-8";
    /**
     * GBK 字符集
     */
    public static final String GBK = "GBK";
    /**
     * www主域
     */
    public static final String WWW = "www.";
    /**
     * RMI 远程方法调用
     */
    public static final String LOOKUP_RMI = "rmi:";
    /**
     * LDAP 远程方法调用
     */
    public static final String LOOKUP_LDAP = "ldap:";
    /**
     * LDAPS 远程方法调用
     */
    public static final String LOOKUP_LDAPS = "ldaps:";
    /**
     * http请求
     */
    public static final String HTTP = "http://";
    /**
     * https请求
     */
    public static final String HTTPS = "https://";
    /**
     * 成功标记
     */
    public static final Integer SUCCESS = 200;
    /**
     * 失败标记
     */
    public static final Integer FAIL = 500;
    /**
     * 登录成功状态
     */
    public static final String LOGIN_SUCCESS_STATUS = "1";
    /**
     * 登录失败状态
     */
    public static final String LOGIN_FAIL_STATUS = "2";
    /**
     * 登录成功
     */
    public static final String LOGIN_SUCCESS = "Success";
    /**
     * 注销
     */
    public static final String LOGOUT = "Logout";
    /**
     * 注册
     */
    public static final String REGISTER = "Register";
    /**
     * 登录失败
     */
    public static final String LOGIN_FAIL = "Error";
    /**
     * 当前记录起始索引
     */
    public static final String PAGE_NUM = "pageNum";
    /**
     * 每页显示记录数
     */
    public static final String PAGE_SIZE = "pageSize";
    /**
     * 排序列
     */
    public static final String ORDER_BY_COLUMN = "orderByColumn";
    /**
     * 排序的方向 "desc" 或者 "asc".
     */
    public static final String IS_ASC = "isAsc";
    /**
     * 验证码有效期(分钟)
     */
    public static final long CAPTCHA_EXPIRATION = 2;
    /**
     * 资源映射路径 前缀
     */
    public static final String RESOURCE_PREFIX = "/profile";
    /**
     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
     */
    public static final String[] JOB_WHITELIST_STR = { "com.ruoyi" };
    /**
     * 时间格式化
     */
    public static final String DATE_FORMATTER_TIME = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_FORMATTER_DATE = "yyyy-MM-dd";
    /**
     * 修改手机号后缀
     */
    public static final String UPDATE_PHONE = "_updatePhone";
    /**
     * 申请建桩后缀
     */
    public static final String APPLY_CHARGING = "_applyCharging";
    /**
     * 定时任务违规的字符
     */
    public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
            "org.springframework", "org.apache", "com.ruoyi.common.core.utils.file" };
}
ruoyi-common/src/main/java/com/ruoyi/common/core/utils/HttpUtils.java
New file
@@ -0,0 +1,311 @@
package com.ruoyi.common.core.utils;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
/**
 * 通用http发送方法
 *
 * @author ruoyi
 */
public class HttpUtils
{
    private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
    /**
     * 向指定 URL 发送GET方法的请求
     *
     * @param url 发送请求的 URL
     * @return 所代表远程资源的响应结果
     */
    public static String sendGet(String url)
    {
        return sendGet(url, StringUtils.EMPTY);
    }
    /**
     * 向指定 URL 发送GET方法的请求
     *
     * @param url 发送请求的 URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param)
    {
        return sendGet(url, param, Constants.UTF8);
    }
    /**
     * 向指定 URL 发送GET方法的请求
     *
     * @param url 发送请求的 URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @param contentType 编码类型
     * @return 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param, String contentType)
    {
        StringBuilder result = new StringBuilder();
        BufferedReader in = null;
        try
        {
            String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
            log.info("sendGet - {}", urlNameString);
            URL realUrl = new URL(urlNameString);
            URLConnection connection = realUrl.openConnection();
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            connection.connect();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
            String line;
            while ((line = in.readLine()) != null)
            {
                result.append(line);
            }
            log.info("recv - {}", result);
        }
        catch (ConnectException e)
        {
            log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
        }
        catch (SocketTimeoutException e)
        {
            log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
        }
        catch (IOException e)
        {
            log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
        }
        catch (Exception e)
        {
            log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
        }
        finally
        {
            try
            {
                if (in != null)
                {
                    in.close();
                }
            }
            catch (Exception ex)
            {
                log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
            }
        }
        return result.toString();
    }
    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url 发送请求的 URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return 所代表远程资源的响应结果
     */
    public static String sendPost(String url, String param)
    {
        PrintWriter out = null;
        BufferedReader in = null;
        StringBuilder result = new StringBuilder();
        try
        {
            log.info("sendPost - {}", url);
            URL realUrl = new URL(url);
            URLConnection conn = realUrl.openConnection();
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("contentType", "utf-8");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            out = new PrintWriter(conn.getOutputStream());
            out.print(param);
            out.flush();
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            String line;
            while ((line = in.readLine()) != null)
            {
                result.append(line);
            }
            log.info("recv - {}", result);
        }
        catch (ConnectException e)
        {
            log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
        }
        catch (SocketTimeoutException e)
        {
            log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
        }
        catch (IOException e)
        {
            log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
        }
        catch (Exception e)
        {
            log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
        }
        finally
        {
            try
            {
                if (out != null)
                {
                    out.close();
                }
                if (in != null)
                {
                    in.close();
                }
            }
            catch (IOException ex)
            {
                log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
            }
        }
        return result.toString();
    }
    public static String sendSSLPost(String url, String param)
    {
        StringBuilder result = new StringBuilder();
        String urlNameString = url + "?" + param;
        try
        {
            log.info("sendSSLPost - {}", urlNameString);
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
            URL console = new URL(urlNameString);
            HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("contentType", "utf-8");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setSSLSocketFactory(sc.getSocketFactory());
            conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
            conn.connect();
            InputStream is = conn.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String ret = "";
            while ((ret = br.readLine()) != null)
            {
                if (ret != null && !"".equals(ret.trim()))
                {
                    result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
                }
            }
            log.info("recv - {}", result);
            conn.disconnect();
            br.close();
        }
        catch (ConnectException e)
        {
            log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
        }
        catch (SocketTimeoutException e)
        {
            log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
        }
        catch (IOException e)
        {
            log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
        }
        catch (Exception e)
        {
            log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
        }
        return result.toString();
    }
    public static String post(String strURL, String params) {
        String result = "";
        BufferedReader reader = null;
        try {
            URL url = new URL(strURL);// 创建连接
            HttpURLConnection connection = (HttpURLConnection) url
                    .openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setInstanceFollowRedirects(true);
            connection.setRequestMethod("POST"); // 设置请求方式
            connection.setRequestProperty("Accept", "application/json"); // 设置接收数据的格式
            connection.setRequestProperty("Content-Type", "application/json"); // 设置发送数据的格式
            connection.connect();
            if (params != null && !StringUtils.isEmpty(params)) {
                byte[] writebytes = params.getBytes();
                // 设置文件长度
                //   connection.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
                OutputStream outwritestream = connection.getOutputStream();
                outwritestream.write(params.getBytes());
                outwritestream.flush();
                outwritestream.close();
                // Log.d("hlhupload", "doJsonPost: conn"+connection.getResponseCode());
            }
            if (connection.getResponseCode() == 200) {
                log.info("<<<<<<<<<<<<<请求响应:{}", connection.getResponseMessage());
                reader = new BufferedReader(
                        new InputStreamReader(connection.getInputStream()));
                result = reader.readLine();
                log.info("<<<<<<<<<<<<<请求响应:{}", result);
            } else {
                throw new ServiceException(connection.getResponseMessage());
            }
        } catch (Exception e) {
            throw new ServiceException("http的post请求异常!" + e.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }
    private static class TrustAnyTrustManager implements X509TrustManager
    {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
        {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
        {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers()
        {
            return new X509Certificate[] {};
        }
    }
    private static class TrustAnyHostnameVerifier implements HostnameVerifier
    {
        @Override
        public boolean verify(String hostname, SSLSession session)
        {
            return true;
        }
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/redis/configure/FastJson2JsonRedisSerializer.java
New file
@@ -0,0 +1,50 @@
package com.ruoyi.common.redis.configure;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
 * Redis使用FastJson序列化
 *
 * @author ruoyi
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Class<T> clazz;
    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }
    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }
    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.common.redis.configure;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * redis配置
 *
 * @author ruoyi
 */
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/redis/service/RedisService.java
New file
@@ -0,0 +1,273 @@
package com.ruoyi.common.redis.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * spring redis 工具类
 *
 * @author ruoyi
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisService
{
    @Autowired
    public RedisTemplate redisTemplate;
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }
    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key)
    {
        return redisTemplate.getExpire(key);
    }
    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key)
    {
        return redisTemplate.hasKey(key);
    }
    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }
    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection) > 0;
    }
    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }
    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }
    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }
    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }
    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap, long timeout)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
            redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }
    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }
    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }
    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }
    /**
     * 删除Hash中的某条数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey)
    {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }
    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
@@ -4,6 +4,8 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import com.ruoyi.common.core.domain.model.LoginUserApplet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -66,6 +68,15 @@
        return getLoginUser(ServletUtils.getRequest());
    }
    /**
     * 小程序获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUserApplet()
    {
        return getLoginUser(ServletUtils.getRequest());
    }
    /**
     * 获取用户身份信息
     *
     * @return 用户信息
@@ -81,6 +92,33 @@
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
                log.error("获取用户信息异常'{}'", e.getMessage());
            }
        }
        return null;
    }
    /**
     * 小程序获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUserApplet(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_APPLET_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
@@ -133,6 +171,23 @@
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }
    /**
     * 创建用户小程序令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createTokenApplet(LoginUserApplet loginUser)
    {
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgentApplet(loginUser);
        refreshTokenApplet(loginUser);
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_APPLET_KEY, token);
        return createTokenApplet(claims);
    }
    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
@@ -163,6 +218,19 @@
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }
    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshTokenApplet(LoginUserApplet loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }
    /**
     * 设置用户代理信息
@@ -170,6 +238,20 @@
     * @param loginUser 登录信息
     */
    public void setUserAgent(LoginUser loginUser)
    {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr();
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }
    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgentApplet(LoginUserApplet loginUser)
    {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr();
@@ -192,6 +274,19 @@
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }
    /**
     * 小程序从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createTokenApplet(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }
    /**
     * 从令牌中获取数据声明
ruoyi-system/pom.xml
@@ -79,6 +79,10 @@
            <artifactId>bankapi</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
    </dependencies>
</project>
ruoyi-system/src/main/java/com/ruoyi/system/model/TTenant.java
@@ -88,5 +88,8 @@
    @ApiModelProperty(value = "登录密码")
    @TableField("password")
    private String password;
    @ApiModelProperty(value = "微信openid")
    @TableField("open_id")
    private String openId;
}
ruoyi-system/src/main/java/com/ruoyi/system/service/TTenantService.java
@@ -2,9 +2,12 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.basic.PageInfo;
import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import com.ruoyi.system.model.TTenant;
import com.ruoyi.system.query.TTenantQuery;
import com.ruoyi.system.vo.TenantVO;
import java.util.Map;
/**
 * <p>
@@ -22,4 +25,7 @@
     * @return
     */
    PageInfo<TenantVO> pageList(TTenantQuery query);
    Map<String, Object> wxLogin(AppletUserDecodeData appletUserDecodeData);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTenantServiceImpl.java
@@ -1,18 +1,23 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.basic.PageInfo;
import com.ruoyi.common.constant.DictConstants;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.system.mapper.TTenantMapper;
import com.ruoyi.system.model.TTenant;
import com.ruoyi.system.query.TTenantQuery;
import com.ruoyi.system.service.TTenantService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.vo.SysUserVO;
import com.ruoyi.system.vo.TenantVO;
import org.springframework.security.core.token.TokenService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
 * <p>
@@ -25,6 +30,8 @@
@Service
public class TTenantServiceImpl extends ServiceImpl<TTenantMapper, TTenant> implements TTenantService {
    @Resource
    private TokenService tokenService;
    @Override
    public PageInfo<TenantVO> pageList(TTenantQuery query) {
        PageInfo<TenantVO> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
@@ -36,4 +43,10 @@
        pageInfo.setRecords(list);
        return pageInfo;
    }
    @Override
    public Map<String, Object> wxLogin(AppletUserDecodeData appletUserDecodeData) {
        return null;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/AccessTokenRespBody.java
New file
@@ -0,0 +1,28 @@
package com.ruoyi.system.utils.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * AccessToken 全局唯一
 *
 * @author xiaochen
 */
@Data
public class AccessTokenRespBody extends RespBody implements Serializable {
    /**
     * 获取到的凭证
     */
    @JsonProperty("access_token")
    private String accessToken;
    /**
     * 凭证有效时间,单位:秒
     */
    @JsonProperty("expires_in")
    private int expiresIn;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/Code2SessionRespBody.java
New file
@@ -0,0 +1,29 @@
package com.ruoyi.system.utils.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName Code2SessionRespBody
 * @Description
 * @date 2021-07-28 12:35
 */
@Data
public class Code2SessionRespBody extends RespBody {
    /**
     * 用户唯一标识
     */
    @JsonProperty("openid")
    private String openid;
    /**
     * 会话密钥
     */
    @JsonProperty("session_key")
    private String sessionKey;
    /**
     * 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
     */
    @JsonProperty("unionid")
    private String unionid;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resp/RespBody.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.system.utils.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName RespBody
 * @Description
 * @date 2021-07-28 11:44
 */
@Data
public class RespBody {
    @JsonProperty("errcode")
    private Integer errorCode;
    @JsonProperty("errmsg")
    private String errorMsg;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/body/resq/Code2SessionResqBody.java
New file
@@ -0,0 +1,21 @@
package com.ruoyi.system.utils.wx.body.resq;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName Code2SessionResqBody
 * @Description
 * @date 2021-07-28 11:47
 */
@Data
public class Code2SessionResqBody {
    @JsonProperty("js_code")
    private String jsCode;
    public Code2SessionResqBody build(String jsCode) {
        this.jsCode = jsCode;
        return this;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/model/WeixinProperties.java
New file
@@ -0,0 +1,79 @@
package com.ruoyi.system.utils.wx.model;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * @author xiaochen
 * @ClassName WeixinProperties
 * @Description
 * @date 2024-08-14 13:55
 */
@ToString
@Component
@ConfigurationProperties(prefix = "wx.conf")
public class WeixinProperties {
    /**
     * 默认开启
     */
    private boolean enabled = true;
    /**
     * 获取 App ID
     *
     * @return App ID
     */
    private String appId;
    /**
     * 获取 Mch ID
     *
     * @return Mch ID
     */
    private String mchId;
    /**
     * 获取 secret ID
     *
     * @return secret ID
     */
    private String secretId;
    public String getSecretId() {
        return secretId;
    }
    public void setSecretId(String secretId) {
        this.secretId = secretId;
    }
    /**
     * HTTP(S) 连接超时时间,单位毫秒
     *
     */
    public int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }
    /**
     * HTTP(S) 读数据超时时间,单位毫秒
     */
    public int getHttpReadTimeoutMs() {
        return 8 * 1000;
    }
    public String getAppId() {
        return appId;
    }
    public void setAppId(String appId) {
        this.appId = appId;
    }
    public String getMchId() {
        return mchId;
    }
    public void setMchId(String mchId) {
        this.mchId = mchId;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletPhoneEncrypteData.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.system.utils.wx.pojo;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName AppletUserDecodeData
 * @Description
 * @date 2021-08-13 17:46
 * 小程序加密数据体
 *
 */
@Data
public class AppletPhoneEncrypteData {
    private String encryptedData;
    private String openid;
    private String unionid;
    private String iv;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletUserDecodeData.java
New file
@@ -0,0 +1,52 @@
package com.ruoyi.system.utils.wx.pojo;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName AppletUserDecodeData
 * @Description
 * 用户主体信息部分
 * {
 *     "openId": "OPENID",
 *     "nickName": "NICKNAME",
 *     "gender": GENDER,
 *     "city": "CITY",
 *     "province": "PROVINCE",
 *     "country": "COUNTRY",
 *     "avatarUrl": "AVATARURL",
 *     "unionId": "UNIONID",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp":TIMESTAMP
 *     }
 * }
 * 电话部分
 * {
 *     "phoneNumber": "13580006666",
 *     "purePhoneNumber": "13580006666",
 *     "countryCode": "86",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp": TIMESTAMP
 *     }
 * }
 *
 */
@Data
public class AppletUserDecodeData {
    private String openId;
    private String unionId;
    private String nickName;
    private int gender;
    private String city;
    private String province;
    private String country;
    private String avatarUrl;
    private Watermark watermark;
    private String phoneNumber;
    private String purePhoneNumber;
    private String countryCode;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/AppletUserEncrypteData.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.system.utils.wx.pojo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName AppletUserDecodeData
 * @Description
 * 小程序加密数据体
 *
 */
@Data
public class AppletUserEncrypteData extends AppletPhoneEncrypteData {
    private String rawData;
    private String signature;
    private String code;
    @ApiModelProperty(value = "邀请用户id")
    private Long inviteUserId;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/pojo/Watermark.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.system.utils.wx.pojo;
import lombok.Data;
@Data
public class Watermark {
    private String appid;
    private String timestamp;
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/SHA1.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.system.utils.wx.tools;
import java.security.MessageDigest;
public class SHA1 {
    /**
     * 用SHA1算法生成安全签名
     *
     * @param str
     * @return
     * @throws WxException
     */
    public static String getSHA1(String str) throws WxException {
        try {
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            StringBuffer hexstr = new StringBuffer();
            String shaHex;
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            throw new WxException(WxException.ComputeSignatureError);
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WebUtils.java
New file
@@ -0,0 +1,48 @@
package com.ruoyi.system.utils.wx.tools;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * @Author xiaochen
 * @Date 2019/08/26 10:28 AM
 * @Description
 */
public final class WebUtils {
    private WebUtils() {
    }
    /**
     * 当前请求
     */
    public static HttpServletRequest request() {
        return contextHolder() == null ? null : contextHolder().getRequest();
    }
    /**
     * 当前响应
     */
    public static HttpServletResponse response() {
        return contextHolder() == null ? null : contextHolder().getResponse();
    }
    /**
     * 当前session
     */
    public static HttpSession session() {
        return request() == null ? null : request().getSession();
    }
    /**
     * 当前ServletRequest
     */
    public static ServletRequestAttributes contextHolder() {
        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxAppletTools.java
New file
@@ -0,0 +1,123 @@
package com.ruoyi.system.utils.wx.tools;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.system.utils.wx.body.resp.AccessTokenRespBody;
import com.ruoyi.system.utils.wx.body.resp.Code2SessionRespBody;
import com.ruoyi.system.utils.wx.body.resq.Code2SessionResqBody;
import com.ruoyi.system.utils.wx.model.WeixinProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;
/**
 * @author xiaochen
 * @ClassName WxAppletTools
 * @Description
 * @date 2024-8-04 13:55
 */
@Slf4j
public class WxAppletTools {
    private final static String ACCESSTOKEN_CACHE_KEY = "accessToken";
    /**
     * 请求参数
     * 属性    类型    默认值    必填    说明
     * appid    string        是    小程序 appId
     * secret    string        是    小程序 appSecret
     * js_code    string        是    登录时获取的 code
     * grant_type    string        是    授权类型,此处只需填写 authorization_cod
     * <p>
     * 返回值:
     * <p>
     * 属性    类型    说明
     * openid    string    用户唯一标识
     * session_key    string    会话密钥
     * unionid    string    用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
     * errcode    number    错误码
     * errmsg    string    错误信息
     */
    private static final String JSCODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
    /**
     * 请求参数
     * 属性    类型    默认值    必填    说明
     * grant_type    string        是    填写 client_credential
     * appid    string        是    小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
     * secret    string        是    小程序唯一凭证密钥,即 AppSecret,获取方式同 appid
     * 返回值
     * Object
     * 返回的 JSON 数据包
     * <p>
     * 属性    类型    说明
     * access_token    string    获取到的凭证
     * expires_in    number    凭证有效时间,单位:秒。目前是7200秒之内的值。
     * errcode    number    错误码
     * errmsg    string    错误信息
     */
    public static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
    private WeixinProperties wxConfig;
    private RestTemplate wxRestTemplate;
    private RedisService redisService;
    public WxAppletTools(RestTemplate wxRestTemplate, WeixinProperties wxConfig, RedisService redisService) {
        this.wxRestTemplate = wxRestTemplate;
        this.wxConfig = wxConfig;
        this.redisService = redisService;
    }
    /**
     * 自定义部分数据
     *
     * @param wxConfig
     * @return
     */
    public WxAppletTools build(WeixinProperties wxConfig) {
        this.wxConfig = wxConfig;
        return this;
    }
    /**
     * @param resqBody
     * @return
     */
    public Code2SessionRespBody getOpenIdByJscode2session(Code2SessionResqBody resqBody) {
        long start = System.currentTimeMillis();
        String requestUrl = MessageFormat.format(JSCODE_2_SESSION_URL, wxConfig.getAppId(), wxConfig.getSecretId(), resqBody.getJsCode());
        long end = System.currentTimeMillis();
        log.info("code换取sessionKey时间:{}", (end - start));
        String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
        end = System.currentTimeMillis();
        log.info("code换取sessionKey时间:{}", (end - start));
        log.info("Jscode2session:{}", respBody);
        Code2SessionRespBody code2SessionRespBody = WxJsonUtils.parseObject(respBody, Code2SessionRespBody.class);
        // 判断有误异常
        if (StringUtils.hasLength(code2SessionRespBody.getErrorMsg())) {
            // 抛出错误
            throw new WxException(code2SessionRespBody.getErrorCode() + ":" + code2SessionRespBody.getErrorMsg());
        }
        return code2SessionRespBody;
    }
    /**
     * @return
     */
    public String getAccessToken(String version) {
        String accessToken = redisService.getCacheObject(ACCESSTOKEN_CACHE_KEY + version);
        if (StringUtils.hasLength(accessToken)) {
            return accessToken;
        }
        String requestUrl = MessageFormat.format(ACCESS_TOKEN_URL, wxConfig.getAppId(), wxConfig.getSecretId());
        String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
        AccessTokenRespBody accessTokenRespBody = WxJsonUtils.parseObject(respBody, AccessTokenRespBody.class);
        // 判断有误异常
        if (StringUtils.hasLength(accessTokenRespBody.getErrorMsg())) {
            // 抛出错误
            throw new WxException(accessTokenRespBody.getErrorCode() + ":" + accessTokenRespBody.getErrorMsg());
        }
        redisService.setCacheObject(ACCESSTOKEN_CACHE_KEY + version, accessTokenRespBody.getAccessToken(), 7200L, TimeUnit.SECONDS);
        return accessTokenRespBody.getAccessToken();
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxCache.java
New file
@@ -0,0 +1,117 @@
package com.ruoyi.system.utils.wx.tools;
import java.util.concurrent.TimeUnit;
/**
 * 缓存
 *
 * @author xiaochen
 */
class WxCache {
    /**
     * 缓存的初始化容量
     */
    private int initialCapacity = 50;
    /**
     * 缓存最大容量
     */
    private long maximumSize = 200L;
    /**
     * 缓存时长
     */
    private long duration = 7000L;
    /**
     * 时长单位,自动转换
     * 支持:
     * 时
     * 分
     * 秒
     * 天
     */
    private TimeUnit timeunit = TimeUnit.SECONDS;
    public int getInitialCapacity() {
        return initialCapacity;
    }
    public void setInitialCapacity(int initialCapacity) {
        this.initialCapacity = initialCapacity;
    }
    public long getMaximumSize() {
        return maximumSize;
    }
    public void setMaximumSize(long maximumSize) {
        this.maximumSize = maximumSize;
    }
    public long getDuration() {
        return duration;
    }
    public void setDuration(long duration) {
        this.duration = duration;
    }
    public TimeUnit getTimeunit() {
        return timeunit;
    }
    public void setTimeunit(TimeUnit timeunit) {
        this.timeunit = timeunit;
    }
    public static class Builder {
        private int initialCapacity;
        private long maximumSize;
        private long duration;
        private TimeUnit timeunit;
        public Builder setInitialCapacity(int initialCapacity) {
            this.initialCapacity = initialCapacity;
            return this;
        }
        public Builder setMaximumSize(long maximumSize) {
            this.maximumSize = maximumSize;
            return this;
        }
        public Builder setDuration(long duration) {
            this.duration = duration;
            return this;
        }
        public Builder setTimeUnit(TimeUnit timeunit) {
            this.timeunit = timeunit;
            return this;
        }
        public WxCache build() {
            return new WxCache(this);
        }
    }
    public static Builder options() {
        return new Builder();
    }
    private WxCache(Builder builder) {
        this.initialCapacity = 0 == builder.initialCapacity ? this.initialCapacity : builder.initialCapacity;
        this.maximumSize = 0L == builder.maximumSize ? this.maximumSize : builder.maximumSize;
        this.duration = 0L == builder.duration ? this.duration : builder.duration;
        this.timeunit = null == builder.timeunit ? this.timeunit : builder.timeunit;
    }
    @Override
    public String toString() {
        return "WxCache{" +
                "initialCapacity=" + initialCapacity +
                ", maximumSize=" + maximumSize +
                ", duration=" + duration +
                ", timeunit=" + timeunit +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxCacheTemplate.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.system.utils.wx.tools;
/**
 * @author xiaochen
 * @ClassName WxCacheTemplate
 * @Description
 * @date 2021-01-11 11:27
 */
public interface WxCacheTemplate<T> {
    /**
     * 保存key
     *
     * @param key
     * @param value
     * @return
     */
     boolean setKey(String key, T value);
    /**
     * 获取缓存
     *
     * @param key
     * @return
     */
    T getKey(String key);
    /**
     * 删除
     *
     * @param key
     * @return
     */
    boolean delKey(String key);
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxException.java
New file
@@ -0,0 +1,55 @@
package com.ruoyi.system.utils.wx.tools;
/**
 * @author lihen
 */
public class WxException extends RuntimeException {
    private final static int OK = 0;
    private final static int ValidateSignatureError = -40001;
    private final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    private final static int IllegalAesKey = -40004;
    private final static int ValidateAppidError = -40005;
    private final static int EncryptAESError = -40006;
    private final static int DecryptAESError = -40007;
    private final static int IllegalBuffer = -40008;
    private int code;
    private static String getMessage(int code) {
        switch (code) {
            case ValidateSignatureError:
                return "签名验证错误";
            case ParseXmlError:
                return "xml解析失败";
            case ComputeSignatureError:
                return "sha加密生成签名失败";
            case IllegalAesKey:
                return "SymmetricKey非法";
            case ValidateAppidError:
                return "appid校验失败";
            case EncryptAESError:
                return "aes加密失败";
            case DecryptAESError:
                return "aes解密失败";
            case IllegalBuffer:
                return "解密后得到的buffer非法";
            default:
                return null;
        }
    }
    public int getCode() {
        return code;
    }
    WxException(int code) {
        super(getMessage(code));
        this.code = code;
    }
    public WxException(String message) {
        super(message);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxJsonUtils.java
New file
@@ -0,0 +1,109 @@
package com.ruoyi.system.utils.wx.tools;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * Json转换工具类
 * 参考:https://blog.csdn.net/weixin_38413579/article/details/82562634
 * @author madman
 */
@Slf4j
public final class WxJsonUtils {
    public static final String dateFormat = "yyyy-MM-dd";
    public static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OM = new ObjectMapper();
    private static final JavaTimeModule timeModule = new JavaTimeModule();
    /**
     * 转换LocalDateTime
     */
    static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern(dateTimeFormat)));
        }
    }
    /**
     * 转换LocalDate
     */
    static class LocalDateSerializer extends JsonSerializer<LocalDate> {
        @Override
        public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDate.format(DateTimeFormatter.ofPattern(dateFormat)));
        }
    }
    /**
     * 设置 ObjectMapper
     *
     * @return
     */
    private static ObjectMapper getObjectMapper() {
        // 序列化
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
        // 反序列化
        timeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
        timeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)));
        // 允许对象忽略json中不存在的属性
        OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OM.registerModule(timeModule);
        return OM;
    }
    /**
     * 将对象序列化
     */
    public static <T> String toJsonString(T obj) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("转json字符串失败:{}", obj);
            return null;
        }
    }
    /**
     * 反序列化对象字符串
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化对象字符串失败");
        }
    }
    /**
     * 反序列化字符串成为对象
     */
    public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, valueTypeRef);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化字符串成为对象失败");
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
New file
@@ -0,0 +1,175 @@
package com.ruoyi.system.utils.wx.tools;
import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
import com.ruoyi.common.utils.sign.Base64;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.CharEncoding;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.Arrays;
/**
 * @Description 获取用户信息工具类
 * @Author xiaochen
 * @Date 2021/8/12 15:45
 */
@Slf4j
public class WxUtils {
    /**
     * 微信小程序API 用户数据的解密
     *
     * @param encryptedData
     * @param sessionKey
     * @param iv
     * @return
     */
    public static AppletUserDecodeData encryptedData(String encryptedData, String sessionKey, String iv) {
        // 被加密的数据
        byte[] dataByte = Base64.decode(encryptedData);
        // 加密秘钥
        byte[] keyByte = Base64.decode(sessionKey);
        // 偏移量
        byte[] ivByte = Base64.decode(iv);
        try {
            // 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }
            // 初始化
            Security.addProvider(new BouncyCastleProvider());
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));
            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, CharEncoding.UTF_8);
                log.info("解密原串:{}", result);
                return WxJsonUtils.parseObject(result, AppletUserDecodeData.class);
            }
            throw new RuntimeException("解密的数据为空");
        } catch (Exception e) {
            log.error("解密失败. error = {}", e.getMessage(), e);
            throw new RuntimeException(e.getMessage());
        }
    }
    /**
     * 微信小程序API 用户数据的签名验证
     * signature = sha1( rawData + session_key )
     *
     * @param rawData    不包括敏感信息的原始数据字符串,用于计算签名。
     * @param sessionKey
     */
    public static void verifySignature(String rawData, String sessionKey, String signature) {
        String serverSignature = SHA1.getSHA1(rawData + sessionKey);
        log.info(rawData + ">>>>>>:" + sessionKey + " === " + serverSignature + "  ======" + signature);
        if (!signature.equals(serverSignature)) {
            throw new RuntimeException("数据验签不通过");
        }
    }
    /**
     * 根据流接收请求数据
     *
     * @param request
     * @return
     */
    public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
        log.info("微信异步回调地址:{}", request.getRequestURL());
        StringBuffer buffer = new StringBuffer();
        InputStream inputStream = request.getInputStream();
        InputStreamReader reader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(reader);
        String body = null;
        while ((body = bufferedReader.readLine()) != null) {
            buffer.append(body);
        }
        String data = buffer.toString();
        reader.close();
        inputStream.close();
        log.info("微信异步回调数据:{}", data);
        return data;
    }
    /**
     * 日志
     *
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }
    /**
     * debug
     *
     * @param msg
     * @param args
     */
    public static void debug(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isDebugEnabled()) {
            log.debug(msg, args);
        }
    }
    /**
     * info
     *
     * @param msg
     * @param args
     */
    public static void info(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isInfoEnabled()) {
            log.info(msg, args);
        }
    }
    /**
     * warn
     *
     * @param msg
     * @param args
     */
    public static void warn(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isWarnEnabled()) {
            log.warn(msg, args);
        }
    }
    /**
     * error
     *
     * @param msg
     * @param args
     */
    public static void error(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isErrorEnabled()) {
            log.error(msg, args);
        }
    }
}