diff --git a/server/like-admin/src/main/resources/application.yml b/server/like-admin/src/main/resources/application.yml index 8bbf4705..1021c514 100644 --- a/server/like-admin/src/main/resources/application.yml +++ b/server/like-admin/src/main/resources/application.yml @@ -33,11 +33,11 @@ spring: matching-strategy: ant_path_matcher # 数据源配置 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 # 数据源类型 driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动 username: root # 数据库账号 - password: 123456 # 数据库密码 + password: 11111111 # 数据库密码 hikari: connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒 minimum-idle: 5 # 最小连接数 @@ -55,9 +55,9 @@ spring: enabled: true # Redis配置 redis: - host: localhost # Redis服务地址 + host: 8.153.111.6 # Redis服务地址 port: 6379 # Redis端口 - password: # Redis密码 + password: 11111111 # Redis密码 database: 0 # 数据库索引 timeout: 5000 # 连接超时 lettuce: diff --git a/server/like-common/src/main/java/com/mdd/common/entity/StudentInfo.java b/server/like-common/src/main/java/com/mdd/common/entity/StudentInfo.java index e49ef833..099fa966 100644 --- a/server/like-common/src/main/java/com/mdd/common/entity/StudentInfo.java +++ b/server/like-common/src/main/java/com/mdd/common/entity/StudentInfo.java @@ -95,4 +95,7 @@ public class StudentInfo implements Serializable { @ApiModelProperty(value = "删除时间") private Date deleteTime; + @ApiModelProperty(value = "绑定缴费订单ID(la_enrollment_pay_order.id)") + private Long payOrderId; + } diff --git a/server/like-common/src/main/java/com/mdd/common/entity/bank/BankFeePayOrder.java b/server/like-common/src/main/java/com/mdd/common/entity/bank/BankFeePayOrder.java new file mode 100644 index 00000000..f79dec7d --- /dev/null +++ b/server/like-common/src/main/java/com/mdd/common/entity/bank/BankFeePayOrder.java @@ -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; +} + diff --git a/server/like-common/src/main/java/com/mdd/common/entity/bank/EnrollmentPayOrder.java b/server/like-common/src/main/java/com/mdd/common/entity/bank/EnrollmentPayOrder.java new file mode 100644 index 00000000..00175e9f --- /dev/null +++ b/server/like-common/src/main/java/com/mdd/common/entity/bank/EnrollmentPayOrder.java @@ -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; +} + diff --git a/server/like-common/src/main/java/com/mdd/common/mapper/bank/BankFeePayOrderMapper.java b/server/like-common/src/main/java/com/mdd/common/mapper/bank/BankFeePayOrderMapper.java new file mode 100644 index 00000000..32499b72 --- /dev/null +++ b/server/like-common/src/main/java/com/mdd/common/mapper/bank/BankFeePayOrderMapper.java @@ -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 { +} + diff --git a/server/like-common/src/main/java/com/mdd/common/mapper/bank/EnrollmentPayOrderMapper.java b/server/like-common/src/main/java/com/mdd/common/mapper/bank/EnrollmentPayOrderMapper.java new file mode 100644 index 00000000..27ce09c0 --- /dev/null +++ b/server/like-common/src/main/java/com/mdd/common/mapper/bank/EnrollmentPayOrderMapper.java @@ -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 { +} + diff --git a/server/like-front/src/main/java/com/mdd/front/config/DfpOpenSdkInitConfig.java b/server/like-front/src/main/java/com/mdd/front/config/DfpOpenSdkInitConfig.java new file mode 100644 index 00000000..06392cea --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/config/DfpOpenSdkInitConfig.java @@ -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 初始化配置。 + *

+ * 注意:私钥/加密密钥仅从服务端配置读取,不允许下发到前端。 + */ +@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 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); + } +} diff --git a/server/like-front/src/main/java/com/mdd/front/controller/BankFeeNotifyController.java b/server/like-front/src/main/java/com/mdd/front/controller/BankFeeNotifyController.java new file mode 100644 index 00000000..16776999 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/controller/BankFeeNotifyController.java @@ -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 解密 + 业务落库)。 + *

+ * 说明: + * - 回调为银行服务器主动调用,不需要登录。 + * - 验签失败建议返回 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) + "..."; + } +} + diff --git a/server/like-front/src/main/java/com/mdd/front/controller/BankFeePayController.java b/server/like-front/src/main/java/com/mdd/front/controller/BankFeePayController.java new file mode 100644 index 00000000..e07d39d6 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/controller/BankFeePayController.java @@ -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 wechatMini(@Validated @RequestBody DfpWechatJspayValidate validate) { + DfpPayPrepareVo vo = dfpOpenPayService.prepareWechatMiniJspay(LikeFrontThreadLocal.getUserId(), validate); + return AjaxResult.success(vo); + } + + @PostMapping("/alipay") + @ApiOperation("支付宝服务窗 jspay 下单") + public AjaxResult alipay(@Validated @RequestBody DfpAlipayJspayValidate validate) { + DfpPayPrepareVo vo = dfpOpenPayService.prepareAlipayJspay(LikeFrontThreadLocal.getUserId(), validate); + return AjaxResult.success(vo); + } +} diff --git a/server/like-front/src/main/java/com/mdd/front/controller/BankFeeTradeController.java b/server/like-front/src/main/java/com/mdd/front/controller/BankFeeTradeController.java new file mode 100644 index 00000000..8dd637e0 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/controller/BankFeeTradeController.java @@ -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 query(@Validated @RequestBody BankFeeTradeQueryValidate validate) { + BankFeeTradeStatusVo vo = bankFeeTradeService.query(LikeFrontThreadLocal.getUserId(), validate); + return AjaxResult.success(vo); + } + + @PostMapping("/close") + @ApiOperation("开放银行关单") + public AjaxResult close(@Validated @RequestBody BankFeeTradeCloseValidate validate) { + BankFeeTradeStatusVo vo = bankFeeTradeService.close(LikeFrontThreadLocal.getUserId(), validate); + return AjaxResult.success(vo); + } +} + diff --git a/server/like-front/src/main/java/com/mdd/front/controller/BankOpenSdkTestController.java b/server/like-front/src/main/java/com/mdd/front/controller/BankOpenSdkTestController.java new file mode 100644 index 00000000..7817fb28 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/controller/BankOpenSdkTestController.java @@ -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 临时测试接口(仅用于联调/测试环境)。 + *

+ * 注意:不要把私钥/加密密钥放到前端或返回给前端。 + */ +@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 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 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 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; + + } +} + diff --git a/server/like-front/src/main/java/com/mdd/front/service/IBankFeeNotifyService.java b/server/like-front/src/main/java/com/mdd/front/service/IBankFeeNotifyService.java new file mode 100644 index 00000000..2bb59f25 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/IBankFeeNotifyService.java @@ -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); +} + diff --git a/server/like-front/src/main/java/com/mdd/front/service/IBankFeeTradeService.java b/server/like-front/src/main/java/com/mdd/front/service/IBankFeeTradeService.java new file mode 100644 index 00000000..c940a38a --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/IBankFeeTradeService.java @@ -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); +} + diff --git a/server/like-front/src/main/java/com/mdd/front/service/IDfpOpenPayService.java b/server/like-front/src/main/java/com/mdd/front/service/IDfpOpenPayService.java new file mode 100644 index 00000000..b5844402 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/IDfpOpenPayService.java @@ -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); +} diff --git a/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeNotifyServiceImpl.java b/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeNotifyServiceImpl.java new file mode 100644 index 00000000..bd078b0c --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeNotifyServiceImpl.java @@ -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() + .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; + } + } +} + diff --git a/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeTradeServiceImpl.java b/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeTradeServiceImpl.java new file mode 100644 index 00000000..5e96fbc3 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/impl/BankFeeTradeServiceImpl.java @@ -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 buildQueryBody(String outTradeNo) { + Map 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 buildCloseBody(String outTradeNo) { + Map 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 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() + .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; + } + } +} + diff --git a/server/like-front/src/main/java/com/mdd/front/service/impl/DfpOpenPayServiceImpl.java b/server/like-front/src/main/java/com/mdd/front/service/impl/DfpOpenPayServiceImpl.java new file mode 100644 index 00000000..0a35df29 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/service/impl/DfpOpenPayServiceImpl.java @@ -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 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 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() + .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() + .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() + .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 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 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 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; + } +} diff --git a/server/like-front/src/main/java/com/mdd/front/service/impl/StudentServiceImpl.java b/server/like-front/src/main/java/com/mdd/front/service/impl/StudentServiceImpl.java index ef4e3689..9876879f 100644 --- a/server/like-front/src/main/java/com/mdd/front/service/impl/StudentServiceImpl.java +++ b/server/like-front/src/main/java/com/mdd/front/service/impl/StudentServiceImpl.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; +import java.util.HashMap; +import java.util.Map; @Service public class StudentServiceImpl extends ServiceImpl implements IStudentInfoService { @@ -63,6 +65,12 @@ public class StudentServiceImpl extends ServiceImpl data = new HashMap<>(1); + data.put("studentId", finalStudentId); + return AjaxResult.success(data); } } diff --git a/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeCloseValidate.java b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeCloseValidate.java new file mode 100644 index 00000000..94cd6155 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeCloseValidate.java @@ -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; +} + diff --git a/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeQueryValidate.java b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeQueryValidate.java new file mode 100644 index 00000000..9cfde029 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/BankFeeTradeQueryValidate.java @@ -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; +} + diff --git a/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpAlipayJspayValidate.java b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpAlipayJspayValidate.java new file mode 100644 index 00000000..ac7c73a9 --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpAlipayJspayValidate.java @@ -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; +} diff --git a/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpWechatJspayValidate.java b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpWechatJspayValidate.java new file mode 100644 index 00000000..7aa867af --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/validate/bankfee/DfpWechatJspayValidate.java @@ -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; +} diff --git a/server/like-front/src/main/java/com/mdd/front/vo/bankfee/BankFeeTradeStatusVo.java b/server/like-front/src/main/java/com/mdd/front/vo/bankfee/BankFeeTradeStatusVo.java new file mode 100644 index 00000000..1ee4f91a --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/vo/bankfee/BankFeeTradeStatusVo.java @@ -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; +} + diff --git a/server/like-front/src/main/java/com/mdd/front/vo/bankfee/DfpPayPrepareVo.java b/server/like-front/src/main/java/com/mdd/front/vo/bankfee/DfpPayPrepareVo.java new file mode 100644 index 00000000..c874d17b --- /dev/null +++ b/server/like-front/src/main/java/com/mdd/front/vo/bankfee/DfpPayPrepareVo.java @@ -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; +} diff --git a/server/like-front/src/main/resources/application.yml b/server/like-front/src/main/resources/application.yml index a117ee36..fc5ac4f1 100644 --- a/server/like-front/src/main/resources/application.yml +++ b/server/like-front/src/main/resources/application.yml @@ -26,11 +26,11 @@ spring: matching-strategy: ant_path_matcher # 数据源配置 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 # 数据源类型 driver-class-name: com.mysql.cj.jdbc.Driver # MySql的驱动 username: root # 数据库账号 - password: 123456 # 数据库密码 + password: 11111111 # 数据库密码 hikari: connection-timeout: 30000 # 等待连接分配连接的最大时长(毫秒),超出时长还没可用连接则发送SQLException,默认30秒 minimum-idle: 5 # 最小连接数 @@ -48,9 +48,9 @@ spring: enabled: true # Redis配置 redis: - host: localhost # Redis服务地址 + host: 8.153.111.6 # Redis服务地址 port: 6379 # Redis端口 - password: # Redis密码 + password: 11111111 # Redis密码 database: 0 # 数据库索引 timeout: 5000 # 连接超时 lettuce: @@ -90,3 +90,29 @@ knife4j: openapi: title: Knife4j前台页面文档 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 \ No newline at end of file diff --git a/uniapp/src/api/pay.ts b/uniapp/src/api/pay.ts index 97aa97ad..3fa7f4aa 100644 --- a/uniapp/src/api/pay.ts +++ b/uniapp/src/api/pay.ts @@ -15,57 +15,75 @@ export function getPayResult(data: any) { return request.get({ url: '/pay/payStatus', data }, { isAuth: true }) } -// 开放银行支付宝服务窗支付 -export function alipayJspay(data: any) { - return request.post({ - url: '/api/payment/wholeNetworkAcquiring/new/payAlipayJspay', - data, - header: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, { isAuth: true }) +/** 开放银行支付宝服务窗:走后端 like-front /api/bank-fee/pay/alipay(JSON) */ +export function alipayJspay(data: { + amountFen: number + body?: string + studentId: number + buyerLogonId?: string + includeRawResponse?: boolean +}) { + return request.post({ url: '/bank-fee/pay/alipay', data }, { isAuth: true }) } -// 开放银行微信支付(JSPay) -export function wechatJspay(data: any) { - return request.post({ - url: '/api/payment/wholeNetworkAcquiring/new/payWechatJspay', - data, - header: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, { isAuth: true }) +/** 开放银行微信小程序 jspay:走后端 like-front /api/bank-fee/pay/wechat(服务端补 openid) */ +export function wechatJspay(data: { + amountFen: number + body?: string + studentId: number + includeRawResponse?: boolean +}) { + return request.post({ url: '/bank-fee/pay/wechat', data }, { 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) { - return request.post({ - url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeQuery', - data, - header: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, { isAuth: true }) + return request.post( + { + url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeQuery', + data, + header: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, + { isAuth: true } + ) } // 开放银行订单关闭 export function unifiedTradeClose(data: any) { - return request.post({ - url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeClose', - data, - header: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, { isAuth: true }) + return request.post( + { + url: '/api/payment/wholeNetworkAcquiring/new/unifiedTradeClose', + data, + header: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, + { isAuth: true } + ) } // 开放银行订单撤销 export function unifiedMicropayReverse(data: any) { - return request.post({ - url: '/api/payment/wholeNetworkAcquiring/new/unifiedMicropayReverse', - data, - header: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, { isAuth: true }) + return request.post( + { + url: '/api/payment/wholeNetworkAcquiring/new/unifiedMicropayReverse', + data, + header: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, + { isAuth: true } + ) } diff --git a/uniapp/src/pages/login/login.vue b/uniapp/src/pages/login/login.vue index 9f7be461..ab08757c 100644 --- a/uniapp/src/pages/login/login.vue +++ b/uniapp/src/pages/login/login.vue @@ -16,17 +16,29 @@ - - + - 用户一键登录 + 微信小程序一键登录 - --> + + + + + + 微信公众号一键登录 + + @@ -205,7 +217,7 @@ @cancel="showModel = false" > - 请先阅读并同意 + 请先阅读并同意 《服务协议》 diff --git a/uniapp/src/pages/payment/payment.vue b/uniapp/src/pages/payment/payment.vue index a66ef54c..2bb7b231 100644 --- a/uniapp/src/pages/payment/payment.vue +++ b/uniapp/src/pages/payment/payment.vue @@ -77,11 +77,7 @@ ¥ {{ amount || '0.00' }} - + 立即缴费 @@ -97,9 +93,7 @@ v-model="password" @finish="onPasswordFinish" /> - - 取消 - + 取消 @@ -107,9 +101,13 @@ + + + + + diff --git a/uniapp/src/pages/pre_registration/pre_registration.vue b/uniapp/src/pages/pre_registration/pre_registration.vue index f8042c9d..7d536988 100644 --- a/uniapp/src/pages/pre_registration/pre_registration.vue +++ b/uniapp/src/pages/pre_registration/pre_registration.vue @@ -370,7 +370,12 @@ const submit = async () => { console.log('========== 准备提交数据 ==========') console.log('提交数据:', submitData) 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({ title: '提交中...', @@ -397,7 +402,13 @@ const submit = async () => { }) 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) } else { console.log('提交失败:', res?.msg || '未知错误')