v1.0:接入谷歌支付

This commit is contained in:
2022-09-28 16:48:11 +08:00
parent d76d74bb48
commit 51fca5a6eb
13 changed files with 571 additions and 2 deletions

View File

@@ -389,6 +389,8 @@ public class Constant {
public static final String lucky_tarot = "lucky_tarot";
// 新公众号:平台助手 h5支付
public static final String wx_pub2_h5 = "wx_pub2_h5";
// google play billing
public static final String google_play_billing = "google_play_billing";
}
public static class DepositStatus {
@@ -1739,6 +1741,8 @@ public class Constant {
/** 魔法学院奖励配置 */
public static final String ACT_MAGIC_SCHOOL_AWARD_CONFIG = "act_magic_school_award_config";
public static final String GOOGLE_PAY_LIMIT_CONFIG = "google_pay_limit_config";
}
public static class ActiveMq {
@@ -5574,5 +5578,23 @@ public class Constant {
// 初次高级探险
public static final Integer FIRST_HIGH = 2;
}
public static final class GooglePurchaseState {
/**
* 已支付
*/
public static final Integer PURCHASED = 0;
/**
* 取消
*/
public static final Integer CANCELED = 1;
/**
* 支付中
*/
public static final Integer PENDING = 2;
}
}

View File

@@ -28,7 +28,11 @@
<artifactId>alipay-sdk-java</artifactId>
<version>3.7.110.ALL</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-androidpublisher</artifactId>
<version>v3-rev24-1.24.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,72 @@
package com.accompany.payment.config;
import com.accompany.payment.google.AndroidPublisherHelper;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.exception.NacosException;
import com.google.api.services.androidpublisher.AndroidPublisher;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
@Configuration
@Order(-1)
@Lazy(false)
@ConfigurationProperties(prefix = "google-play")
@RefreshScope
@Data
public class GooglePlayConfig {
private String applicationName;
private String credentialJson;
private static final String CONFIG_NAME_APPLICATIONNAME = "google-play.applicationName";
private static final String CONFIG_NAME_JSON = "google-play.credentialJson";
@Autowired
private ConfigurableApplicationContext applicationContext;
@Bean("androidPublisher")
public AndroidPublisher initAndroidPublisher() throws IOException, GeneralSecurityException {
return AndroidPublisherHelper.init(applicationName, credentialJson);
}
/**
* 配置修改监听器用于当配置修改后重新生成AndroidPublisher bean
* @param event
*/
@EventListener
@Async
public void handle(EnvironmentChangeEvent event) throws IOException, GeneralSecurityException {
List<String> needReinitConfigFields = Arrays.asList(CONFIG_NAME_APPLICATIONNAME, CONFIG_NAME_JSON);
boolean needReInit = false;
for (String needReinitConfigField : needReinitConfigFields) {
if (event.getKeys().contains(needReinitConfigField)) {
needReInit = true;
break;
}
}
if (needReInit) {
String application = applicationContext.getEnvironment().getProperty(CONFIG_NAME_APPLICATIONNAME);
String json = applicationContext.getEnvironment().getProperty(CONFIG_NAME_JSON);
AndroidPublisherHelper.init(application, json);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.accompany.payment.dto;
import lombok.Data;
/**
* Created by 北岭山下 on 2017/7/13.
*/
@Data
public class AppInnerPayRecordDTO {
private String recordId;
}

View File

@@ -0,0 +1,29 @@
package com.accompany.payment.dto;
import lombok.Data;
import java.util.List;
@Data
public class GooglePayLimitConfigDTO {
/**
* 每日总共限制总额金额
*/
private Long limitEveryDayAmount;
/**
* 每日单人总共限制总额金额
*/
private Long limitEveryOneDaySumAmount;
/**
* 错误提示
*/
private String errorTip;
/**
* google内购项id
*/
private List<String> googleChargeProdIds;
}

View File

@@ -0,0 +1,107 @@
package com.accompany.payment.google;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.Strings;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.Assert;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to initialize the publisher APIs client library.
* <p>
* Before making any calls to the API through the client library you need to
* call the {@link AndroidPublisherHelper#init(String, String, String, String)} method. This will run
* all precondition checks for for client id and secret setup properly in
* resources/client_secrets.json and authorize this client against the API.
* </p>
*/
public class AndroidPublisherHelper {
private static final Log log = LogFactory.getLog(AndroidPublisherHelper.class);
/** Global instance of the JSON factory. */
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
/** Global instance of the HTTP transport. */
private static HttpTransport HTTP_TRANSPORT;
private static volatile AndroidPublisher androidPublisher;
private static volatile String oldApplicationName;
private static volatile String oldCredentialJson;
public static AndroidPublisher init(String applicationName, String json) throws IOException, GeneralSecurityException {
if (needInit(applicationName, json)) {
synchronized (AndroidPublisherHelper.class) {
if (needInit(applicationName, json) || androidPublisher == null) {
log.info("start init AndroidPublisher");
Preconditions.checkArgument(!Strings.isNullOrEmpty(applicationName),
"applicationName cannot be null or empty!");
List<String> scopes = new ArrayList<>();
scopes.add(AndroidPublisherScopes.ANDROIDPUBLISHER);
ByteArrayResource resource = new ByteArrayResource(json.getBytes());
GoogleCredential credential = GoogleCredential.fromStream(resource.getInputStream())
.createScoped(scopes);
newTrustedTransport();
//使用谷歌凭据和收据从谷歌获取购买信息
androidPublisher = new AndroidPublisher.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName(applicationName)
.build();
oldApplicationName = applicationName;
oldCredentialJson = json;
}
}
}
return androidPublisher;
}
/**
* 判断是否需要初始化
* @param applicationName
* @param credentialJsonPath
* @return
*/
private static boolean needInit(String applicationName, String credentialJsonPath) {
boolean firstInit = AndroidPublisherHelper.oldApplicationName == null || AndroidPublisherHelper.oldCredentialJson == null;
// json配置修改后重新初始化
boolean configChanged = StringUtils.isNotBlank(AndroidPublisherHelper.oldCredentialJson)
&& !AndroidPublisherHelper.oldCredentialJson.equals(credentialJsonPath);
return firstInit || configChanged;
}
private static void newTrustedTransport() throws GeneralSecurityException,
IOException {
if (null == HTTP_TRANSPORT) {
HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
}
}
public static AndroidPublisher getPublisher() {
Assert.notNull(androidPublisher, "should init before get bean");
return androidPublisher;
}
}

View File

@@ -0,0 +1,200 @@
package com.accompany.payment.google;
import com.accompany.common.constant.Constant;
import com.accompany.common.redis.RedisKey;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.DateTimeUtil;
import com.accompany.common.utils.UUIDUitl;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.model.Users;
import com.accompany.core.service.SysConfService;
import com.accompany.core.service.user.UsersBaseService;
import com.accompany.payment.dto.AppInnerPayRecordDTO;
import com.accompany.payment.dto.GooglePayLimitConfigDTO;
import com.accompany.payment.model.ChargeProd;
import com.accompany.payment.model.ChargeRecord;
import com.accompany.payment.service.ChargeProdService;
import com.accompany.payment.service.ChargeRecordService;
import com.alibaba.fastjson.JSONObject;
import com.google.api.services.androidpublisher.model.ProductPurchase;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* google 内购
*/
@Service
@Slf4j
public class GooglePlayBillingService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ChargeRecordService chargeRecordService;
@Autowired
private ChargeProdService chargeProdService;
@Autowired
private SysConfService sysConfService;
@Autowired
private UsersBaseService usersBaseService;
protected String getPayChannel() {
return Constant.ChargeChannel.google_play_billing;
}
public AppInnerPayRecordDTO placeOrder(Long uid, String chargeProdId, String clientIp, String deviceId) {
validMoneyLimit(uid, chargeProdId);
RLock lock = redissonClient.getLock(RedisKey.lock_apply_charge.getKey(uid.toString()));
try {
lock.tryLock(10, TimeUnit.SECONDS);
ChargeProd chargeProd = chargeProdService.getChargeProdById(chargeProdId);
if (chargeProd == null) {
log.error("充值产品不存在prodId: {} ", chargeProdId);
throw new ServiceException(BusiStatus.CHARGE_PROD_NOT_EXIST);
}
String payChannel = getPayChannel();
log.info("用户 {} 内购充值,渠道: {}", uid, payChannel);
//保存充值记录
//1.创建订单号
//UUID不会重复所以不需要判断是否生成重复的订单号
String chargeRecordId = UUIDUitl.get();
Users users = usersBaseService.getUsersByUid(uid);
//Integer region = users.getRegion() == null ? Constant.region.overseas : users.getRegion();
ChargeRecord chargeRecord = new ChargeRecord();
chargeRecord.setChargeRecordId(chargeRecordId);
chargeRecord.setChargeProdId(chargeProdId);
chargeRecord.setUid(uid);
//chargeRecord.setRegion(region.byteValue());
chargeRecord.setChannel(payChannel);
chargeRecord.setChargeStatus(Constant.ChargeRecordStatus.create);
// product中money单位为分
chargeRecord.setAmount(chargeProd.getMoney());
chargeRecord.setSubject(chargeProd.getProdName());
chargeRecord.setBody(chargeProd.getProdName());
chargeRecord.setClientIp(clientIp);
//写入数据库
chargeRecordService.insertChargeRecord(chargeRecord);
log.info("用户 {} 内购充值,本地订单号: {}", uid, chargeRecordId);
//订单创建成功返回订单号
AppInnerPayRecordDTO recordIdVo = new AppInnerPayRecordDTO();
recordIdVo.setRecordId(chargeRecordId);
return recordIdVo;
} catch (InterruptedException e) {
throw new ServiceException(BusiStatus.SERVERBUSY);
} finally {
if (lock.isLocked()){
lock.unlock();
}
}
}
private void validMoneyLimit(Long uid, String chargeProdId) {
GooglePayLimitConfigDTO config = getLimitConfig();
Date now = new Date();
Date beginTimeOfDay = DateTimeUtil.getBeginTimeOfDay(now);
Date endTimeOfDay = DateTimeUtil.getEndTimeOfDay(now);
ChargeProd chargeProd = chargeProdService.getChargeProdById(chargeProdId);
if (chargeProd == null) {
log.error("充值产品不存在prodId: {} ", chargeProdId);
throw new ServiceException(BusiStatus.CHARGE_PROD_NOT_EXIST);
}
Long userChargeAmmount = chargeRecordService.getChargeUserAmountWithProdIds(Collections.singletonList(uid), config.getGoogleChargeProdIds(), beginTimeOfDay, endTimeOfDay);
log.info("{}在{}-{}时间段内使用google内购充值了{}", uid, DateTimeUtil.convertDate(beginTimeOfDay), DateTimeUtil.convertDate(endTimeOfDay), userChargeAmmount);
// 配置的单位是元,充值记录和产品的数据单位为分
if (config.getLimitEveryOneDaySumAmount() * 100 < (chargeProd.getMoney() + userChargeAmmount)) {
throw new ServiceException(config.getErrorTip());
}
Long allUserChargeAmount = chargeRecordService.getChargeUserAmountWithProdIds(null, config.getGoogleChargeProdIds(), beginTimeOfDay, endTimeOfDay);
log.info("全部用户{}-{}内使用google内购充值了{}", DateTimeUtil.convertDate(beginTimeOfDay), DateTimeUtil.convertDate(endTimeOfDay), allUserChargeAmount);
if (config.getLimitEveryDayAmount() * 100 < (chargeProd.getMoney() + allUserChargeAmount)) {
throw new ServiceException(config.getErrorTip());
}
}
private GooglePayLimitConfigDTO getLimitConfig() {
GooglePayLimitConfigDTO limitConfig = JSONObject.parseObject(sysConfService.getDefaultSysConfValueById(Constant.SysConfId.GOOGLE_PAY_LIMIT_CONFIG, "{}"), GooglePayLimitConfigDTO.class);
if (limitConfig.getLimitEveryDayAmount() == null) {
limitConfig.setLimitEveryDayAmount(9999999L);
}
if (limitConfig.getLimitEveryOneDaySumAmount() == null) {
limitConfig.setLimitEveryOneDaySumAmount(9999999L);
}
if (limitConfig.getGoogleChargeProdIds() == null) {
limitConfig.setGoogleChargeProdIds(Collections.emptyList());
}
if (StringUtils.isEmpty(limitConfig.getErrorTip())) {
limitConfig.setErrorTip("充值失败,请联系客服处理");
}
return limitConfig;
}
public ChargeRecord verifyOrder(String chargeRecordId, String packageName, String googlePlayProdId, String purchaseToken) {
String lockKey = RedisKey.lock_pay_callback_notify.getKey(chargeRecordId);
RLock lock = redissonClient.getLock(lockKey);
try {
lock.tryLock(5L, TimeUnit.SECONDS);
ChargeRecord chargeRecord = chargeRecordService.getChargeRecordById(chargeRecordId);
if (chargeRecord == null) {
log.error("[google play billing]充值记录不存在。chargeRecordId: {}", chargeRecordId);
throw new ServiceException(BusiStatus.RECORD_NOT_EXIST);
}
if (!Constant.ChargeRecordStatus.create.equals(chargeRecord.getChargeStatus()) && !Constant.ChargeRecordStatus.error.equals(chargeRecord.getChargeStatus())) {
log.info("[google play billing]订单状态不是创建或错误不进行处理。chargeRecordId: {}", chargeRecordId);
throw new ServiceException(BusiStatus.RECORD_ALREADY_EXIST);
}
if (!chargeRecord.getChargeProdId().equalsIgnoreCase(googlePlayProdId)) {
log.error("[google play billing]记录中的产品id和待查询内购产品id不一致。ChargeProdId: {}, googlePlayProdId: {}", chargeRecord.getChargeProdId(), googlePlayProdId);
throw new ServiceException(BusiStatus.RECORD_NOT_EXIST);
}
ProductPurchase purchase = AndroidPublisherHelper.getPublisher().purchases().products().get(packageName, googlePlayProdId, purchaseToken).execute();
log.info("purchase: {}", JSONObject.toJSONString(purchase));
if (purchase == null) {
log.error("查询google购买记录返回为空。packageName: {}, prodId: {}, purchaseToken: {}", packageName, googlePlayProdId, purchaseToken);
throw new ServiceException(BusiStatus.RECORD_NOT_EXIST);
}
if (!Constant.GooglePurchaseState.PURCHASED.equals(purchase.getPurchaseState())) {
log.error("[google play billing]订单未完成支付。当前状态: {}", purchase.getPurchaseState());
throw new ServiceException(BusiStatus.RECORD_NOT_EXIST);
}
chargeRecord.setPingxxChargeId(purchase.getOrderId());
return chargeRecord;
} catch (Exception e) {
log.error("[google play billing]校验google内购失败", e);
if (e instanceof ServiceException) {
throw (ServiceException)e;
} else {
throw new ServiceException(BusiStatus.BUSIERROR);
}
} finally {
if (lock.isLocked()){
lock.unlock();
}
}
}
}

View File

@@ -66,4 +66,8 @@ public interface ChargeRecordMapperMgr {
Long getHistoryRechargeAmountByChannel(@Param("userId") long userId, @Param("channel") String channel);
Long getChargeUserAmountWithProdIds(@Param("list") List<Long> uids, @Param("prodIds") List<String> prodIds,
@Param("startTime") Date startTime, @Param("endTime") Date endTime);
}

View File

@@ -173,4 +173,15 @@ public class ChargeRecordService extends BaseService {
jedisLockService.unlock(RedisKey.ios__pay_user_toatl_amount_lock.getKey(userIdStr), lockVal);
}
}
/**
* 获取当前时间段内用户充值金额
**/
public Long getChargeUserAmountWithProdIds(List<Long> uid, List<String> prods, Date startDate, Date endDate) {
Long chargeUserAmountWithProdIds = chargeRecordMapperMgr.getChargeUserAmountWithProdIds(uid, prods, startDate, endDate);
if (chargeUserAmountWithProdIds == null) {
return 0L;
}
return chargeUserAmountWithProdIds;
}
}

View File

@@ -145,7 +145,8 @@
#{wxPubOpenid,jdbcType=VARCHAR}, #{subject,jdbcType=VARCHAR}, #{body,jdbcType=VARCHAR},
#{extra,jdbcType=VARCHAR}, #{metadata,jdbcType=VARCHAR}, #{chargeDesc,jdbcType=VARCHAR},
#{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP})
</insert>
</insert>
<insert id="insertSelective" parameterType="com.accompany.payment.model.ChargeRecord">
insert into charge_record
<trim prefix="(" suffix=")" suffixOverrides=",">

View File

@@ -283,4 +283,19 @@
<select id="getHistoryRechargeAmountByChannel" resultType="java.lang.Long">
select IFNULL(sum(amount)/100,0) from charge_record WHERE buss_type in (0,4) and charge_status = 2 and uid = #{userId} and channel &lt;&gt; 'exchange' and channel = #{channel};
</select>
<select id="getChargeUserAmountWithProdIds" resultType="java.lang.Long">
select sum(amount) from charge_record
where charge_status = 2
<if test="list != null and list.size()>0">
and uid in <foreach collection="list" item="uid" open="(" separator="," close=")">#{uid}</foreach>
</if>
and charge_prod_id in <foreach collection="prodIds" item="prodId" open="(" separator="," close=")">#{prodId}</foreach>
<if test="startTime != null">
and create_time &gt; #{startTime}
</if>
<if test="endTime != null">
and create_time &lt; #{endTime}
</if>
</select>
</mapper>

View File

@@ -0,0 +1,56 @@
package com.accompany.business.controller.apppay;
import com.accompany.business.service.ChargeService;
import com.accompany.common.annotation.Authorization;
import com.accompany.common.utils.IPUitls;
import com.accompany.core.enumeration.BusinessStatusCodeEnum;
import com.accompany.core.vo.BaseRequestVO;
import com.accompany.core.vo.BaseResponseVO;
import com.accompany.payment.dto.AppInnerPayRecordDTO;
import com.accompany.payment.google.GooglePlayBillingService;
import com.accompany.payment.model.ChargeProd;
import com.accompany.payment.model.ChargeRecord;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Api(tags = {"google内购"}, value = "google内购")
@RestController
@RequestMapping("/googlePlayBilling")
@Slf4j
public class GooglePlayBillingChargeController {
@Autowired
private ChargeService chargeService;
@Autowired
private GooglePlayBillingService googlePlayBillingService;
@ApiOperation("google内购预下单")
@PostMapping("/placeOrder")
@Authorization
public BaseResponseVO<AppInnerPayRecordDTO> placeOrder(String chargeProdId, HttpServletRequest request) {
BaseRequestVO baseRequestVO = new BaseRequestVO();
Long uid = baseRequestVO.getMyUserId();
String clientIp = IPUitls.getRealIpAddress(request);
String deviceId = baseRequestVO.getDeviceId();
AppInnerPayRecordDTO appInnerPayRecordDTO = googlePlayBillingService.placeOrder(uid, chargeProdId, clientIp, deviceId);
return new BaseResponseVO<>(BusinessStatusCodeEnum.SUCCESS, appInnerPayRecordDTO);
}
@ApiOperation("google内购订单校验")
@PostMapping("/verifyOrder")
@Authorization
public BaseResponseVO<String> verifyOrder(String chargeRecordId, String packageName, String googlePlayProdId, String purchaseToken) {
ChargeRecord chargeRecord = googlePlayBillingService.verifyOrder(chargeRecordId, packageName, googlePlayProdId, purchaseToken);
chargeService.updateAppPayData(chargeRecord);
return new BaseResponseVO<>(BusinessStatusCodeEnum.SUCCESS, BusinessStatusCodeEnum.SUCCESS.getReasonPhrase(), purchaseToken);
}
}

View File

@@ -0,0 +1,35 @@
package servicetest;
import com.accompany.payment.google.AndroidPublisherHelper;
import com.accompany.payment.google.GooglePlayBillingService;
import com.alibaba.fastjson.JSONObject;
import com.google.api.services.androidpublisher.model.ProductPurchase;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
public class GooglePlayBillingServiceTest extends CommonTest {
@Autowired
private GooglePlayBillingService googlePlayBillingVerifyService;
@Test
public void verifyOrderTest() throws Exception {
String chargeRecordId = "d9e457c4e7bc4d918d1a869bc49003fd ";
String packageName = "com.vele.ananplay";
String prodId = "goods_cent_1499";
String token ="lefpcedapihlpjcmjgnombhd.AO-J1OyZHdP6rv5D1sqFUyWsk1QbBuMS1oV4aTuet7CtgeKLZe_S6yjpOdkLpSTRT123gIQ8LSR7mWcayJtXCizwmMis7tqnTg";
googlePlayBillingVerifyService.verifyOrder(chargeRecordId, packageName, prodId, token);
}
@Test
public void getGooglePurchaseTest() throws IOException {
String packageName = "com.vele.ananplay";
String prodId = "goods_cent_1499";
String token ="lefpcedapihlpjcmjgnombhd.AO-J1OyZHdP6rv5D1sqFUyWsk1QbBuMS1oV4aTuet7CtgeKLZe_S6yjpOdkLpSTRT123gIQ8LSR7mWcayJtXCizwmMis7tqnTg";
ProductPurchase purchase = AndroidPublisherHelper.getPublisher().purchases().products().get(packageName, prodId, token).execute();
System.out.println(JSONObject.toJSONString(purchase));
//System.out.println(JSONObject.parse(purchase.getObfuscatedExternalProfileId()).toString());
}
}