支付系统对接商户

target:离开柬埔寨倒计时-214day

在这里插入图片描述

还是美女作为开篇

前言

昨天没有写文章,因为部门团建,我得去给他们画饼,说起来也真的是唏嘘,我一个已经都在计划着离开柬埔寨的人,昨天聚餐还一个个给他们描述未来的前景,看来我还是比较负责人的,在其位谋其政!!!

支付系统对接商户

作为一个支付系统,他除了普通业务的手续费收取之外,还是很需要外部商户接入的,而我负责整个支付系统的上层业务,那么对接商户自然是我推不掉的责任。这个商户系统我写的还是比较早的,核心都是我完成的,今天也由于时间关系,我就不画图了,下次有机会可以画一下。

  • 作为支付系统对外对接的时候我还是很有成就感的,因为看到自己的心血在被别人使用。
  • 前期亲自对接的几家商户都顺利在运用了,有这边的外卖软件(如简单点E-gets),也有这边的打车软件(如move),这些前期的商户都是我的成果呀,当时对接好了外卖软件后,我当天就用他们外面软件下单使用我们的支付软件支付,感觉很爽。
  • 在没有对接商户之前,我们点外卖、打车都是要付现金的,而有时候打车最容易被坑,有一次其实路程价格不到2美金的,结果给了司机20美金,他硬是不补我钱,跟他说了有两分钟吧,最后输的肯定是身为外国人的我了。
  • 后期我专门安排了人对接新的商户,现在接入我们支付系统的商户还是有点多了。

对接商户验签处理

在对接商户的时候,跟商户的交互肯定要做很多校验的,验签就是其中之一,我把当时做的验签方案简化后写成了实例代码,今天一直在写这个,所以博客才这么晚还没写完。

  • 首先就是验签工具了,我们支持商户签名算法有RSA和ECC,我就以ECC工具来举例,下面就是ECC的工具类
package com.littlehow.security;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * @author littlehow
 * @since 6/1/24 12:14
 */
@Slf4j
public class Ecc {

    private static final String SIGN = "sign";
    private static final String EMPTY = "";
    // 提供者的名字,此处是常量BC
    private static final String PROVIDER = BouncyCastleProvider.PROVIDER_NAME;
    private static final String KEY_ALGORITHM = "EC";
    private static final Base64.Encoder ENCODER = Base64.getEncoder();
    private static final Base64.Decoder DECODER = Base64.getDecoder();

    static {
        // 主要提供BC的provider
        Security.addProvider(new BouncyCastleProvider());
    }

    public static Pair<String, String> initKey() {
        try {
            // 这里可以使用默认的provider,也就是说可以不用BC的provider,默认会根据前置算法找到对应的provider
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM, PROVIDER);
            keyPairGen.initialize(new ECGenParameterSpec("secp256r1"));
            KeyPair keyPair = keyPairGen.generateKeyPair();
            ECPrivateKey privateKey = (ECPrivateKey)keyPair.getPrivate();
            ECPublicKey publicKey = (ECPublicKey)keyPair.getPublic();
            return Pair.of(ENCODER.encodeToString(privateKey.getEncoded()),
                    ENCODER.encodeToString(publicKey.getEncoded()));
        } catch (Exception e) {
            log.error("不支持EC的key初始化", e);
        }
        return null;
    }

    /**
     * 私钥进行签名
     * @param obj        - 待签名对象
     * @param privateKey - 私钥
     */
    public static String sign(Object obj, String privateKey) {
        try {
            // 待签名字符串
            String toSign = createLinkString(obj);
            // 处理私钥
            PKCS8EncodedKeySpec PKCS8 = new PKCS8EncodedKeySpec(DECODER.decode(privateKey));
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM, PROVIDER);
            // 私钥
            PrivateKey key = keyFactory.generatePrivate(PKCS8);
            // 签名验签
            Signature signature = Signature.getInstance("SHA256withECDSA");
            signature.initSign(key);
            signature.update(toSign.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signature.sign());
        } catch (Exception e) {
            log.error("签名失败", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 验证签名
     * @param obj            - 验证对象
     * @param sign           - 签名串
     * @param publicKey      - 公钥
     * @return  -
     */
    public static boolean verify(Object obj, String sign, String publicKey) {
        try {
            // 待验签字符串
            String orig = createLinkString(obj);
            // 处理公钥
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(DECODER.decode(publicKey));
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM, PROVIDER);
            PublicKey key = keyFactory.generatePublic(keySpec);
            // 签名验签
            Signature signature = Signature.getInstance("SHA256withECDSA");
            signature.initVerify(key);
            signature.update(orig.getBytes(StandardCharsets.UTF_8));
            return signature.verify(DECODER.decode(sign));
        } catch (Exception e) {
            log.error("验签失败", e);
            return false;
        }
    }

    /**
     * 这里就尽量定义一个层级,二层级对象不能保证签名串的标准组装
     * @param obj  - 签名对象
     * @return - 待签名串
     */
    private static String createLinkString(Object obj) {
        if (obj instanceof String) {
            return (String) obj;
        }
        JSONObject json = JSONObject.parseObject(JSONObject.toJSONString(obj));
        TreeMap<String, String> treeMap = paraFilter(json);
        StringBuilder stringBuilder = new StringBuilder();
        treeMap.forEach((key, value) ->
                stringBuilder.append(key).append("=").append(value).append("&")
        );
        return stringBuilder.substring(0, stringBuilder.length() - 1);
    }

    /**
     * 过滤掉参数为空和空字符串的,因为项目需要,再过滤掉参数sign
     * 如果定义的是BigDecimal这种金额形式的,那么需要调用双方对无用0做处理
     * @param map   - 原始参数
     * @return - 过滤后的参数,treemap是以key排序的,所以直接使用treeMap返回结果
     */
    private static TreeMap<String, String> paraFilter(Map<String, Object> map) {
        TreeMap<String, String> result = new TreeMap<>();
        if (map == null || map.size() <= 0) {
            return result;
        }
        for (String key : map.keySet()) {
            Object value = map.get(key);
            if (value == null || EMPTY.equals(value) || SIGN.equalsIgnoreCase(key)) {
                continue;
            }
            String v1;
            if (value instanceof BigDecimal) {
                v1 = ((BigDecimal) value).stripTrailingZeros().toPlainString();
            } else {
                v1 = value.toString();
            }
            result.put(key, v1);
        }
        return result;
    }



    public static void main(String[] args) {
        Pair<String, String> key = initKey();
        if (key == null) {
            System.out.println("生成key失败");
            return;
        }
        System.out.println("privateKey=" + key.getFirst());
        System.out.println("publicKey=" + key.getSecond());
        // 测试参数
        Map<String, Object> param = new HashMap<>();
        param.put("name", "littlehow");
        param.put("age", "36");
        param.put("nonce", "82341");
        param.put("time", System.currentTimeMillis());
        String sign = sign(param, key.getFirst());
        System.out.println(sign);
        // 测试验签通过
        System.out.println(verify(param, sign, key.getSecond()));
    }
}

  • 还有一个就是我不想要业务在实现的时候每一个都需要去处理签名这块的逻辑,所以就做了一个统一处理,大概思路是有一个拦截器获取商户信息,并且把信息放入上下文,然后就是有一个统一前置处理器来处理验签,涉及到的类如下:

1.上下文类

package com.littlehow.web.context;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

/**
 * @author littlehow
 * @since 6/1/24 19:51
 */
@Setter
@Getter
@Accessors(chain = true)
public class SignCommon {
    /**
     * 商户公钥
     */
    private String publicKey;

    /**
     * 商户编号
     */
    private String merchantId;
}

====================================================================================
package com.littlehow.web.context;

import org.springframework.util.Assert;

/**
 * @author littlehow
 * @since 6/1/24 19:37
 */
public class SignContext {
    private static ThreadLocal<SignCommon> needVerify = new ThreadLocal<>();

    public static void set(SignCommon signCommon) {
        needVerify.set(signCommon);
    }

    public static boolean isNeed() {
        SignCommon common = needVerify.get();
        return common != null;
    }

    public static String publicKey() {
        SignCommon common = needVerify.get();
        // 如果没有值表示并没有进入拦截器,所以不能使用这个方法
        Assert.notNull(common, "SdkErrorCode.ILLEGAL_CALL_API");
        return common.getPublicKey();
    }

    public static void clear() {
        needVerify.remove();
    }
}

2.拦截器类


package com.littlehow.web.req;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * @author JimChery
 * @since 6/1/24 19:30
 */
@Setter
@Getter
public class BaseSignReq {
    @ApiModelProperty(value = "时间戳")
    @NotNull(message = "timestamp required")
    protected Long timestamp;

    @ApiModelProperty(value = "nonce,可以做一定时间内的防重处理")
    @NotBlank(message = "nonce required")
    protected String nonce;

    @ApiModelProperty(value = "签名")
    @NotBlank(message = "sign required")
    protected String sign;
}

====================================================================================

package com.littlehow.web.interceptor;

import com.littlehow.web.context.SignCommon;
import com.littlehow.web.context.SignContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 此拦截器配置统一拦截uri为/open/sdk/**的路径所以请求都保持post的签名体格式
 * @author littlehow
 * @since 6/1/24 20:03
 */
@Slf4j
public class OpenSdkInterceptor implements HandlerInterceptor {

    private static final String OPEN_SDK_APP_ID = "App-Id";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws IOException {
        // 获取header的key
        String appId = request.getHeader(OPEN_SDK_APP_ID);
        log.info("request_appId: {}"+appId);
        Assert.hasText(appId, "SdkErrorCode.APP_NOT_EXISTS");
        // 根据appId去db或者缓存里面获取商户信息
        Map<String, String> info = getMerchantInfo(appId);
        Assert.notNull(info, "SdkErrorCode.APP_NOT_EXISTS");
        //ip白名单校验
        checkCallIp(info.get("whiteIps"));
        SignContext.set(new SignCommon().setMerchantId(info.get("merchantId"))
                .setPublicKey(info.get("publicKey"))
        );
        return true;
    }

    /**
     * 此处模拟获取商户信息
     * @param appId - 应用id
     * @return - 商户信息
     */
    private Map<String, String> getMerchantInfo(String appId) {
        if (appId.length() < 10) {
            // 模拟商户不存在
            return null;
        }
        Map<String, String> map = new HashMap<>();
        map.put("publicKey", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZ7N/yJGVE/4rcokcTT/W3Ja5mOSTJbU1EMLSL+oBSVTirYdtiNDW9ASoVysOi1bZPMnJGh96uGtJY0/R4kdoxg==");
        map.put("merchantId", "" + System.currentTimeMillis());
        map.put("whiteIps", "192.168.1.1");
        return map;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {
        //删除设置的上下文
        SignContext.clear();
    }

    /**
     * 检查白名单ip
     * @param ips - 商户ip白名单
     */
    private void checkCallIp(String ips) {
        Assert.hasText(ips, "SdkErrorCode.API_CALL_IP_INVALID");
        // 此处ip从网络请求提取ip方法里面读,此处就写死了
        String ip = "192.168.1.2";
        log.info("whiteIP: {},sourceIp: {}",ips,ip);
        // 多个ip白名单用,分割
        String[] ipInfo = ips.split(",");
        boolean check = false;
        for (String s : ipInfo) {
            if (s.trim().equalsIgnoreCase(ip)) {
                check = true;
                break;
            }
        }
        Assert.isTrue(check, "SdkErrorCode.API_CALL_IP_INVALID");
    }
}

====================================================================================

package com.littlehow.web.interceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    private static final String OPEN_SDK = "/open/sdk/**";
    private static final String OPEN_SDK_MANAGE = "/open/manage/sdk/**";
    // 也可以配置其他的拦截器处理真实的业务
    // .....

    /**
     * 该方法用于注册拦截器
     * 可注册多个拦截器,多个拦截器组成一个拦截器链
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 增加一个拦截器  涉及提供给外部商户的接口做统一验签
        registry.addInterceptor(openSdkInterceptor())
                        .addPathPatterns(OPEN_SDK);
        // 实例:此处管理接口,使用管理接口的权限验证拦截器

        //registry.addInterceptor(authInterceptor())
        //       .addPathPatterns(OPEN_SDK_MANAGE);
    }

    @Bean
    public OpenSdkInterceptor openSdkInterceptor(){return new OpenSdkInterceptor();}
}

3.统一处理签名类

package com.littlehow.web.advice;

import com.littlehow.web.context.SignContext;
import com.littlehow.web.req.BaseSignReq;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.lang.reflect.Type;

/**
 * @author littlehow
 * @since 6/1/24 19:42
 */
@ControllerAdvice(value = "com.littlehow.web.controller")
public class OpenSdkRequestBodyAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 判定是否需要验签
        return SignContext.isNeed();
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        if(body instanceof BaseSignReq){
            // 此处进行验签处理,验签不通过将抛出异常,由统一异常处理器抛向调用端
            OpenSdkSignAssist.execute((BaseSignReq) body);
        }
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

====================================================================================

package com.littlehow.web.advice;


import com.littlehow.security.Ecc;
import com.littlehow.web.context.SignContext;
import com.littlehow.web.req.BaseSignReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

/**
 * @author littlehow
 * @since 6/1/24 19:51
 */
@Component
@Slf4j
public class OpenSdkSignAssist {

    // 执行签名验证
   static void execute(BaseSignReq req) {
        long now = System.currentTimeMillis();
        // 支持时间误差5分钟 抛出签名时间超时的错误码,这里就简单抛出个异常
        Assert.isTrue(Math.abs(now - req.getTimestamp()) <= 5 * 60 * 1000, "SdkErrorCode.SIGN_TIMEOUT");
        // 然后进行签名验证,如果验签不过抛出错误码
        Assert.isTrue(Ecc.verify(req, req.getSign(), SignContext.publicKey()), "SdkErrorCode.SIGN_INVALID");
    }

}


4.实际的业务类

package com.littlehow.web.controller.vo;

import com.littlehow.web.req.BaseSignReq;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;

/**
 * @author littlehow
 * @since 6/1/24 20:25
 */
@Setter
@Getter
public class CreateOrderReq extends BaseSignReq {
    @ApiModelProperty(value = "外部订单编号", required = true)
    @NotBlank(message = "outOrderNo must be not null")
    private String outOrderNo;

    @ApiModelProperty(value = "金额", required = true)
    @NotNull(message = "amount must be not null")
    private BigDecimal amount;

    @ApiModelProperty(value = "币种", required = true)
    @NotBlank(message = "currency must be not null")
    private String currency;

    @ApiModelProperty("交易备注")
    private String remark;
}

====================================================================================

package com.littlehow.web.controller;

import com.alibaba.fastjson.JSONObject;
import com.littlehow.web.controller.vo.CreateOrderReq;
import lombok.extern.slf4j.Slf4j;
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.validation.Valid;

/**
 * @author littlehow
 * @since 6/1/24 20:24
 */
@RestController
@RequestMapping("/open/sdk/order")
@Slf4j
public class CreateOrderController {
    @PostMapping("create")
    public void create(@RequestBody @Valid CreateOrderReq req) {
        // 此处只要打印请求即可
        log.info(JSONObject.toJSONString(req));
    }
}


这样在写所有关于对外开放接口里面的签名都统一处理了,每个业务就不再关心验签的逻辑,只需要关心的自己的业务逻辑处理即可

后记

这里是我简化了验证以及业务,实际的验证更严格,毕竟是支付系统,安全才是头等大事,但是大体的处理思路就是这样的,尽量的解放业务。

在这里插入图片描述

加油吧littlehow

北京时间:2024-06-01 21:02

金边时间:2024-05-30 20:02

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/668972.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5G无线标准演进综述及新技术引入

摘 要 随着经济和社会的发展&#xff0c;5G业务越来越丰富多彩&#xff0c;1080P高清视频、裸眼3D、网联汽车、云手机等新业务、新终端对网络的要求也越来越高&#xff1b;另一方面&#xff0c;5G标准持续演进&#xff0c;在MIMO、载波聚合、移动性管理、uRLLC、切片、定位等方…

海思SD3403,SS928/926,hi3519dv500,hi3516dv500移植yolov7,yolov8(19)-Yolov10探索

YOLOv10 开源有几天了,看性能是比较强的,但是试过的一些人说没有YOLOv8好,实际效果以测试结果为准,这里创新点算是去掉了之前YOLO的NMS步骤,论文题目也说了NMS-Free,以此来提高小目标检测率,减少计算冗余,也没有NMS的计算时间提高实时性。 这个倒是让我看到了以后可以…

以sqlilabs靶场为例,讲解SQL注入攻击原理【18-24关】

【less-18】 打开时&#xff0c;获取了自己的IP地址。&#xff0c;通过分析源码知道&#xff0c;会将用户的user-agent作为参数记录到数据库中。 提交的是信息有user-Agent、IP、uname信息。 此时可以借助Burp Suite 工具&#xff0c;修改user_agent&#xff0c;实现sql注入。…

STM32之USART(串口)通信学习

1.通信接口 在开始通信之前&#xff0c;我们要了解什么是通信&#xff0c;通信就是将一个设备的数据传送到另一个设备。 同时按照双方规定的协议即通信协议&#xff0c;指定通信规则通信双方按照规则进行数据的收发。 应用场景&#xff1a;单片机的串口可以使单片机与单片机…

软件架构设计属性之5:可维护性属性分析与应用

文章目录 引言一、可维护性定义和重要性1.1 定义1.2 重要性 二、可维护性关键要素2.1 模块化2.2 单一职责2.3 低耦合2.4 高内聚2.5 抽象和封装2.6 实践建议 三、设计原则3.1 开闭原则3.2 依赖倒置原则3.3 评估方法3.4 挑战与解决方案 四、实战应用总结 引言 在当今数字化飞速发…

利用GNSS IMU集成提高车道级定位精度

准确的定位对于很多不同的事情都是至关重要的。导航系统可以引导我们去某个地方&#xff0c;自动驾驶汽车可以利用这些数据在道路上安全行驶。尽管全球导航卫星系统(GNSS)在定位方面非常出色&#xff0c;但它们可能并不总是提供最准确的车道水平事实。解决这个问题的一个有希望…

大模型对齐方法笔记四:针对领域问答来进行知识对齐方法KnowPAT

KnowPAT KnowPAT(Knowledgeable Preference AlignmenT) 出自2023年11月的论文《Knowledgeable Preference Alignment for LLMs in Domain-specific Question Answering》&#xff0c;主要针对领域问答来进行知识对齐。 在领域问答有两个挑战&#xff1a;希望输出满足用户的要…

15-通过JS代码处理窗口滚动条

selenium并不是万能的&#xff0c;页面上有些操作无法实现时&#xff0c;就需要借助JS代码来完成了。selenium提供了一个方法&#xff1a;execute_script()&#xff0c;可以执行JS脚本代码。 比如&#xff1a;当页面上的元素超过一屏后&#xff0c;想操作屏幕下方的元素&#x…

git报错prohibited by Gerrit: not permitted: update

git push报错&#xff1a; Push to refs/for/[branch] to create a review, or get Push rights to update the branch. Contact an administrator to fix the permissions (prohibited by Gerrit: not permitted: update)原因&#xff1a; 使用Gerrit代码审核时&#xff0c;本…

IsoBench:多模态基础模型性能的基准测试与优化

随着多模态基础模型的快速发展&#xff0c;如何准确评估这些模型在不同输入模态下的性能成为了一个重要课题。本文提出了IsoBench&#xff0c;一个基准数据集&#xff0c;旨在通过提供多种同构&#xff08;isomorphic&#xff09;表示形式的问题&#xff0c;来测试和评估多模态…

React-表单受控绑定

概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 1.准备一个React状态值 2.通过value属性绑定状态&#xff0c;通过onChange属性绑定状态同步的函数

Web自动化测试-掌握selenium工具用法,使用WebDriver测试Chrome/FireFox网页(Java

目录 一、在Eclipse中构建Maven项目 1.全局配置Maven 2.配置JDK路径 3.创建Maven项目 4.引入selenium-java依赖 二、Chrome自动化脚本编写 1.创建一个ChromeTest类 2.测试ChromeDriver 3.下载chromedriver驱动 4.在脚本中通过System.setProperty方法指定chromedriver的…

《软件方法(下)》8.3.4.5和《设计模式》中用语的区别

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 8.3 建模步骤C-2 识别类的关系 8.3.4 识别关联关系 8.3.4.4 类关系再整理 有了前面的知识&#xff0c;我们需要再整理一下类的关系。用类图表示类的关系如图8-134。 图8-134 “类的…

NextJs 数据篇 - 数据获取 | 缓存 | Server Actions

NextJs 数据篇 - 数据获取 | 缓存 | Server Actions 前言一. 数据获取 fetch1.1 缓存 caching① 服务端组件使用fetch② 路由处理器 GET 请求使用fetch 1.2 重新验证 revalidating① 基于时间的重新验证② 按需重新验证revalidatePathrevalidateTag 1.3 缓存的退出方式 二. Ser…

录制gif 强推LICEcap

LICEcap 官网&#xff1a;https://www.cockos.com/licecap/ 即按即用&#xff0c;录制好的gif可直接插入博客&#xff0c;yyds~

算法练习第25天|491. 非递减子序列

491. 非递减子序列 491. 非递减子序列https://leetcode.cn/problems/non-decreasing-subsequences/ 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案…

文件IO(三)

文件IO&#xff08;三&#xff09; 左移右移Linux的man 手册文件IO打开文件操作文件关闭文件 caps lock开灯关灯读取按键文件IO操作目录文件打开目录文件操作目录文件 库动态库和静态库的优缺点创建静态库创建动态库 按下右ctrl键 亮灭灯 左移右移 Linux的man 手册 文件IO 打开…

知网AI查重:AI工具如何助力通过检测?

论文降重一直是困扰各界毕业生的“拦路虎”&#xff0c;还不容易熬过修改的苦&#xff0c;又要迎来降重的痛。 其实想要给论文降重达标&#xff0c;我有一些独家秘诀。话不多说直接上干货&#xff01; 1、同义词改写&#xff08;针对整段整句重复&#xff09; 这是最靠谱也是…

旅游门票预订系统小程序源码购票源码

功能介绍&#xff1a; 景点项目 支持发布多个景点项目、景点门票等。 在线支付 支持整合微信支付功能 一款基于ThinkPHP Uniapp开发的旅游i ]票预订系 统 支持景点]票、导游产品便捷预订、美食打卡、景点分 享、旅游笔记分享等综合系统,提供前后台无加密源码&#xff0c;支…

JS脚本打包成一个 Chrome 扩展(CRX 插件)

受这篇博客 如何把CSDN的文章导出为PDF_csdn文章怎么导出-CSDN博客 启发&#xff0c;将 JavaScript 代码打包成一个 Chrome 扩展&#xff08;CRX 插件&#xff09;。 步骤&#xff1a; 1.创建必要的文件结构和文件&#xff1a; manifest.jsonbackground.jscontent.js 2.编写…