更新代码

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

View File

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

View File

@@ -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<OaBillVo> pageQueryWaeBill(BillQueryCondition condition) throws Exception {
return oaBillService.pageQueryWaeBill(condition);
}
public PageResult<OaBillVo> pageQueryUnpaidMargin(BillQueryCondition condition) throws Exception {
return oaBillService.pageQueryUnpaidMargin(condition);
}
public PageResult<OaPayRecordVo> pageQueryContractPayRecord(PayRecordQueryConditon condition) throws Exception {
return oaBillService.pageQueryPayRecord(condition);
}
public Integer countUnpayRentBills(String cusNo) throws Exception {
if(StringUtils.isBlank(cusNo)){
return 0;
}
Map<String,Object> 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<String,Object> 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<String,Object> 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());
}
}

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_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";
/**
* 统计未缴水电费账单数
*/

View File

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

View File

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

View File

@@ -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<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 {
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<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 {
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<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;
}
public CusVo matchPersonCus(String phone) throws Exception {
public CusVo matchPersonCus(String cardNo) throws Exception {
Map<String,Object> 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();

View File

@@ -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<NoticeVo> vos = null;
List<OaNoticeVo> 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<NoticeVo> noticeVos = new ArrayList<>();
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.setPageSize(condition.getPageSize());
pageResult.setRecordTotal(totalCount.intValue());
pageResult.setResult(vos);
pageResult.setResult(noticeVos);
return pageResult;
}

View File

@@ -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<String,Object> params = new HashMap<>();
params.put("openId",openId);
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.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<UnpaidBillVo> 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<WeAppSubOrder> 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;
}
}

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.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<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";
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<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.setAppId(miniRequestMap.get("appId"));
miniPayRequest.setNonceStr(miniRequestMap.get("nonceStr"));

View File

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

View File

@@ -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> {
WeAppOrder findByBizId(String bizId);
WeAppOrder selectByTmOrderId(String tmOrderNo);
List<WeAppOrder> selectTimeoutUnpaidOrder();
}

View File

@@ -4,10 +4,14 @@
<mapper namespace="org.chenyon.pay.WeAppOrderDao">
<select id="findByBizId" resultType="WeAppOrder">
select * from order where bizId = #{bizId};
select * from `weapp_order` where bizId = #{bizId}
</select>
<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>
</mapper>

View File

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

View File

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

View File

@@ -3,5 +3,13 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>

View File

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

View File

@@ -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<String, Object> respMap = JsonUtils.parseObject(respStr, Map.class);
Map<String, Object> phoneInfo = (Map<String, Object>) 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<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.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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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<String,Object> 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 {

View File

@@ -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