2023-11-22 14:47:42
为了进行个人财产管理,在先前的几个月都是用的excel表格进行记账,每次回家都要进行一步记录当日消费入账的操作,非常的繁琐,而且也不方便处理数据。于是乎就想着能不能接入支付宝的官方api来获取每日的账单,并且同步到个人系统中。
因为账单模块与个人主页是相对独立的两个主题模块,所以账单模块单独写了一个微服务,个人主页只要负责调用即可。本文重点在于接入官方API并且实现记录账单模块。
1. 首先前往官方开放平台网址登录(点击前往)。
2. 接着点击“控制台”,首次点击控制台会让你绑定个人手机号码。
3. 成功后能够找到一个默认的应用,如下图所示。
要获取账单,需要进行如下的操作:
1. 点击应用详情,进入到该应用的详情页
2. 选择产品绑定,绑定产品选择下面两个产品勾选加入。
完成上述步骤后,接下来就是对接接口部分,需要先完成加签步骤。
前往 开发设置 > 接口加签设置 。可以采用密钥方式或者证书模式,自行按照需求选择。
我采用的是密钥方式,具体的操作步骤有 官方文档 。使用官方工具生成应用的公私钥后,上传即可获得支付宝公钥得以进一步对接。(注意保护好这些密钥信息,不要泄露)。如果安全要求较高,可以设置 服务器IP白名单 ,也在 “开发设置”页签下。
后续需要登录支付宝账号获取code,此code将会在回调地址中附带,所以还需要设置一个回调地址,以接受授权码等信息。
完成上述前置步骤后,就可以正式接入官方的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▽, ̄)╭