mirror of
http://124.126.16.154:8888/singularity/hdk-pay.git
synced 2026-01-15 07:35:09 +08:00
feat(product): 新增充值产品相关数据结构和接口
- 添加 RechargeProduct、ProductItem、RechargeEffect 等实体和值对象 - 实现充值产品相关的数据传输对象 (DTO) - 定义充值产品仓库接口并提供具体实现 - 增加相关单元测试
This commit is contained in:
29
src/Application/Dto/AbstractDto.php
Normal file
29
src/Application/Dto/AbstractDto.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AbstractDto.php@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Application\Dto;
|
||||
|
||||
use Hyperf\Codec\Json;
|
||||
use Hyperf\Contract\Jsonable;
|
||||
use Swoole\ArrayObject;
|
||||
|
||||
abstract class AbstractDto extends ArrayObject implements Jsonable
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toJson();
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
return Json::encode($this->toArray());
|
||||
}
|
||||
}
|
||||
70
src/Application/Dto/Product/RechargeProductsDto.php
Normal file
70
src/Application/Dto/Product/RechargeProductsDto.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RechargeProductsDto.php@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Application\Dto\Product;
|
||||
|
||||
|
||||
use Singularity\HDK\Pay\Application\Dto\AbstractDto;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\Entities\ProductItem;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\RechargeProduct;
|
||||
|
||||
final class RechargeProductsDto extends AbstractDto
|
||||
{
|
||||
public function __construct(RechargeProduct $product)
|
||||
{
|
||||
parent::__construct(array_filter([
|
||||
'one_time' => $this->parseProductItem($product->getOneTime()),
|
||||
'renew' => $this->parseProductItem($product->getRenew()),
|
||||
'plan' => array_map(
|
||||
fn(ProductItem $item) => $this->parseProductItem($item),
|
||||
$product->getPlans(),
|
||||
),
|
||||
'package' => array_map(
|
||||
fn(ProductItem $item) => $this->parseProductItem($item),
|
||||
$product->getPackages(),
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
private function parseProductItem(?ProductItem $product): array
|
||||
{
|
||||
if (is_null($product)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$effect = $product->effect;
|
||||
$price = $product->unitPrice;
|
||||
|
||||
return [
|
||||
'id' => $product->id,
|
||||
'name' => $product->description,
|
||||
'currency' => $price->getCurrency()->getCode(),
|
||||
'price' => (float)$price->getAmount(),
|
||||
'total_points' => $effect->getPointTotal(),
|
||||
'bonus_rate_pct' => (float)(bcmul(
|
||||
num1: $effect->getPointTotal() == 0
|
||||
? '0'
|
||||
: bcdiv(
|
||||
(string)$effect->pointBonus,
|
||||
(string)$effect->getPointTotal(),
|
||||
3,
|
||||
),
|
||||
num2: '100',
|
||||
scale: 0,
|
||||
)),
|
||||
'point' => [
|
||||
'total' => $effect->getPointTotal(),
|
||||
'number' => $effect->pointBasic,
|
||||
'bonus' => $effect->pointBonus,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ namespace Singularity\HDK\Pay;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\Framework\Logger\StdoutLogger;
|
||||
use Singularity\HDK\Pay\Domain\Account\Repository\AccountRepoInterface;
|
||||
use Singularity\HDK\Pay\Domain\Product\Repository\ExchangeRepoInterface;
|
||||
use Singularity\HDK\Pay\Domain\Product\Repository\RechargeProductRepoInterface;
|
||||
use Singularity\HDK\Pay\Infrastructure\Repository\AccountBalanceRepo;
|
||||
use Singularity\HDK\Pay\Infrastructure\Repository\ProductRepo;
|
||||
|
||||
/**
|
||||
* ConfigProvider.php@HyperfAuth
|
||||
@@ -27,6 +30,8 @@ class ConfigProvider
|
||||
|
||||
// Repository
|
||||
AccountRepoInterface::class => AccountBalanceRepo::class,
|
||||
ExchangeRepoInterface::class => ProductRepo::class,
|
||||
RechargeProductRepoInterface::class => ProductRepo::class,
|
||||
],
|
||||
// 合并到 config/autoload/annotations.php 文件
|
||||
'annotations' => [
|
||||
|
||||
27
src/Domain/Product/Aggregate/Entities/ProductItem.php
Normal file
27
src/Domain/Product/Aggregate/Entities/ProductItem.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RechargeProduct.php@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Domain\Product\Aggregate\Entities;
|
||||
|
||||
use Money\Money;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\ValueObject\RechargeEffect;
|
||||
use Singularity\HDK\Pay\Domain\Product\Enum\ProductType;
|
||||
|
||||
final class ProductItem
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $description,
|
||||
public Money $unitPrice,
|
||||
public ProductType $productType,
|
||||
public RechargeEffect $effect,
|
||||
) {}
|
||||
}
|
||||
80
src/Domain/Product/Aggregate/RechargeProduct.php
Normal file
80
src/Domain/Product/Aggregate/RechargeProduct.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RechargeProduct.php@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Domain\Product\Aggregate;
|
||||
|
||||
use Singularity\HDK\Pay\Domain\AggregateRoot;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\Entities\ProductItem;
|
||||
|
||||
/**
|
||||
* App\Domain\Product\Aggregate\RechargeProduct@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*
|
||||
* @property-read ProductItem[] $plans
|
||||
* @property-read ProductItem[] $packages
|
||||
* @property-read ProductItem $oneTime
|
||||
*/
|
||||
final class RechargeProduct extends AggregateRoot
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?ProductItem $oneTime,
|
||||
private readonly ?ProductItem $renew = null,
|
||||
private readonly array $plans = [],
|
||||
private readonly array $packages = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array $idList
|
||||
* @return array{'id': int, 'quantity': int, 'detail': ProductItem}
|
||||
*/
|
||||
public function selectItemList(array $idList): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
/** @var ProductItem $item */
|
||||
foreach ([$this->oneTime, $this->renew, ...$this->plans, ...$this->packages] as $item) {
|
||||
if (in_array($item->id, $idList)) {
|
||||
$result[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProductItem[]
|
||||
*/
|
||||
public function getPlans(): array
|
||||
{
|
||||
return $this->plans;
|
||||
}
|
||||
|
||||
public function getRenew(): ?ProductItem
|
||||
{
|
||||
return $this->renew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProductItem[]
|
||||
*/
|
||||
public function getPackages(): array
|
||||
{
|
||||
return $this->packages;
|
||||
}
|
||||
|
||||
public function getOneTime(): ?ProductItem
|
||||
{
|
||||
return $this->oneTime;
|
||||
}
|
||||
}
|
||||
86
src/Domain/Product/Aggregate/ValueObject/RechargeEffect.php
Normal file
86
src/Domain/Product/Aggregate/ValueObject/RechargeEffect.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RechargeEffect.php@LuxDesign
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Domain\Product\Aggregate\ValueObject;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Hyperf\Framework\Exception\NotImplementedException;
|
||||
use Singularity\HDK\Pay\Domain\Account\Enum\PointType;
|
||||
|
||||
final class RechargeEffect
|
||||
{
|
||||
private float $pointTotal;
|
||||
|
||||
public function __construct(
|
||||
public PointType $pointType,
|
||||
|
||||
public float $pointBasic,
|
||||
public float $pointBonus,
|
||||
|
||||
public ?Carbon $expiredAt = null,
|
||||
|
||||
public ?string $version = null,
|
||||
) {
|
||||
$this->pointTotal = (float)bcadd((string)$pointBasic, (string)$pointBonus, 4);
|
||||
}
|
||||
|
||||
public function getPointTotal(): float
|
||||
{
|
||||
return $this->pointTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TPoints of array{'total': float, 'basic': float, 'bonus': float}
|
||||
* @return array{'points'?: TPoints, 'version'?: string, 'expired_at'?: Carbon}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_filter([
|
||||
'points' => $this->pointTotal == 0
|
||||
? null
|
||||
: [
|
||||
'total' => $this->pointTotal,
|
||||
'basic' => $this->pointBasic,
|
||||
'bonus' => $this->pointBonus,
|
||||
],
|
||||
'version' => $this->version,
|
||||
'expired_at' => $this->expiredAt,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PointType $pointType
|
||||
* @param string $range
|
||||
* @return Carbon
|
||||
*/
|
||||
private static function parseExpiredAt(PointType $pointType, string $range): Carbon
|
||||
{
|
||||
if ($pointType !== PointType::EMA) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
preg_match('/^(\d+)(\w)$/', $range, $matches);
|
||||
$quantity = $matches[1]; // 数字部分
|
||||
$unit = $matches[2]; // 单位部分
|
||||
|
||||
return match ($unit) {
|
||||
'Y' => $now->addYears($quantity),
|
||||
'm' => $now->addMonths($quantity),
|
||||
'd' => $now->addDays($quantity),
|
||||
'H' => $now->addHours($quantity),
|
||||
'i' => $now->addMinutes($quantity),
|
||||
's' => $now->addSeconds($quantity),
|
||||
default => $now
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* RechargeProductRepoInterface.php@LuxPay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/5
|
||||
*/
|
||||
|
||||
namespace Singularity\HDK\Pay\Domain\Product\Repository;
|
||||
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\Entities\ProductItem;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\RechargeProduct;
|
||||
|
||||
interface RechargeProductRepoInterface
|
||||
{
|
||||
/**
|
||||
* @return RechargeProduct
|
||||
*/
|
||||
public function findLuxPointProduct(): RechargeProduct;
|
||||
|
||||
/**
|
||||
* @param string $currentVersion
|
||||
* @return RechargeProduct
|
||||
*/
|
||||
public function findEmaProduct(string $currentVersion): RechargeProduct;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return ProductItem
|
||||
*/
|
||||
public function findProductItem(int $id): ProductItem;
|
||||
|
||||
/**
|
||||
* @param int[] $idList
|
||||
* @return ProductItem[]
|
||||
*/
|
||||
public function selectProductList(array $idList): array;
|
||||
}
|
||||
133
src/Infrastructure/Repository/ProductRepo.php
Normal file
133
src/Infrastructure/Repository/ProductRepo.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ProductRepo.php@Pay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/18
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Singularity\HDK\Pay\Infrastructure\Repository;
|
||||
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Hyperf\Codec\Json;
|
||||
use Money\Currency;
|
||||
use Money\Money;
|
||||
use Singularity\HDK\Pay\Domain\Account\Enum\PointType;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\Entities\ProductItem;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\RechargeProduct;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\ValueObject\RechargeEffect;
|
||||
use Singularity\HDK\Pay\Domain\Product\Enum\ProductType;
|
||||
use Singularity\HDK\Pay\Domain\Product\Repository\ExchangeRepoInterface;
|
||||
use Singularity\HDK\Pay\Domain\Product\Repository\RechargeProductRepoInterface;
|
||||
|
||||
final class ProductRepo extends AbstractRepo implements RechargeProductRepoInterface, ExchangeRepoInterface
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function findLuxPointProduct(): RechargeProduct
|
||||
{
|
||||
$response = $this->requestService->requestGet(
|
||||
url: "/rpc/v2/products/lux-point",
|
||||
options: [
|
||||
'headers' => $this->headerBuilder(),
|
||||
],
|
||||
);
|
||||
|
||||
$content = $response->getBody()->getContents();
|
||||
$result = Json::decode($content);
|
||||
|
||||
return new RechargeProduct(
|
||||
oneTime: new ProductItem(
|
||||
id: $result['one_time']['id'],
|
||||
description: $result['one_time']['name'],
|
||||
unitPrice: new Money(
|
||||
amount: $result['one_time']['price'],
|
||||
currency: new Currency($result['one_time']['currency']),
|
||||
),
|
||||
productType: ProductType::oneTime,
|
||||
effect: new RechargeEffect(
|
||||
pointType: PointType::LuxPoint,
|
||||
pointBasic: $result['one_time']['point']['number'],
|
||||
pointBonus: $result['one_time']['point']['bonus'],
|
||||
),
|
||||
),
|
||||
packages: array_map(fn(array $item) => new ProductItem(
|
||||
id: $item['id'],
|
||||
description: $item['name'],
|
||||
unitPrice: new Money(
|
||||
amount: $item['price'],
|
||||
currency: new Currency($item['currency']),
|
||||
),
|
||||
productType: ProductType::pack,
|
||||
effect: new RechargeEffect(
|
||||
pointType: PointType::LuxPoint,
|
||||
pointBasic: $item['point']['number'],
|
||||
pointBonus: $item['point']['bonus'],
|
||||
),
|
||||
), $result['package']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function findEmaProduct(string $currentVersion,
|
||||
): RechargeProduct {
|
||||
$response = $this->requestService->requestGet(
|
||||
url: "/rpc/v2/products/ema",
|
||||
options: [
|
||||
'headers' => $this->headerBuilder(),
|
||||
],
|
||||
);
|
||||
|
||||
$content = $response->getBody()->getContents();
|
||||
$result = Json::decode($content);
|
||||
|
||||
return new RechargeProduct(
|
||||
oneTime: $result['one_time'],
|
||||
renew: $result['renew'],
|
||||
plans: $result['plan'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function findProductItem(int $id): ProductItem
|
||||
{
|
||||
// TODO: Implement findProductItem() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function selectProductList(array $idList): array
|
||||
{
|
||||
// TODO: Implement selectProductList() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PointType $source
|
||||
* @param PointType $target
|
||||
* @return float
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getRate(PointType $source, PointType $target): float
|
||||
{
|
||||
$response = $this->requestService->requestGet(
|
||||
url: "/rpc/v2/products/$target->value/exchange-rate/$source->value",
|
||||
options: [
|
||||
'headers' => $this->headerBuilder(),
|
||||
],
|
||||
);
|
||||
|
||||
$content = $response->getBody()->getContents();
|
||||
$result = Json::decode($content);
|
||||
|
||||
return $result['rate'];
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,4 @@ it('can query point rate', function () {
|
||||
|
||||
$rate = $repo->getRate($from, $to);
|
||||
expect($rate)->toBeFloat();
|
||||
})->only();
|
||||
});
|
||||
|
||||
26
tests/Feature/Product/QueryProductsTest.php
Normal file
26
tests/Feature/Product/QueryProductsTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* QueryProductsTest.php@Pay
|
||||
*
|
||||
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2025/8/18
|
||||
*/
|
||||
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\Entities\ProductItem;
|
||||
use Singularity\HDK\Pay\Domain\Product\Aggregate\RechargeProduct;
|
||||
use Singularity\HDK\Pay\Infrastructure\Repository\ProductRepo;
|
||||
|
||||
use function Hyperf\Support\make;
|
||||
|
||||
it('should can query LuxPoint products', function () {
|
||||
$repo = make(ProductRepo::class);
|
||||
|
||||
$products = $repo->findLuxPointProduct();
|
||||
var_dump($products);
|
||||
expect($products)
|
||||
->toBeInstanceOf(RechargeProduct::class)
|
||||
->and($products->getOneTime())->toBeInstanceOf(ProductItem::class)
|
||||
->and($products->getPackages())->each->toBeInstanceOf(ProductItem::class);
|
||||
})->only();
|
||||
Reference in New Issue
Block a user