调用支付宝账单接口记账

前往原站点查看

2023-11-22 14:47:42

    为了进行个人财产管理,在先前的几个月都是用的excel表格进行记账,每次回家都要进行一步记录当日消费入账的操作,非常的繁琐,而且也不方便处理数据。于是乎就想着能不能接入支付宝的官方api来获取每日的账单,并且同步到个人系统中。

    因为账单模块个人主页是相对独立的两个主题模块,所以账单模块单独写了一个微服务,个人主页只要负责调用即可。本文重点在于接入官方API并且实现记录账单模块。

支付宝开通应用

    1. 首先前往官方开放平台网址登录(点击前往)。

    2. 接着点击“控制台”,首次点击控制台会让你绑定个人手机号码。

    3. 成功后能够找到一个默认的应用,如下图所示。

    

服务接入

    要获取账单,需要进行如下的操作:

    1. 点击应用详情,进入到该应用的详情页

    2. 选择产品绑定,绑定产品选择下面两个产品勾选加入。



设置接口加签方式

    完成上述步骤后,接下来就是对接接口部分,需要先完成加签步骤。

    前往 开发设置 > 接口加签设置 。可以采用密钥方式或者证书模式,自行按照需求选择。


    我采用的是密钥方式,具体的操作步骤有 官方文档 。使用官方工具生成应用的公私钥后,上传即可获得支付宝公钥得以进一步对接。(注意保护好这些密钥信息,不要泄露)。如果安全要求较高,可以设置 服务器IP白名单 ,也在 “开发设置”页签下。



设置回调地址

    后续需要登录支付宝账号获取code,此code将会在回调地址中附带,所以还需要设置一个回调地址,以接受授权码等信息。

引入sdk

    完成上述前置步骤后,就可以正式接入官方的API了,除去极其特殊的编程语言需要手动对接外,官方都有提供sdk。(官方sdk地址

    对于java语言maven依赖配置如下:

<dependency>
  <groupId>com.alipay.sdk</groupId>
  <artifactId>alipay-sdk-java</artifactId>
  <version>4.34.0.ALL</version>
</dependency>

配置授权回调接口

    在上文中,我们有配置一个授权回调的地址,现在我们就需要实现该地址,并且能够正常获得code。

    可以配置的很简单,比如我的授权回调接口就只打印了code而已,当然如果有更进一步的需求,可以自行扩展。

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/alipay")
public class AlipayCallBackController {

    // https://auth.dreamcenter.top/alipay/callback?app_id=123456789&source=alipay_wallet&scope=auth_user&auth_code=a1b2c3
    @RequestMapping("/callback")
    public String callback(String auth_code, String app_id, String scope) {
        System.out.printf("alipay => \nappId:\t%s \ncode:\t%s \nscope:\t%s\n\n", app_id, auth_code, scope);
        return "ok";
    }

}

    然后是进入支付宝应用授权页面,页面地址如下所示。

https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?app_id=123456&scope=auth_user&redirect_uri=https%3A%2F%2Fauth.dreamcenter.top%2Falipay%2Fcallback

    这里主要需要修改两个参数,第一个是app_id,为自己的应用id,可在这里看到:


    第二个是redirect_uri,即对回调地址进行url编码,编码方式可以如下图所示,打开控制台,输入encodeURIComponent('your callback url'):



    之后,只要我们访问支付宝应用授权链接,就可以跳到授权页面,通过支付宝授权后,会跳转到回调地址,并且携带后续调用API所需的code。

官方接口的基础使用

    通过上述步骤,我们成功的开通了所需产品,并且获得了 APP_ID(应用id)、APP_PRIVATE_KEY(应用私钥)、ALIPAY_PUBLIC_KEY(支付宝公钥)、TOKEN(code),接着我们就可以尝试调用了。

    对于个人支出账单的官方文档在此:查看。通过 alipay.data.bill.buy.query(支付宝商家账户买入交易查询) 接口,可以获取指定时间段的个人支出账单。

    下面是一个基础示例,可以获取 2023-11-01 到 2023-11-10 的所有支出账单。第一步:构建alipayClient;第二步:从授权的code获取accessToken;第三步:调用接口。

final String APP_ID="123";
final String APP_PRIVATE_KEY="???";
final String ALIPAY_PUBLIC_KEY="???";

AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do",
		APP_ID, APP_PRIVATE_KEY, "json", "UTF8", ALIPAY_PUBLIC_KEY, "RSA2");


// 依据code,获取accessToken
AlipaySystemOauthTokenRequest oauth = new AlipaySystemOauthTokenRequest();
oauth.setGrantType("authorization_code");
oauth.setCode("code");
AlipaySystemOauthTokenResponse oauthRes = alipayClient.execute(oauth);
String accessToken = oauthRes.getAccessToken();
System.out.println(accessToken);


// 支出
AlipayDataBillBuyQueryRequest billReq = new AlipayDataBillBuyQueryRequest();
AlipayDataBillBuyQueryModel billMod = new AlipayDataBillBuyQueryModel();
billMod.setStartTime("2023-11-01 00:00:00");
billMod.setEndTime("2023-11-10 00:00:00");
billReq.setBizModel(billMod);

AlipayDataBillBuyQueryResponse billRes = alipayClient.execute(billReq, accessToken);
billRes.getDetailList().forEach(item -> {
	System.out.println(item.getAlipayOrderNo() + ":" + item.getGmtCreate() + " : " + item.getTotalAmount() + "\t:" + item.getGoodsTitle() + " /// " + item.getTradeStatus());
});

    当然,经过计算,一个accessToken默认只有15天的有效时间,实际上,上面代码oauthRes除了携带了accessToken外,还有若干信息,如令牌有效时间、刷新令牌、刷新令牌有效时间等。刷新令牌有效时间为30天,为了实现全覆盖,我们需要记录一个令牌创建时间,更新的时间判断依据大致是: 当前时间 - 创建时间 >= 令牌失效时间。一旦失效就调用刷新令牌,更新accessToken,并且保存这次更新。

封装与应用

    首先需要封装令牌的刷新方法工具类,将该部分逻辑与主体业务分离开来:

package top.dreamcenter.bill.util;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.domain.TradeItemResult;
import com.alipay.api.request.AlipaySystemOauthTokenRequest;
import com.alipay.api.response.AlipaySystemOauthTokenResponse;
import lombok.*;
import top.dreamcenter.bill.entity.AlipayBill;

/**
 * 支付宝相关工具类
 */
public class AlipayUtil {

    /**
     * 依据code来获取token信息,包含续期的令牌信息
     * @param alipayClient cli
     * @param code 回调得到的code
     * @return token信息
     */
    public static AlipayOauthToken getAccessToken(AlipayClient alipayClient, String code) {
        return tokenHandler(alipayClient, false, code);
    }

    /**
     * 依据refreshToken来刷新token信息,包含续期的令牌信息
     * @param alipayClient cli
     * @param refreshToken 刷新令牌
     * @return token信息
     */
    public static AlipayOauthToken refreshToken(AlipayClient alipayClient, String refreshToken) {
        return tokenHandler(alipayClient, true, refreshToken);
    }

    /**
     * 对已有的一组token记录进行判断,如果过期则刷新并且返回最新的记录,否则返回原来的记录。
     * @param alipayClient cli
     * @param alipayOauthToken 原始token
     * @return 结果
     */
    public static AlipayOauthToken refreshIfExpired(AlipayClient alipayClient, AlipayOauthToken alipayOauthToken) {
        // 判断是否过期
        long createAt = alipayOauthToken.getCreateTime();
        String expireStr = alipayOauthToken.getExpiresIn();
        int expire = Integer.parseInt(expireStr) * 1000;
        long cur = System.currentTimeMillis();

        if (cur - createAt >= expire) {
            // 过期
            return AlipayUtil.refreshToken(alipayClient, alipayOauthToken.getRefreshToken());
        } else {
            // 未过期
            return alipayOauthToken;
        }
    }

    /**
     * 根据官方账单返回数据结果,映射到本工程的实体
     * @param res 官方数据结构
     * @return AlipayBill
     * @deprecated 似乎没有必要多套一层
     */
    @Deprecated
    public static AlipayBill billTranslator(TradeItemResult res) {
        return AlipayBill.builder()
                .gmtCreate(res.getGmtCreate())
                .gmtPay(res.getGmtPay())
                .gmtRefund(res.getGmtRefund())
                .alipayOrderNo(res.getAlipayOrderNo())
                .merchantOrderNo(res.getMerchantOrderNo())
                .otherAccount(res.getOtherAccount())
                .goodsTitle(res.getGoodsTitle())
                .totalAmount(res.getTotalAmount())
                .netMDiscount(res.getNetMdiscount())
                .refundAmount(res.getRefundAmount())
                .serviceFee(res.getServiceFee())
                .tradeStatus(res.getTradeStatus())
                .tradeType(res.getTradeType())
                .storeNo(res.getStoreNo())
                .storeName(res.getStoreName())
                .goodsMemo(res.getGoodsMemo())
                .build();
    }

    /**
     * token获取处理器
     * @param alipayClient cli
     * @param isRefresh 是否是刷新token操作
     * @param inf 操作需要的信息
     * @return token信息
     */
    private static AlipayOauthToken tokenHandler(AlipayClient alipayClient, boolean isRefresh, String inf) {
        AlipaySystemOauthTokenRequest oauth = new AlipaySystemOauthTokenRequest();

        oauth.setGrantType(isRefresh ? "refresh_token" : "authorization_code");

        if (isRefresh) oauth.setRefreshToken(inf);
        else oauth.setCode(inf);

        AlipaySystemOauthTokenResponse oauthRes;

        try {
            oauthRes = alipayClient.execute(oauth);
        } catch (AlipayApiException e) {
            return null;
        }

        return AlipayOauthToken.builder()
                .accessToken(oauthRes.getAccessToken())
                .userId(oauthRes.getUserId())
                .expiresIn(oauthRes.getExpiresIn())
                .reExpiresIn(oauthRes.getReExpiresIn())
                .refreshToken(oauthRes.getRefreshToken())
                .createTime(System.currentTimeMillis())
                .build();
    }

    /**
     * 支付宝授权token信息
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AlipayOauthToken {

        /**
         * 用户的userId (支付宝用户的唯一 userId)
         */
        private String userId;

        /**
         * 交换令牌 (用于获取用户信息)
         */
        private String accessToken;

        /**
         * 令牌有效期 (交换令牌的有效期,单位秒)
         */
        private String expiresIn;

        /**
         *
         * 刷新令牌有效期 (刷新令牌有效期,单位秒)
         */
        private String reExpiresIn;

        /**
         * 刷新令牌 (通过该令牌可以刷新 access_token)
         */
        private String refreshToken;

        /**
         * 创建时间戳
         */
        private long createTime;

    }
}

    基本的流程是先通过getAccessToken方法解析code获取一个自定义的oauth对象,并且 保存在redis 中,之后每天调用refreshIfExpired函数,来检查是否到期,到期则更新并且返回。

    因为应用可能会重启,所以每次应用启动时先检查redis是否有oauth对象,有则解析并且注入到容器,没有则依据初始配置的refreshToken获取一个。

package top.dreamcenter.bill.config;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import top.dreamcenter.bill.constant.RedisConstant;
import top.dreamcenter.bill.properties.AlipayProperties;
import top.dreamcenter.bill.util.AlipayUtil;

@Configuration
public class AlipayConfig {

    @Autowired
    private AlipayProperties alipayProperties;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Bean
    public AlipayClient alipayClient() {
        return new DefaultAlipayClient(
                "https://openapi.alipay.com/gateway.do",
                alipayProperties.getAppId(),
                alipayProperties.getAppPrivateKey(),
                "json", "UTF8",
                alipayProperties.getAlipayPublicKey(),
                "RSA2"
        );
    }


    /**
     * 初始化token(带刷新)
     * @return token
     */
    @Bean
    public AlipayUtil.AlipayOauthToken oauthToken() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        AlipayUtil.AlipayOauthToken alipayOauthToken;

        // 获取redis数据库中的 oauthToken
        String oauthToken = ops.get(RedisConstant.OAUTH_TOKEN);

        if (oauthToken != null) { // 存在记录,依据记录进行处理
            // 解析
            alipayOauthToken = JSONObject.parseObject(oauthToken, AlipayUtil.AlipayOauthToken.class);
            // 过期判断
            AlipayUtil.AlipayOauthToken res = AlipayUtil.refreshIfExpired(alipayClient(), alipayOauthToken);
            // 如果对象未变,说明未过期,直接返回;否则结果走一遍redis数据库,刷新记录。
            if (res == alipayOauthToken) return alipayOauthToken;
        } else  { // 不存在记录,依据默认值进行处理
            String refreshToken = alipayProperties.getInitRefreshToken();
            alipayOauthToken = AlipayUtil.refreshToken(alipayClient(), refreshToken);
        }

        // 将最新的记录写入redis
        String json = JSONObject.toJSONString(alipayOauthToken);
        ops.set(RedisConstant.OAUTH_TOKEN, json);

        // 返回数据
        return alipayOauthToken;
    }

}

    接着是定时任务,每天除了拉取账单外,还要刷新令牌。拉取账单的时间不要卡在0点,否则如果出现卡点的交易,如果定时任务稍稍提前了一点,如23:59:59执行,账单的拉取时间就会有致命错误,所以推荐向后延长一些时间,以错过这个冲突时间点。

package top.dreamcenter.bill.schedule;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import top.dreamcenter.bill.constant.RedisConstant;
import top.dreamcenter.bill.service.AlipayService;
import top.dreamcenter.bill.util.AlipayUtil;

import java.text.SimpleDateFormat;
import java.util.Calendar;

@Service
public class AlipaySchedule {

    @Autowired
    private AlipayClient alipayClient;

    @Autowired
    private AlipayUtil.AlipayOauthToken oauthToken;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private AlipayService alipayService;

    /**
     * 每天0点查看是否需要更新token
     */
    @Scheduled(cron = "0 0 0 * * ?")
    public void refreshToken() {
        AlipayUtil.AlipayOauthToken res = AlipayUtil.refreshIfExpired(alipayClient, oauthToken);
        // 对象改变,说明需要更新
        if (res != oauthToken) {
            // 替换spring容器中的token为最新的值,并且存入redis数据库
            oauthToken = res;
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            String json = JSONObject.toJSONString(oauthToken);
            ops.set(RedisConstant.OAUTH_TOKEN, json);
        }
    }

    /**
     * 每天1点,将昨日的账单入库
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void dailyBillInsertIntoDB() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 00:00:00");

        Calendar now = Calendar.getInstance();
        String today = sdf.format(now.getTime());
        now.add(Calendar.DATE, -1);
        String yesterday = sdf.format(now.getTime());

        alipayService.insertBills(yesterday, today);
    }
}

    附service层账单获取与插入逻辑:

package top.dreamcenter.bill.service.impl;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.domain.AlipayDataBillBuyQueryModel;
import com.alipay.api.domain.TradeItemResult;
import com.alipay.api.request.AlipayDataBillBuyQueryRequest;
import com.alipay.api.response.AlipayDataBillBuyQueryResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import top.dreamcenter.bill.mapper.AlipayMapper;
import top.dreamcenter.bill.service.AlipayService;
import top.dreamcenter.bill.util.AlipayUtil;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class AlipayServiceImpl implements AlipayService {

    @Autowired
    private AlipayClient alipayClient;

    @Autowired
    private AlipayUtil.AlipayOauthToken oauthToken;

    @Autowired
    private AlipayMapper alipayMapper;

    @Override
    public List<TradeItemResult> getBills(String from, String end) {

        AlipayDataBillBuyQueryRequest billReq = new AlipayDataBillBuyQueryRequest();
        AlipayDataBillBuyQueryModel billMod = new AlipayDataBillBuyQueryModel();
        billMod.setStartTime(from);
        billMod.setEndTime(end);
        billReq.setBizModel(billMod);

        AlipayDataBillBuyQueryResponse billRes = null;
        try {
            billRes = alipayClient.execute(billReq, oauthToken.getAccessToken());
        } catch (AlipayApiException e) {
            log.error(e.getErrMsg());
        }

        List<TradeItemResult> list = new ArrayList<>();

        if (billRes != null) {
            list.addAll(billRes.getDetailList());
        }

        return list;
    }

    public Integer insertBills(String from, String end){
        List<TradeItemResult> bills = getBills(from, end);
        return alipayMapper.insertBills(bills);
    }

    public List<TradeItemResult> selectByTime(String from, String end){
        return alipayMapper.selectByTime(from, end);
    }

}


结果展示

    最终接入个人主页后,效果如下:



    非常的NICE,不是嘛( ̄y▽, ̄)╭ 

    [[爽啊]]



上一篇: Springboot的依赖包分离
下一篇: 网页背景添加粒子飘动效果