From 8d0f158dc3549bc0043383164443d83fd2adf587 Mon Sep 17 00:00:00 2001 From: RuicyWu <1063154311@qq.com> Date: Mon, 15 Jun 2026 14:51:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/chenyon/bill/BillController.java | 45 +++- .../org/chenyon/bill/BillQueryCondition.java | 18 ++ .../java/org/chenyon/bill/BillService.java | 28 +++ .../java/org/chenyon/constants/OaApiUrl.java | 12 ++ .../org/chenyon/contract/ContractService.java | 3 + .../org/chenyon/message/MessageService.java | 4 + .../java/org/chenyon/oa/OaBillService.java | 34 +++ .../java/org/chenyon/oa/OaCusService.java | 4 +- .../java/org/chenyon/oa/OaNoticeService.java | 16 +- .../org/chenyon/oa/OaReservationService.java | 4 + .../java/org/chenyon/pay/OrderService.java | 197 ++++++++++++------ .../org/chenyon/pay/PayOrderCheckTask.java | 63 ++++++ .../java/org/chenyon/pay/TmPayService.java | 45 +++- src/main/java/org/chenyon/pay/WeAppOrder.java | 11 +- .../java/org/chenyon/pay/WeAppOrderDao.java | 3 + .../java/org/chenyon/pay/WeAppOrderDao.xml | 8 +- .../java/org/chenyon/pay/WeAppSubOrder.java | 10 + .../org/chenyon/pay/WeAppSubOrderDao.java | 4 +- .../java/org/chenyon/pay/WeAppSubOrderDao.xml | 10 +- .../chenyon/pay/shopcar/TmPayOrderRespVo.java | 27 +++ .../org/chenyon/user/LoginController.java | 52 +++-- .../java/org/chenyon/user/UserService.java | 5 +- .../chenyon/wx/WeChatAccessTokenService.java | 7 +- .../java/org/chenyon/wx/WxUserSession.java | 10 + .../org/chenyon/wx/WxUserSessionService.java | 66 ++++-- src/main/scripts/startup.sh | 97 ++++++++- 26 files changed, 654 insertions(+), 129 deletions(-) create mode 100644 src/main/java/org/chenyon/pay/PayOrderCheckTask.java diff --git a/src/main/java/org/chenyon/bill/BillController.java b/src/main/java/org/chenyon/bill/BillController.java index e3ac995..7c0a9b8 100644 --- a/src/main/java/org/chenyon/bill/BillController.java +++ b/src/main/java/org/chenyon/bill/BillController.java @@ -70,6 +70,22 @@ public class BillController { return ResultMessage.success(new PageResult()); } + @PostMapping("/pageQueryUnpaidMargin") + @LoginCheck + public ResultMessage pageQueryUnpaidMargin(@RequestBody BillQueryCondition condition) { + try { + UserContext userContext = UserContext.get(); + if(userContext == null || userContext.getCusNo() == null) { + return ResultMessage.success(new PageResult()); + } + condition.setCusNo(userContext.getCusNo()); + return ResultMessage.success(billService.pageQueryUnpaidMargin(condition)); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return ResultMessage.success(new PageResult()); + } + @LoginCheck @GetMapping("/countUnpayRentBills") public ResultMessage countUnpayRentBills() { @@ -85,6 +101,21 @@ public class BillController { return ResultMessage.success(0); } + @LoginCheck + @GetMapping("/countUnpaidMargin") + public ResultMessage countUnpaidRentMargin() { + try { + UserContext userContext = UserContext.get(); + if(userContext == null || userContext.getCusNo() == null) { + return ResultMessage.success(0); + } + return ResultMessage.success(billService.countUnpaidRentMargin(userContext.getCusNo())); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return ResultMessage.success(0); + } + @LoginCheck @GetMapping("/countUnpayWaeBills") public ResultMessage countUnpayWaeBills() { @@ -115,8 +146,19 @@ public class BillController { return ResultMessage.success(orderService.createOrder(orderVo)); }catch (Exception e) { log.error(e.getMessage(),e); + return ResultMessage.error(e.getMessage()); } - return ResultMessage.success(0); + } + + @PostMapping("/paycallback") + public ResultMessage payCallBack(@RequestParam("orderId") String orderId,@RequestParam("payResult")String payResult) { + try { + orderService.tmPayCallback(orderId,payResult); + return ResultMessage.success(); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return ResultMessage.error(); } @LoginCheck @@ -133,4 +175,5 @@ public class BillController { } return ResultMessage.success(0); } + } diff --git a/src/main/java/org/chenyon/bill/BillQueryCondition.java b/src/main/java/org/chenyon/bill/BillQueryCondition.java index 083b47a..ab3912c 100644 --- a/src/main/java/org/chenyon/bill/BillQueryCondition.java +++ b/src/main/java/org/chenyon/bill/BillQueryCondition.java @@ -7,6 +7,8 @@ public class BillQueryCondition extends PageQueryRequest { private String cusNo; //客商编码 private String year; //年份 private String billStatus; //账单状态 + private String startDate; //账单开始日期 + private String endDate; //账单结束日期 public String getCusNo() { return cusNo; @@ -31,4 +33,20 @@ public class BillQueryCondition extends PageQueryRequest { public void setBillStatus(String billStatus) { this.billStatus = billStatus; } + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } } diff --git a/src/main/java/org/chenyon/bill/BillService.java b/src/main/java/org/chenyon/bill/BillService.java index 3972b67..d17602c 100644 --- a/src/main/java/org/chenyon/bill/BillService.java +++ b/src/main/java/org/chenyon/bill/BillService.java @@ -1,12 +1,16 @@ package org.chenyon.bill; +import org.apache.commons.lang3.StringUtils; import org.chenyon.oa.OaBillService; import org.chenyon.oa.bill.OaBillVo; import org.chenyon.oa.bill.OaPayRecordVo; import org.chenyon.pay.OrderService; import org.rcy.framework.api.entity.PageResult; +import org.rcy.framework.data.id.LongIdGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.IdGenerator; +import org.springframework.util.JdkIdGenerator; import java.util.HashMap; import java.util.Map; @@ -26,17 +30,37 @@ public class BillService { public PageResult pageQueryWaeBill(BillQueryCondition condition) throws Exception { return oaBillService.pageQueryWaeBill(condition); } + + public PageResult pageQueryUnpaidMargin(BillQueryCondition condition) throws Exception { + return oaBillService.pageQueryUnpaidMargin(condition); + } + public PageResult pageQueryContractPayRecord(PayRecordQueryConditon condition) throws Exception { return oaBillService.pageQueryPayRecord(condition); } public Integer countUnpayRentBills(String cusNo) throws Exception { + if(StringUtils.isBlank(cusNo)){ + return 0; + } Map params = new HashMap<>(); params.put("cusNo",cusNo); return oaBillService.countUnpayRentBills(params); } + public Integer countUnpaidRentMargin(String cusNo) throws Exception { + if(StringUtils.isBlank(cusNo)){ + return 0; + } + Map params = new HashMap<>(); + params.put("cusNo",cusNo); + return oaBillService.countUnpaidRentMargin(params); + } + public Integer countUnpayWaeBills(String cusNo) throws Exception { + if(StringUtils.isBlank(cusNo)){ + return 0; + } Map params = new HashMap<>(); params.put("cusNo",cusNo); return oaBillService.countUnpayWaeBills(params); @@ -48,4 +72,8 @@ public class BillService { params.put("bizType",bizType); return oaBillService.getBillPayReceiver(params); } + + public static void main(String[] args) { + System.out.println(LongIdGenerator.generate()); + } } diff --git a/src/main/java/org/chenyon/constants/OaApiUrl.java b/src/main/java/org/chenyon/constants/OaApiUrl.java index 9f4b5f6..ed197d8 100644 --- a/src/main/java/org/chenyon/constants/OaApiUrl.java +++ b/src/main/java/org/chenyon/constants/OaApiUrl.java @@ -61,6 +61,10 @@ public final class OaApiUrl { * 分页查询水电费账单 */ public static final String PAGE_QUERY_WAE_BILL = "/bill/waeBill/queryPage"; + /** + * 分页查询保证金账单 + */ + public static final String PAGE_QUERY_UNPAID_MARGIN = "/bill/unpaidMargin/queryPage"; /** * 分页查询缴费记录 */ @@ -69,10 +73,18 @@ public final class OaApiUrl { * 统计未缴租金账单数 */ public static final String COUNT_UNPAY_RENT_BILLS = "/bill/countUnpay/rent"; + /** + * 统计未缴租金账单数 + */ + public static final String COUNT_UNPAID_RENT_MARGIN = "/bill/countUnpay/margin"; /** * 获取账单收款方 */ public static final String BILL_PAY_RECEIVER = "/bill/payreceiver"; + /** + * 支付回调 + */ + public static final String PAY_CALLBACK = "/bill/pay/callback"; /** * 统计未缴水电费账单数 */ diff --git a/src/main/java/org/chenyon/contract/ContractService.java b/src/main/java/org/chenyon/contract/ContractService.java index 42fe190..7f8618a 100644 --- a/src/main/java/org/chenyon/contract/ContractService.java +++ b/src/main/java/org/chenyon/contract/ContractService.java @@ -117,6 +117,9 @@ public class ContractService { return assetsInfo; } public Integer countUnsign(String cusNo) throws Exception { + if(StringUtils.isBlank(cusNo)){ + return 0; + } Integer count = oaContractService.countUnsign(cusNo); return count; } diff --git a/src/main/java/org/chenyon/message/MessageService.java b/src/main/java/org/chenyon/message/MessageService.java index 4e1573d..a05a03d 100644 --- a/src/main/java/org/chenyon/message/MessageService.java +++ b/src/main/java/org/chenyon/message/MessageService.java @@ -1,5 +1,6 @@ package org.chenyon.message; +import org.apache.commons.lang3.StringUtils; import org.chenyon.wx.AesException; import org.rcy.framework.api.entity.PageResult; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +38,9 @@ public class MessageService { } public Integer countUnread(String cusNo){ + if(StringUtils.isBlank(cusNo)){ + return 0; + } MessageQueryCondition messageQueryCondition = new MessageQueryCondition(); messageQueryCondition.setMessageReceiver(cusNo); messageQueryCondition.setHasRead(false); diff --git a/src/main/java/org/chenyon/oa/OaBillService.java b/src/main/java/org/chenyon/oa/OaBillService.java index e139ed5..3529d42 100644 --- a/src/main/java/org/chenyon/oa/OaBillService.java +++ b/src/main/java/org/chenyon/oa/OaBillService.java @@ -11,6 +11,8 @@ import org.rcy.framework.utils.json.JsonUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,6 +42,26 @@ public class OaBillService { return pageResult; } + public PageResult pageQueryUnpaidMargin(BillQueryCondition condition) throws Exception { + OaResp oaResp = seeyonHttpClient.sendPost("分页查询保障金账单信息", OaApiUrl.PAGE_QUERY_UNPAID_MARGIN, JsonUtils.convertJson(condition)); + String data = oaResp.getDataStr(); + Map mapRoot = JsonUtils.parseObject(data, Map.class); + List oaBillVos = null; + Long totalCount = 0L; + if(mapRoot.get("totalCount") != null){ + totalCount = mapRoot.get("totalCount") instanceof Long ? (Long) mapRoot.get("totalCount") : Long.parseLong(mapRoot.get("totalCount") + ""); + } + if(mapRoot.get("datas") != null) { + oaBillVos = JsonUtils.parseArray(JsonUtils.convertJson(mapRoot.get("datas")), OaBillVo.class); + } + PageResult pageResult = new PageResult<>(); + pageResult.setPageNo(condition.getPageNo()); + pageResult.setPageSize(condition.getPageSize()); + pageResult.setRecordTotal(totalCount.intValue()); + pageResult.setResult(oaBillVos); + return pageResult; + } + public PageResult pageQueryWaeBill(BillQueryCondition condition) throws Exception { OaResp oaResp = seeyonHttpClient.sendPost("分页查询水电费账单信息", OaApiUrl.PAGE_QUERY_WAE_BILL, JsonUtils.convertJson(condition)); String data = oaResp.getDataStr(); @@ -86,6 +108,11 @@ public class OaBillService { return (Integer) oaResp.getData(); } + public Integer countUnpaidRentMargin(Map params) throws Exception { + OaResp oaResp = seeyonHttpClient.sendPost("统计未缴保证金账单数", OaApiUrl.COUNT_UNPAID_RENT_MARGIN, JsonUtils.convertJson(params)); + return (Integer) oaResp.getData(); + } + public Integer countUnpayWaeBills(Map params) throws Exception { OaResp oaResp = seeyonHttpClient.sendPost("统计未缴水电费账单数", OaApiUrl.COUNT_UNPAY_WAE_BILLS, JsonUtils.convertJson(params)); return (Integer) oaResp.getData(); @@ -96,4 +123,11 @@ public class OaBillService { return (String) oaResp.getData(); } + public void payCallBack(String billNo, String bizType, String payDate) throws Exception { + Map params = new HashMap<>(); + params.put("billNo",billNo); + params.put("bizType",bizType); + params.put("payDate",payDate); + OaResp oaResp = seeyonHttpClient.sendPost("支付回调", OaApiUrl.PAY_CALLBACK ,JsonUtils.convertJson(params)); + } } diff --git a/src/main/java/org/chenyon/oa/OaCusService.java b/src/main/java/org/chenyon/oa/OaCusService.java index f172170..5a45155 100644 --- a/src/main/java/org/chenyon/oa/OaCusService.java +++ b/src/main/java/org/chenyon/oa/OaCusService.java @@ -39,9 +39,9 @@ public class OaCusService { return null; } - public CusVo matchPersonCus(String phone) throws Exception { + public CusVo matchPersonCus(String cardNo) throws Exception { Map params = new HashMap<>(); - params.put("phone",phone); + params.put("cardNo",cardNo); OaResp oaResp = seeyonHttpClient.sendPost("匹配个人客户",OaApiUrl.MATCH_PERSON, params); Object data = oaResp.getData(); CusVo cusVo = new CusVo(); diff --git a/src/main/java/org/chenyon/oa/OaNoticeService.java b/src/main/java/org/chenyon/oa/OaNoticeService.java index 0dfa8cc..4a3fd44 100644 --- a/src/main/java/org/chenyon/oa/OaNoticeService.java +++ b/src/main/java/org/chenyon/oa/OaNoticeService.java @@ -11,6 +11,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,18 +29,27 @@ public class OaNoticeService { String data = oaResp.getDataStr(); Map mapRoot = JsonUtils.parseObject(data, Map.class); Long totalCount = 0L; - List vos = null; + List vos = null; if(mapRoot.get("totalCount") != null){ totalCount = mapRoot.get("totalCount") instanceof Long ? (Long) mapRoot.get("totalCount") : Long.parseLong(mapRoot.get("totalCount") + ""); } if(mapRoot.get("datas") != null) { - vos = JsonUtils.parseArray(JsonUtils.convertJson(mapRoot.get("datas")), NoticeVo.class); + vos = JsonUtils.parseArray(JsonUtils.convertJson(mapRoot.get("datas")), OaNoticeVo.class); } + List noticeVos = new ArrayList<>(); PageResult pageResult = new PageResult<>(); + if(vos == null) { + return pageResult; + } + for (OaNoticeVo vo : vos) { + NoticeVo noticeVo = new NoticeVo(); + BeanUtils.copyProperties(vo, noticeVo); + noticeVos.add(noticeVo); + } pageResult.setPageNo(condition.getPageNo()); pageResult.setPageSize(condition.getPageSize()); pageResult.setRecordTotal(totalCount.intValue()); - pageResult.setResult(vos); + pageResult.setResult(noticeVos); return pageResult; } diff --git a/src/main/java/org/chenyon/oa/OaReservationService.java b/src/main/java/org/chenyon/oa/OaReservationService.java index b8464d4..157ac8f 100644 --- a/src/main/java/org/chenyon/oa/OaReservationService.java +++ b/src/main/java/org/chenyon/oa/OaReservationService.java @@ -1,5 +1,6 @@ package org.chenyon.oa; +import org.apache.commons.lang3.StringUtils; import org.chenyon.constants.OaApiUrl; import org.chenyon.fallback.FallbackSubmitVo; import org.chenyon.reservation.ReservationQueryCondition; @@ -41,6 +42,9 @@ public class OaReservationService { } public Integer countHandling(String openId) throws Exception { + if(StringUtils.isBlank(openId)){ + return 0; + } Map params = new HashMap<>(); params.put("openId",openId); OaResp oaResp = seeyonHttpClient.sendPost("查询处理中预约", OaApiUrl.RESERVATION_COUNT_HANDLING,JsonUtils.convertJson(params)); diff --git a/src/main/java/org/chenyon/pay/OrderService.java b/src/main/java/org/chenyon/pay/OrderService.java index 30a7c0b..2dd97db 100644 --- a/src/main/java/org/chenyon/pay/OrderService.java +++ b/src/main/java/org/chenyon/pay/OrderService.java @@ -9,12 +9,15 @@ import org.chenyon.pay.shopcar.TmPayOrderRespVo; import org.chenyon.pay.shopcar.VaFld; import org.chenyon.user.UserService; import org.rcy.framework.data.id.LongIdGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; + import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; @@ -22,6 +25,8 @@ import java.util.*; @Service public class OrderService { + private static final Logger log = LoggerFactory.getLogger(OrderService.class); + @Autowired private WeAppOrderDao weAppOrderDao; @Autowired @@ -31,8 +36,10 @@ public class OrderService { private String mid; @Value("${chinaums.tid}") private String tid; - @Value("${chinaums.sourceNo}") + @Value("${chinaums.systemNo}") private String sourceNo; + @Value("${chinaums.sysSource}") + private String sysSource; @Value("${wx.appId}") private String miniProgramAppId; @@ -49,94 +56,113 @@ public class OrderService { private String fwglMid; @Value("${wx.appId}") private String wxAppId; - @Value("${chinaums.pay.notifyUrl}") - private String notifyUrl; + @Value("${cdn.domain}") + private String domain; + private DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - /** - * 批量创建支付订单 - * 【核心】批量前 校验所有 billNo 是否已经存在子订单 - * 子订单唯一键:billNo - */ @Transactional(rollbackFor = Exception.class) public TmPayOrderRespVo createOrder(OrderVo orderVo) throws Exception { - // 1. 基础入参校验 + // 1. 基础参数校验 if (orderVo == null || orderVo.getBillVoList() == null || orderVo.getBillVoList().isEmpty()) { throw new Exception("批量支付账单不能为空"); } if (StringUtils.isEmpty(orderVo.getPayerOpenId())) { throw new Exception("支付人OPENID不能为空"); } - List billVoList = orderVo.getBillVoList(); - // ====================================================================== - // 【批量支付 最强幂等校验】 - // 遍历所有子账单,根据 billNo 查询是否已存在支付记录 - // 只要有一个已存在 → 整批拒绝支付 + // 【第1步:最前置全局幂等锁单校验(核心防重,在所有操作之前!)】 + // 只要任意账单存在 【支付中未完成】订单,直接拒绝,后续所有流程全部不执行 + // 不创建本地订单、不调用银联、不生成支付单,从源头杜绝重复支付 // ====================================================================== for (UnpaidBillVo billVo : billVoList) { String billNo = billVo.getBillNo(); - // 子订单唯一键:billNo WeAppSubOrder existSubOrder = weAppSubOrderDao.selectByBillNo(billNo); - if (existSubOrder != null) { - throw new Exception("批量支付失败:账单号【" + billNo + "】已存在支付记录,禁止重复支付"); + if (existSubOrder == null) { + continue; + } + + // 规则1:账单已支付成功,永久禁止支付 + if ("1".equals(existSubOrder.getPayStatus())) { + throw new Exception("账单【" + billNo + "】已支付完成,无法再次支付"); + } + + // 规则2:子订单绑定的旧主订单 处于【待支付/支付中】,直接拒绝本次请求 + Long oldMainOrderId = existSubOrder.getMainOrderId(); + WeAppOrder oldMainOrder = weAppOrderDao.get(oldMainOrderId); + if (oldMainOrder != null && "0".equals(oldMainOrder.getPayStatus())) { + throw new Exception("账单【" + billNo + "】存在进行中的支付订单,请等待支付结果或支付超时关闭后重试"); } } - // 2. 生成主订单ID - Long mainOrderId = LongIdGenerator.generate(); - String bizId = mainOrderId.toString(); + // ====================================================================== + // 【第2步:全部校验通过,生成全新主订单】 + // ====================================================================== + Long newMainOrderId = LongIdGenerator.generate(); + String bizId = String.valueOf(newMainOrderId); orderVo.setBizId(bizId); - // 3. 主订单幂等校验 - WeAppOrder existMainOrder = weAppOrderDao.findByBizId(bizId); - if (existMainOrder != null) { - TmPayOrderRespVo resp = new TmPayOrderRespVo(); -// resp.setOrderNo(existMainOrder.getTmOrderId()); -// resp.setAmount(existMainOrder.getAmount()); - return resp; - } - - // 4. 生成第三方支付订单号 + // 生成银联唯一支付单号 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); int random = 1000000 + new Random().nextInt(9000000); - String tmOrderNo = sourceNo + sdf.format(new Date()) + random; + String tmOrderNo = "14BF" + sdf.format(new Date()) + random; - // 第三方单号唯一兜底 - WeAppOrder orderByTmNo = weAppOrderDao.selectByTmOrderId(tmOrderNo); - if (orderByTmNo != null) { + // 银联单号唯一性校验 + if (weAppOrderDao.selectByTmOrderId(tmOrderNo) != null) { throw new Exception("支付单号重复,请重新提交"); } - // 5. 调用支付创建分账订单 - ShopCarOrderCreateRequest request = buildShopCarOrderCreateRequest(orderVo, tmOrderNo); - TmPayOrderRespVo payResp = tmPayService.weChatPayCreateSplitOrder(request); - - // 6. 批量插入子订单 + 修复 BigDecimal 金额BUG + // ====================================================================== + // 【第3步:本地子订单、主订单全部先落库(数据先行锁定)】 + // 不存在则新增,旧单已关闭则更新绑定最新主订单 + // ====================================================================== BigDecimal totalAmount = BigDecimal.ZERO; for (UnpaidBillVo billVo : billVoList) { - WeAppSubOrder subOrder = new WeAppSubOrder(); - subOrder.setMainOrderId(mainOrderId); - subOrder.setBillNo(billVo.getBillNo()); // 唯一键 - subOrder.setAmount(billVo.getAmount()); - subOrder.setPayStatus("0"); - weAppSubOrderDao.insert(subOrder); + String billNo = billVo.getBillNo(); + WeAppSubOrder existSub = weAppSubOrderDao.selectByBillNo(billNo); + + if (existSub == null) { + // 子订单首次不存在:新增,绑定本次新主订单 + WeAppSubOrder subOrder = new WeAppSubOrder(); + subOrder.setMainOrderId(newMainOrderId); + subOrder.setBillNo(billNo); + subOrder.setAmount(billVo.getAmount()); + subOrder.setBizSubType(billVo.getBizType()); + subOrder.setPayStatus("0"); + subOrder.setCreateTime(new Date()); + weAppSubOrderDao.insert(subOrder); + } else { + // 子订单已存在:走到这里已经经过前置校验 + // 旧主订单一定是【失败/关闭】状态,安全更新绑定最新主订单 + existSub.setMainOrderId(newMainOrderId); + existSub.setPayStatus("0"); // 重置为待支付状态 + weAppSubOrderDao.updateByPrimaryKeySelective(existSub); + } - // 正确累加金额(修复原BUG) totalAmount = totalAmount.add(new BigDecimal(billVo.getAmount())); } - // 7. 插入主订单 + // 插入全新主订单(本地数据全部落库完成) WeAppOrder weAppOrder = new WeAppOrder(); - weAppOrder.setId(mainOrderId); + weAppOrder.setId(newMainOrderId); weAppOrder.setBizId(bizId); weAppOrder.setPayerId(orderVo.getPayer()); weAppOrder.setPayerName(orderVo.getPayerName()); weAppOrder.setTmOrderId(tmOrderNo); weAppOrder.setAmount(totalAmount.toString()); - weAppOrder.setPayStatus("0"); + weAppOrder.setPayStatus("0"); // 待支付(支付中) + weAppOrder.setCreateTime(new Date()); + weAppOrder.setPayerOpenId(orderVo.getPayerOpenId()); weAppOrderDao.insert(weAppOrder); + // ====================================================================== + // 【第4步:本地所有订单数据全部安全落库后,最后才调用银联创建支付单】 + // 顺序彻底修正!业务数据优先,第三方支付最后执行 + // ====================================================================== + ShopCarOrderCreateRequest request = buildShopCarOrderCreateRequest(orderVo, tmOrderNo); + TmPayOrderRespVo payResp = tmPayService.weChatPayCreateSplitOrder(request); + + payResp.setMainOrderId(newMainOrderId); return payResp; } @@ -150,10 +176,11 @@ public class OrderService { request.setSubAppId(wxAppId); request.setMchntOrderId(orderNo); request.setMsgType("wx.unifiedOrder"); + String notifyUrl = domain + "/api" + "/bill/paycallback" + "?orderId=" + orderVo.getBizId() + "&" + "payResult=SUCCESS"; request.setNotifyUrl(notifyUrl); request.setTradeType("MINI"); request.setSubOpenId(orderVo.getPayerOpenId()); - request.setSysSource("OPT"); + request.setSysSource(sysSource); VaFld vaFld = new VaFld(); vaFld.setNewShopFlag("GWCXS"); @@ -169,6 +196,9 @@ public class OrderService { param.put("billNo", billVo.getBillNo()); param.put("bizType", billVo.getBizType()); String receiver = oaBillService.getBillPayReceiver(param); + if(StringUtils.isBlank(receiver) || "null".equals(receiver)){ + throw new RuntimeException("获取账单收款方失败,请联系管理员"); + } billVo.setPayee(receiver); int amountCent = (int) (Double.parseDouble(billVo.getAmount()) * 100); @@ -184,14 +214,12 @@ public class OrderService { if (gyAmount.compareTo(BigDecimal.ZERO) > 0) { SatInfo info = new SatInfo(); info.setMerId(gyMid); - info.setFeeBear("Y"); info.setSatAmt(gyAmount.toString()); satInfoList.add(info); } if (fwglAmount.compareTo(BigDecimal.ZERO) > 0) { SatInfo info = new SatInfo(); info.setMerId(fwglMid); - info.setFeeBear("Y"); info.setSatAmt(fwglAmount.toString()); satInfoList.add(info); } @@ -202,39 +230,70 @@ public class OrderService { } /** - * 支付回调(幂等 + 批量更新子订单) + * 最终版支付回调(100%幂等 + 无并发问题 + 安全) */ @Transactional(rollbackFor = Exception.class) - public void tmPayCallback(String tmOrderNo, String payResult) { - WeAppOrder order = weAppOrderDao.selectByTmOrderId(tmOrderNo); - if (order == null || "1".equals(order.getPayStatus())) { + public void tmPayCallback(String mainOrderId, String payResult) { + // 1. 查询主订单 + WeAppOrder order = weAppOrderDao.findByBizId(mainOrderId); + if (order == null) { + log.warn("[回调] 主订单不存在,bizId:{}", mainOrderId); return; } + // ==================== 【核心幂等】 ==================== + // 已支付 -> 直接返回,绝对不重复处理 + // ====================================================== + if ("1".equals(order.getPayStatus())) { + log.info("[回调幂等] 订单已支付,无需重复处理,bizId:{}", mainOrderId); + return; + } + + // ==================== 支付成功 ==================== if ("SUCCESS".equalsIgnoreCase(payResult)) { + // 更新主订单状态 order.setPayStatus("1"); + order.setPayDate(new Date()); weAppOrderDao.updateByPrimaryKeySelective(order); - // 批量更新该主订单下 所有子订单(billNo)状态 + // 查询该主订单下的所有子订单(只会查一次,因为子订单终身唯一) List subList = weAppSubOrderDao.selectByMainOrderId(order.getId()); + for (WeAppSubOrder sub : subList) { - sub.setPayStatus("1"); - weAppSubOrderDao.updateById(sub); + // ================== 子订单幂等 ================== + if ("1".equals(sub.getPayStatus())) { + continue; + } + + try { + // 更新子订单为已支付 + sub.setPayStatus("1"); + weAppSubOrderDao.updateByPrimaryKeySelective(sub); + String payDate = df.format(order.getPayDate() != null ? order.getPayDate() : new Date()); + try { + TmPayOrderRespVo tmPayOrderRespVo = tmPayService.queryOrder(mid, tid, order.getTmOrderId()); + if (tmPayOrderRespVo != null && tmPayOrderRespVo.getPayTime() != null) { + payDate = tmPayOrderRespVo.getPayTime(); + } + } catch (Exception ex) { + log.warn("[回调] 查询银联支付时间失败,使用本地时间,账单号:{}", sub.getBillNo(), ex); + } + // 通知OA(失败只记录,不影响支付状态) + oaBillService.payCallBack(sub.getBillNo(), sub.getBizSubType(), payDate); + log.info("[回调] OA通知成功,账单号:{}", sub.getBillNo()); + + } catch (Exception e) { + log.error("[回调] OA通知失败,账单号:{}", sub.getBillNo(), e); + } } + + log.info("[回调] 主订单支付成功处理完成,bizId:{}", mainOrderId); } else { + // ================== 支付失败 ================== order.setPayStatus("2"); weAppOrderDao.updateByPrimaryKeySelective(order); + log.info("[回调] 主订单支付失败,bizId:{}", mainOrderId); } } - public void callBack(String bizId) { - WeAppOrder order = weAppOrderDao.findByBizId(bizId); - if (order != null) { - tmPayCallback(order.getTmOrderId(), "SUCCESS"); - } - } - - private UnifiedOrderRequest buildUnifiedOrderRequest(OrderVo orderVo) { - return null; - } } diff --git a/src/main/java/org/chenyon/pay/PayOrderCheckTask.java b/src/main/java/org/chenyon/pay/PayOrderCheckTask.java new file mode 100644 index 0000000..13f4148 --- /dev/null +++ b/src/main/java/org/chenyon/pay/PayOrderCheckTask.java @@ -0,0 +1,63 @@ +package org.chenyon.pay; + +import org.chenyon.pay.shopcar.TmPayOrderRespVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import java.util.List; + +@Component +public class PayOrderCheckTask { + + private static final Logger log = LoggerFactory.getLogger(PayOrderCheckTask.class); + + @Autowired + private WeAppOrderDao weAppOrderDao; + + @Autowired + private TmPayService tmPayService; + + @Autowired + private OrderService orderService; + @Value("${chinaums.mid}") + private String mid; + @Value("${chinaums.tid}") + private String tid; + + /** + * 每 5 分钟执行一次:查询支付中订单,修正状态 + */ + @Scheduled(cron = "0 0/5 * * * ?") + public void checkUnpaidOrder() { + // 查询 超过 5 分钟 还在支付中的订单 + List orderList = weAppOrderDao.selectTimeoutUnpaidOrder(); + + for (WeAppOrder order : orderList) { + try { + // 1. 调用银联【查询订单状态接口】 + String tmOrderId = order.getTmOrderId(); + TmPayOrderRespVo queryResult = tmPayService.queryOrder(mid, tid, tmOrderId); + if (queryResult == null) { + log.warn("[定时补偿] 银联查询返回空,主订单bizId:{}, tmOrderId:{}", order.getBizId(), tmOrderId); + continue; + } + String status = queryResult.getStatus(); + // 2. 根据银联返回结果,修正本地订单 + if ("TRADE_SUCCESS".equals(status)) { + // 支付成功 → 执行成功回调逻辑(核销、入账、修改状态) + orderService.tmPayCallback(order.getBizId(), "SUCCESS"); + } else if ("TRADE_CLOSED".equals(status)) { + // 支付失败 / 已关闭 → 修改订单状态 + orderService.tmPayCallback(order.getBizId(), "FAIL"); + } + + } catch (Exception e) { + // 异常不影响其他订单 + log.error("[定时补偿] 处理订单异常,bizId:{}", order.getBizId(), e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/chenyon/pay/TmPayService.java b/src/main/java/org/chenyon/pay/TmPayService.java index 9bda417..201672f 100644 --- a/src/main/java/org/chenyon/pay/TmPayService.java +++ b/src/main/java/org/chenyon/pay/TmPayService.java @@ -4,16 +4,22 @@ package org.chenyon.pay; import org.chenyon.pay.shopcar.MiniPayRequest; import org.chenyon.pay.shopcar.ShopCarOrderCreateRequest; import org.chenyon.pay.shopcar.TmPayOrderRespVo; +import org.chenyon.pay.shopcar.VaFld; import org.rcy.framework.utils.json.JsonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.HashMap; import java.util.Map; @Service public class TmPayService { + private static final Logger log = LoggerFactory.getLogger(TmPayService.class); + @Autowired private TmHttpUtil tmHttpUtil; @@ -37,17 +43,50 @@ public class TmPayService { } } - public TmPayOrderRespVo weChatPayCreateSplitOrder(ShopCarOrderCreateRequest request) { + public TmPayOrderRespVo queryOrder(String vaMchntNo,String vaTermNo,String tmOrderNo) { + //支付结果查询 + String url = "/v1/inip/upsp/shop/mini/pay-query"; + try { + Map params = new HashMap<>(); + params.put("mchntOrderId",tmOrderNo); + params.put("vaMchntNo",vaMchntNo); + params.put("vaTermNo",vaTermNo); + VaFld vaFld = new VaFld(); + vaFld.setNewShopFlag(""); + params.put("vaFld",vaFld); + String paramStr = JsonUtils.convertJson(params); + log.info("查询银联支付结果参数为: {}",paramStr ); + String respStr = tmHttpUtil.post(url, paramStr, TmHttpUtil.AuthMode.SIGNATURE); + Map map = JsonUtils.parseObject(respStr, Map.class); + String code = (String)map.get("respCode"); + if(!"0000".equals(code)){ + throw new RuntimeException("查询失败"); + } + String payTime = (String)map.get("payTime"); + String status = (String)map.get("status"); + TmPayOrderRespVo tmPayOrderRespVo = new TmPayOrderRespVo(); + tmPayOrderRespVo.setPayTime(payTime); + tmPayOrderRespVo.setStatus(status); + return tmPayOrderRespVo; + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return null; + } + + public TmPayOrderRespVo weChatPayCreateSplitOrder(ShopCarOrderCreateRequest request) { String url = "/v1/inip/upsp/shop/mini/pay"; try { - String respStr = tmHttpUtil.post(url, JsonUtils.convertJson(request), TmHttpUtil.AuthMode.SIGNATURE); + String paramStr = JsonUtils.convertJson(request); + log.info("创建银联支付订单参数为: {}",paramStr ); + String respStr = tmHttpUtil.post(url, paramStr, TmHttpUtil.AuthMode.SIGNATURE); Map map = JsonUtils.parseObject(respStr, Map.class); String code = (String)map.get("respCode"); if(!"0000".equals(code)){ throw new RuntimeException("创建订单失败"); } String forwardUrl = (String)map.get("url"); - Map miniRequestMap = (Map) map.get("miniPayRequest"); + Map miniRequestMap = JsonUtils.parseObject((String)map.get("miniPayRequest"), Map.class); MiniPayRequest miniPayRequest = new MiniPayRequest(); miniPayRequest.setAppId(miniRequestMap.get("appId")); miniPayRequest.setNonceStr(miniRequestMap.get("nonceStr")); diff --git a/src/main/java/org/chenyon/pay/WeAppOrder.java b/src/main/java/org/chenyon/pay/WeAppOrder.java index f8efd77..c41fc45 100644 --- a/src/main/java/org/chenyon/pay/WeAppOrder.java +++ b/src/main/java/org/chenyon/pay/WeAppOrder.java @@ -7,7 +7,7 @@ import javax.persistence.Table; import java.util.Date; @Entity -@Table(name = "ORDER") +@Table(name = "WEAPP_ORDER") public class WeAppOrder extends BaseEntity { private String bizId; @@ -18,6 +18,7 @@ public class WeAppOrder extends BaseEntity { private String payStatus; //支付状态 private String payerOpenId; // private String tmOrderId; //银联支付订单号 + private Date createTime; public String getBizId() { @@ -83,4 +84,12 @@ public class WeAppOrder extends BaseEntity { public void setTmOrderId(String tmOrderId) { this.tmOrderId = tmOrderId; } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } } diff --git a/src/main/java/org/chenyon/pay/WeAppOrderDao.java b/src/main/java/org/chenyon/pay/WeAppOrderDao.java index ca41ebe..69054eb 100644 --- a/src/main/java/org/chenyon/pay/WeAppOrderDao.java +++ b/src/main/java/org/chenyon/pay/WeAppOrderDao.java @@ -2,7 +2,10 @@ package org.chenyon.pay; import org.rcy.framework.data.dao.BaseDao; +import java.util.List; + public interface WeAppOrderDao extends BaseDao { WeAppOrder findByBizId(String bizId); WeAppOrder selectByTmOrderId(String tmOrderNo); + List selectTimeoutUnpaidOrder(); } diff --git a/src/main/java/org/chenyon/pay/WeAppOrderDao.xml b/src/main/java/org/chenyon/pay/WeAppOrderDao.xml index ebac760..cc8cc20 100644 --- a/src/main/java/org/chenyon/pay/WeAppOrderDao.xml +++ b/src/main/java/org/chenyon/pay/WeAppOrderDao.xml @@ -4,10 +4,14 @@ + \ No newline at end of file diff --git a/src/main/java/org/chenyon/pay/WeAppSubOrder.java b/src/main/java/org/chenyon/pay/WeAppSubOrder.java index 3c1aaf5..6815321 100644 --- a/src/main/java/org/chenyon/pay/WeAppSubOrder.java +++ b/src/main/java/org/chenyon/pay/WeAppSubOrder.java @@ -4,6 +4,7 @@ import org.rcy.framework.api.entity.BaseEntity; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.Date; @Entity @Table(name = "SUB_ORDER") @@ -13,6 +14,7 @@ public class WeAppSubOrder extends BaseEntity { private String amount; private Long mainOrderId; private String payStatus; + private Date createTime; public String getBillNo() { return billNo; @@ -53,4 +55,12 @@ public class WeAppSubOrder extends BaseEntity { public void setPayStatus(String payStatus) { this.payStatus = payStatus; } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } } diff --git a/src/main/java/org/chenyon/pay/WeAppSubOrderDao.java b/src/main/java/org/chenyon/pay/WeAppSubOrderDao.java index cd94c58..ac09054 100644 --- a/src/main/java/org/chenyon/pay/WeAppSubOrderDao.java +++ b/src/main/java/org/chenyon/pay/WeAppSubOrderDao.java @@ -8,8 +8,8 @@ public interface WeAppSubOrderDao extends BaseDao { WeAppSubOrder selectByBillNo(String billNo); + WeAppSubOrder selectByBillNoForUpdate(String billNo); + List selectByMainOrderId(Long mainOrderId); - Integer updateById(WeAppSubOrder subOrder); - } diff --git a/src/main/java/org/chenyon/pay/WeAppSubOrderDao.xml b/src/main/java/org/chenyon/pay/WeAppSubOrderDao.xml index 4893a5f..58d2852 100644 --- a/src/main/java/org/chenyon/pay/WeAppSubOrderDao.xml +++ b/src/main/java/org/chenyon/pay/WeAppSubOrderDao.xml @@ -3,5 +3,13 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - + + + \ No newline at end of file diff --git a/src/main/java/org/chenyon/pay/shopcar/TmPayOrderRespVo.java b/src/main/java/org/chenyon/pay/shopcar/TmPayOrderRespVo.java index 90df7e3..3c6474a 100644 --- a/src/main/java/org/chenyon/pay/shopcar/TmPayOrderRespVo.java +++ b/src/main/java/org/chenyon/pay/shopcar/TmPayOrderRespVo.java @@ -4,6 +4,9 @@ public class TmPayOrderRespVo { private String url; private MiniPayRequest miniPayRequest; + private Long mainOrderId; + private String status; + private String payTime; public String getUrl() { return url; @@ -20,4 +23,28 @@ public class TmPayOrderRespVo { public void setMiniPayRequest(MiniPayRequest miniPayRequest) { this.miniPayRequest = miniPayRequest; } + + public Long getMainOrderId() { + return mainOrderId; + } + + public void setMainOrderId(Long mainOrderId) { + this.mainOrderId = mainOrderId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getPayTime() { + return payTime; + } + + public void setPayTime(String payTime) { + this.payTime = payTime; + } } diff --git a/src/main/java/org/chenyon/user/LoginController.java b/src/main/java/org/chenyon/user/LoginController.java index 29c587b..4818716 100644 --- a/src/main/java/org/chenyon/user/LoginController.java +++ b/src/main/java/org/chenyon/user/LoginController.java @@ -38,30 +38,54 @@ public class LoginController { @PostMapping("/weChatLogin") public ResultMessage weChatLogin(@RequestBody LoginVo loginVo) throws Exception{ - String accessToken = weChatAccessTokenService.getAccessToken(); + // 1. 通过 code 获取微信 session_key + openid WeChatAuthInfo sessionInfo = weChatApi.getSessionInfo(loginVo.getLoginCode()); + + // 2. 生成第三方会话 key + String thirdSessionKey = UUID.randomUUID().toString().replace("-", ""); + + // 3. 构建 session 对象(核心:设置2小时过期) WxUserSession wxUserSession = new WxUserSession(); wxUserSession.setSessionKey(sessionInfo.getSession_key()); wxUserSession.setOpenId(sessionInfo.getOpenid()); wxUserSession.setUnionid(sessionInfo.getUnionid()); - String thirdSessionKey = UUID.randomUUID().toString().replace("-", ""); wxUserSession.setThirdSession(thirdSessionKey); - String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken; - HttpResponse httpResponse = HttpRequestUtils.sendPost(url, "{\"code\":\"" + loginVo.getPhoneGetCode() + "\"}", null); - String respStr = httpResponse.getRespStr(); - Map respMap = JsonUtils.parseObject(respStr, Map.class); - Map phoneInfo = (Map) respMap.get("phone_info"); - User user = new User(); - if (phoneInfo != null) { - String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); + + // ===================== 关键修复 ===================== + // 过期时间:当前时间 + 2小时(微信session_key标准有效期) + wxUserSession.setExpiresAt(System.currentTimeMillis() + 7200 * 1000); + // ==================================================== + + // 4. 获取用户手机号(安全获取,带异常处理) + String accessToken = weChatAccessTokenService.getAccessToken(); + String phoneNumber = null; + try { + String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken; + HttpResponse httpResponse = HttpRequestUtils.sendPost(url, "{\"code\":\"" + loginVo.getPhoneGetCode() + "\"}", null); + String respStr = httpResponse.getRespStr(); + Map respMap = JsonUtils.parseObject(respStr, Map.class); + Map phoneInfo = (Map) respMap.get("phone_info"); + if (phoneInfo != null) { + phoneNumber = (String) phoneInfo.get("purePhoneNumber"); + } + } catch (Exception e) { + log.error("获取微信手机号失败", e); + } + + // 5. 保存/更新用户 + if (phoneNumber != null) { + User user = new User(); user.setOpenId(sessionInfo.getOpenid()); user.setPhone(phoneNumber); user.setUserType(loginVo.getLoginType()); userService.upsert(user); } - wxUserSessionService.upsertWxUserSession(wxUserSession); - return ResultMessage.success(wxUserSession.getThirdSession()); + // 6. 保存会话(你原来的 upsert 会自动更新) + wxUserSessionService.upsertWxUserSession(wxUserSession); + + // 7. 返回 thirdSession 给前端 + return ResultMessage.success(wxUserSession.getThirdSession()); } @GetMapping("/checkExpiration") @@ -103,9 +127,9 @@ public class LoginController { public ResultMessage updateVerifyCode(HttpServletRequest request,@RequestBody AuthInfoVo authInfoVo) throws Exception { String token = request.getHeader("WT"); WxUserSession wxUserSession = wxUserSessionService.getSessionInfoByThirdSession(token); - userService.updateAuthInfo(wxUserSession.getOpenId(), + boolean authSuccess = userService.updateAuthInfo(wxUserSession.getOpenId(), authInfoVo.getUserType(), "0".equals(authInfoVo.getUserType()) ? RSAUtil.decrypt(authInfoVo.getCode(),rasPrivateKey) : authInfoVo.getCode()); - return ResultMessage.success(); + return ResultMessage.success(authSuccess); } } diff --git a/src/main/java/org/chenyon/user/UserService.java b/src/main/java/org/chenyon/user/UserService.java index 4c98b39..7bc6cf3 100644 --- a/src/main/java/org/chenyon/user/UserService.java +++ b/src/main/java/org/chenyon/user/UserService.java @@ -68,7 +68,7 @@ public class UserService { return userVo; } - public void updateAuthInfo(String openId,String userType,String verifyCode) throws Exception { + public boolean updateAuthInfo(String openId,String userType,String verifyCode) throws Exception { //调用oa接口客商信息 User user = userDao.findByOpenIdAndUserType(openId, userType); if(user == null) { @@ -76,7 +76,7 @@ public class UserService { } String cusNo = null; if("0".equals(userType)) { - CusVo cusVo = oaCusService.matchPersonCus(user.getPhone()); + CusVo cusVo = oaCusService.matchPersonCus(verifyCode); cusNo = cusVo == null ? null : cusVo.getCusNo(); user.setCardNo(AESUtils.encrypt(verifyCode,"abc123")); }else if("1".equals(user.getUserType())) { @@ -89,5 +89,6 @@ public class UserService { user.setOaAuth(true); userDao.updateByPrimaryKeySelective(user); } + return cusNo != null; } } diff --git a/src/main/java/org/chenyon/wx/WeChatAccessTokenService.java b/src/main/java/org/chenyon/wx/WeChatAccessTokenService.java index 0f7bd3b..de4b7e9 100644 --- a/src/main/java/org/chenyon/wx/WeChatAccessTokenService.java +++ b/src/main/java/org/chenyon/wx/WeChatAccessTokenService.java @@ -53,14 +53,17 @@ public class WeChatAccessTokenService { HttpResponse response = HttpRequestUtils.sendGet(url, headers); Map respMap = JsonUtils.parseObject(response.getRespStr(), Map.class); if (respMap.get("errcode") != null) { - throw new IOException("获取 access_token 请求失败: " + respMap.get("errmsg")); + log.error("获取access_token失败: {}", respMap); + return null; } String tokenStr = respMap.get("access_token") + ""; long ttl = Long.parseLong(respMap.get("expires_in") + ""); + // 提前 100 秒过期,更安全 WxTokenCache tokenCache = new WxTokenCache(); tokenCache.setToken(tokenStr); - tokenCache.setExpireTime(now + (ttl - 20000)); + tokenCache.setExpireTime(now + (ttl - 100) * 1000); cache.put(appId, tokenCache); + return tokenStr; }catch (Exception e){ log.error(e.getMessage(),e); diff --git a/src/main/java/org/chenyon/wx/WxUserSession.java b/src/main/java/org/chenyon/wx/WxUserSession.java index 9166817..dc3fbfa 100644 --- a/src/main/java/org/chenyon/wx/WxUserSession.java +++ b/src/main/java/org/chenyon/wx/WxUserSession.java @@ -22,6 +22,8 @@ public class WxUserSession extends BaseEntity { private String unionid; + private Long expiresAt; // 会话过期时间戳(毫秒) + public String getOpenId() { return openId; } @@ -69,4 +71,12 @@ public class WxUserSession extends BaseEntity { public void setUnionid(String unionid) { this.unionid = unionid; } + + public Long getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Long expiresAt) { + this.expiresAt = expiresAt; + } } diff --git a/src/main/java/org/chenyon/wx/WxUserSessionService.java b/src/main/java/org/chenyon/wx/WxUserSessionService.java index 1787c41..6227661 100644 --- a/src/main/java/org/chenyon/wx/WxUserSessionService.java +++ b/src/main/java/org/chenyon/wx/WxUserSessionService.java @@ -42,28 +42,54 @@ public class WxUserSessionService { return wxUserSession; } + /** + * 校验session是否过期 + 自动续期(纯数据库版) + * 返回 true = 过期 + * 返回 false = 有效(且会自动续期) + */ public Boolean sessionIsExpired(String thirdSession) { - try { - WxUserSession session = wxUserSessionDao.findByThirdSession(thirdSession); - if(session == null) { - return true; - } - String accessToken = tokenService.getAccessToken(); - String url = "https://api.weixin.qq.com/wxa/checksession?access_token=" + accessToken + "&signature=" + HmacSha256Util.hmacSha256Hex("",session.getSessionKey()) + "&openid=" + session.getOpenId() +"&sig_method=hmac_sha256"; - HttpResponse response = HttpRequestUtils.sendGet(url, null); - if(response.getCode() != 200) { - return true; - } - Map map = JsonUtils.parseObject(response.getRespStr(), Map.class); - if((Integer) map.get("errcode") == 0) { - return false; - }else { - return true; - } - }catch (Exception e) { - log.error(e.getMessage(),e); + if (thirdSession == null || thirdSession.isEmpty()) { + return true; + } + + try { + // 1. 查数据库 + WxUserSession session = wxUserSessionDao.findByThirdSession(thirdSession); + if (session == null) { + return true; + } + + Long now = System.currentTimeMillis(); + Long expiresAt = session.getExpiresAt(); + + // 2. 已经过期 + if (expiresAt == null || expiresAt <= now) { + return true; + } + + // ===================== 自动续期逻辑 ===================== + // 规则:如果剩余时间 < 30分钟,自动续期 2小时 + long remainTime = expiresAt - now; + long thirtyMinutes = 30 * 60 * 1000; // 30分钟 + + if (remainTime < thirtyMinutes) { + // 新过期时间 = 当前 + 2小时 + long newExpiresAt = now + 7200 * 1000; + session.setExpiresAt(newExpiresAt); + + // 更新数据库 + wxUserSessionDao.updateByPrimaryKeySelective(session); + log.info("thirdSession自动续期成功:{}", thirdSession); + } + // ========================================================== + + // 3. 有效 + return false; + + } catch (Exception e) { + log.error("session校验&续期异常", e); + return true; } - return true; } public CusVo matchCus(String phone, String orgNo, String loginType) throws Exception { diff --git a/src/main/scripts/startup.sh b/src/main/scripts/startup.sh index 75a5266..e666519 100644 --- a/src/main/scripts/startup.sh +++ b/src/main/scripts/startup.sh @@ -1,19 +1,102 @@ #!/bin/bash +# ====================== 配置项(你只需要改这里)====================== APP_NAME=/usr/local/qichenrent/qichenrent.jar -CONFIG_DIR=file:/usr/local/qichenrent/config/ +CONFIG_DIR=/usr/local/qichenrent/config/ LOG_DIR=/usr/local/qichenrent/logs PID_FILE=/usr/local/qichenrent/qichenrent.pid JAVA_OPTS="-Xms512m -Xmx1024m -Dfile.encoding=UTF-8" +# ==================================================================== +# 创建目录 mkdir -p $LOG_DIR +mkdir -p $CONFIG_DIR -echo "Starting $APP_NAME ..." +# 获取当前PID +get_pid() { + if [ -f "$PID_FILE" ]; then + cat $PID_FILE + fi +} -nohup java $JAVA_OPTS -jar $APP_NAME \ ---spring.config.additional-location=$CONFIG_DIR \ -> $LOG_DIR/console.log 2>&1 & +# 检查是否正在运行 +is_running() { + pid=$(get_pid) + if [ -n "$pid" ] && ps -p $pid > /dev/null 2>&1; then + return 0 + else + return 1 + fi +} -echo $! > $PID_FILE +# 启动 +start() { + if is_running; then + echo "应用已启动,PID=$(get_pid),无需重复启动" + exit 0 + fi -echo "Application started, PID=$(cat $PID_FILE)" + echo "正在启动应用:$APP_NAME ..." + + # 后台启动 SpringBoot + nohup java $JAVA_OPTS -jar $APP_NAME \ + --spring.config.additional-location=file:$CONFIG_DIR \ + > $LOG_DIR/console.log 2>&1 & + + # 记录PID + echo $! > $PID_FILE + sleep 1 + echo "应用启动成功!PID=$(get_pid)" +} + +# 停止 +stop() { + if ! is_running; then + echo "应用未运行" + rm -f $PID_FILE + return + fi + + pid=$(get_pid) + echo "正在停止应用,PID=$pid ..." + kill -15 $pid + sleep 2 + + # 强制kill + if ps -p $pid > /dev/null 2>&1; then + kill -9 $pid + fi + + rm -f $PID_FILE + echo "应用已停止" +} + +# 状态 +status() { + if is_running; then + echo "应用运行中,PID=$(get_pid)" + else + echo "应用未运行" + fi +} + +# 命令入口 +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + status) + status + ;; + *) + echo "用法:$0 {start|stop|restart|status}" + exit 1 + ;; +esac \ No newline at end of file