Nestjs构建Certeasy证书自动化平台 - 业务实现(充值模块)

94 阅读4分钟

Certeasy

摘要

作为一个商业系统,交易功能是必不可少的。本章介绍金币充值功能相关内容,从金币产品-> 下订单 -> 支付 -> 回调处理 -> 金币入账 几个方面介绍

模块初始化

执行 NestJS CLI三件套

    nest g mo modules/coin
    nest g co modules/coin --no-spec
    nest g s modules/coin --no-spec

业务实现

1. 控制器

  • 获取充值配置 pods
@Get('pods')
@ApiOperation({
  summary: '获取充值配置',
})
@ApiResponse({
  status: HttpStatus.BAD_REQUEST,
  type: BaseApiErrorResponse,
})
async pods() {
  return await this.coinService.pods();
}

响应结果:image.png

  • 下订单 order
@Post('order')
@ApiOperation({
  summary: '充值下单',
})
@ApiResponse({
  status: HttpStatus.BAD_REQUEST,
  type: BaseApiErrorResponse,
})
@HttpCode(HttpStatus.OK)
async placeOrder(
  @IRequestUser() user: IUserPayload,
  @Body() data: CreateOrderDto,
) {
  return await this.coinService.placeOrder(user, data);
}
  • 订单查询 order/:id
@Get('order/:id')
@ApiOperation({
  summary: '订单信息',
})
@ApiResponse({
  status: HttpStatus.BAD_REQUEST,
  type: BaseApiErrorResponse,
})
@ApiParam({ name: 'id', type: Number, description: '订单ID' })
async order(
  @IRequestUser() user: IUserPayload,
  @Param('id', ParseIntPipe) id: number,
) {
  return await this.coinService.orderInfo(user, id);
}

响应结果: image.png

  • 异步回调 notify/:provider

支付异步回调里,需要对外开放,并且响应数据不能按统一数据格式。因此配置了@Public()@IgnoreTransform() 两个装饰器。@Public() 开放访问 , @IgnoreTransform() 忽略数据转换

@Post('notify/:provider')
@Public()
@IgnoreTransform()
@ApiOperation({
  summary: '异步回调',
})
@HttpCode(HttpStatus.OK)
@ApiParam({ name: 'provider', type: String, description: '支付服务商' })
async notify(
  @Param('provider') provider: string,
  @Body() data: string,
  @Res() res: Response,
) {
  const result = await this.coinService.payCallback(provider, data);
  // 这里需要直接输出
  res.status(HttpStatus.OK).send(result);
  return;
}

2. 实体

实体是一个功能的数据基础,每个模块必不可少。充值模块包含coin.entitycoin-transaction.entity 两个实体。

其中 coin.entity 定义了可进行充值的 金币产品coin-transaction.entity 定义了 金币交易记录

具体如下

  • coin.entity 金币产品表

@Entity('coin')
export class CoinEntity {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
  id: number;

  @Expose()
  @Column({ type: 'varchar', name: 'name', comment: '产品名称' })
  name: string;

  @Column({
    type: 'decimal',
    name: 'coin',
    comment: '金币',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  coin: number;

  @Column({
    type: 'decimal',
    name: 'gift_coin',
    comment: '赠送金币',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  giftCoin: number;

  @Column({
    type: 'decimal',
    name: 'price',
    comment: '单价',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  price: number;

  @Expose()
  @Column({ type: 'tinyint', name: 'status' })
  status: number;

  @CreateDateColumn({ type: 'datetime', name: 'create_time' })
  createTime: Date | string;

  @UpdateDateColumn({ type: 'datetime', name: 'update_time' })
  updateTime: Date | string;
}
  • coin-transaction.entity 金币交易记录表

@Entity('coin_transaction')
export class CoinTransactionEntity extends BaseEntity {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
  id: number;

  @Column({ type: 'int', name: 'user_id', comment: '用户id' })
  userId: number;

  @Column({ type: 'int', name: 'coin_id', comment: '产品id' })
  coinId: number;

  @Column({ type: 'varchar', name: 'out_trade_no', comment: '交易单号' })
  outTradeNo: string;

  @Column({ type: 'varchar', name: 'subject', comment: '交易主题' })
  subject: string;

  @Column({ type: 'varchar', name: 'body', comment: '交易内容' })
  body: string;

  @Column({
    type: 'decimal',
    name: 'price',
    comment: '单价',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  price: number;

  @Column({
    type: 'decimal',
    name: 'amount',
    comment: '金额',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  amount: number;

  @Column({
    type: 'decimal',
    name: 'coin',
    comment: '金币',
    transformer: {
      to: (value: number) => value,
      from: (value: string) => parseFloat(value),
    },
  })
  coin: number;

  @Column({
    type: 'datetime',
    name: 'paid_time',
    comment: '支付时间',
    transformer: {
      to: (value: any) => value,
      from: (value: any) => dayjs(value).format(DATE_FORMAT),
    },
  })
  paidTime: Date | string;

  @Column({ type: 'tinyint', name: 'status' })
  status: number;

  @CreateDateColumn({ type: 'datetime', name: 'create_time' })
  createTime: Date | string;

  @UpdateDateColumn({ type: 'datetime', name: 'update_time' })
  updateTime: Date | string;
}

3. 服务

  • 下订单 placeOrder

用户在前台进行充值操作,提交充值产品 podId传递给方法,查找对应产品并生成订单(交易记录)信息

image.png

而后进行订单支付产品信息提交,目前只接入了支付宝nativeweb 两种支付方式

image.png

响应结果:image.png

  • 异步回调 payCallback 支付宝异步回调中,需要注意的是使用支付宝公钥进行数据验签。验签通过之后进行金币数据更新操作,金币操作的用户数据需要加,系统使用了 pessimistic_write 悲观锁写入。

image.png

  • 查询订单 orderInfo 系统中,进行订单查询时,未支付的订单会优先进行支付状态检查(进行支付宝接口查询),保证支付状态实时性。

image.png

4. 超时队列

用户下订单,生成订单之后,需要推送一条异步队列事件。用于处理订单超时未支付,关闭订单操作。

  • 服务内推送队列 image.png

  • processor 进行处理 image.png

更多代码请查看 certeasy_nest_open/src/modules/coin at main · CerteasyTeam/certeasy_nest_open (github.com)

系列文章

开源

联系

wechat: zuxcloud

Email: zuxing.xu@lettered.cn