Skip to content

08. 实现支付服务(0.0.5)

zhengyangyong edited this page Jul 3, 2018 · 2 revisions

我们使用ServiceComb Java Chassis构建第二个微服务——支付服务,并实现缴纳定金功能:

模块名 描述
common 在这个模块中增加支付服务的接口和DTO
payment-service 支付微服务

common模块

在common模块中,我们定义PaymentService接口:

public interface PaymentService {
  //缴纳定金
  ResponseEntity<Boolean> deposit(PaymentDTO payment);
}

为了能够未来更好的扩展请求和响应的内容,我们使用DTO类包装参数。

payment-service微服务

引入必要的依赖

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.servicecomb.scaffold</groupId>
  <artifactId>common</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.servicecomb</groupId>
  <artifactId>spring-boot-starter-provider</artifactId>
</dependency>

支付信息持久化

模拟与银行成功完成一笔支付后需要将支付的信息保存下来(可以理解为刷卡后的签名单),定义存储Payment信息的实体PaymentEntity:

@Entity
@Table(name = "T_Payment")
public class PaymentEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String transactionId;

  private String userName;

  private String bankName;

  private String cardNumber;

  private double amount;

  private Date time;

  private String type;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getTransactionId() {
    return transactionId;
  }

  public void setTransactionId(String transactionId) {
    this.transactionId = transactionId;
  }

  public String getUserName() {
    return userName;
  }

  public void setUserName(String userName) {
    this.userName = userName;
  }

  public String getBankName() {
    return bankName;
  }

  public void setBankName(String bankName) {
    this.bankName = bankName;
  }

  public String getCardNumber() {
    return cardNumber;
  }

  public void setCardNumber(String cardNumber) {
    this.cardNumber = cardNumber;
  }

  public double getAmount() {
    return amount;
  }

  public void setAmount(double amount) {
    this.amount = amount;
  }

  public Date getTime() {
    return time;
  }

  public void setTime(Date time) {
    this.time = time;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

  public PaymentEntity() {
  }

  public PaymentEntity(String transactionId, String userName, String bankName, String cardNumber,
      double amount, Date time, String type) {
    this.transactionId = transactionId;
    this.userName = userName;
    this.bankName = bankName;
    this.cardNumber = cardNumber;
    this.amount = amount;
    this.time = time;
    this.type = type;
  }
}

在CodeFist模式下,会在数据库中自动创建T_Payment表与此实体映射,id通过@GeneratedValue(strategy = GenerationType.AUTO)标记为自增长字段,userName存储支付用户,bankName是付款银行,cardNumber是付款银行卡号,amount是支付金额,time保存支付时间,type是交易的类型。

public class PaymentType {
  //支付定金
  public static final String DEPOSIT = "deposit";

  //交易
  public static final String TRADE = "trade";
}

然后我们继承JPA的PagingAndSortingRepository来实现ORM操作:

@Repository
@EnableTransactionManagement
public interface PaymentRepository extends PagingAndSortingRepository<PaymentEntity, Long> {
  List<PaymentEntity> findByUserName(String userName);

  PaymentEntity findByTransactionId(String TransactionId);
}

用户定金信息持久化

用户支付完定金后,需要保存已支付的定金总额,定义存储Deposit信息的实体DepositEntity:

@Entity
@Table(name = "T_Deposit")
public class DepositEntity {
  @Id
  private String userName;

  private double amount;

  public String getUserName() {
    return userName;
  }

  public void setUserName(String userName) {
    this.userName = userName;
  }

  public double getAmount() {
    return amount;
  }

  public void setAmount(double amount) {
    this.amount = amount;
  }

  public DepositEntity() {
  }

  public DepositEntity(String userName, double amount) {
    this.userName = userName;
    this.amount = amount;
  }
}

在CodeFist模式下,会在数据库中自动创建T_Deposit表与此实体映射,userName是用户名,amount保存了已经支付的定金总额。

然后我们继承JPA的PagingAndSortingRepository来实现ORM操作:

@Repository
@EnableTransactionManagement
public interface DepositRepository extends PagingAndSortingRepository<DepositEntity, String> {
  DepositEntity findByUserName(String userName);
}

模拟支付逻辑

@Override
@PostMapping(path = "deposit")
@Transactional(isolation = Isolation.SERIALIZABLE)
public ResponseEntity<Boolean> deposit(@RequestBody PaymentDTO payment) {
  if (validatePayment(payment)) {
    if (checkBalance(payment)) {
      if (recordPayment(payment, PaymentType.DEPOSIT)) {
        if (recordDeposit(payment)) {
          if (cutWithBank(payment)) {
            return new ResponseEntity<>(true, HttpStatus.OK);
          }
          throw new InvocationException(BAD_REQUEST, "cut with bank failed");
        }
        throw new InvocationException(BAD_REQUEST, "record deposit failed");
      }
      throw new InvocationException(BAD_REQUEST, "record payment failed");
    }
    throw new InvocationException(BAD_REQUEST, "check balance failed");
  }
  throw new InvocationException(BAD_REQUEST, "incorrect payment request");
}

private boolean validatePayment(PaymentDTO payment) {
  if (StringUtils.isNotEmpty(payment.getUserName()) && payment.getAmount() > 0 && StringUtils
      .isNotEmpty(payment.getTransactionId())
      && StringUtils.isNotEmpty(payment.getBankName()) && StringUtils.isNotEmpty(payment.getCardNumber())) {
    //TransactionId需要不重复,未被使用过
    PaymentEntity pay = paymentRepository.findByTransactionId(payment.getTransactionId());
    return pay == null;
  }
  return false;
}

//检查用户的余额,这里我们假设每一个用户都有一百万授信
private boolean checkBalance(PaymentDTO payment) {
  //我们先要查一下系统里面已经用了多少
  List<PaymentEntity> pays = paymentRepository.findByUserName(payment.getUserName());
  double used = 0;
  for (PaymentEntity pay : pays) {
    used += pay.getAmount();
  }
  //预估一下账户余额够不够
  return payment.getAmount() <= (CREDIT_LIMIT - used);
}

//本地记账保留扣款凭据
private boolean recordPayment(PaymentDTO payment, String paymentType) {
  paymentRepository
      .save(new PaymentEntity(payment.getTransactionId(), payment.getUserName(), payment.getBankName(),
          payment.getCardNumber(), payment.getAmount(), new Date(), paymentType));
  return true;
}

//登记缴纳的定金
private boolean recordDeposit(PaymentDTO payment) {
  DepositEntity deposit = depositRepository.findByUserName(payment.getUserName());
  if (deposit == null) {
    deposit = new DepositEntity(payment.getUserName(), payment.getAmount());
  } else {
    deposit.setAmount(deposit.getAmount() + payment.getAmount());
  }
  depositRepository.save(deposit);
  return true;
}

//走银行接口请求银行划账,Demo不对接直接返回为true
private boolean cutWithBank(PaymentDTO payment) {
  return true;
}

可以看到,支付定金会先后调用五个函数:

  1. validatePayment:首先我们需要检查一下调用入参是否正确;
  2. checkBalance:然后我们检查一下这个用户是否有足够的余额。模拟的逻辑比较简单,我们假设每一个用户都有一百万授信,扣掉在本系统中支付(包含支付定金)历史的总款项,如果大于需要支付的款项,那么我们认为有足够的余额;
  3. recordPayment:本地记录扣款凭据,注意:deposit方法标记了@Transactional,因此事务在成功返回前不会提交
  4. recordDeposit:记录支付的定金,注意:deposit方法标记了@Transactional,因此事务在成功返回前不会提交
  5. cutWithBank:请求银行划款(扣款),由于我们不走银行系统,直接返回true,代表扣款成功。如果将这一步的返回结果设置为false,则recordPayment会回滚不会写入数据库。

配置数据库连接

数据库的连接配置在application.properties中,包含了目标数据库,数据库用户名,数据库密码和JPA配置。

ServiceComb Java Chassis微服务配置

ServiceComb Java Chassis微服务的所有配置都在microservice.yaml中,主要包括ServiceCenter(服务注册&发现)地址,Rest Provider发布地址以及治理策略等。

相关资料

  1. Spring Boot中的JPA示例:

https://spring.io/guides/gs/accessing-data-jpa/

  1. 在Spring Boot中使用ServiceComb:

http://servicecomb.incubator.apache.org/cn/users/use-servicecomb-in-spring-boot/

  1. ServiceComb Java Chassis微服务配置:

http://servicecomb.incubator.apache.org/cn/users/service-definition/

http://servicecomb.incubator.apache.org/cn/users/service-configurations/

http://servicecomb.incubator.apache.org/cn/users/communicate-protocol/