更新代码

This commit is contained in:
2026-06-15 14:51:35 +08:00
parent b394cc6d13
commit 8d0f158dc3
26 changed files with 654 additions and 129 deletions

View File

@@ -70,6 +70,22 @@ public class BillController {
return ResultMessage.success(new PageResult()); 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 @LoginCheck
@GetMapping("/countUnpayRentBills") @GetMapping("/countUnpayRentBills")
public ResultMessage countUnpayRentBills() { public ResultMessage countUnpayRentBills() {
@@ -85,6 +101,21 @@ public class BillController {
return ResultMessage.success(0); 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 @LoginCheck
@GetMapping("/countUnpayWaeBills") @GetMapping("/countUnpayWaeBills")
public ResultMessage countUnpayWaeBills() { public ResultMessage countUnpayWaeBills() {
@@ -115,8 +146,19 @@ public class BillController {
return ResultMessage.success(orderService.createOrder(orderVo)); return ResultMessage.success(orderService.createOrder(orderVo));
}catch (Exception e) { }catch (Exception e) {
log.error(e.getMessage(),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 @LoginCheck
@@ -133,4 +175,5 @@ public class BillController {
} }
return ResultMessage.success(0); return ResultMessage.success(0);
} }
} }

View File

@@ -7,6 +7,8 @@ public class BillQueryCondition extends PageQueryRequest {
private String cusNo; //客商编码 private String cusNo; //客商编码
private String year; //年份 private String year; //年份
private String billStatus; //账单状态 private String billStatus; //账单状态
private String startDate; //账单开始日期
private String endDate; //账单结束日期
public String getCusNo() { public String getCusNo() {
return cusNo; return cusNo;
@@ -31,4 +33,20 @@ public class BillQueryCondition extends PageQueryRequest {
public void setBillStatus(String billStatus) { public void setBillStatus(String billStatus) {
this.billStatus = 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;
}
} }

View File

@@ -1,12 +1,16 @@
package org.chenyon.bill; package org.chenyon.bill;
import org.apache.commons.lang3.StringUtils;
import org.chenyon.oa.OaBillService; import org.chenyon.oa.OaBillService;
import org.chenyon.oa.bill.OaBillVo; import org.chenyon.oa.bill.OaBillVo;
import org.chenyon.oa.bill.OaPayRecordVo; import org.chenyon.oa.bill.OaPayRecordVo;
import org.chenyon.pay.OrderService; import org.chenyon.pay.OrderService;
import org.rcy.framework.api.entity.PageResult; import org.rcy.framework.api.entity.PageResult;
import org.rcy.framework.data.id.LongIdGenerator;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.IdGenerator;
import org.springframework.util.JdkIdGenerator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -26,17 +30,37 @@ public class BillService {
public PageResult<OaBillVo> pageQueryWaeBill(BillQueryCondition condition) throws Exception { public PageResult<OaBillVo> pageQueryWaeBill(BillQueryCondition condition) throws Exception {
return oaBillService.pageQueryWaeBill(condition); return oaBillService.pageQueryWaeBill(condition);
} }
public PageResult<OaBillVo> pageQueryUnpaidMargin(BillQueryCondition condition) throws Exception {
return oaBillService.pageQueryUnpaidMargin(condition);
}
public PageResult<OaPayRecordVo> pageQueryContractPayRecord(PayRecordQueryConditon condition) throws Exception { public PageResult<OaPayRecordVo> pageQueryContractPayRecord(PayRecordQueryConditon condition) throws Exception {
return oaBillService.pageQueryPayRecord(condition); return oaBillService.pageQueryPayRecord(condition);
} }
public Integer countUnpayRentBills(String cusNo) throws Exception { public Integer countUnpayRentBills(String cusNo) throws Exception {
if(StringUtils.isBlank(cusNo)){
return 0;
}
Map<String,Object> params = new HashMap<>(); Map<String,Object> params = new HashMap<>();
params.put("cusNo",cusNo); params.put("cusNo",cusNo);
return oaBillService.countUnpayRentBills(params); return oaBillService.countUnpayRentBills(params);
} }
public Integer countUnpaidRentMargin(String cusNo) throws Exception {
if(StringUtils.isBlank(cusNo)){
return 0;
}
Map<String,Object> params = new HashMap<>();
params.put("cusNo",cusNo);
return oaBillService.countUnpaidRentMargin(params);
}
public Integer countUnpayWaeBills(String cusNo) throws Exception { public Integer countUnpayWaeBills(String cusNo) throws Exception {
if(StringUtils.isBlank(cusNo)){
return 0;
}
Map<String,Object> params = new HashMap<>(); Map<String,Object> params = new HashMap<>();
params.put("cusNo",cusNo); params.put("cusNo",cusNo);
return oaBillService.countUnpayWaeBills(params); return oaBillService.countUnpayWaeBills(params);
@@ -48,4 +72,8 @@ public class BillService {
params.put("bizType",bizType); params.put("bizType",bizType);
return oaBillService.getBillPayReceiver(params); return oaBillService.getBillPayReceiver(params);
} }
public static void main(String[] args) {
System.out.println(LongIdGenerator.generate());
}
} }

View File

@@ -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_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_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 BILL_PAY_RECEIVER = "/bill/payreceiver";
/**
* 支付回调
*/
public static final String PAY_CALLBACK = "/bill/pay/callback";
/** /**
* 统计未缴水电费账单数 * 统计未缴水电费账单数
*/ */

View File

@@ -117,6 +117,9 @@ public class ContractService {
return assetsInfo; return assetsInfo;
} }
public Integer countUnsign(String cusNo) throws Exception { public Integer countUnsign(String cusNo) throws Exception {
if(StringUtils.isBlank(cusNo)){
return 0;
}
Integer count = oaContractService.countUnsign(cusNo); Integer count = oaContractService.countUnsign(cusNo);
return count; return count;
} }

View File

@@ -1,5 +1,6 @@
package org.chenyon.message; package org.chenyon.message;
import org.apache.commons.lang3.StringUtils;
import org.chenyon.wx.AesException; import org.chenyon.wx.AesException;
import org.rcy.framework.api.entity.PageResult; import org.rcy.framework.api.entity.PageResult;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +38,9 @@ public class MessageService {
} }
public Integer countUnread(String cusNo){ public Integer countUnread(String cusNo){
if(StringUtils.isBlank(cusNo)){
return 0;
}
MessageQueryCondition messageQueryCondition = new MessageQueryCondition(); MessageQueryCondition messageQueryCondition = new MessageQueryCondition();
messageQueryCondition.setMessageReceiver(cusNo); messageQueryCondition.setMessageReceiver(cusNo);
messageQueryCondition.setHasRead(false); messageQueryCondition.setHasRead(false);

View File

@@ -11,6 +11,8 @@ import org.rcy.framework.utils.json.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -40,6 +42,26 @@ public class OaBillService {
return pageResult; return pageResult;
} }
public PageResult<OaBillVo> 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<OaBillVo> 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<OaBillVo> pageResult = new PageResult<>();
pageResult.setPageNo(condition.getPageNo());
pageResult.setPageSize(condition.getPageSize());
pageResult.setRecordTotal(totalCount.intValue());
pageResult.setResult(oaBillVos);
return pageResult;
}
public PageResult<OaBillVo> pageQueryWaeBill(BillQueryCondition condition) throws Exception { public PageResult<OaBillVo> pageQueryWaeBill(BillQueryCondition condition) throws Exception {
OaResp oaResp = seeyonHttpClient.sendPost("分页查询水电费账单信息", OaApiUrl.PAGE_QUERY_WAE_BILL, JsonUtils.convertJson(condition)); OaResp oaResp = seeyonHttpClient.sendPost("分页查询水电费账单信息", OaApiUrl.PAGE_QUERY_WAE_BILL, JsonUtils.convertJson(condition));
String data = oaResp.getDataStr(); String data = oaResp.getDataStr();
@@ -86,6 +108,11 @@ public class OaBillService {
return (Integer) oaResp.getData(); return (Integer) oaResp.getData();
} }
public Integer countUnpaidRentMargin(Map<String,Object> params) throws Exception {
OaResp oaResp = seeyonHttpClient.sendPost("统计未缴保证金账单数", OaApiUrl.COUNT_UNPAID_RENT_MARGIN, JsonUtils.convertJson(params));
return (Integer) oaResp.getData();
}
public Integer countUnpayWaeBills(Map<String,Object> params) throws Exception { public Integer countUnpayWaeBills(Map<String,Object> params) throws Exception {
OaResp oaResp = seeyonHttpClient.sendPost("统计未缴水电费账单数", OaApiUrl.COUNT_UNPAY_WAE_BILLS, JsonUtils.convertJson(params)); OaResp oaResp = seeyonHttpClient.sendPost("统计未缴水电费账单数", OaApiUrl.COUNT_UNPAY_WAE_BILLS, JsonUtils.convertJson(params));
return (Integer) oaResp.getData(); return (Integer) oaResp.getData();
@@ -96,4 +123,11 @@ public class OaBillService {
return (String) oaResp.getData(); return (String) oaResp.getData();
} }
public void payCallBack(String billNo, String bizType, String payDate) throws Exception {
Map<String,Object> 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));
}
} }

View File

@@ -39,9 +39,9 @@ public class OaCusService {
return null; return null;
} }
public CusVo matchPersonCus(String phone) throws Exception { public CusVo matchPersonCus(String cardNo) throws Exception {
Map<String,Object> params = new HashMap<>(); Map<String,Object> params = new HashMap<>();
params.put("phone",phone); params.put("cardNo",cardNo);
OaResp oaResp = seeyonHttpClient.sendPost("匹配个人客户",OaApiUrl.MATCH_PERSON, params); OaResp oaResp = seeyonHttpClient.sendPost("匹配个人客户",OaApiUrl.MATCH_PERSON, params);
Object data = oaResp.getData(); Object data = oaResp.getData();
CusVo cusVo = new CusVo(); CusVo cusVo = new CusVo();

View File

@@ -11,6 +11,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -28,18 +29,27 @@ public class OaNoticeService {
String data = oaResp.getDataStr(); String data = oaResp.getDataStr();
Map mapRoot = JsonUtils.parseObject(data, Map.class); Map mapRoot = JsonUtils.parseObject(data, Map.class);
Long totalCount = 0L; Long totalCount = 0L;
List<NoticeVo> vos = null; List<OaNoticeVo> vos = null;
if(mapRoot.get("totalCount") != null){ if(mapRoot.get("totalCount") != null){
totalCount = mapRoot.get("totalCount") instanceof Long ? (Long) mapRoot.get("totalCount") : Long.parseLong(mapRoot.get("totalCount") + ""); totalCount = mapRoot.get("totalCount") instanceof Long ? (Long) mapRoot.get("totalCount") : Long.parseLong(mapRoot.get("totalCount") + "");
} }
if(mapRoot.get("datas") != null) { 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<NoticeVo> noticeVos = new ArrayList<>();
PageResult<NoticeVo> pageResult = new PageResult<>(); PageResult<NoticeVo> 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.setPageNo(condition.getPageNo());
pageResult.setPageSize(condition.getPageSize()); pageResult.setPageSize(condition.getPageSize());
pageResult.setRecordTotal(totalCount.intValue()); pageResult.setRecordTotal(totalCount.intValue());
pageResult.setResult(vos); pageResult.setResult(noticeVos);
return pageResult; return pageResult;
} }

View File

@@ -1,5 +1,6 @@
package org.chenyon.oa; package org.chenyon.oa;
import org.apache.commons.lang3.StringUtils;
import org.chenyon.constants.OaApiUrl; import org.chenyon.constants.OaApiUrl;
import org.chenyon.fallback.FallbackSubmitVo; import org.chenyon.fallback.FallbackSubmitVo;
import org.chenyon.reservation.ReservationQueryCondition; import org.chenyon.reservation.ReservationQueryCondition;
@@ -41,6 +42,9 @@ public class OaReservationService {
} }
public Integer countHandling(String openId) throws Exception { public Integer countHandling(String openId) throws Exception {
if(StringUtils.isBlank(openId)){
return 0;
}
Map<String,Object> params = new HashMap<>(); Map<String,Object> params = new HashMap<>();
params.put("openId",openId); params.put("openId",openId);
OaResp oaResp = seeyonHttpClient.sendPost("查询处理中预约", OaApiUrl.RESERVATION_COUNT_HANDLING,JsonUtils.convertJson(params)); OaResp oaResp = seeyonHttpClient.sendPost("查询处理中预约", OaApiUrl.RESERVATION_COUNT_HANDLING,JsonUtils.convertJson(params));

View File

@@ -9,12 +9,15 @@ import org.chenyon.pay.shopcar.TmPayOrderRespVo;
import org.chenyon.pay.shopcar.VaFld; import org.chenyon.pay.shopcar.VaFld;
import org.chenyon.user.UserService; import org.chenyon.user.UserService;
import org.rcy.framework.data.id.LongIdGenerator; 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
@@ -22,6 +25,8 @@ import java.util.*;
@Service @Service
public class OrderService { public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
@Autowired @Autowired
private WeAppOrderDao weAppOrderDao; private WeAppOrderDao weAppOrderDao;
@Autowired @Autowired
@@ -31,8 +36,10 @@ public class OrderService {
private String mid; private String mid;
@Value("${chinaums.tid}") @Value("${chinaums.tid}")
private String tid; private String tid;
@Value("${chinaums.sourceNo}") @Value("${chinaums.systemNo}")
private String sourceNo; private String sourceNo;
@Value("${chinaums.sysSource}")
private String sysSource;
@Value("${wx.appId}") @Value("${wx.appId}")
private String miniProgramAppId; private String miniProgramAppId;
@@ -49,94 +56,113 @@ public class OrderService {
private String fwglMid; private String fwglMid;
@Value("${wx.appId}") @Value("${wx.appId}")
private String wxAppId; private String wxAppId;
@Value("${chinaums.pay.notifyUrl}") @Value("${cdn.domain}")
private String notifyUrl; private String domain;
private DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 批量创建支付订单
* 【核心】批量前 校验所有 billNo 是否已经存在子订单
* 子订单唯一键billNo
*/
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public TmPayOrderRespVo createOrder(OrderVo orderVo) throws Exception { public TmPayOrderRespVo createOrder(OrderVo orderVo) throws Exception {
// 1. 基础参校验 // 1. 基础参校验
if (orderVo == null || orderVo.getBillVoList() == null || orderVo.getBillVoList().isEmpty()) { if (orderVo == null || orderVo.getBillVoList() == null || orderVo.getBillVoList().isEmpty()) {
throw new Exception("批量支付账单不能为空"); throw new Exception("批量支付账单不能为空");
} }
if (StringUtils.isEmpty(orderVo.getPayerOpenId())) { if (StringUtils.isEmpty(orderVo.getPayerOpenId())) {
throw new Exception("支付人OPENID不能为空"); throw new Exception("支付人OPENID不能为空");
} }
List<UnpaidBillVo> billVoList = orderVo.getBillVoList(); List<UnpaidBillVo> billVoList = orderVo.getBillVoList();
// ====================================================================== // ======================================================================
// 【批量支付 最强幂等校验 // 【第1步最前置全局幂等锁单校验核心防重在所有操作之前
// 遍历所有子账单,根据 billNo 查询是否已存在支付记录 // 只要任意账单存在 【支付中未完成】订单,直接拒绝,后续所有流程全部不执行
// 只要有一个已存在 → 整批拒绝支付 // 不创建本地订单、不调用银联、不生成支付单,从源头杜绝重复支付
// ====================================================================== // ======================================================================
for (UnpaidBillVo billVo : billVoList) { for (UnpaidBillVo billVo : billVoList) {
String billNo = billVo.getBillNo(); String billNo = billVo.getBillNo();
// 子订单唯一键billNo
WeAppSubOrder existSubOrder = weAppSubOrderDao.selectByBillNo(billNo); WeAppSubOrder existSubOrder = weAppSubOrderDao.selectByBillNo(billNo);
if (existSubOrder != null) { if (existSubOrder == null) {
throw new Exception("批量支付失败:账单号【" + billNo + "】已存在支付记录,禁止重复支付"); 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(); // 【第2步全部校验通过生成全新主订单】
String bizId = mainOrderId.toString(); // ======================================================================
Long newMainOrderId = LongIdGenerator.generate();
String bizId = String.valueOf(newMainOrderId);
orderVo.setBizId(bizId); 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"); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
int random = 1000000 + new Random().nextInt(9000000); 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 (weAppOrderDao.selectByTmOrderId(tmOrderNo) != null) {
if (orderByTmNo != null) {
throw new Exception("支付单号重复,请重新提交"); throw new Exception("支付单号重复,请重新提交");
} }
// 5. 调用支付创建分账订单 // ======================================================================
ShopCarOrderCreateRequest request = buildShopCarOrderCreateRequest(orderVo, tmOrderNo); // 【第3步本地子订单、主订单全部先落库数据先行锁定
TmPayOrderRespVo payResp = tmPayService.weChatPayCreateSplitOrder(request); // 不存在则新增,旧单已关闭则更新绑定最新主订单
// ======================================================================
// 6. 批量插入子订单 + 修复 BigDecimal 金额BUG
BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal totalAmount = BigDecimal.ZERO;
for (UnpaidBillVo billVo : billVoList) { for (UnpaidBillVo billVo : billVoList) {
WeAppSubOrder subOrder = new WeAppSubOrder(); String billNo = billVo.getBillNo();
subOrder.setMainOrderId(mainOrderId); WeAppSubOrder existSub = weAppSubOrderDao.selectByBillNo(billNo);
subOrder.setBillNo(billVo.getBillNo()); // 唯一键
subOrder.setAmount(billVo.getAmount()); if (existSub == null) {
subOrder.setPayStatus("0"); // 子订单首次不存在:新增,绑定本次新主订单
weAppSubOrderDao.insert(subOrder); 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())); totalAmount = totalAmount.add(new BigDecimal(billVo.getAmount()));
} }
// 7. 插入主订单 // 插入全新主订单(本地数据全部落库完成)
WeAppOrder weAppOrder = new WeAppOrder(); WeAppOrder weAppOrder = new WeAppOrder();
weAppOrder.setId(mainOrderId); weAppOrder.setId(newMainOrderId);
weAppOrder.setBizId(bizId); weAppOrder.setBizId(bizId);
weAppOrder.setPayerId(orderVo.getPayer()); weAppOrder.setPayerId(orderVo.getPayer());
weAppOrder.setPayerName(orderVo.getPayerName()); weAppOrder.setPayerName(orderVo.getPayerName());
weAppOrder.setTmOrderId(tmOrderNo); weAppOrder.setTmOrderId(tmOrderNo);
weAppOrder.setAmount(totalAmount.toString()); weAppOrder.setAmount(totalAmount.toString());
weAppOrder.setPayStatus("0"); weAppOrder.setPayStatus("0"); // 待支付(支付中)
weAppOrder.setCreateTime(new Date());
weAppOrder.setPayerOpenId(orderVo.getPayerOpenId());
weAppOrderDao.insert(weAppOrder); weAppOrderDao.insert(weAppOrder);
// ======================================================================
// 【第4步本地所有订单数据全部安全落库后最后才调用银联创建支付单】
// 顺序彻底修正!业务数据优先,第三方支付最后执行
// ======================================================================
ShopCarOrderCreateRequest request = buildShopCarOrderCreateRequest(orderVo, tmOrderNo);
TmPayOrderRespVo payResp = tmPayService.weChatPayCreateSplitOrder(request);
payResp.setMainOrderId(newMainOrderId);
return payResp; return payResp;
} }
@@ -150,10 +176,11 @@ public class OrderService {
request.setSubAppId(wxAppId); request.setSubAppId(wxAppId);
request.setMchntOrderId(orderNo); request.setMchntOrderId(orderNo);
request.setMsgType("wx.unifiedOrder"); request.setMsgType("wx.unifiedOrder");
String notifyUrl = domain + "/api" + "/bill/paycallback" + "?orderId=" + orderVo.getBizId() + "&" + "payResult=SUCCESS";
request.setNotifyUrl(notifyUrl); request.setNotifyUrl(notifyUrl);
request.setTradeType("MINI"); request.setTradeType("MINI");
request.setSubOpenId(orderVo.getPayerOpenId()); request.setSubOpenId(orderVo.getPayerOpenId());
request.setSysSource("OPT"); request.setSysSource(sysSource);
VaFld vaFld = new VaFld(); VaFld vaFld = new VaFld();
vaFld.setNewShopFlag("GWCXS"); vaFld.setNewShopFlag("GWCXS");
@@ -169,6 +196,9 @@ public class OrderService {
param.put("billNo", billVo.getBillNo()); param.put("billNo", billVo.getBillNo());
param.put("bizType", billVo.getBizType()); param.put("bizType", billVo.getBizType());
String receiver = oaBillService.getBillPayReceiver(param); String receiver = oaBillService.getBillPayReceiver(param);
if(StringUtils.isBlank(receiver) || "null".equals(receiver)){
throw new RuntimeException("获取账单收款方失败,请联系管理员");
}
billVo.setPayee(receiver); billVo.setPayee(receiver);
int amountCent = (int) (Double.parseDouble(billVo.getAmount()) * 100); int amountCent = (int) (Double.parseDouble(billVo.getAmount()) * 100);
@@ -184,14 +214,12 @@ public class OrderService {
if (gyAmount.compareTo(BigDecimal.ZERO) > 0) { if (gyAmount.compareTo(BigDecimal.ZERO) > 0) {
SatInfo info = new SatInfo(); SatInfo info = new SatInfo();
info.setMerId(gyMid); info.setMerId(gyMid);
info.setFeeBear("Y");
info.setSatAmt(gyAmount.toString()); info.setSatAmt(gyAmount.toString());
satInfoList.add(info); satInfoList.add(info);
} }
if (fwglAmount.compareTo(BigDecimal.ZERO) > 0) { if (fwglAmount.compareTo(BigDecimal.ZERO) > 0) {
SatInfo info = new SatInfo(); SatInfo info = new SatInfo();
info.setMerId(fwglMid); info.setMerId(fwglMid);
info.setFeeBear("Y");
info.setSatAmt(fwglAmount.toString()); info.setSatAmt(fwglAmount.toString());
satInfoList.add(info); satInfoList.add(info);
} }
@@ -202,39 +230,70 @@ public class OrderService {
} }
/** /**
* 支付回调(幂等 + 批量更新子订单 * 最终版支付回调(100%幂等 + 无并发问题 + 安全
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void tmPayCallback(String tmOrderNo, String payResult) { public void tmPayCallback(String mainOrderId, String payResult) {
WeAppOrder order = weAppOrderDao.selectByTmOrderId(tmOrderNo); // 1. 查询主订单
if (order == null || "1".equals(order.getPayStatus())) { WeAppOrder order = weAppOrderDao.findByBizId(mainOrderId);
if (order == null) {
log.warn("[回调] 主订单不存在bizId:{}", mainOrderId);
return; return;
} }
// ==================== 【核心幂等】 ====================
// 已支付 -> 直接返回,绝对不重复处理
// ======================================================
if ("1".equals(order.getPayStatus())) {
log.info("[回调幂等] 订单已支付无需重复处理bizId:{}", mainOrderId);
return;
}
// ==================== 支付成功 ====================
if ("SUCCESS".equalsIgnoreCase(payResult)) { if ("SUCCESS".equalsIgnoreCase(payResult)) {
// 更新主订单状态
order.setPayStatus("1"); order.setPayStatus("1");
order.setPayDate(new Date());
weAppOrderDao.updateByPrimaryKeySelective(order); weAppOrderDao.updateByPrimaryKeySelective(order);
// 批量更新该主订单下 所有子订单(billNo状态 // 查询该主订单下所有子订单(只会查一次,因为子订单终身唯一)
List<WeAppSubOrder> subList = weAppSubOrderDao.selectByMainOrderId(order.getId()); List<WeAppSubOrder> subList = weAppSubOrderDao.selectByMainOrderId(order.getId());
for (WeAppSubOrder sub : subList) { 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 { } else {
// ================== 支付失败 ==================
order.setPayStatus("2"); order.setPayStatus("2");
weAppOrderDao.updateByPrimaryKeySelective(order); 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;
}
} }

View File

@@ -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<WeAppOrder> 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);
}
}
}
}

View File

@@ -4,16 +4,22 @@ package org.chenyon.pay;
import org.chenyon.pay.shopcar.MiniPayRequest; import org.chenyon.pay.shopcar.MiniPayRequest;
import org.chenyon.pay.shopcar.ShopCarOrderCreateRequest; import org.chenyon.pay.shopcar.ShopCarOrderCreateRequest;
import org.chenyon.pay.shopcar.TmPayOrderRespVo; import org.chenyon.pay.shopcar.TmPayOrderRespVo;
import org.chenyon.pay.shopcar.VaFld;
import org.rcy.framework.utils.json.JsonUtils; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Service @Service
public class TmPayService { public class TmPayService {
private static final Logger log = LoggerFactory.getLogger(TmPayService.class);
@Autowired @Autowired
private TmHttpUtil tmHttpUtil; 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<String,Object> 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"; String url = "/v1/inip/upsp/shop/mini/pay";
try { 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); Map map = JsonUtils.parseObject(respStr, Map.class);
String code = (String)map.get("respCode"); String code = (String)map.get("respCode");
if(!"0000".equals(code)){ if(!"0000".equals(code)){
throw new RuntimeException("创建订单失败"); throw new RuntimeException("创建订单失败");
} }
String forwardUrl = (String)map.get("url"); String forwardUrl = (String)map.get("url");
Map<String,String> miniRequestMap = (Map<String, String>) map.get("miniPayRequest"); Map<String,String> miniRequestMap = JsonUtils.parseObject((String)map.get("miniPayRequest"), Map.class);
MiniPayRequest miniPayRequest = new MiniPayRequest(); MiniPayRequest miniPayRequest = new MiniPayRequest();
miniPayRequest.setAppId(miniRequestMap.get("appId")); miniPayRequest.setAppId(miniRequestMap.get("appId"));
miniPayRequest.setNonceStr(miniRequestMap.get("nonceStr")); miniPayRequest.setNonceStr(miniRequestMap.get("nonceStr"));

View File

@@ -7,7 +7,7 @@ import javax.persistence.Table;
import java.util.Date; import java.util.Date;
@Entity @Entity
@Table(name = "ORDER") @Table(name = "WEAPP_ORDER")
public class WeAppOrder extends BaseEntity { public class WeAppOrder extends BaseEntity {
private String bizId; private String bizId;
@@ -18,6 +18,7 @@ public class WeAppOrder extends BaseEntity {
private String payStatus; //支付状态 private String payStatus; //支付状态
private String payerOpenId; // private String payerOpenId; //
private String tmOrderId; //银联支付订单号 private String tmOrderId; //银联支付订单号
private Date createTime;
public String getBizId() { public String getBizId() {
@@ -83,4 +84,12 @@ public class WeAppOrder extends BaseEntity {
public void setTmOrderId(String tmOrderId) { public void setTmOrderId(String tmOrderId) {
this.tmOrderId = tmOrderId; this.tmOrderId = tmOrderId;
} }
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
} }

View File

@@ -2,7 +2,10 @@ package org.chenyon.pay;
import org.rcy.framework.data.dao.BaseDao; import org.rcy.framework.data.dao.BaseDao;
import java.util.List;
public interface WeAppOrderDao extends BaseDao<WeAppOrder> { public interface WeAppOrderDao extends BaseDao<WeAppOrder> {
WeAppOrder findByBizId(String bizId); WeAppOrder findByBizId(String bizId);
WeAppOrder selectByTmOrderId(String tmOrderNo); WeAppOrder selectByTmOrderId(String tmOrderNo);
List<WeAppOrder> selectTimeoutUnpaidOrder();
} }

View File

@@ -4,10 +4,14 @@
<mapper namespace="org.chenyon.pay.WeAppOrderDao"> <mapper namespace="org.chenyon.pay.WeAppOrderDao">
<select id="findByBizId" resultType="WeAppOrder"> <select id="findByBizId" resultType="WeAppOrder">
select * from order where bizId = #{bizId}; select * from `weapp_order` where bizId = #{bizId}
</select> </select>
<select id="selectByTmOrderId" resultType="WeAppOrder"> <select id="selectByTmOrderId" resultType="WeAppOrder">
select * from order where bizId = #{bizId}; select * from `weapp_order` where tmOrderId = #{tmOrderNo}
</select>
<select id="selectTimeoutUnpaidOrder" resultType="WeAppOrder">
select * from `weapp_order` where payStatus = '0'
and TIMESTAMPDIFF(MINUTE, createTime, NOW()) > 5
</select> </select>
</mapper> </mapper>

View File

@@ -4,6 +4,7 @@ import org.rcy.framework.api.entity.BaseEntity;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date;
@Entity @Entity
@Table(name = "SUB_ORDER") @Table(name = "SUB_ORDER")
@@ -13,6 +14,7 @@ public class WeAppSubOrder extends BaseEntity {
private String amount; private String amount;
private Long mainOrderId; private Long mainOrderId;
private String payStatus; private String payStatus;
private Date createTime;
public String getBillNo() { public String getBillNo() {
return billNo; return billNo;
@@ -53,4 +55,12 @@ public class WeAppSubOrder extends BaseEntity {
public void setPayStatus(String payStatus) { public void setPayStatus(String payStatus) {
this.payStatus = payStatus; this.payStatus = payStatus;
} }
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
} }

View File

@@ -8,8 +8,8 @@ public interface WeAppSubOrderDao extends BaseDao<WeAppSubOrder> {
WeAppSubOrder selectByBillNo(String billNo); WeAppSubOrder selectByBillNo(String billNo);
WeAppSubOrder selectByBillNoForUpdate(String billNo);
List<WeAppSubOrder> selectByMainOrderId(Long mainOrderId); List<WeAppSubOrder> selectByMainOrderId(Long mainOrderId);
Integer updateById(WeAppSubOrder subOrder);
} }

View File

@@ -3,5 +3,13 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.chenyon.pay.WeAppSubOrderDao"> <mapper namespace="org.chenyon.pay.WeAppSubOrderDao">
<select id="selectByBillNo" resultType="WeAppSubOrder">
select * from sub_order where billNo = #{billNo}
</select>
<select id="selectByBillNoForUpdate" resultType="WeAppSubOrder">
select * from sub_order where billNo = #{billNo} FOR UPDATE
</select>
<select id="selectByMainOrderId" resultType="WeAppSubOrder">
select * from sub_order where mainOrderId = #{mainOrderId}
</select>
</mapper> </mapper>

View File

@@ -4,6 +4,9 @@ public class TmPayOrderRespVo {
private String url; private String url;
private MiniPayRequest miniPayRequest; private MiniPayRequest miniPayRequest;
private Long mainOrderId;
private String status;
private String payTime;
public String getUrl() { public String getUrl() {
return url; return url;
@@ -20,4 +23,28 @@ public class TmPayOrderRespVo {
public void setMiniPayRequest(MiniPayRequest miniPayRequest) { public void setMiniPayRequest(MiniPayRequest miniPayRequest) {
this.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;
}
} }

View File

@@ -38,30 +38,54 @@ public class LoginController {
@PostMapping("/weChatLogin") @PostMapping("/weChatLogin")
public ResultMessage weChatLogin(@RequestBody LoginVo loginVo) throws Exception{ public ResultMessage weChatLogin(@RequestBody LoginVo loginVo) throws Exception{
String accessToken = weChatAccessTokenService.getAccessToken(); // 1. 通过 code 获取微信 session_key + openid
WeChatAuthInfo sessionInfo = weChatApi.getSessionInfo(loginVo.getLoginCode()); WeChatAuthInfo sessionInfo = weChatApi.getSessionInfo(loginVo.getLoginCode());
// 2. 生成第三方会话 key
String thirdSessionKey = UUID.randomUUID().toString().replace("-", "");
// 3. 构建 session 对象核心设置2小时过期
WxUserSession wxUserSession = new WxUserSession(); WxUserSession wxUserSession = new WxUserSession();
wxUserSession.setSessionKey(sessionInfo.getSession_key()); wxUserSession.setSessionKey(sessionInfo.getSession_key());
wxUserSession.setOpenId(sessionInfo.getOpenid()); wxUserSession.setOpenId(sessionInfo.getOpenid());
wxUserSession.setUnionid(sessionInfo.getUnionid()); wxUserSession.setUnionid(sessionInfo.getUnionid());
String thirdSessionKey = UUID.randomUUID().toString().replace("-", "");
wxUserSession.setThirdSession(thirdSessionKey); 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(); // 过期时间:当前时间 + 2小时微信session_key标准有效期
Map<String, Object> respMap = JsonUtils.parseObject(respStr, Map.class); wxUserSession.setExpiresAt(System.currentTimeMillis() + 7200 * 1000);
Map<String, Object> phoneInfo = (Map<String, Object>) respMap.get("phone_info"); // ====================================================
User user = new User();
if (phoneInfo != null) { // 4. 获取用户手机号(安全获取,带异常处理)
String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); 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<String, Object> respMap = JsonUtils.parseObject(respStr, Map.class);
Map<String, Object> phoneInfo = (Map<String, Object>) 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.setOpenId(sessionInfo.getOpenid());
user.setPhone(phoneNumber); user.setPhone(phoneNumber);
user.setUserType(loginVo.getLoginType()); user.setUserType(loginVo.getLoginType());
userService.upsert(user); 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") @GetMapping("/checkExpiration")
@@ -103,9 +127,9 @@ public class LoginController {
public ResultMessage updateVerifyCode(HttpServletRequest request,@RequestBody AuthInfoVo authInfoVo) throws Exception { public ResultMessage updateVerifyCode(HttpServletRequest request,@RequestBody AuthInfoVo authInfoVo) throws Exception {
String token = request.getHeader("WT"); String token = request.getHeader("WT");
WxUserSession wxUserSession = wxUserSessionService.getSessionInfoByThirdSession(token); WxUserSession wxUserSession = wxUserSessionService.getSessionInfoByThirdSession(token);
userService.updateAuthInfo(wxUserSession.getOpenId(), boolean authSuccess = userService.updateAuthInfo(wxUserSession.getOpenId(),
authInfoVo.getUserType(), authInfoVo.getUserType(),
"0".equals(authInfoVo.getUserType()) ? RSAUtil.decrypt(authInfoVo.getCode(),rasPrivateKey) : authInfoVo.getCode()); "0".equals(authInfoVo.getUserType()) ? RSAUtil.decrypt(authInfoVo.getCode(),rasPrivateKey) : authInfoVo.getCode());
return ResultMessage.success(); return ResultMessage.success(authSuccess);
} }
} }

View File

@@ -68,7 +68,7 @@ public class UserService {
return userVo; 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接口客商信息 //调用oa接口客商信息
User user = userDao.findByOpenIdAndUserType(openId, userType); User user = userDao.findByOpenIdAndUserType(openId, userType);
if(user == null) { if(user == null) {
@@ -76,7 +76,7 @@ public class UserService {
} }
String cusNo = null; String cusNo = null;
if("0".equals(userType)) { if("0".equals(userType)) {
CusVo cusVo = oaCusService.matchPersonCus(user.getPhone()); CusVo cusVo = oaCusService.matchPersonCus(verifyCode);
cusNo = cusVo == null ? null : cusVo.getCusNo(); cusNo = cusVo == null ? null : cusVo.getCusNo();
user.setCardNo(AESUtils.encrypt(verifyCode,"abc123")); user.setCardNo(AESUtils.encrypt(verifyCode,"abc123"));
}else if("1".equals(user.getUserType())) { }else if("1".equals(user.getUserType())) {
@@ -89,5 +89,6 @@ public class UserService {
user.setOaAuth(true); user.setOaAuth(true);
userDao.updateByPrimaryKeySelective(user); userDao.updateByPrimaryKeySelective(user);
} }
return cusNo != null;
} }
} }

View File

@@ -53,14 +53,17 @@ public class WeChatAccessTokenService {
HttpResponse response = HttpRequestUtils.sendGet(url, headers); HttpResponse response = HttpRequestUtils.sendGet(url, headers);
Map respMap = JsonUtils.parseObject(response.getRespStr(), Map.class); Map respMap = JsonUtils.parseObject(response.getRespStr(), Map.class);
if (respMap.get("errcode") != null) { 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") + ""; String tokenStr = respMap.get("access_token") + "";
long ttl = Long.parseLong(respMap.get("expires_in") + ""); long ttl = Long.parseLong(respMap.get("expires_in") + "");
// 提前 100 秒过期,更安全
WxTokenCache tokenCache = new WxTokenCache(); WxTokenCache tokenCache = new WxTokenCache();
tokenCache.setToken(tokenStr); tokenCache.setToken(tokenStr);
tokenCache.setExpireTime(now + (ttl - 20000)); tokenCache.setExpireTime(now + (ttl - 100) * 1000);
cache.put(appId, tokenCache); cache.put(appId, tokenCache);
return tokenStr; return tokenStr;
}catch (Exception e){ }catch (Exception e){
log.error(e.getMessage(),e); log.error(e.getMessage(),e);

View File

@@ -22,6 +22,8 @@ public class WxUserSession extends BaseEntity {
private String unionid; private String unionid;
private Long expiresAt; // 会话过期时间戳(毫秒)
public String getOpenId() { public String getOpenId() {
return openId; return openId;
} }
@@ -69,4 +71,12 @@ public class WxUserSession extends BaseEntity {
public void setUnionid(String unionid) { public void setUnionid(String unionid) {
this.unionid = unionid; this.unionid = unionid;
} }
public Long getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Long expiresAt) {
this.expiresAt = expiresAt;
}
} }

View File

@@ -42,28 +42,54 @@ public class WxUserSessionService {
return wxUserSession; return wxUserSession;
} }
/**
* 校验session是否过期 + 自动续期(纯数据库版)
* 返回 true = 过期
* 返回 false = 有效(且会自动续期)
*/
public Boolean sessionIsExpired(String thirdSession) { public Boolean sessionIsExpired(String thirdSession) {
try { if (thirdSession == null || thirdSession.isEmpty()) {
WxUserSession session = wxUserSessionDao.findByThirdSession(thirdSession); return true;
if(session == null) { }
return true;
} try {
String accessToken = tokenService.getAccessToken(); // 1. 查数据库
String url = "https://api.weixin.qq.com/wxa/checksession?access_token=" + accessToken + "&signature=" + HmacSha256Util.hmacSha256Hex("",session.getSessionKey()) + "&openid=" + session.getOpenId() +"&sig_method=hmac_sha256"; WxUserSession session = wxUserSessionDao.findByThirdSession(thirdSession);
HttpResponse response = HttpRequestUtils.sendGet(url, null); if (session == null) {
if(response.getCode() != 200) { return true;
return true; }
}
Map<String,Object> map = JsonUtils.parseObject(response.getRespStr(), Map.class); Long now = System.currentTimeMillis();
if((Integer) map.get("errcode") == 0) { Long expiresAt = session.getExpiresAt();
return false;
}else { // 2. 已经过期
return true; if (expiresAt == null || expiresAt <= now) {
} return true;
}catch (Exception e) { }
log.error(e.getMessage(),e);
// ===================== 自动续期逻辑 =====================
// 规则:如果剩余时间 < 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 { public CusVo matchCus(String phone, String orgNo, String loginType) throws Exception {

View File

@@ -1,19 +1,102 @@
#!/bin/bash #!/bin/bash
# ====================== 配置项(你只需要改这里)======================
APP_NAME=/usr/local/qichenrent/qichenrent.jar 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 LOG_DIR=/usr/local/qichenrent/logs
PID_FILE=/usr/local/qichenrent/qichenrent.pid PID_FILE=/usr/local/qichenrent/qichenrent.pid
JAVA_OPTS="-Xms512m -Xmx1024m -Dfile.encoding=UTF-8" JAVA_OPTS="-Xms512m -Xmx1024m -Dfile.encoding=UTF-8"
# ====================================================================
# 创建目录
mkdir -p $LOG_DIR 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 \ is_running() {
> $LOG_DIR/console.log 2>&1 & 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