开发运维
natapp - 内网穿透
NATAPP
1 2 3 4 5 6 7 8 9
|
[default] authtoken=79f9d0d50e929248 clienttoken= log=none loglevel=ERROR
|
配置完环境后直接打开 exe 程序,启动内网穿透

之后 http://lbwxxc.natapp1.cc 就可以替代 http://localhost:8080 了

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @SpringBootApplication @RestController() @RequestMapping("/api/") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); }
@RequestMapping(value = "/test", method = RequestMethod.GET) public ResponseBodyEmitter test(HttpServletResponse response) { response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); response.setHeader("Cache-Control", "no-cache"); ResponseBodyEmitter emitter = new ResponseBodyEmitter(); String[] words = new String[]{"恭喜💐 ", "你的", " NatApp 内网穿透 ", "部", "署", "测", "试", "成", "功", "了啦🌶!"}; new Thread(() -> { for (String word : words) { try { emitter.send(word); Thread.sleep(250); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } }).start(); return emitter; } }
|
微信公众号
接口配置信息

- URL :当微信平台验证 接口配置信息 时,会向这个 URL 发送 GET 请求,包含 Token
| 参数 |
描述 |
| signature |
微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
| timestamp |
时间戳 |
| nonce |
随机数 |
| echostr |
随机字符串 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Slf4j @RestController() @CrossOrigin("*") @RequestMapping("/api/v1/weixin/portal/") public class WeixinPortalController { @Value("${weixin.config.originalid}") private String originalid; @Value("${weixin.config.token}") private String token; @GetMapping(value = "receive", produces = "text/plain;charset=utf-8") public String validate(@RequestParam(value = "signature", required = false) String signature, @RequestParam(value = "timestamp", required = false) String timestamp, @RequestParam(value = "nonce", required = false) String nonce, @RequestParam(value = "echostr", required = false) String echostr) { try { log.info("微信公众号验签信息开始 [{}, {}, {}, {}]", signature, timestamp, nonce, echostr); if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) { throw new IllegalArgumentException("请求参数非法,请核实!"); } boolean check = SignatureUtil.check(token, signature, timestamp, nonce); log.info("微信公众号验签信息完成 check:{}", check); if (!check) { return null; } return echostr; } catch (Exception e) { log.error("微信公众号验签信息失败 [{}, {}, {}, {}]", signature, timestamp, nonce, echostr, e); return null; } } }
|
当微信平台进行验证时,就会请求这个方法,当返回 echostr 时,微信平台就会认为此端口配置正确
发送文本消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @PostMapping(value = "receive", produces = "application/xml; charset=UTF-8") public String post(@RequestBody String requestBody, @RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("openid") String openid, @RequestParam(name = "encrypt_type", required = false) String encType, @RequestParam(name = "msg_signature", required = false) String msgSignature) { try { log.info("接收微信公众号信息请求{}开始\n{}", openid, requestBody); MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class); return buildMessageTextEntity(openid, "你好," + message.getContent()); } catch (Exception e) { log.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e); return ""; } }
|
当有用户向公众号发送文本消息时,微信平台会将信息转发到这个方法,该方法处理完请求后,会把信息返回给微信平台,再有微信平台把处理后的消息发送给用户
1 2 3 4 5 6 7 8 9 10
| <xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[this is a test]]></Content> <MsgId>1234567890123456</MsgId> <MsgDataId>xxxx</MsgDataId> <Idx>xxxx</Idx> </xml>
|
| 参数 |
描述 |
| ToUserName |
开发者微信号 |
| FromUserName |
发送方账号(一个OpenID) |
| CreateTime |
消息创建时间 (整型) |
| MsgType |
消息类型,文本为text |
| Content |
文本消息内容 |
| MsgId |
消息id,64位整型 |
| MsgDataId |
消息的数据ID(消息如果来自文章时才有) |
| Idx |
多图文时第几篇文章,从1开始(消息如果来自文章时才有) |
Alipay 沙箱支付
支付宝|开放平台 地址: 沙箱应用 - 开放平台

配置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static String app_id = "";
public static String merchant_private_key = "";
public static String alipay_public_key = "";
public static String notify_url = "http://lbwxxc.natapp1.cc/api/v1/alipay/alipay_notify_url";
public static String return_url = "https://gaga.plus";
public static String gatewayUrl = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
public static String sign_type = "RSA2";
public static String charset = "utf-8";
|



发送订单


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@RequestMapping(value = "alipay_notify_url", method = RequestMethod.POST) public String payNotify(HttpServletRequest request) throws AlipayApiException { log.info("支付回调,消息接收 {}", request.getParameter("trade_status"));
if (!request.getParameter("trade_status").equals("TRADE_SUCCESS")) { return "false"; }
Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); }
String tradeNo = params.get("out_trade_no"); String gmtPayment = params.get("gmt_payment"); String alipayTradeNo = params.get("trade_no");
String sign = params.get("sign"); String content = AlipaySignature.getSignCheckContentV1(params); boolean checkSignature = AlipaySignature.rsa256CheckContent(content, sign, alipayPublicKey, "UTF-8"); if (!checkSignature) { return "false"; }
log.info("支付回调,交易名称: {}", params.get("subject"));
return "success"; }
|
当用户付款后,就会触发这个回调方法
1 2 3 4 5 6 7 8 9 10
| 25-05-16.16:39:01.052 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,消息接收 TRADE_SUCCESS 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,交易名称: 测试商品 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,交易状态: TRADE_SUCCESS 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,支付宝交易凭证号: 2025051622001489770505612628 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,商户订单号: xfg2024092711320005 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,交易金额: 113.00 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,买家在支付宝唯一id: 2088722067789772 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,买家付款时间: 2025-05-16 16:39:52 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,买家付款金额: 113.00 25-05-16.16:39:01.067 [http-nio-8080-exec-1] INFO AliPayController - 支付回调,支付回调,更新订单 xfg2024092711320005
|
功能实现 - MVC
MVC 工程框架搭建、基础配置
在父工程的 pom 文件里
1 2 3 4
| <dependencyManagement> ... </dependencyManagement>
|
在 Application 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@SpringBootApplication @Configurable @EnableScheduling public class Application {
public static void main(String[] args) { SpringApplication.run(Application.class); }
}
|

1、用户点击登录需要二维码,而二维码生成是「不需要走服务端」,直接调用微信的接口「https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket...」,可是这个接口需要一个 ticket,这个ticket就需要服务端提供了,因此才会去请求「/api/v1/login/weixin_qrcode_ticket」接口,如果浏览器本身就缓存了一个可用的ticket,是可以不需要请求服务端的。
2、因此,在步骤1的情况下,关键就是拿到ticket !!! 而 ticket 怎么拿?就是请求服务端接口「/api/v1/login/weixin_qrcode_ticket」。服务端怎么拿ticket?请求微信接口「https://api.weixin.qq.com/cgi-bin/qrcode/create?ac...」,这时会发现,又需要一个参数 access_token。同样的,如果服务端已经缓存了access_token,就不需要请求微信接口来拿 access_token 了。
3、继续,那如果请求微信接口,这个access_toke怎么拿?「https://api.weixin.qq.com/cgi-bin/token?grant_type...」,这个接口需要的参数appid、secret就是配置里面的了,填充就行~
【重点】4、最后,就是从结果反向推导各个流程:请求二维码需要ticket —没有ticket—> 请求服务端拿ticket —没有ticket—> 请求ticket需要access_token — 没有access_toke—> 请求access_toke需要appid&secret「这两个参数现成的,填充上去即可」
代码细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Slf4j @Configuration public class Retrofit2Config {
private static final String BASE_URL = "https://api.weixin.qq.com/";
@Bean public Retrofit retrofit() { return new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(JacksonConverterFactory.create()).build(); }
@Bean public IWeixinApiService weixinApiService(Retrofit retrofit) { return retrofit.create(IWeixinApiService.class); }
}
|
retrofit 实例化 IWeixinApiService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
public interface IWeixinApiService {
@GET("cgi-bin/token") Call<WeixinTokenRes> getToken(@Query("grant_type") String grantType, @Query("appid") String appId, @Query("secret") String appSecret);
@POST("cgi-bin/qrcode/create") Call<WeixinQrCodeRes> createQrCode(@Query("access_token") String accessToken, @Body WeixinQrCodeReq weixinQrCodeReq);
@POST("cgi-bin/message/template/send") Call<Void> sendMessage(@Query("access_token") String accessToken, @Body WeixinTemplateMessageVO weixinTemplateMessageVO);
}
|
获取 Access token、获取凭据 ticket、发送微信公众号模板消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@RequestMapping(value = "weixin_qrcode_ticket", method = RequestMethod.GET) public Response<String> weixinQrCodeTicket() { try { String qrCodeTicket = loginService.createQrCodeTicket(); log.info("生成微信扫码登录 ticket:{}", qrCodeTicket); return Response.<String>builder() .code(Constants.ResponseCode.SUCCESS.getCode()) .info(Constants.ResponseCode.SUCCESS.getInfo()) .data(qrCodeTicket) .build(); } catch (Exception e) { log.error("生成微信扫码登录 ticket 失败", e); return Response.<String>builder() .code(Constants.ResponseCode.UN_ERROR.getCode()) .info(Constants.ResponseCode.UN_ERROR.getInfo()) .build(); } }
|
用户向服务端请求 ticket,用户可以凭借此 ticket 获取二维码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @PostMapping(value = "receive", produces = "application/xml; charset=UTF-8") public String post(@RequestBody String requestBody, @RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("openid") String openid, @RequestParam(name = "encrypt_type", required = false) String encType, @RequestParam(name = "msg_signature", required = false) String msgSignature) { try { log.info("接收微信公众号信息请求 {} 开始\n{}", openid, requestBody); MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class); if ("event".equals(message.getMsgType()) && "SCAN".equals(message.getEvent())) { loginService.saveLoginState(message.getTicket(), openid); return buildMessageTextEntity(openid, "登录成功"); } return buildMessageTextEntity(openid, "你好," + message.getContent()); } catch (Exception e) { log.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e); return ""; } }
|
当用户扫码成功后,微信会请求这个接口,服务端可以保存用户的登录状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
@RequestMapping(value = "check_login", method = RequestMethod.GET) public Response<String> checkLogin(@RequestParam String ticket) { try { String openidToken = loginService.checkLogin(ticket); log.info("扫码检测登录结果 ticket:{} openidToken:{}", ticket, openidToken); if (StringUtils.isNotBlank(openidToken)) { return Response.<String>builder() .code(Constants.ResponseCode.SUCCESS.getCode()) .info(Constants.ResponseCode.SUCCESS.getInfo()) .data(openidToken) .build(); } else { return Response.<String>builder() .code(Constants.ResponseCode.NO_LOGIN.getCode()) .info(Constants.ResponseCode.NO_LOGIN.getInfo()) .build(); } } catch (Exception e) { log.error("扫码检测登录结果失败 ticket:{}", ticket, e); return Response.<String>builder() .code(Constants.ResponseCode.UN_ERROR.getCode()) .info(Constants.ResponseCode.UN_ERROR.getInfo()) .build(); } }
|
用户会轮询检测自己是否登录成功,如果成功会获取 token并跳转页面

代码细节
1 2 3 4 5 6 7 8 9 10
| @Bean("alipayClient") public AlipayClient alipayClient(AliPayConfigProperties properties) { return new DefaultAlipayClient(properties.getGatewayUrl(), properties.getApp_id(), properties.getMerchant_private_key(), properties.getFormat(), properties.getCharset(), properties.getAlipay_public_key(), properties.getSign_type()); }
|
创建支付宝客户端 (AlipayClient) 的 Bean,并将其注册到 Spring 容器中。这个客户端实例可以用于后续与支付宝支付系统的交互,如创建订单、查询交易状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private PayOrder doPrepayOrder(String productId, String productName, String orderId, BigDecimal totalAmount) throws AlipayApiException { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setNotifyUrl(notifyUrl); request.setReturnUrl(returnUrl); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", orderId); bizContent.put("total_amount", totalAmount.toString()); bizContent.put("subject", productName); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); request.setBizContent(bizContent.toString()); String form = alipayClient.pageExecute(request).getBody(); PayOrder payOrder = new PayOrder(); payOrder.setOrderId(orderId); payOrder.setPayUrl(form); payOrder.setStatus(Constants.OrderStatusEnum.PAY_WAIT.getCode()); orderDao.updateOrderPayInfo(payOrder); return payOrder; }
|
成功创建的订单会返回一个 form 表单,用户可以访问这个表单进行支付
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @RequestMapping(value = "alipay_notify_url", method = RequestMethod.POST) public String payNotify(HttpServletRequest request) throws AlipayApiException { log.info("支付回调,消息接收 {}", request.getParameter("trade_status")); if (!request.getParameter("trade_status").equals("TRADE_SUCCESS")) { return "false"; } Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } String tradeNo = params.get("out_trade_no"); String gmtPayment = params.get("gmt_payment"); String alipayTradeNo = params.get("trade_no"); String sign = params.get("sign"); String content = AlipaySignature.getSignCheckContentV1(params); boolean checkSignature = AlipaySignature.rsa256CheckContent(content, sign, alipayPublicKey, "UTF-8"); if (!checkSignature) { return "false"; } log.info("支付回调,交易状态: {}", params.get("trade_status"));
orderService.changeOrderPaySuccess(tradeNo); return "success"; }
|
用户支付完成后,支付宝会进行异步回调访问这个这个接口,服务端可以进行相应的处理
改造为 DDD
- s-pay-mall-app : 启动、配置项
- s-pay-mall-api : 接口,方便其他模块调用
- s-pay-mall-trigger : 引用 api 包, 一个平台,接收什么请求,实现请求
- s-pay-mall-domain :分领域,比如说一个登录领域,建立一个登录包,专门放有关登录的对象,充血对象和充血包,存放原 service 的实现
- s-pay-mall-infrastructure :基础设施层,依赖倒置,会实现 domain 层的接口
- s-pay-mall-types : 一般类

在 domain 一个领域中,分为:
- adapter:外部接口适配器层,当需要调用外部接口时,则创建出这一层,并定义接口,之后由基础设施层的 adapter 层具体实现
- port : 当需要调用外部接口时,则创建出这一层,并定义接口,之后由基础设施层的 adapter 层具体实现
- repository : 定义仓储接口,之后由基础设施层做具体实现
- model :对象
- aggregate : 聚合对象,聚合实体和值对象
- entity : 实体对象,一般和数据库持久化对象1v1的关系
- valobj : 值对象,用于描述对象属性的值
- service :具体业务逻辑的实现
在 infrastructure 基础设施层,分为:
- adapter : 适配器,实现 domain 接口
- dao :操作数据库
- gateway :定义http、rpc接口,调用外部。在 adapter 中调用这部分内容。
在 trigger 中 :
- http :实现 http 接口
- job :
- listener :

如图所示将 mvc 中的 web 模块中的配置项拆分到 ddd 中的 app 模块

先将 mvc 中的 web 模块中的可以对外提供接口的 controller 抽象为接口放入到 ddd 中的 api 模块,如果 web 模块中有些 controller 不需要对外提供接口,则可以不需要抽象
在 ddd 的 trigger 模块中,实现 api 的两个接口

将 mvc 的 service 模块按领域拆分到 ddd 中的 domain 中,变为充血模型,domain 中各个领域中的 port 和 repository 通过依赖倒置的方式交由 infrastructure 实现

infrastructure 实现数据库操作,实现 port 和 repository,以及调用外部接口