添加缴费相关功能
This commit is contained in:
parent
54f5a128cf
commit
fff37d2935
|
|
@ -33,11 +33,11 @@ spring:
|
||||||
matching-strategy: ant_path_matcher
|
matching-strategy: ant_path_matcher
|
||||||
# 数据源配置
|
# 数据源配置
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://localhost:3306/la?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&sql_mode=ANSI_QUOTES
|
url: jdbc:mysql://8.153.111.6:3306/la?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&sql_mode=ANSI_QUOTES
|
||||||
type: com.zaxxer.hikari.HikariDataSource # 数据源类型
|
type: com.zaxxer.hikari.HikariDataSource # 数据源类型
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动
|
driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动
|
||||||
username: root # 数据库账号
|
username: root # 数据库账号
|
||||||
password: 123456 # 数据库密码
|
password: 11111111 # 数据库密码
|
||||||
hikari:
|
hikari:
|
||||||
connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒
|
connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒
|
||||||
minimum-idle: 5 # 最小连接数
|
minimum-idle: 5 # 最小连接数
|
||||||
|
|
@ -55,9 +55,9 @@ spring:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Redis配置
|
# Redis配置
|
||||||
redis:
|
redis:
|
||||||
host: localhost # Redis服务地址
|
host: 8.153.111.6 # Redis服务地址
|
||||||
port: 6379 # Redis端口
|
port: 6379 # Redis端口
|
||||||
password: # Redis密码
|
password: 11111111 # Redis密码
|
||||||
database: 0 # 数据库索引
|
database: 0 # 数据库索引
|
||||||
timeout: 5000 # 连接超时
|
timeout: 5000 # 连接超时
|
||||||
lettuce:
|
lettuce:
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,7 @@ public class StudentInfo implements Serializable {
|
||||||
@ApiModelProperty(value = "删除时间")
|
@ApiModelProperty(value = "删除时间")
|
||||||
private Date deleteTime;
|
private Date deleteTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "绑定缴费订单ID(la_enrollment_pay_order.id)")
|
||||||
|
private Long payOrderId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.mdd.common.entity.bank;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行缴费支付流水")
|
||||||
|
public class BankFeePayOrder implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@ApiModelProperty("ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@ApiModelProperty("报名编号/业务单号(application_number)")
|
||||||
|
private String applicationNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty("商户订单号(out_trade_no)")
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付方式: [2=微信, 3=支付宝]")
|
||||||
|
private Integer payWay;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付金额(分)")
|
||||||
|
private Integer totalFeeFen;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付状态: [0=待支付, 1=已支付, 2=已关闭]")
|
||||||
|
private Integer payStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行交易流水(transaction_id / trade_no 等)")
|
||||||
|
private String bankTransactionId;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行回调/查单返回的 trade_state(如 SUCCESS/NOTPAY/CLOSED)")
|
||||||
|
private String tradeState;
|
||||||
|
|
||||||
|
@ApiModelProperty("下单返回的 pay_info(可选)")
|
||||||
|
private String payInfo;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付宝 H5 可能返回 pay_url(可选)")
|
||||||
|
private String payUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付时间(秒)")
|
||||||
|
private Long payTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("最近一次原始回调/查单 JSON(可选,便于排障)")
|
||||||
|
private String lastBankBody;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间(秒)")
|
||||||
|
private Long createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("更新时间(秒)")
|
||||||
|
private Long updateTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("删除时间(秒)")
|
||||||
|
private Long deleteTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.mdd.common.entity.bank;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("报名缴费开放银行订单流水")
|
||||||
|
public class EnrollmentPayOrder implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@ApiModelProperty("ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty("下单用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@ApiModelProperty("学生ID(来自 la_student_info.student_id)")
|
||||||
|
private Long studentId;
|
||||||
|
|
||||||
|
@ApiModelProperty("商户订单号(out_trade_no,银行原样回传)")
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付方式: 2=微信, 3=支付宝")
|
||||||
|
private Integer payWay;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付金额(分)")
|
||||||
|
private Integer totalFeeFen;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付状态: 0=待支付 1=已支付 2=已关闭")
|
||||||
|
private Integer payStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行交易流水(transaction_id/trade_no 等)")
|
||||||
|
private String bankTransactionId;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行交易状态(SUCCESS/NOTPAY/CLOSED...)")
|
||||||
|
private String tradeState;
|
||||||
|
|
||||||
|
@ApiModelProperty("下单返回 pay_info(可选)")
|
||||||
|
private String payInfo;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付宝H5可能返回 pay_url(可选)")
|
||||||
|
private String payUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付时间(秒)")
|
||||||
|
private Long payTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("最近一次回调/查单明文JSON(可选)")
|
||||||
|
private String lastBankBody;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间(秒)")
|
||||||
|
private Long createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("更新时间(秒)")
|
||||||
|
private Long updateTime;
|
||||||
|
|
||||||
|
@ApiModelProperty("删除时间(秒)")
|
||||||
|
private Long deleteTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.mdd.common.mapper.bank;
|
||||||
|
|
||||||
|
import com.mdd.common.core.basics.IBaseMapper;
|
||||||
|
import com.mdd.common.entity.bank.BankFeePayOrder;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BankFeePayOrderMapper extends IBaseMapper<BankFeePayOrder> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.mdd.common.mapper.bank;
|
||||||
|
|
||||||
|
import com.mdd.common.core.basics.IBaseMapper;
|
||||||
|
import com.mdd.common.entity.bank.EnrollmentPayOrder;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EnrollmentPayOrderMapper extends IBaseMapper<EnrollmentPayOrder> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package com.mdd.front.config;
|
||||||
|
|
||||||
|
import com.cib.fintech.dfp.open.sdk.config.Configure;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.config.KeyConfigure;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.enums.KeySignTypeEnum;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.enums.RespSignAlgorithmEnum;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴业数金开放银行 SDK 初始化配置。
|
||||||
|
* <p>
|
||||||
|
* 注意:私钥/加密密钥仅从服务端配置读取,不允许下发到前端。
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DfpOpenSdkInitConfig {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DfpOpenSdkInitConfig.class);
|
||||||
|
|
||||||
|
@Value("${dfp-open.dev-env:true}")
|
||||||
|
private boolean devEnv;
|
||||||
|
|
||||||
|
@Value("${dfp-open.keyId:}")
|
||||||
|
private String keyId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.priKey:}")
|
||||||
|
private String priKey;
|
||||||
|
|
||||||
|
@Value("${dfp-open.respPubKey:}")
|
||||||
|
private String respPubKey;
|
||||||
|
|
||||||
|
@Value("${dfp-open.reqParamEncryptKey:}")
|
||||||
|
private String reqParamEncryptKey;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (isBlank(keyId) || isBlank(priKey) || isBlank(respPubKey) || isBlank(reqParamEncryptKey)) {
|
||||||
|
log.warn("dfp-open 配置未完整,跳过 SDK 初始化。keyId={}, devEnv={}", safeKeyId(keyId), devEnv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 设置环境(测试/生产网关地址会随 devEnv 变化)
|
||||||
|
Configure.setDevEnv(devEnv);
|
||||||
|
// 2) 配置默认 keyId(确保 OpenSdk.gatewayWithKeyId 可传入正确的 keyId)
|
||||||
|
Configure.setKeyId(keyId);
|
||||||
|
|
||||||
|
// 2) 配置应用密钥
|
||||||
|
KeyConfigure keyConfigure = new KeyConfigure();
|
||||||
|
keyConfigure.setKeyId(keyId);
|
||||||
|
keyConfigure.setPriKey(priKey);
|
||||||
|
keyConfigure.setRespPubKey(respPubKey);
|
||||||
|
keyConfigure.setReqParamEncryptKey(reqParamEncryptKey);
|
||||||
|
|
||||||
|
// 请求签名算法与响应验签算法
|
||||||
|
keyConfigure.setKeySignType(KeySignTypeEnum.SM3WITHSM2);
|
||||||
|
keyConfigure.setRespSignSwitch(true);
|
||||||
|
keyConfigure.setRespSignAlgorithm(RespSignAlgorithmEnum.SM3WITHSM2);
|
||||||
|
|
||||||
|
Map<String, KeyConfigure> keyConfigures = new HashMap<>(4);
|
||||||
|
keyConfigures.put(keyId, keyConfigure);
|
||||||
|
Configure.setKeyConfigures(keyConfigures);
|
||||||
|
|
||||||
|
log.info("dfp-open SDK 初始化完成。keyIdPrefix={}, devEnv={}", safeKeyIdPrefix(keyId), devEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String s) {
|
||||||
|
return s == null || s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeKeyId(String s) {
|
||||||
|
if (isBlank(s)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
s = s.trim();
|
||||||
|
if (s.length() <= 6) {
|
||||||
|
return "******";
|
||||||
|
}
|
||||||
|
return s.substring(0, 2) + "******" + s.substring(s.length() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeKeyIdPrefix(String s) {
|
||||||
|
if (isBlank(s)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
s = s.trim();
|
||||||
|
return s.length() <= 4 ? s : s.substring(0, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
package com.mdd.front.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.config.Configure;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.config.KeyConfigure;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.exception.SdkException;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.util.CallbackUtil;
|
||||||
|
import com.mdd.common.aop.NotLogin;
|
||||||
|
import com.mdd.common.exception.OperateException;
|
||||||
|
import com.mdd.common.util.StringUtils;
|
||||||
|
import com.mdd.front.service.IBankFeeNotifyService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴业数金开放银行:异步通知(验签 + SM4 解密 + 业务落库)。
|
||||||
|
* <p>
|
||||||
|
* 说明:
|
||||||
|
* - 回调为银行服务器主动调用,不需要登录。
|
||||||
|
* - 验签失败建议返回 fail,让银行重试;验签成功但业务幂等已处理,可直接 success。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/bank-fee")
|
||||||
|
@Api(tags = "开放银行支付回调")
|
||||||
|
public class BankFeeNotifyController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BankFeeNotifyController.class);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IBankFeeNotifyService bankFeeNotifyService;
|
||||||
|
|
||||||
|
@PostMapping("/notify")
|
||||||
|
@NotLogin
|
||||||
|
@ApiOperation("开放银行异步通知(验签解密后更新缴费状态)")
|
||||||
|
public String notify(HttpServletRequest request, @RequestBody(required = false) String rawBody) {
|
||||||
|
String keyId = header(request, "Keyid");
|
||||||
|
String timestamp = header(request, "Timestamp");
|
||||||
|
String nonce = header(request, "Nonce");
|
||||||
|
String signature = header(request, "Signature");
|
||||||
|
String pwd = header(request, "Pwd"); // V2 可能存在
|
||||||
|
|
||||||
|
String ciphertext = extractCiphertext(rawBody);
|
||||||
|
if (StringUtils.isBlank(ciphertext)) {
|
||||||
|
log.warn("bank-fee notify missing ciphertext. keyIdPrefix={}, rawLen={}", safePrefix(keyId, 6),
|
||||||
|
rawBody == null ? 0 : rawBody.length());
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyConfigure keyConfigure = resolveKeyConfigure(keyId);
|
||||||
|
if (keyConfigure == null) {
|
||||||
|
log.error("bank-fee notify no keyConfigure. keyIdPrefix={}", safePrefix(keyId, 6));
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
String decrypted;
|
||||||
|
try {
|
||||||
|
if (StringUtils.isNotBlank(pwd)) {
|
||||||
|
decrypted = CallbackUtil.decryptAndVerifyV2(
|
||||||
|
keyId,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
pwd,
|
||||||
|
signature,
|
||||||
|
ciphertext,
|
||||||
|
keyConfigure.getPriKey(),
|
||||||
|
keyConfigure.getRespPubKey()
|
||||||
|
).get("body");
|
||||||
|
} else {
|
||||||
|
decrypted = CallbackUtil.decryptAndVerify(
|
||||||
|
keyId,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
signature,
|
||||||
|
ciphertext,
|
||||||
|
keyConfigure.getReqParamEncryptKey(),
|
||||||
|
keyConfigure.getRespPubKey()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (SdkException e) {
|
||||||
|
log.error("bank-fee notify verify/decrypt failed. code={}, msg={}, keyIdPrefix={}", e.getCode(),
|
||||||
|
e.getMsg(), safePrefix(keyId, 6));
|
||||||
|
return "fail";
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("bank-fee notify verify/decrypt failed. keyIdPrefix={}, err={}", safePrefix(keyId, 6), e.toString());
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject bodyJson;
|
||||||
|
try {
|
||||||
|
bodyJson = JSONObject.parseObject(decrypted);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("bank-fee notify decrypted not json. keyIdPrefix={}, decryptedPrefix={}",
|
||||||
|
safePrefix(keyId, 6), safePrefix(decrypted, 80));
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
bankFeeNotifyService.handlePaidNotify(bodyJson);
|
||||||
|
} catch (OperateException oe) {
|
||||||
|
// 业务异常:为避免银行无限重试,这里仍返回 success,但记录错误
|
||||||
|
log.error("bank-fee notify business error. msg={}, bodyKeys={}", oe.getMessage(), bodyJson.keySet());
|
||||||
|
return "success";
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("bank-fee notify business error. err={}, bodyKeys={}", e.toString(), bodyJson.keySet());
|
||||||
|
// 未知异常可让银行重试(你也可以改为 success 并自行补偿)
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyConfigure resolveKeyConfigure(String keyIdHeader) {
|
||||||
|
if (StringUtils.isNotBlank(keyIdHeader)) {
|
||||||
|
KeyConfigure cfg = Configure.getKeyConfigures().get(keyIdHeader.trim());
|
||||||
|
if (cfg != null) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: 取默认 keyId 的配置
|
||||||
|
try {
|
||||||
|
return Configure.getKeyConfigure();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractCiphertext(String rawBody) {
|
||||||
|
if (rawBody == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s = rawBody.trim();
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (s.startsWith("{") && s.endsWith("}")) {
|
||||||
|
try {
|
||||||
|
JSONObject json = JSONObject.parseObject(s);
|
||||||
|
String ciphertext = json.getString("ciphertext");
|
||||||
|
if (StringUtils.isNotBlank(ciphertext)) {
|
||||||
|
return ciphertext.trim();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 有些环境可能直接把 ciphertext 作为 body 传入
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String header(HttpServletRequest request, String name) {
|
||||||
|
String v = request.getHeader(name);
|
||||||
|
return v == null ? "" : v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safePrefix(String s, int maxLen) {
|
||||||
|
if (s == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
s = s.trim();
|
||||||
|
if (s.length() <= maxLen) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return s.substring(0, maxLen) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.mdd.front.controller;
|
||||||
|
|
||||||
|
import com.mdd.common.core.AjaxResult;
|
||||||
|
import com.mdd.front.LikeFrontThreadLocal;
|
||||||
|
import com.mdd.front.service.IDfpOpenPayService;
|
||||||
|
import com.mdd.front.validate.bankfee.DfpAlipayJspayValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.DfpWechatJspayValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.DfpPayPrepareVo;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴业数金开放银行:正式下单入口(需登录)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/bank-fee/pay")
|
||||||
|
@Api(tags = "开放银行聚合支付")
|
||||||
|
public class BankFeePayController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDfpOpenPayService dfpOpenPayService;
|
||||||
|
|
||||||
|
@PostMapping("/wechat")
|
||||||
|
@ApiOperation("微信小程序 jspay 下单(服务端根据登录用户查询 sub_openid)")
|
||||||
|
public AjaxResult<DfpPayPrepareVo> wechatMini(@Validated @RequestBody DfpWechatJspayValidate validate) {
|
||||||
|
DfpPayPrepareVo vo = dfpOpenPayService.prepareWechatMiniJspay(LikeFrontThreadLocal.getUserId(), validate);
|
||||||
|
return AjaxResult.success(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/alipay")
|
||||||
|
@ApiOperation("支付宝服务窗 jspay 下单")
|
||||||
|
public AjaxResult<DfpPayPrepareVo> alipay(@Validated @RequestBody DfpAlipayJspayValidate validate) {
|
||||||
|
DfpPayPrepareVo vo = dfpOpenPayService.prepareAlipayJspay(LikeFrontThreadLocal.getUserId(), validate);
|
||||||
|
return AjaxResult.success(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.mdd.front.controller;
|
||||||
|
|
||||||
|
import com.mdd.common.core.AjaxResult;
|
||||||
|
import com.mdd.front.LikeFrontThreadLocal;
|
||||||
|
import com.mdd.front.service.IBankFeeTradeService;
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeCloseValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeQueryValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.BankFeeTradeStatusVo;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/bank-fee/trade")
|
||||||
|
@Api(tags = "开放银行-查单关单")
|
||||||
|
public class BankFeeTradeController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IBankFeeTradeService bankFeeTradeService;
|
||||||
|
|
||||||
|
@PostMapping("/query")
|
||||||
|
@ApiOperation("开放银行查单(查到 SUCCESS 会自动落库更新缴费状态)")
|
||||||
|
public AjaxResult<BankFeeTradeStatusVo> query(@Validated @RequestBody BankFeeTradeQueryValidate validate) {
|
||||||
|
BankFeeTradeStatusVo vo = bankFeeTradeService.query(LikeFrontThreadLocal.getUserId(), validate);
|
||||||
|
return AjaxResult.success(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/close")
|
||||||
|
@ApiOperation("开放银行关单")
|
||||||
|
public AjaxResult<BankFeeTradeStatusVo> close(@Validated @RequestBody BankFeeTradeCloseValidate validate) {
|
||||||
|
BankFeeTradeStatusVo vo = bankFeeTradeService.close(LikeFrontThreadLocal.getUserId(), validate);
|
||||||
|
return AjaxResult.success(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
package com.mdd.front.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.OpenSdk;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum;
|
||||||
|
import com.mdd.common.aop.NotLogin;
|
||||||
|
import com.mdd.common.core.AjaxResult;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴业数金 dfp-open 临时测试接口(仅用于联调/测试环境)。
|
||||||
|
* <p>
|
||||||
|
* 注意:不要把私钥/加密密钥放到前端或返回给前端。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/frontapi/bank-open/test")
|
||||||
|
@Api(tags = "银行SDK测试(临时)")
|
||||||
|
public class BankOpenSdkTestController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BankOpenSdkTestController.class);
|
||||||
|
|
||||||
|
@Value("${dfp-open.keyId:}")
|
||||||
|
private String keyId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.mchId:}")
|
||||||
|
private String mchId;
|
||||||
|
|
||||||
|
@PostMapping("/terminalInfoInput")
|
||||||
|
@NotLogin
|
||||||
|
@ApiOperation("4.3 终端信息采集/入网(临时测试)")
|
||||||
|
public AjaxResult<Object> terminalInfoInput(@Validated @RequestBody TerminalInfoInputReq req) {
|
||||||
|
String outTermId = blankToNull(req.getOutTermId());
|
||||||
|
if (outTermId == null) {
|
||||||
|
return AjaxResult.success(0, "outTermId 参数必填", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
String termType = blankToNull(req.getTermType());
|
||||||
|
if (termType == null) {
|
||||||
|
termType = "01";
|
||||||
|
}
|
||||||
|
|
||||||
|
String operationFlag = blankToNull(req.getOperationFlag());
|
||||||
|
if (operationFlag == null) {
|
||||||
|
operationFlag = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
String version = blankToNull(req.getVersion());
|
||||||
|
if (version == null) {
|
||||||
|
version = "1.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
String mchIdResolved = blankToNull(req.getMchId());
|
||||||
|
if (mchIdResolved == null) {
|
||||||
|
mchIdResolved = blankToNull(this.mchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mchIdResolved == null) {
|
||||||
|
return AjaxResult.success(0, "mchId 缺失:请在 yml 配置 dfp-open.mchId,或在请求体传 mchId", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blankToNull(keyId) == null) {
|
||||||
|
return AjaxResult.success(0, "keyId 缺失:请在 yml 配置 dfp-open.keyId", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> bodyParams = new HashMap<>(8);
|
||||||
|
bodyParams.put("version", version);
|
||||||
|
bodyParams.put("mchId", mchIdResolved);
|
||||||
|
bodyParams.put("termType", termType);
|
||||||
|
bodyParams.put("operationFlag", operationFlag);
|
||||||
|
bodyParams.put("outTermId", outTermId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String response = OpenSdk.gatewayWithKeyId(
|
||||||
|
"/api/payment/wholeNetworkAcquiring/new/terminalInfoInput",
|
||||||
|
ReqMethodEnum.POST,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
bodyParams,
|
||||||
|
keyId
|
||||||
|
);
|
||||||
|
|
||||||
|
String termId = null;
|
||||||
|
String termIdAlt = null;
|
||||||
|
boolean showRaw = Boolean.TRUE.equals(req.getShowRaw());
|
||||||
|
try {
|
||||||
|
JSONObject json = JSONObject.parseObject(response);
|
||||||
|
termId = json.getString("termId");
|
||||||
|
if (termId == null) {
|
||||||
|
termIdAlt = json.getString("term_id");
|
||||||
|
if (termIdAlt == null) {
|
||||||
|
termIdAlt = json.getString("terminal_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception parseEx) {
|
||||||
|
// 如果返回不是 JSON(SDK 异常 toString),termId 可能为空
|
||||||
|
log.warn("terminalInfoInput 返回非 JSON,无法解析 termId。responsePrefix={}", safePrefix(response, 120));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>(3);
|
||||||
|
data.put("termId", termId != null ? termId : termIdAlt);
|
||||||
|
data.put("outTermId", outTermId);
|
||||||
|
// 如果 termId 解析不到,自动返回原始响应,方便定位错误原因
|
||||||
|
if (showRaw || (termId == null && termIdAlt == null)) {
|
||||||
|
data.put("response", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AjaxResult.success(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("terminalInfoInput 调用银行网关失败。outTermId={}, err={}", outTermId, e.toString());
|
||||||
|
return AjaxResult.success(0, "调用银行网关失败:" + e.getMessage(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String s) {
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
s = s.trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safePrefix(String s, int maxLen) {
|
||||||
|
if (s == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (s.length() <= maxLen) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return s.substring(0, maxLen) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public static class TerminalInfoInputReq {
|
||||||
|
// 商户侧终端编号(必填)
|
||||||
|
private String outTermId;
|
||||||
|
// 终端类型(默认 01)
|
||||||
|
private String termType;
|
||||||
|
// 操作标志(默认 0)
|
||||||
|
private String operationFlag;
|
||||||
|
// 版本(默认 1.0.0)
|
||||||
|
private String version;
|
||||||
|
// 商户号(可从 yml dfp-open.mchId 默认)
|
||||||
|
private String mchId;
|
||||||
|
// 是否返回银行原始 response(临时联调用)
|
||||||
|
private Boolean showRaw = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.mdd.front.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开放银行回调处理(缴费/业务订单)。
|
||||||
|
*/
|
||||||
|
public interface IBankFeeNotifyService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理开放银行支付成功回调(明文 JSON)。
|
||||||
|
*
|
||||||
|
* @param notifyBody 已解密且已验签后的明文 JSON
|
||||||
|
*/
|
||||||
|
void handlePaidNotify(JSONObject notifyBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.mdd.front.service;
|
||||||
|
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeCloseValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeQueryValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.BankFeeTradeStatusVo;
|
||||||
|
|
||||||
|
public interface IBankFeeTradeService {
|
||||||
|
|
||||||
|
BankFeeTradeStatusVo query(Integer userId, BankFeeTradeQueryValidate validate);
|
||||||
|
|
||||||
|
BankFeeTradeStatusVo close(Integer userId, BankFeeTradeCloseValidate validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.mdd.front.service;
|
||||||
|
|
||||||
|
import com.mdd.front.validate.bankfee.DfpAlipayJspayValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.DfpWechatJspayValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.DfpPayPrepareVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴业数金开放银行:小程序微信 / 支付宝服务窗 下单(jspay)
|
||||||
|
*/
|
||||||
|
public interface IDfpOpenPayService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序 pay.weixin.jspay
|
||||||
|
*/
|
||||||
|
DfpPayPrepareVo prepareWechatMiniJspay(Integer userId, DfpWechatJspayValidate validate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝服务窗 pay.alipay.jspay
|
||||||
|
*/
|
||||||
|
DfpPayPrepareVo prepareAlipayJspay(Integer userId, DfpAlipayJspayValidate validate);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
package com.mdd.front.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.mdd.common.entity.StudentInfo;
|
||||||
|
import com.mdd.common.entity.bank.EnrollmentPayOrder;
|
||||||
|
import com.mdd.common.mapper.StudentInfoMapper;
|
||||||
|
import com.mdd.common.mapper.bank.EnrollmentPayOrderMapper;
|
||||||
|
import com.mdd.common.util.StringUtils;
|
||||||
|
import com.mdd.common.util.TimeUtils;
|
||||||
|
import com.mdd.front.service.IBankFeeNotifyService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BankFeeNotifyServiceImpl implements IBankFeeNotifyService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BankFeeNotifyServiceImpl.class);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StudentInfoMapper studentInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private EnrollmentPayOrderMapper enrollmentPayOrderMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void handlePaidNotify(JSONObject notifyBody) {
|
||||||
|
if (notifyBody == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String outTradeNo = firstNonBlank(
|
||||||
|
notifyBody.getString("out_trade_no"),
|
||||||
|
notifyBody.getString("outTradeNo"),
|
||||||
|
notifyBody.getString("application_number"),
|
||||||
|
notifyBody.getString("applicationNumber")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(outTradeNo)) {
|
||||||
|
log.warn("bank-fee notify missing out_trade_no. bodyKeys={}", notifyBody.keySet());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功判断:尽量兼容不同回调字段
|
||||||
|
boolean success = isSuccess(notifyBody);
|
||||||
|
if (!success) {
|
||||||
|
log.info("bank-fee notify not success. outTradeNo={}, brief={}", outTradeNo, brief(notifyBody));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
|
||||||
|
// 先更新/补齐流水表(幂等)
|
||||||
|
EnrollmentPayOrder payOrder = enrollmentPayOrderMapper.selectOne(new QueryWrapper<EnrollmentPayOrder>()
|
||||||
|
.eq("out_trade_no", outTradeNo)
|
||||||
|
.isNull("delete_time")
|
||||||
|
.last("limit 1"));
|
||||||
|
if (payOrder == null) {
|
||||||
|
payOrder = new EnrollmentPayOrder();
|
||||||
|
payOrder.setStudentId(tryParseStudentId(outTradeNo));
|
||||||
|
payOrder.setOutTradeNo(outTradeNo);
|
||||||
|
payOrder.setPayStatus(0);
|
||||||
|
payOrder.setCreateTime(now);
|
||||||
|
payOrder.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.insert(payOrder);
|
||||||
|
}
|
||||||
|
if (payOrder.getPayStatus() == null || payOrder.getPayStatus() != 1) {
|
||||||
|
payOrder.setPayStatus(1);
|
||||||
|
payOrder.setPayTime(now);
|
||||||
|
payOrder.setTradeState(firstNonBlank(notifyBody.getString("trade_state"), notifyBody.getString("tradeState")));
|
||||||
|
payOrder.setBankTransactionId(firstNonBlank(
|
||||||
|
notifyBody.getString("transaction_id"),
|
||||||
|
notifyBody.getString("transactionId"),
|
||||||
|
notifyBody.getString("trade_no"),
|
||||||
|
notifyBody.getString("tradeNo")
|
||||||
|
));
|
||||||
|
payOrder.setLastBankBody(storeRaw(notifyBody));
|
||||||
|
payOrder.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.updateById(payOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定到学生信息表(幂等)
|
||||||
|
if (payOrder.getStudentId() != null) {
|
||||||
|
StudentInfo studentInfo = studentInfoMapper.selectById(payOrder.getStudentId());
|
||||||
|
if (studentInfo != null && studentInfo.getPayOrderId() == null) {
|
||||||
|
studentInfo.setPayOrderId(payOrder.getId());
|
||||||
|
studentInfoMapper.updateById(studentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("bank-fee notify marked paid. outTradeNo={}, payOrderId={}", outTradeNo, payOrder.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSuccess(JSONObject body) {
|
||||||
|
// 常见:status/result_code=0;或 trade_state=SUCCESS;或 pay_result=0
|
||||||
|
String status = firstNonBlank(body.getString("status"), body.getString("Status"));
|
||||||
|
String resultCode = firstNonBlank(body.getString("result_code"), body.getString("resultCode"));
|
||||||
|
String tradeState = firstNonBlank(body.getString("trade_state"), body.getString("tradeState"));
|
||||||
|
String payResult = firstNonBlank(body.getString("pay_result"), body.getString("payResult"));
|
||||||
|
|
||||||
|
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("0".equals(status) && ("0".equals(resultCode) || StringUtils.isBlank(resultCode))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("0".equals(payResult)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 兜底:有时仅给 result_code
|
||||||
|
return "0".equals(resultCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... arr) {
|
||||||
|
if (arr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String s : arr) {
|
||||||
|
if (StringUtils.isNotBlank(s)) {
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String brief(JSONObject body) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("status", body.getString("status"));
|
||||||
|
o.put("result_code", body.getString("result_code"));
|
||||||
|
o.put("trade_state", body.getString("trade_state"));
|
||||||
|
o.put("err_msg", body.getString("err_msg"));
|
||||||
|
o.put("message", body.getString("message"));
|
||||||
|
return o.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String storeRaw(JSONObject body) {
|
||||||
|
String raw = body == null ? null : body.toJSONString();
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw.length() > 20000 ? raw.substring(0, 20000) : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long tryParseStudentId(String outTradeNo) {
|
||||||
|
if (StringUtils.isBlank(outTradeNo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(outTradeNo.trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
package com.mdd.front.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.OpenSdk;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum;
|
||||||
|
import com.mdd.common.entity.StudentInfo;
|
||||||
|
import com.mdd.common.entity.bank.EnrollmentPayOrder;
|
||||||
|
import com.mdd.common.exception.OperateException;
|
||||||
|
import com.mdd.common.mapper.StudentInfoMapper;
|
||||||
|
import com.mdd.common.mapper.bank.EnrollmentPayOrderMapper;
|
||||||
|
import com.mdd.common.util.StringUtils;
|
||||||
|
import com.mdd.common.util.TimeUtils;
|
||||||
|
import com.mdd.front.service.IBankFeeTradeService;
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeCloseValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.BankFeeTradeQueryValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.BankFeeTradeStatusVo;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BankFeeTradeServiceImpl implements IBankFeeTradeService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BankFeeTradeServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String PATH_UNIFIED_TRADE_QUERY = "/api/payment/wholeNetworkAcquiring/new/unifiedTradeQuery";
|
||||||
|
private static final String PATH_UNIFIED_TRADE_CLOSE = "/api/payment/wholeNetworkAcquiring/new/unifiedTradeClose";
|
||||||
|
|
||||||
|
@Value("${dfp-open.keyId:}")
|
||||||
|
private String keyId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.mchId:}")
|
||||||
|
private String mchId;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private EnrollmentPayOrderMapper enrollmentPayOrderMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StudentInfoMapper studentInfoMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public BankFeeTradeStatusVo query(Integer userId, BankFeeTradeQueryValidate validate) {
|
||||||
|
assertLogin(userId);
|
||||||
|
String outTradeNo = validate.getOutTradeNo().trim();
|
||||||
|
|
||||||
|
String raw = callBank(PATH_UNIFIED_TRADE_QUERY, buildQueryBody(outTradeNo));
|
||||||
|
BankFeeTradeStatusVo vo = toStatusVo(outTradeNo, raw, Boolean.TRUE.equals(validate.getIncludeRawResponse()));
|
||||||
|
|
||||||
|
// 如果查到成功,收口幂等:更新流水表与业务表
|
||||||
|
if (isSuccess(vo) && "SUCCESS".equalsIgnoreCase(vo.getTradeState())) {
|
||||||
|
markPaid(outTradeNo, vo, raw);
|
||||||
|
} else {
|
||||||
|
touchTradeState(outTradeNo, vo, raw);
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public BankFeeTradeStatusVo close(Integer userId, BankFeeTradeCloseValidate validate) {
|
||||||
|
assertLogin(userId);
|
||||||
|
String outTradeNo = validate.getOutTradeNo().trim();
|
||||||
|
|
||||||
|
String raw = callBank(PATH_UNIFIED_TRADE_CLOSE, buildCloseBody(outTradeNo));
|
||||||
|
BankFeeTradeStatusVo vo = toStatusVo(outTradeNo, raw, Boolean.TRUE.equals(validate.getIncludeRawResponse()));
|
||||||
|
|
||||||
|
if (isSuccess(vo)) {
|
||||||
|
markClosed(outTradeNo, vo, raw);
|
||||||
|
} else {
|
||||||
|
touchTradeState(outTradeNo, vo, raw);
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLogin(Integer userId) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new OperateException("请先登录");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(keyId) || StringUtils.isBlank(mchId)) {
|
||||||
|
throw new OperateException("dfp-open.keyId / dfp-open.mchId 未配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> buildQueryBody(String outTradeNo) {
|
||||||
|
Map<String, String> body = new HashMap<>(8);
|
||||||
|
body.put("version", "3.0");
|
||||||
|
body.put("service", "unified.trade.query");
|
||||||
|
body.put("charset", "UTF-8");
|
||||||
|
body.put("mch_id", mchId);
|
||||||
|
body.put("out_trade_no", outTradeNo);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> buildCloseBody(String outTradeNo) {
|
||||||
|
Map<String, String> body = new HashMap<>(8);
|
||||||
|
body.put("version", "3.0");
|
||||||
|
body.put("service", "unified.trade.close");
|
||||||
|
body.put("charset", "UTF-8");
|
||||||
|
body.put("mch_id", mchId);
|
||||||
|
body.put("out_trade_no", outTradeNo);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callBank(String path, Map<String, String> bodyParams) {
|
||||||
|
try {
|
||||||
|
return OpenSdk.gatewayWithKeyId(path, ReqMethodEnum.POST, null, null, bodyParams, keyId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OperateException("调用银行网关失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BankFeeTradeStatusVo toStatusVo(String outTradeNo, String raw, boolean includeRaw) {
|
||||||
|
BankFeeTradeStatusVo vo = new BankFeeTradeStatusVo();
|
||||||
|
vo.setOutTradeNo(outTradeNo);
|
||||||
|
vo.setBankResponseRaw(includeRaw ? raw : null);
|
||||||
|
if (StringUtils.isBlank(raw)) {
|
||||||
|
vo.setErrMsg("银行返回为空");
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSONObject json = JSONObject.parseObject(raw);
|
||||||
|
vo.setStatus(json.getString("status"));
|
||||||
|
vo.setResultCode(json.getString("result_code"));
|
||||||
|
vo.setTradeState(firstNonBlank(json.getString("trade_state"), json.getString("tradeState")));
|
||||||
|
vo.setBankTransactionId(firstNonBlank(
|
||||||
|
json.getString("transaction_id"),
|
||||||
|
json.getString("transactionId"),
|
||||||
|
json.getString("trade_no"),
|
||||||
|
json.getString("tradeNo")
|
||||||
|
));
|
||||||
|
String totalFee = firstNonBlank(json.getString("total_fee"), json.getString("totalFee"));
|
||||||
|
if (StringUtils.isNotBlank(totalFee)) {
|
||||||
|
try {
|
||||||
|
vo.setTotalFeeFen(Integer.parseInt(totalFee.trim()));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vo.setErrMsg(firstNonBlank(json.getString("err_msg"), json.getString("message")));
|
||||||
|
} catch (Exception e) {
|
||||||
|
vo.setErrMsg("银行响应非 JSON,无法解析");
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSuccess(BankFeeTradeStatusVo vo) {
|
||||||
|
return "0".equals(vo.getStatus()) && "0".equals(vo.getResultCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markPaid(String outTradeNo, BankFeeTradeStatusVo vo, String raw) {
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
|
||||||
|
EnrollmentPayOrder payOrder = findOrCreate(outTradeNo, now);
|
||||||
|
if (payOrder.getPayStatus() != null && payOrder.getPayStatus() == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payOrder.setPayStatus(1);
|
||||||
|
payOrder.setPayTime(now);
|
||||||
|
payOrder.setTradeState(vo.getTradeState());
|
||||||
|
payOrder.setBankTransactionId(vo.getBankTransactionId());
|
||||||
|
payOrder.setLastBankBody(safeStoreRaw(raw));
|
||||||
|
payOrder.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.updateById(payOrder);
|
||||||
|
|
||||||
|
// 绑定到学生信息表(幂等)
|
||||||
|
if (payOrder.getStudentId() != null) {
|
||||||
|
StudentInfo studentInfo = studentInfoMapper.selectById(payOrder.getStudentId());
|
||||||
|
if (studentInfo != null && studentInfo.getPayOrderId() == null) {
|
||||||
|
studentInfo.setPayOrderId(payOrder.getId());
|
||||||
|
studentInfoMapper.updateById(studentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markClosed(String outTradeNo, BankFeeTradeStatusVo vo, String raw) {
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
EnrollmentPayOrder payOrder = findOrCreate(outTradeNo, now);
|
||||||
|
if (payOrder.getPayStatus() != null && payOrder.getPayStatus() == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payOrder.setPayStatus(2);
|
||||||
|
payOrder.setTradeState(vo.getTradeState());
|
||||||
|
payOrder.setLastBankBody(safeStoreRaw(raw));
|
||||||
|
payOrder.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.updateById(payOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touchTradeState(String outTradeNo, BankFeeTradeStatusVo vo, String raw) {
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
EnrollmentPayOrder payOrder = findOrCreate(outTradeNo, now);
|
||||||
|
if (payOrder.getPayStatus() != null && payOrder.getPayStatus() == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getTradeState())) {
|
||||||
|
payOrder.setTradeState(vo.getTradeState());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getBankTransactionId())) {
|
||||||
|
payOrder.setBankTransactionId(vo.getBankTransactionId());
|
||||||
|
}
|
||||||
|
payOrder.setLastBankBody(safeStoreRaw(raw));
|
||||||
|
payOrder.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.updateById(payOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnrollmentPayOrder findOrCreate(String outTradeNo, long now) {
|
||||||
|
EnrollmentPayOrder existing = enrollmentPayOrderMapper.selectOne(new QueryWrapper<EnrollmentPayOrder>()
|
||||||
|
.eq("out_trade_no", outTradeNo)
|
||||||
|
.isNull("delete_time")
|
||||||
|
.last("limit 1"));
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
EnrollmentPayOrder created = new EnrollmentPayOrder();
|
||||||
|
created.setStudentId(tryParseStudentId(outTradeNo));
|
||||||
|
created.setOutTradeNo(outTradeNo);
|
||||||
|
created.setPayStatus(0);
|
||||||
|
created.setCreateTime(now);
|
||||||
|
created.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.insert(created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeStoreRaw(String raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw.length() > 20000 ? raw.substring(0, 20000) : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... arr) {
|
||||||
|
if (arr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String s : arr) {
|
||||||
|
if (StringUtils.isNotBlank(s)) {
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long tryParseStudentId(String outTradeNo) {
|
||||||
|
if (StringUtils.isBlank(outTradeNo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(outTradeNo.trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
package com.mdd.front.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.OpenSdk;
|
||||||
|
import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum;
|
||||||
|
import com.mdd.common.entity.StudentInfo;
|
||||||
|
import com.mdd.common.entity.bank.EnrollmentPayOrder;
|
||||||
|
import com.mdd.common.entity.user.UserAuth;
|
||||||
|
import com.mdd.common.enums.ClientEnum;
|
||||||
|
import com.mdd.common.exception.OperateException;
|
||||||
|
import com.mdd.common.mapper.StudentInfoMapper;
|
||||||
|
import com.mdd.common.mapper.bank.EnrollmentPayOrderMapper;
|
||||||
|
import com.mdd.common.mapper.user.UserAuthMapper;
|
||||||
|
import com.mdd.common.util.ConfigUtils;
|
||||||
|
import com.mdd.common.util.RequestUtils;
|
||||||
|
import com.mdd.common.util.StringUtils;
|
||||||
|
import com.mdd.common.util.TimeUtils;
|
||||||
|
import com.mdd.front.service.IDfpOpenPayService;
|
||||||
|
import com.mdd.front.validate.bankfee.DfpAlipayJspayValidate;
|
||||||
|
import com.mdd.front.validate.bankfee.DfpWechatJspayValidate;
|
||||||
|
import com.mdd.front.vo.bankfee.DfpPayPrepareVo;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DfpOpenPayServiceImpl implements IDfpOpenPayService {
|
||||||
|
|
||||||
|
private static final String PATH_WECHAT_JSPAY = "/api/payment/wholeNetworkAcquiring/new/payWechatJspay";
|
||||||
|
private static final String PATH_ALIPAY_JSPAY = "/api/payment/wholeNetworkAcquiring/new/payAlipayJspay";
|
||||||
|
|
||||||
|
@Value("${dfp-open.keyId:}")
|
||||||
|
private String keyId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.mchId:}")
|
||||||
|
private String mchId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.termId:}")
|
||||||
|
private String termId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 appId,优先 yml;否则读库表 mnp_setting.app_id
|
||||||
|
*/
|
||||||
|
@Value("${dfp-open.appId:}")
|
||||||
|
private String dfpMiniAppId;
|
||||||
|
|
||||||
|
@Value("${dfp-open.notify-url:}")
|
||||||
|
private String notifyUrlOverride;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserAuthMapper userAuthMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private EnrollmentPayOrderMapper enrollmentPayOrderMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StudentInfoMapper studentInfoMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DfpPayPrepareVo prepareWechatMiniJspay(Integer userId, DfpWechatJspayValidate validate) {
|
||||||
|
assertUser(userId);
|
||||||
|
String openId = resolveMiniOpenId(userId);
|
||||||
|
String subAppId = resolveMiniAppId();
|
||||||
|
String outTradeNo = createEnrollmentPayOrderAndReturnOutTradeNo(userId, validate.getStudentId(), 2, validate.getAmountFen());
|
||||||
|
String bodyText = StringUtils.isBlank(validate.getBody()) ? "缴费支付" : validate.getBody().trim();
|
||||||
|
String notifyUrl = resolveNotifyUrl();
|
||||||
|
|
||||||
|
Map<String, String> bodyParams = baseBodyParams(outTradeNo, validate.getAmountFen(), bodyText, notifyUrl);
|
||||||
|
bodyParams.put("service", "pay.weixin.jspay");
|
||||||
|
bodyParams.put("is_minipg", "1");
|
||||||
|
bodyParams.put("sub_appid", subAppId);
|
||||||
|
bodyParams.put("sub_openid", openId);
|
||||||
|
|
||||||
|
String raw = callBank(PATH_WECHAT_JSPAY, bodyParams);
|
||||||
|
DfpPayPrepareVo vo = toVo(outTradeNo, raw, Boolean.TRUE.equals(validate.getIncludeRawResponse()));
|
||||||
|
updatePayOrderAfterBankPlace(outTradeNo, vo, raw);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DfpPayPrepareVo prepareAlipayJspay(Integer userId, DfpAlipayJspayValidate validate) {
|
||||||
|
assertUser(userId);
|
||||||
|
String outTradeNo = createEnrollmentPayOrderAndReturnOutTradeNo(userId, validate.getStudentId(), 3, validate.getAmountFen());
|
||||||
|
String bodyText = StringUtils.isBlank(validate.getBody()) ? "缴费支付" : validate.getBody().trim();
|
||||||
|
String notifyUrl = resolveNotifyUrl();
|
||||||
|
|
||||||
|
Map<String, String> bodyParams = baseBodyParams(outTradeNo, validate.getAmountFen(), bodyText, notifyUrl);
|
||||||
|
bodyParams.put("service", "pay.alipay.jspay");
|
||||||
|
bodyParams.put("buyer_logon_id", validate.getBuyerLogonId() == null ? "" : validate.getBuyerLogonId().trim());
|
||||||
|
|
||||||
|
String raw = callBank(PATH_ALIPAY_JSPAY, bodyParams);
|
||||||
|
DfpPayPrepareVo vo = toVo(outTradeNo, raw, Boolean.TRUE.equals(validate.getIncludeRawResponse()));
|
||||||
|
updatePayOrderAfterBankPlace(outTradeNo, vo, raw);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertUser(Integer userId) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new OperateException("请先登录");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMiniOpenId(Integer userId) {
|
||||||
|
UserAuth primary = userAuthMapper.selectOne(new QueryWrapper<UserAuth>()
|
||||||
|
.select("openid")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.eq("terminal", ClientEnum.MNP.getCode())
|
||||||
|
.last("limit 1"));
|
||||||
|
if (primary != null && StringUtils.isNotBlank(primary.getOpenid())) {
|
||||||
|
return primary.getOpenid();
|
||||||
|
}
|
||||||
|
UserAuth fallback = userAuthMapper.selectOne(new QueryWrapper<UserAuth>()
|
||||||
|
.select("openid,terminal")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
.orderByDesc("id")
|
||||||
|
.last("limit 1"));
|
||||||
|
if (fallback != null && StringUtils.isNotBlank(fallback.getOpenid())) {
|
||||||
|
return fallback.getOpenid();
|
||||||
|
}
|
||||||
|
throw new OperateException("未找到微信 openid,请使用微信小程序一键登录或先绑定微信");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMiniAppId() {
|
||||||
|
if (StringUtils.isNotBlank(dfpMiniAppId)) {
|
||||||
|
return dfpMiniAppId.trim();
|
||||||
|
}
|
||||||
|
String fromDb = ConfigUtils.get("mnp_setting", "app_id", "");
|
||||||
|
if (StringUtils.isNotBlank(fromDb)) {
|
||||||
|
return fromDb.trim();
|
||||||
|
}
|
||||||
|
throw new OperateException("未配置小程序 appId:请在 yml 配置 dfp-open.appId 或后台 mnp_setting.app_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createEnrollmentPayOrderAndReturnOutTradeNo(Integer userId, Long studentId, int payWay, int amountFen) {
|
||||||
|
StudentInfo studentInfo = null;
|
||||||
|
// studentId 改为可选:只有传入时才校验并尝试绑定
|
||||||
|
if (studentId != null) {
|
||||||
|
if (studentId <= 0) {
|
||||||
|
throw new OperateException("studentId 参数非法");
|
||||||
|
}
|
||||||
|
studentInfo = studentInfoMapper.selectById(studentId);
|
||||||
|
if (studentInfo == null || studentInfo.getDeleteTime() != null) {
|
||||||
|
throw new OperateException("studentId 不存在或已删除:" + studentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
// 先插入一条临时 out_trade_no 获取自增 id
|
||||||
|
EnrollmentPayOrder created = new EnrollmentPayOrder();
|
||||||
|
created.setUserId(userId);
|
||||||
|
created.setStudentId(studentId);
|
||||||
|
created.setOutTradeNo("TMP-" + userId + "-" + System.currentTimeMillis());
|
||||||
|
created.setPayWay(payWay);
|
||||||
|
created.setTotalFeeFen(amountFen);
|
||||||
|
created.setPayStatus(0);
|
||||||
|
created.setCreateTime(now);
|
||||||
|
created.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.insert(created);
|
||||||
|
|
||||||
|
if (created.getId() == null) {
|
||||||
|
throw new OperateException("创建缴费订单失败(未返回ID)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正式规则:QDKXJG-YBMJF-序号(序号=自增ID)
|
||||||
|
String outTradeNo = "QDKXJG-YBMJF-" + created.getId();
|
||||||
|
created.setOutTradeNo(outTradeNo);
|
||||||
|
enrollmentPayOrderMapper.updateById(created);
|
||||||
|
|
||||||
|
// 绑定学生信息
|
||||||
|
if (studentInfo != null && studentInfo.getPayOrderId() == null) {
|
||||||
|
studentInfo.setPayOrderId(created.getId());
|
||||||
|
studentInfoMapper.updateById(studentInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outTradeNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePayOrderAfterBankPlace(String outTradeNo, DfpPayPrepareVo vo, String raw) {
|
||||||
|
long now = TimeUtils.timestamp();
|
||||||
|
EnrollmentPayOrder order = enrollmentPayOrderMapper.selectOne(new QueryWrapper<EnrollmentPayOrder>()
|
||||||
|
.eq("out_trade_no", outTradeNo)
|
||||||
|
.isNull("delete_time")
|
||||||
|
.last("limit 1"));
|
||||||
|
if (order == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
order.setPayInfo(vo == null ? null : vo.getPayInfo());
|
||||||
|
order.setPayUrl(vo == null ? null : vo.getPayUrl());
|
||||||
|
order.setLastBankBody(safeRaw(raw));
|
||||||
|
order.setUpdateTime(now);
|
||||||
|
enrollmentPayOrderMapper.updateById(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeRaw(String raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw.length() > 20000 ? raw.substring(0, 20000) : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveNotifyUrl() {
|
||||||
|
if (StringUtils.isNotBlank(notifyUrlOverride)) {
|
||||||
|
String u = notifyUrlOverride.trim();
|
||||||
|
if (!u.startsWith("http://") && !u.startsWith("https://")) {
|
||||||
|
throw new OperateException("dfp-open.notify-url 必须是完整 http(s) 地址");
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
String base = RequestUtils.uri();
|
||||||
|
if (StringUtils.isBlank(base)) {
|
||||||
|
throw new OperateException("无法解析回调域名,请在 yml 配置 dfp-open.notify-url 为完整 URL");
|
||||||
|
}
|
||||||
|
return trimTrailingSlash(base) + "/api/bank-fee/notify";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimTrailingSlash(String base) {
|
||||||
|
if (base.endsWith("/")) {
|
||||||
|
return base.substring(0, base.length() - 1);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> baseBodyParams(String outTradeNo, int amountFen, String body, String notifyUrl) {
|
||||||
|
if (StringUtils.isBlank(keyId)) {
|
||||||
|
throw new OperateException("dfp-open.keyId 未配置");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(mchId)) {
|
||||||
|
throw new OperateException("dfp-open.mchId 未配置");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(termId)) {
|
||||||
|
throw new OperateException("dfp-open.termId 未配置(请先完成终端入网 4.3)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> bodyParams = new HashMap<>(20);
|
||||||
|
bodyParams.put("version", "3.0");
|
||||||
|
bodyParams.put("charset", "UTF-8");
|
||||||
|
bodyParams.put("mch_id", mchId);
|
||||||
|
bodyParams.put("out_trade_no", outTradeNo);
|
||||||
|
bodyParams.put("total_fee", String.valueOf(amountFen));
|
||||||
|
bodyParams.put("body", StringUtils.abbreviate(body, 120));
|
||||||
|
bodyParams.put("mch_create_ip", resolveClientIp());
|
||||||
|
bodyParams.put("notify_url", notifyUrl);
|
||||||
|
|
||||||
|
bodyParams.put("terminal_info.terminal_type", "11");
|
||||||
|
bodyParams.put("terminal_info.terminal_id", termId);
|
||||||
|
bodyParams.put("terminal_info.app_version", "1.000000");
|
||||||
|
return bodyParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveClientIp() {
|
||||||
|
HttpServletRequest request = RequestUtils.handler();
|
||||||
|
if (request == null) {
|
||||||
|
return "127.0.0.1";
|
||||||
|
}
|
||||||
|
String xff = request.getHeader("X-Forwarded-For");
|
||||||
|
if (StringUtils.isNotBlank(xff) && !"unknown".equalsIgnoreCase(xff)) {
|
||||||
|
int idx = xff.indexOf(',');
|
||||||
|
return (idx > 0 ? xff.substring(0, idx) : xff).trim();
|
||||||
|
}
|
||||||
|
return request.getRemoteAddr() == null ? "127.0.0.1" : request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callBank(String path, Map<String, String> bodyParams) {
|
||||||
|
try {
|
||||||
|
return OpenSdk.gatewayWithKeyId(path, ReqMethodEnum.POST, null, null, bodyParams, keyId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OperateException("调用银行网关失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DfpPayPrepareVo toVo(String outTradeNo, String raw, boolean includeRaw) {
|
||||||
|
DfpPayPrepareVo vo = new DfpPayPrepareVo();
|
||||||
|
vo.setOutTradeNo(outTradeNo);
|
||||||
|
vo.setBankResponseRaw(includeRaw ? raw : null);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(raw)) {
|
||||||
|
throw new OperateException("银行返回为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.trim().startsWith("com.cib.fintech.dfp.open.sdk.exception.SdkException")
|
||||||
|
|| raw.contains("KEY_CONFIGURE_ERR")) {
|
||||||
|
throw new OperateException("开放银行 SDK 配置错误或未初始化:" + safeMsg(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject json = JSONObject.parseObject(raw);
|
||||||
|
vo.setStatus(json.getString("status"));
|
||||||
|
vo.setResultCode(json.getString("result_code"));
|
||||||
|
vo.setPayInfo(json.getString("pay_info"));
|
||||||
|
vo.setPayUrl(json.getString("pay_url"));
|
||||||
|
vo.setErrMsg(json.getString("err_msg"));
|
||||||
|
if (json.getString("message") != null && StringUtils.isBlank(vo.getErrMsg())) {
|
||||||
|
vo.setErrMsg(json.getString("message"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OperateException("银行响应非 JSON,无法解析:" + safeMsg(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"0".equals(vo.getStatus()) || !"0".equals(vo.getResultCode())) {
|
||||||
|
String msg = StringUtils.isNotBlank(vo.getErrMsg()) ? vo.getErrMsg() : "银行下单失败";
|
||||||
|
throw new OperateException(msg);
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(vo.getPayInfo()) && StringUtils.isBlank(vo.getPayUrl())) {
|
||||||
|
throw new OperateException("银行返回成功但未提供 pay_info / pay_url");
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeMsg(String raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return raw.length() > 200 ? raw.substring(0, 200) + "..." : raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class StudentServiceImpl extends ServiceImpl<StudentInfoMapper, StudentInfo> implements IStudentInfoService {
|
public class StudentServiceImpl extends ServiceImpl<StudentInfoMapper, StudentInfo> implements IStudentInfoService {
|
||||||
|
|
@ -63,6 +65,12 @@ public class StudentServiceImpl extends ServiceImpl<StudentInfoMapper, StudentIn
|
||||||
studentBaseInfo.setStudentId(studentInfo.getStudentId());
|
studentBaseInfo.setStudentId(studentInfo.getStudentId());
|
||||||
studentBaseInfoMapper.insert(studentBaseInfo);
|
studentBaseInfoMapper.insert(studentBaseInfo);
|
||||||
}
|
}
|
||||||
return AjaxResult.success();
|
Long finalStudentId = studentInfo.getStudentId();
|
||||||
|
if (finalStudentId == null && baseInfo != null) {
|
||||||
|
finalStudentId = baseInfo.getStudentId();
|
||||||
|
}
|
||||||
|
Map<String, Object> data = new HashMap<>(1);
|
||||||
|
data.put("studentId", finalStudentId);
|
||||||
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.mdd.front.validate.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-关单参数")
|
||||||
|
public class BankFeeTradeCloseValidate implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotBlank(message = "outTradeNo不能为空")
|
||||||
|
@ApiModelProperty(value = "商户订单号(out_trade_no)", required = true)
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否返回银行原始响应(仅联调用)")
|
||||||
|
private Boolean includeRawResponse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.mdd.front.validate.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-查单参数")
|
||||||
|
public class BankFeeTradeQueryValidate implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotBlank(message = "outTradeNo不能为空")
|
||||||
|
@ApiModelProperty(value = "商户订单号(out_trade_no)", required = true)
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否返回银行原始响应(仅联调用)")
|
||||||
|
private Boolean includeRawResponse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.mdd.front.validate.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-支付宝服务窗下单参数")
|
||||||
|
public class DfpAlipayJspayValidate implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotNull(message = "amountFen不能为空")
|
||||||
|
@Min(value = 1, message = "金额至少1分")
|
||||||
|
@Max(value = 100000000, message = "金额超出限制")
|
||||||
|
@ApiModelProperty(value = "支付金额(分)", required = true, example = "1")
|
||||||
|
private Integer amountFen;
|
||||||
|
|
||||||
|
@ApiModelProperty("商品描述,默认:缴费支付")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "学生ID(la_student_info.student_id,可选)", required = false)
|
||||||
|
private Long studentId;
|
||||||
|
|
||||||
|
@ApiModelProperty("买家支付宝账号(可选)")
|
||||||
|
private String buyerLogonId;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否把银行原始响应一并返回(仅联调建议 true)")
|
||||||
|
private Boolean includeRawResponse = false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.mdd.front.validate.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-微信小程序下单参数")
|
||||||
|
public class DfpWechatJspayValidate implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotNull(message = "amountFen不能为空")
|
||||||
|
@Min(value = 1, message = "金额至少1分")
|
||||||
|
@Max(value = 100000000, message = "金额超出限制")
|
||||||
|
@ApiModelProperty(value = "支付金额(分)", required = true, example = "1")
|
||||||
|
private Integer amountFen;
|
||||||
|
|
||||||
|
@ApiModelProperty("商品描述,默认:缴费支付")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "学生ID(la_student_info.student_id,可选)", required = false)
|
||||||
|
private Long studentId;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否把银行原始响应一并返回(仅联调建议 true)")
|
||||||
|
private Boolean includeRawResponse = false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.mdd.front.vo.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-查单/关单返回")
|
||||||
|
public class BankFeeTradeStatusVo implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("商户订单号(out_trade_no)")
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行返回 status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行返回 result_code")
|
||||||
|
private String resultCode;
|
||||||
|
|
||||||
|
@ApiModelProperty("交易状态(trade_state)")
|
||||||
|
private String tradeState;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行交易流水(transaction_id/trade_no等)")
|
||||||
|
private String bankTransactionId;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付金额(分),如果银行返回了该字段")
|
||||||
|
private Integer totalFeeFen;
|
||||||
|
|
||||||
|
@ApiModelProperty("错误信息")
|
||||||
|
private String errMsg;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行完整响应(仅 includeRawResponse=true 时返回)")
|
||||||
|
private String bankResponseRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.mdd.front.vo.bankfee;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("开放银行-拉起支付参数")
|
||||||
|
public class DfpPayPrepareVo implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("商户订单号(发往银行的 out_trade_no)")
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行返回 status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行返回 result_code")
|
||||||
|
private String resultCode;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行返回 err_msg(失败时有值)")
|
||||||
|
private String errMsg;
|
||||||
|
|
||||||
|
@ApiModelProperty("微信/支付宝拉起支付所需原文(一般为 pay_info,格式以银行为准)")
|
||||||
|
private String payInfo;
|
||||||
|
|
||||||
|
@ApiModelProperty("支付宝 H5 等情况可能返回的 pay_url")
|
||||||
|
private String payUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("银行完整响应(仅 includeRawResponse=true 时有值)")
|
||||||
|
private String bankResponseRaw;
|
||||||
|
}
|
||||||
|
|
@ -26,11 +26,11 @@ spring:
|
||||||
matching-strategy: ant_path_matcher
|
matching-strategy: ant_path_matcher
|
||||||
# 数据源配置
|
# 数据源配置
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://localhost:3306/la?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
|
url: jdbc:mysql://8.153.111.6:3306/la?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
|
||||||
type: com.zaxxer.hikari.HikariDataSource # 数据源类型
|
type: com.zaxxer.hikari.HikariDataSource # 数据源类型
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动
|
driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动
|
||||||
username: root # 数据库账号
|
username: root # 数据库账号
|
||||||
password: 123456 # 数据库密码
|
password: 11111111 # 数据库密码
|
||||||
hikari:
|
hikari:
|
||||||
connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒
|
connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒
|
||||||
minimum-idle: 5 # 最小连接数
|
minimum-idle: 5 # 最小连接数
|
||||||
|
|
@ -48,9 +48,9 @@ spring:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Redis配置
|
# Redis配置
|
||||||
redis:
|
redis:
|
||||||
host: localhost # Redis服务地址
|
host: 8.153.111.6 # Redis服务地址
|
||||||
port: 6379 # Redis端口
|
port: 6379 # Redis端口
|
||||||
password: # Redis密码
|
password: 11111111 # Redis密码
|
||||||
database: 0 # 数据库索引
|
database: 0 # 数据库索引
|
||||||
timeout: 5000 # 连接超时
|
timeout: 5000 # 连接超时
|
||||||
lettuce:
|
lettuce:
|
||||||
|
|
@ -90,3 +90,29 @@ knife4j:
|
||||||
openapi:
|
openapi:
|
||||||
title: Knife4j前台页面文档
|
title: Knife4j前台页面文档
|
||||||
description: ""
|
description: ""
|
||||||
|
|
||||||
|
# 对接银行参数
|
||||||
|
dfp-open:
|
||||||
|
# 是否测试用
|
||||||
|
dev-env: true
|
||||||
|
# 应用ID
|
||||||
|
keyId: KYVoTG4wsm7iKc8pn7zMyV88
|
||||||
|
# 应用签名私钥
|
||||||
|
priKey: FP3/5ciXVmEexoVJ4fWo4DU3BU9ssJYwLIjlA/vZzaQ=
|
||||||
|
# 平台验签公钥
|
||||||
|
respPubKey: BKD5RpG0XAsIyCowMQttyub3YrMQD4xFicOlNJgYAHpu/mSqiwJyyZwk52W2m6ARBfDrf31WdOeSnEqNS10wvbU=
|
||||||
|
# 字段加密密钥
|
||||||
|
reqParamEncryptKey: U9jnqp48wK5fqL9hdOPGLw==
|
||||||
|
# 商户ID
|
||||||
|
mchId: 999216001000BHK
|
||||||
|
# outTermId
|
||||||
|
outTermId: QDKXJG-TERM-01
|
||||||
|
# 终端ID
|
||||||
|
termId: P0003698
|
||||||
|
# 小程序appId
|
||||||
|
appId: wxaf99770eb7b49cb7
|
||||||
|
# 开放银行异步通知完整 URL(公网可达,银行服务器必须能访问)
|
||||||
|
# 注意:如果不配置,会尝试自动拼接 RequestUtils.uri() + /api/bank-fee/notify(在反向代理/HTTPS 场景可能不准确)
|
||||||
|
# notify-url: https://你的域名/api/bank-fee/notify
|
||||||
|
# 异步通知完整 URL(公网可达,银行服务器必须能访问)。不填则使用 RequestUtils.uri() + /api/bank-fee/notify
|
||||||
|
# notify-url: https://你的域名/api/bank-fee/notify
|
||||||
|
|
@ -15,57 +15,75 @@ export function getPayResult(data: any) {
|
||||||
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
|
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行支付宝服务窗支付
|
/** 开放银行支付宝服务窗:走后端 like-front /api/bank-fee/pay/alipay(JSON) */
|
||||||
export function alipayJspay(data: any) {
|
export function alipayJspay(data: {
|
||||||
return request.post({
|
amountFen: number
|
||||||
url: '/api/payment/wholeNetworkAcquiring/new/payAlipayJspay',
|
body?: string
|
||||||
data,
|
studentId: number
|
||||||
header: {
|
buyerLogonId?: string
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
includeRawResponse?: boolean
|
||||||
}
|
}) {
|
||||||
}, { isAuth: true })
|
return request.post({ url: '/bank-fee/pay/alipay', data }, { isAuth: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行微信支付(JSPay)
|
/** 开放银行微信小程序 jspay:走后端 like-front /api/bank-fee/pay/wechat(服务端补 openid) */
|
||||||
export function wechatJspay(data: any) {
|
export function wechatJspay(data: {
|
||||||
return request.post({
|
amountFen: number
|
||||||
url: '/api/payment/wholeNetworkAcquiring/new/payWechatJspay',
|
body?: string
|
||||||
data,
|
studentId: number
|
||||||
header: {
|
includeRawResponse?: boolean
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
}) {
|
||||||
}
|
return request.post({ url: '/bank-fee/pay/wechat', data }, { isAuth: true })
|
||||||
}, { isAuth: true })
|
}
|
||||||
|
|
||||||
|
/** 开放银行查单:走后端 /api/bank-fee/trade/query(JSON) */
|
||||||
|
export function bankFeeTradeQuery(data: { outTradeNo: string; includeRawResponse?: boolean }) {
|
||||||
|
return request.post({ url: '/bank-fee/trade/query', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开放银行关单:走后端 /api/bank-fee/trade/close(JSON) */
|
||||||
|
export function bankFeeTradeClose(data: { outTradeNo: string; includeRawResponse?: boolean }) {
|
||||||
|
return request.post({ url: '/bank-fee/trade/close', data }, { isAuth: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行订单查询
|
// 开放银行订单查询
|
||||||
export function unifiedTradeQuery(data: any) {
|
export function unifiedTradeQuery(data: any) {
|
||||||
return request.post({
|
return request.post(
|
||||||
url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeQuery',
|
{
|
||||||
data,
|
url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeQuery',
|
||||||
header: {
|
data,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
header: {
|
||||||
}
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
}, { isAuth: true })
|
}
|
||||||
|
},
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行订单关闭
|
// 开放银行订单关闭
|
||||||
export function unifiedTradeClose(data: any) {
|
export function unifiedTradeClose(data: any) {
|
||||||
return request.post({
|
return request.post(
|
||||||
url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeClose',
|
{
|
||||||
data,
|
url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeClose',
|
||||||
header: {
|
data,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
header: {
|
||||||
}
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
}, { isAuth: true })
|
}
|
||||||
|
},
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行订单撤销
|
// 开放银行订单撤销
|
||||||
export function unifiedMicropayReverse(data: any) {
|
export function unifiedMicropayReverse(data: any) {
|
||||||
return request.post({
|
return request.post(
|
||||||
url: '/api/payment/wholeNetworkAcquiring/new/unifiedMicropayReverse',
|
{
|
||||||
data,
|
url: '/api/payment/wholeNetworkAcquiring/new/unifiedMicropayReverse',
|
||||||
header: {
|
data,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
header: {
|
||||||
}
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
}, { isAuth: true })
|
}
|
||||||
|
},
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,29 @@
|
||||||
</view>
|
</view>
|
||||||
<view class="w-full mt-[140rpx] pb-[60rpx]">
|
<view class="w-full mt-[140rpx] pb-[60rpx]">
|
||||||
<block v-if="!phoneLogin">
|
<block v-if="!phoneLogin">
|
||||||
<!-- #ifdef MP-WEIXIN || H5 -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<!-- <view v-if="isOpenOtherAuth && isWeixin && inWxAuth">
|
<view v-if="isOpenOtherAuth" class="mt-[40rpx]">
|
||||||
<u-button
|
<u-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="wxLogin"
|
@click="wxLogin"
|
||||||
:customStyle="{ height: '100rpx' }"
|
:customStyle="{ height: '100rpx' }"
|
||||||
hover-class="none"
|
hover-class="none"
|
||||||
>
|
>
|
||||||
用户一键登录
|
微信小程序一键登录
|
||||||
</u-button>
|
</u-button>
|
||||||
</view> -->
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<view v-if="isOpenOtherAuth && isWeixin && inWxAuth" class="mt-[40rpx]">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
@click="wxLogin"
|
||||||
|
:customStyle="{ height: '100rpx' }"
|
||||||
|
hover-class="none"
|
||||||
|
>
|
||||||
|
微信公众号一键登录
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|
||||||
<view class="mt-[40rpx]">
|
<view class="mt-[40rpx]">
|
||||||
|
|
@ -205,7 +217,7 @@
|
||||||
@cancel="showModel = false"
|
@cancel="showModel = false"
|
||||||
>
|
>
|
||||||
<view class="text-center px-[70rpx] py-[60rpx]">
|
<view class="text-center px-[70rpx] py-[60rpx]">
|
||||||
<view> 请先阅读并同意 </view>
|
<view> 请先阅读并同意</view>
|
||||||
<view class="flex justify-center">
|
<view class="flex justify-center">
|
||||||
<navigator data-theme="" url="/pages/agreement/agreement?type=service">
|
<navigator data-theme="" url="/pages/agreement/agreement?type=service">
|
||||||
<view class="text-primary">《服务协议》</view>
|
<view class="text-primary">《服务协议》</view>
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,7 @@
|
||||||
<text class="currency">¥</text>
|
<text class="currency">¥</text>
|
||||||
<text class="amount">{{ amount || '0.00' }}</text>
|
<text class="amount">{{ amount || '0.00' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view class="submit-btn" :class="{ disabled: !canSubmit }" @click="handleSubmit">
|
||||||
class="submit-btn"
|
|
||||||
:class="{ disabled: !canSubmit }"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
立即缴费
|
立即缴费
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -97,9 +93,7 @@
|
||||||
v-model="password"
|
v-model="password"
|
||||||
@finish="onPasswordFinish"
|
@finish="onPasswordFinish"
|
||||||
/>
|
/>
|
||||||
<view class="popup-cancel" @click="showPasswordPopup = false">
|
<view class="popup-cancel" @click="showPasswordPopup = false"> 取消</view>
|
||||||
取消
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</u-popup>
|
</u-popup>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -107,9 +101,13 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getPayWay, prepay, getPayResult, alipayJspay, wechatJspay, unifiedTradeQuery } from '@/api/pay'
|
import { getPayWay, getPayResult, alipayJspay, wechatJspay, bankFeeTradeQuery } from '@/api/pay'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
declare const WeixinJSBridge: any
|
||||||
|
declare const AlipayJSBridge: any
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
|
@ -123,12 +121,13 @@ const payWayList = ref<any[]>([])
|
||||||
const selectedPayWay = ref('')
|
const selectedPayWay = ref('')
|
||||||
const showPasswordPopup = ref(false)
|
const showPasswordPopup = ref(false)
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const orderId = ref('')
|
const studentId = ref<number | null>(null)
|
||||||
|
const outTradeNo = ref('')
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const canSubmit = computed(() => {
|
const canSubmit = computed(() => {
|
||||||
const numAmount = parseFloat(amount.value)
|
const numAmount = parseFloat(amount.value)
|
||||||
return numAmount > 0 && selectedPayWay.value
|
return numAmount > 0 && selectedPayWay.value && !!studentId.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取支付方式 - 使用内置的开放银行支付方式
|
// 获取支付方式 - 使用内置的开放银行支付方式
|
||||||
|
|
@ -155,9 +154,9 @@ const selectPayWay = (payWay: string) => {
|
||||||
// 获取支付方式图标
|
// 获取支付方式图标
|
||||||
const getPayWayIcon = (payWay: string) => {
|
const getPayWayIcon = (payWay: string) => {
|
||||||
const iconMap: Record<string, string> = {
|
const iconMap: Record<string, string> = {
|
||||||
'wechat': 'weixin-fill',
|
wechat: 'weixin-fill',
|
||||||
'alipay': 'zhifubao-circle-fill',
|
alipay: 'zhifubao-circle-fill',
|
||||||
'balance': 'rmb-circle-fill'
|
balance: 'rmb-circle-fill'
|
||||||
}
|
}
|
||||||
return iconMap[payWay] || 'rmb-circle-fill'
|
return iconMap[payWay] || 'rmb-circle-fill'
|
||||||
}
|
}
|
||||||
|
|
@ -165,9 +164,9 @@ const getPayWayIcon = (payWay: string) => {
|
||||||
// 获取支付方式颜色
|
// 获取支付方式颜色
|
||||||
const getPayWayColor = (payWay: string) => {
|
const getPayWayColor = (payWay: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
'wechat': '#07C160',
|
wechat: '#07C160',
|
||||||
'alipay': '#1677FF',
|
alipay: '#1677FF',
|
||||||
'balance': '#FF6B00'
|
balance: '#FF6B00'
|
||||||
}
|
}
|
||||||
return colorMap[payWay] || '#999'
|
return colorMap[payWay] || '#999'
|
||||||
}
|
}
|
||||||
|
|
@ -201,37 +200,17 @@ const onPasswordFinish = () => {
|
||||||
const doPay = async () => {
|
const doPay = async () => {
|
||||||
try {
|
try {
|
||||||
uni.showLoading({ title: '支付中...' })
|
uni.showLoading({ title: '支付中...' })
|
||||||
|
if (!studentId.value) {
|
||||||
const params: any = {
|
throw new Error('缺少学生ID,请从预报名详情进入缴费')
|
||||||
pay_way: selectedPayWay.value,
|
|
||||||
amount: parseFloat(amount.value),
|
|
||||||
remark: remark.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPayWay.value === 'balance') {
|
// 只走开放银行正式接口:由后端生成 out_trade_no(QDKXJG-YBMJF-序号)
|
||||||
params.password = password.value
|
if (selectedPayWay.value === 'wechat') {
|
||||||
}
|
await wechatJspayPay()
|
||||||
|
} else if (selectedPayWay.value === 'alipay') {
|
||||||
const res = await prepay(params)
|
await alipayJspayPay()
|
||||||
|
|
||||||
if (res && res.order_id) {
|
|
||||||
orderId.value = res.order_id
|
|
||||||
|
|
||||||
// 微信支付 - 开放银行微信支付
|
|
||||||
if (selectedPayWay.value === 'wechat' && res.pay_params) {
|
|
||||||
await wechatJspayPay(res.pay_params)
|
|
||||||
}
|
|
||||||
// 支付宝支付 - 开放银行服务窗支付
|
|
||||||
else if (selectedPayWay.value === 'alipay' && res.pay_params) {
|
|
||||||
await alipayJspayPay(res.pay_params)
|
|
||||||
}
|
|
||||||
// 余额支付直接成功
|
|
||||||
else {
|
|
||||||
uni.hideLoading()
|
|
||||||
goToResult(true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('支付参数错误')
|
throw new Error('暂不支持该支付方式')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|
@ -263,7 +242,14 @@ const wechatPay = (payParams: any) => {
|
||||||
window.location.href = payParams.mweb_url
|
window.location.href = payParams.mweb_url
|
||||||
} else if (payParams.jsapi_params) {
|
} else if (payParams.jsapi_params) {
|
||||||
// JSAPI支付
|
// JSAPI支付
|
||||||
const { appId, timeStamp, nonceStr, package: packageStr, signType, paySign } = payParams.jsapi_params
|
const {
|
||||||
|
appId,
|
||||||
|
timeStamp,
|
||||||
|
nonceStr,
|
||||||
|
package: packageStr,
|
||||||
|
signType,
|
||||||
|
paySign
|
||||||
|
} = payParams.jsapi_params
|
||||||
if (typeof WeixinJSBridge !== 'undefined') {
|
if (typeof WeixinJSBridge !== 'undefined') {
|
||||||
WeixinJSBridge.invoke(
|
WeixinJSBridge.invoke(
|
||||||
'getBrandWCPayRequest',
|
'getBrandWCPayRequest',
|
||||||
|
|
@ -328,35 +314,30 @@ const alipay = (payParams: any) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 开放银行支付宝服务窗支付
|
// 开放银行支付宝服务窗支付
|
||||||
const alipayJspayPay = async (payParams: any) => {
|
const alipayJspayPay = async () => {
|
||||||
try {
|
try {
|
||||||
// 构建开放银行支付请求参数
|
const res = await alipayJspay({
|
||||||
const params = {
|
amountFen: Math.round(parseFloat(String(amount.value)) * 100),
|
||||||
version: '3.0',
|
|
||||||
charset: 'UTF-8',
|
|
||||||
service: 'pay.alipay.jspay',
|
|
||||||
mch_id: paymentConfig.mch_id, // 商户号
|
|
||||||
out_trade_no: orderId.value,
|
|
||||||
total_fee: String(Math.round(parseFloat(amount.value) * 100)), // 金额转换为分
|
|
||||||
body: remark.value || '缴费支付',
|
body: remark.value || '缴费支付',
|
||||||
mch_create_ip: '127.0.0.1', // 需要获取真实IP
|
studentId: Number(studentId.value),
|
||||||
notify_url: paymentConfig.alipay.notify_url,
|
buyerLogonId: '',
|
||||||
buyer_logon_id: '', // 买家支付宝账号(可选)
|
includeRawResponse: false
|
||||||
'terminal_info.terminal_type': '11', // 终端类型
|
})
|
||||||
'terminal_info.terminal_id': paymentConfig.terminal_id,
|
|
||||||
'terminal_info.app_version': '1.000000'
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await alipayJspay(params)
|
// 检查返回结果(后端 AjaxResult.data 为驼峰字段)
|
||||||
|
if (res && res.status === '0' && res.resultCode === '0') {
|
||||||
|
outTradeNo.value = res.outTradeNo || ''
|
||||||
|
// #ifdef H5
|
||||||
|
if (res.payUrl) {
|
||||||
|
window.location.href = res.payUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
// 检查返回结果
|
const payInfo = JSON.parse(res.payInfo || '{}')
|
||||||
if (res && res.status === '0' && res.result_code === '0') {
|
|
||||||
// 解析 pay_info
|
|
||||||
const payInfo = JSON.parse(res.pay_info || '{}')
|
|
||||||
|
|
||||||
if (payInfo.tradeNO) {
|
if (payInfo.tradeNO) {
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
// APP 环境使用 tradeNO 唤起支付宝
|
|
||||||
uni.requestPayment({
|
uni.requestPayment({
|
||||||
provider: 'alipay',
|
provider: 'alipay',
|
||||||
orderInfo: {
|
orderInfo: {
|
||||||
|
|
@ -373,30 +354,27 @@ const alipayJspayPay = async (payParams: any) => {
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
// H5 支付宝服务窗支付
|
if (typeof AlipayJSBridge !== 'undefined') {
|
||||||
// 方式1:使用 pay_url 跳转
|
AlipayJSBridge.call(
|
||||||
if (res.pay_url) {
|
'tradePay',
|
||||||
window.location.href = res.pay_url
|
{
|
||||||
return
|
tradeNO: payInfo.tradeNO
|
||||||
}
|
},
|
||||||
// 方式2:使用 tradeNO 唤起支付宝
|
(result: any) => {
|
||||||
if (payInfo.tradeNO && typeof AlipayJSBridge !== 'undefined') {
|
if (result.resultCode === '9000') {
|
||||||
AlipayJSBridge.call('tradePay', {
|
checkPayResult()
|
||||||
tradeNO: payInfo.tradeNO
|
} else {
|
||||||
}, (result: any) => {
|
goToResult(false)
|
||||||
if (result.resultCode === '9000') {
|
}
|
||||||
checkPayResult()
|
|
||||||
} else {
|
|
||||||
goToResult(false)
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
} else {
|
} else {
|
||||||
throw new Error('获取支付参数失败')
|
throw new Error('获取支付参数失败')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.err_msg || res.message || '支付请求失败')
|
throw new Error(res.errMsg || res.message || '支付请求失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('开放银行支付错误:', error)
|
console.error('开放银行支付错误:', error)
|
||||||
|
|
@ -406,60 +384,29 @@ const alipayJspayPay = async (payParams: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开放银行微信支付(JSPay)
|
// 开放银行微信支付(JSPay)
|
||||||
const wechatJspayPay = async (payParams: any) => {
|
const wechatJspayPay = async () => {
|
||||||
try {
|
try {
|
||||||
// 构建开放银行微信支付请求参数
|
// 走后端正式接口:openid / termId / mchId / notify 均由服务端处理
|
||||||
const params: any = {
|
const res = await wechatJspay({
|
||||||
version: '3.0',
|
amountFen: Math.round(parseFloat(String(amount.value)) * 100),
|
||||||
charset: 'UTF-8',
|
|
||||||
service: 'pay.weixin.jspay',
|
|
||||||
mch_id: paymentConfig.mch_id, // 商户号
|
|
||||||
out_trade_no: orderId.value,
|
|
||||||
total_fee: String(Math.round(parseFloat(amount.value) * 100)), // 金额转换为分
|
|
||||||
body: remark.value || '缴费支付',
|
body: remark.value || '缴费支付',
|
||||||
mch_create_ip: '127.0.0.1', // 需要获取真实IP
|
studentId: Number(studentId.value),
|
||||||
notify_url: paymentConfig.wechat.notify_url,
|
includeRawResponse: false
|
||||||
'terminal_info.terminal_type': '11', // 终端类型
|
})
|
||||||
'terminal_info.terminal_id': paymentConfig.terminal_id,
|
|
||||||
'terminal_info.app_version': '1.000000'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公众号/小程序支付需要 sub_appid 和 sub_openid
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
params.is_minipg = '1' // 小程序支付标识
|
|
||||||
params.sub_appid = paymentConfig.wechat.sub_appid // 小程序 appid
|
|
||||||
// 获取用户 openid
|
|
||||||
const openid = userStore.userInfo?.openid || ''
|
|
||||||
if (openid) {
|
|
||||||
params.sub_openid = openid
|
|
||||||
}
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
// #ifdef H5
|
|
||||||
// H5 公众号支付 - 用户在微信内打开网页
|
|
||||||
params.is_raw = '1' // 原生态 JS 支付
|
|
||||||
params.sub_appid = paymentConfig.wechat.sub_appid // 公众号 appid
|
|
||||||
// 获取用户 openid(如果在微信内)
|
|
||||||
const openid = userStore.userInfo?.openid || ''
|
|
||||||
if (openid) {
|
|
||||||
params.sub_openid = openid
|
|
||||||
}
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
const res = await wechatJspay(params)
|
|
||||||
|
|
||||||
// 检查返回结果
|
// 检查返回结果
|
||||||
if (res && res.status === '0' && res.result_code === '0') {
|
if (res && res.status === '0' && res.resultCode === '0') {
|
||||||
const payInfo = res.pay_info
|
outTradeNo.value = res.outTradeNo || ''
|
||||||
|
const payInfo = res.payInfo
|
||||||
|
|
||||||
if (payInfo) {
|
if (payInfo) {
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
// 小程序支付 - 解析 XML 格式的 pay_info
|
// 小程序支付 - 解析 XML 格式的 pay_info
|
||||||
const payParams = parseXmlPayInfo(payInfo)
|
const miniPayParams = parseXmlPayInfo(payInfo)
|
||||||
if (payParams) {
|
if (miniPayParams) {
|
||||||
uni.requestPayment({
|
;(uni as any).requestPayment({
|
||||||
provider: 'wxpay',
|
provider: 'wxpay',
|
||||||
...payParams,
|
...miniPayParams,
|
||||||
success: () => {
|
success: () => {
|
||||||
checkPayResult()
|
checkPayResult()
|
||||||
},
|
},
|
||||||
|
|
@ -475,24 +422,20 @@ const wechatJspayPay = async (payParams: any) => {
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
// H5 公众号支付 - 解析 XML 格式的 pay_info
|
// H5 公众号支付 - 解析 XML 格式的 pay_info
|
||||||
const payParams = parseXmlPayInfo(payInfo)
|
const h5PayParams = parseXmlPayInfo(payInfo)
|
||||||
if (payParams && typeof WeixinJSBridge !== 'undefined') {
|
if (h5PayParams && typeof WeixinJSBridge !== 'undefined') {
|
||||||
WeixinJSBridge.invoke(
|
WeixinJSBridge.invoke('getBrandWCPayRequest', h5PayParams, (res: any) => {
|
||||||
'getBrandWCPayRequest',
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
payParams,
|
checkPayResult()
|
||||||
(res: any) => {
|
} else {
|
||||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
goToResult(false)
|
||||||
checkPayResult()
|
|
||||||
} else {
|
|
||||||
goToResult(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
} else if (payParams) {
|
} else if (h5PayParams) {
|
||||||
// 使用 uni.requestPayment
|
// 使用 uni.requestPayment
|
||||||
uni.requestPayment({
|
;(uni as any).requestPayment({
|
||||||
provider: 'wxpay',
|
provider: 'wxpay',
|
||||||
...payParams,
|
...h5PayParams,
|
||||||
success: () => {
|
success: () => {
|
||||||
checkPayResult()
|
checkPayResult()
|
||||||
},
|
},
|
||||||
|
|
@ -524,7 +467,7 @@ const wechatJspayPay = async (payParams: any) => {
|
||||||
throw new Error('获取支付参数失败')
|
throw new Error('获取支付参数失败')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.err_msg || res.message || '支付请求失败')
|
throw new Error(res.errMsg || res.message || '支付请求失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('开放银行微信支付错误:', error)
|
console.error('开放银行微信支付错误:', error)
|
||||||
|
|
@ -538,9 +481,12 @@ const parseXmlPayInfo = (xmlStr: string) => {
|
||||||
try {
|
try {
|
||||||
// 简单的 XML 解析
|
// 简单的 XML 解析
|
||||||
const getValue = (xml: string, tag: string) => {
|
const getValue = (xml: string, tag: string) => {
|
||||||
const regex = new RegExp(`<${tag}><!\[CDATA\[(.*?)\]\]></${tag}>|<${tag}>(.*?)</${tag}>`, 'i')
|
const regex = new RegExp(
|
||||||
|
`<${tag}><!\[CDATA\[(.*?)\]\]></${tag}>|<${tag}>(.*?)</${tag}>`,
|
||||||
|
'i'
|
||||||
|
)
|
||||||
const match = xml.match(regex)
|
const match = xml.match(regex)
|
||||||
return match ? (match[1] || match[2]) : ''
|
return match ? match[1] || match[2] : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -561,24 +507,18 @@ const parseXmlPayInfo = (xmlStr: string) => {
|
||||||
const checkPayResult = async () => {
|
const checkPayResult = async () => {
|
||||||
try {
|
try {
|
||||||
uni.showLoading({ title: '查询中...' })
|
uni.showLoading({ title: '查询中...' })
|
||||||
|
if (!outTradeNo.value) {
|
||||||
// 构建订单查询参数
|
throw new Error('缺少 outTradeNo')
|
||||||
const params = {
|
|
||||||
version: '3.0',
|
|
||||||
service: 'unified.trade.query',
|
|
||||||
mch_id: paymentConfig.mch_id, // 商户号
|
|
||||||
out_trade_no: orderId.value
|
|
||||||
}
|
}
|
||||||
|
const res = await bankFeeTradeQuery({ outTradeNo: outTradeNo.value })
|
||||||
const res = await unifiedTradeQuery(params)
|
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|
||||||
// 检查查询结果
|
// 检查查询结果
|
||||||
if (res && res.status === '0' && res.result_code === '0') {
|
if (res && res.status === '0' && res.resultCode === '0') {
|
||||||
// trade_state: SUCCESS-支付成功, REFUND-转入退款, NOTPAY-未支付,
|
// trade_state: SUCCESS-支付成功, REFUND-转入退款, NOTPAY-未支付,
|
||||||
// CLOSED-已关闭, REVOKED-已撤销, USERPAYING-用户支付中, PAYERROR-支付失败
|
// CLOSED-已关闭, REVOKED-已撤销, USERPAYING-用户支付中, PAYERROR-支付失败
|
||||||
const tradeState = res.trade_state
|
const tradeState = res.tradeState
|
||||||
|
|
||||||
if (tradeState === 'SUCCESS') {
|
if (tradeState === 'SUCCESS') {
|
||||||
// 支付成功
|
// 支付成功
|
||||||
|
|
@ -610,12 +550,12 @@ const checkPayResult = async () => {
|
||||||
// 备用查询方式 - 使用原系统接口
|
// 备用查询方式 - 使用原系统接口
|
||||||
const fallbackCheckPayResult = async () => {
|
const fallbackCheckPayResult = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getPayResult({ order_id: orderId.value })
|
// 兜底:仍然用开放银行查单
|
||||||
if (res && res.pay_status === 1) {
|
if (!outTradeNo.value) return goToResult(false)
|
||||||
goToResult(true)
|
const res = await bankFeeTradeQuery({ outTradeNo: outTradeNo.value })
|
||||||
} else {
|
if (res && res.status === '0' && res.resultCode === '0' && res.tradeState === 'SUCCESS')
|
||||||
goToResult(false)
|
return goToResult(true)
|
||||||
}
|
return goToResult(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
goToResult(false)
|
goToResult(false)
|
||||||
}
|
}
|
||||||
|
|
@ -624,19 +564,28 @@ const fallbackCheckPayResult = async () => {
|
||||||
// 跳转到结果页
|
// 跳转到结果页
|
||||||
const goToResult = (success: boolean) => {
|
const goToResult = (success: boolean) => {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: `/pages/payment_result/payment_result?status=${success ? 'success' : 'fail'}&order_id=${orderId.value}`
|
url: `/pages/payment_result/payment_result?status=${success ? 'success' : 'fail'}&id=${
|
||||||
|
outTradeNo.value
|
||||||
|
}&from=bankFee`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getPayWayData()
|
getPayWayData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
const sid = Number(options?.studentId || options?.student_id || 0)
|
||||||
|
if (sid > 0) {
|
||||||
|
studentId.value = sid
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.payment {
|
.payment {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #F5F7FA;
|
background-color: #f5f7fa;
|
||||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,7 +601,7 @@ onMounted(() => {
|
||||||
|
|
||||||
// 金额输入区域
|
// 金额输入区域
|
||||||
.amount-section {
|
.amount-section {
|
||||||
background: #FFFFFF;
|
background: #ffffff;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
|
@ -660,7 +609,7 @@ onMounted(() => {
|
||||||
.amount-input-wrapper {
|
.amount-input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 2rpx solid #E5E7EB;
|
border-bottom: 2rpx solid #e5e7eb;
|
||||||
padding: 20rpx 0;
|
padding: 20rpx 0;
|
||||||
|
|
||||||
.currency {
|
.currency {
|
||||||
|
|
@ -688,7 +637,7 @@ onMounted(() => {
|
||||||
|
|
||||||
// 备注区域
|
// 备注区域
|
||||||
.remark-section {
|
.remark-section {
|
||||||
background: #FFFFFF;
|
background: #ffffff;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
|
@ -718,7 +667,7 @@ onMounted(() => {
|
||||||
|
|
||||||
// 支付方式区域
|
// 支付方式区域
|
||||||
.payway-section {
|
.payway-section {
|
||||||
background: #FFFFFF;
|
background: #ffffff;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
|
|
||||||
|
|
@ -728,7 +677,7 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 30rpx 0;
|
padding: 30rpx 0;
|
||||||
border-bottom: 2rpx solid #F3F4F6;
|
border-bottom: 2rpx solid #f3f4f6;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
@ -756,7 +705,7 @@ onMounted(() => {
|
||||||
.check-circle {
|
.check-circle {
|
||||||
width: 40rpx;
|
width: 40rpx;
|
||||||
height: 40rpx;
|
height: 40rpx;
|
||||||
border: 2rpx solid #D1D5DB;
|
border: 2rpx solid #d1d5db;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -770,7 +719,7 @@ onMounted(() => {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: #FFFFFF;
|
background: #ffffff;
|
||||||
padding: 20rpx 30rpx;
|
padding: 20rpx 30rpx;
|
||||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -789,20 +738,20 @@ onMounted(() => {
|
||||||
|
|
||||||
.currency {
|
.currency {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
color: #FF6B00;
|
color: #ff6b00;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
font-size: 48rpx;
|
font-size: 48rpx;
|
||||||
color: #FF6B00;
|
color: #ff6b00;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||||
color: #FFFFFF;
|
color: #ffffff;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 24rpx 60rpx;
|
padding: 24rpx 60rpx;
|
||||||
|
|
@ -813,8 +762,8 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background: #D1D5DB;
|
background: #d1d5db;
|
||||||
color: #9CA3AF;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -823,7 +772,7 @@ onMounted(() => {
|
||||||
.password-popup {
|
.password-popup {
|
||||||
width: 600rpx;
|
width: 600rpx;
|
||||||
padding: 40rpx;
|
padding: 40rpx;
|
||||||
background: #FFFFFF;
|
background: #ffffff;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
|
|
||||||
.popup-title {
|
.popup-title {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getPayResult } from '@/api/pay'
|
import { bankFeeTradeQuery, getPayResult } from '@/api/pay'
|
||||||
import { PageStatusEnum } from '@/enums/appEnums'
|
import { PageStatusEnum } from '@/enums/appEnums'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { computed, reactive, ref } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
|
@ -106,7 +106,8 @@ const pageOptions = ref({
|
||||||
from: ''
|
from: ''
|
||||||
})
|
})
|
||||||
const orderInfo = reactive<any>({
|
const orderInfo = reactive<any>({
|
||||||
order: {}
|
order: {},
|
||||||
|
pay_status: 0
|
||||||
})
|
})
|
||||||
const paymentStatus = computed(() => {
|
const paymentStatus = computed(() => {
|
||||||
const status = !!orderInfo.pay_status
|
const status = !!orderInfo.pay_status
|
||||||
|
|
@ -115,17 +116,34 @@ const paymentStatus = computed(() => {
|
||||||
|
|
||||||
const initPageData = () => {
|
const initPageData = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
getPayResult({
|
// 报名缴费(开放银行)结果页:使用 outTradeNo 查单
|
||||||
order_id: pageOptions.value.id,
|
if (pageOptions.value.from === 'bankFee') {
|
||||||
from: pageOptions.value.from
|
bankFeeTradeQuery({ outTradeNo: pageOptions.value.id })
|
||||||
})
|
.then((data: any) => {
|
||||||
|
const tradeState = data?.tradeState
|
||||||
|
const totalFeeFen = Number(data?.totalFeeFen || 0)
|
||||||
|
Object.assign(orderInfo, {
|
||||||
|
pay_status: tradeState === 'SUCCESS' ? 1 : 0,
|
||||||
|
order: {
|
||||||
|
order_amount: totalFeeFen ? (totalFeeFen / 100).toFixed(2) : '-',
|
||||||
|
order_sn: pageOptions.value.id,
|
||||||
|
pay_time: '-',
|
||||||
|
pay_way: data?.payWay ? String(data.payWay) : '-'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧的充值/框架支付结果页
|
||||||
|
getPayResult({ order_id: pageOptions.value.id, from: pageOptions.value.from })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
Object.assign(orderInfo, data)
|
Object.assign(orderInfo, data)
|
||||||
resolve(data)
|
resolve(data)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => reject(err))
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,12 @@ const submit = async () => {
|
||||||
console.log('========== 准备提交数据 ==========')
|
console.log('========== 准备提交数据 ==========')
|
||||||
console.log('提交数据:', submitData)
|
console.log('提交数据:', submitData)
|
||||||
console.log('majorId:', submitData.majorId, '类型:', typeof submitData.majorId)
|
console.log('majorId:', submitData.majorId, '类型:', typeof submitData.majorId)
|
||||||
console.log('recruitmentTeacherId:', submitData.recruitmentTeacherId, '类型:', typeof submitData.recruitmentTeacherId)
|
console.log(
|
||||||
|
'recruitmentTeacherId:',
|
||||||
|
submitData.recruitmentTeacherId,
|
||||||
|
'类型:',
|
||||||
|
typeof submitData.recruitmentTeacherId
|
||||||
|
)
|
||||||
|
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: '提交中...',
|
title: '提交中...',
|
||||||
|
|
@ -397,7 +402,13 @@ const submit = async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.navigateTo('/pages/submit_success/submit_success')
|
// 预报名提交成功后,直接进入缴费页,并带上 studentId 用于绑定
|
||||||
|
const sid = res?.data?.studentId || res?.data?.id || null
|
||||||
|
if (sid) {
|
||||||
|
router.navigateTo(`/pages/payment/payment?studentId=${sid}`)
|
||||||
|
} else {
|
||||||
|
router.navigateTo('/pages/submit_success/submit_success')
|
||||||
|
}
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} else {
|
} else {
|
||||||
console.log('提交失败:', res?.msg || '未知错误')
|
console.log('提交失败:', res?.msg || '未知错误')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue