mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-23 02:24:27 +08:00
重构初始化
This commit is contained in:
@@ -2,49 +2,44 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use app\exception\ValidationException;
|
||||
use support\Context;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 控制器基础父类
|
||||
* HTTP 层基础控制器。
|
||||
*
|
||||
* 约定统一的 JSON 返回结构:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "message": "success",
|
||||
* "data": ...
|
||||
* }
|
||||
* 统一提供响应封装、参数校验、请求上下文读取等通用能力。
|
||||
*/
|
||||
class BaseController
|
||||
{
|
||||
/**
|
||||
* 成功返回
|
||||
* 返回成功响应。
|
||||
*/
|
||||
protected function success(mixed $data = null, string $message = 'success', int $code = 200): Response
|
||||
protected function success(mixed $data = null, string $message = '操作成功', int $code = 200): Response
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回
|
||||
* 返回失败响应。
|
||||
*/
|
||||
protected function fail(string $message = 'error', int $code = 500, mixed $data = null): Response
|
||||
protected function fail(string $message = '操作失败', int $code = 500, mixed $data = null): Response
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一分页返回结构
|
||||
*
|
||||
* @param mixed $paginator Laravel/Eloquent paginator
|
||||
* 返回统一分页响应。
|
||||
*/
|
||||
protected function page(mixed $paginator): Response
|
||||
{
|
||||
@@ -58,28 +53,76 @@ class BaseController
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'list' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
'list' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的 token 载荷
|
||||
* 通过校验器类验证请求数据。
|
||||
*
|
||||
* 从 AuthMiddleware 注入的用户信息中获取
|
||||
* @param class-string $validatorClass
|
||||
*/
|
||||
protected function currentUser(Request $request): ?array
|
||||
protected function validated(array $data, string $validatorClass, ?string $scene = null): array
|
||||
{
|
||||
return $request->user ?? null;
|
||||
$validator = $validatorClass::make($data);
|
||||
|
||||
if ($scene !== null) {
|
||||
$validator = $validator->withScene($scene);
|
||||
}
|
||||
|
||||
return $validator
|
||||
->withException(ValidationException::class)
|
||||
->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
* 获取中间件预处理后的标准化参数。
|
||||
*/
|
||||
protected function currentUserId(Request $request): int
|
||||
protected function payload(Request $request): array
|
||||
{
|
||||
return (int) ($request->userId ?? 0);
|
||||
$payload = (array) $request->all();
|
||||
$normalized = Context::get('mpay.normalized_input', []);
|
||||
|
||||
if (is_array($normalized) && !empty($normalized)) {
|
||||
$payload = array_replace($payload, $normalized);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取请求属性。
|
||||
*/
|
||||
protected function requestAttribute(Request $request, string $key, mixed $default = null): mixed
|
||||
{
|
||||
return Context::get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件注入的当前管理员 ID。
|
||||
*/
|
||||
protected function currentAdminId(Request $request): int
|
||||
{
|
||||
return (int) $this->requestAttribute($request, 'auth.admin_id', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件注入的当前商户 ID。
|
||||
*/
|
||||
protected function currentMerchantId(Request $request): int
|
||||
{
|
||||
return (int) $this->requestAttribute($request, 'auth.merchant_id', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件注入的当前商户编号。
|
||||
*/
|
||||
protected function currentMerchantNo(Request $request): string
|
||||
{
|
||||
return (string) $this->requestAttribute($request, 'auth.merchant_no', '');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,35 +2,50 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use app\common\util\FormatHelper;
|
||||
use DateTimeInterface;
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* 所有业务模型的基础父类
|
||||
* 所有业务模型的基础父类。
|
||||
*
|
||||
* 统一主键、时间戳和默认批量赋值策略。
|
||||
*/
|
||||
class BaseModel extends Model
|
||||
{
|
||||
/**
|
||||
* 约定所有主键字段名
|
||||
* 默认主键字段名。
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* 是否自动维护 created_at / updated_at
|
||||
* 是否自动维护 created_at / updated_at。
|
||||
*
|
||||
* 大部分业务表都有这两个字段,如不需要可在子类里覆盖为 false。
|
||||
* 大部分业务表都包含这两个字段,如有例外可在子类中覆盖为 false。
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* 默认不禁止任何字段的批量赋值
|
||||
* 默认仅保护主键,其他字段按子类 fillable 约束。
|
||||
*
|
||||
* 建议在具体模型中按需设置 $fillable 或 $guarded。
|
||||
* 建议在具体模型中显式声明 $fillable。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* 统一模型时间字段的 JSON 输出格式。
|
||||
*
|
||||
* 避免前端收到 ISO8601(如 2026-04-02T01:50:40.000000Z)这类不直观的时间串,
|
||||
* 统一改为后台常用的本地展示格式。
|
||||
*/
|
||||
protected function serializeDate(DateTimeInterface $date): string
|
||||
{
|
||||
return FormatHelper::dateTime($date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use app\common\contracts\PayPluginInterface;
|
||||
use app\exceptions\PaymentException;
|
||||
use app\common\interface\PayPluginInterface;
|
||||
use app\exception\PaymentException;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -21,7 +21,7 @@ use support\Log;
|
||||
* - 子类可在 `init()` 中配置第三方 SDK(例如 yansongda/pay)或读取必填参数。
|
||||
*
|
||||
* 约定:
|
||||
* - 这里的 `$channelConfig` 来源通常是 `ma_pay_channel.config_json`,属于“通道级配置”。
|
||||
* - 这里的 `$channelConfig` 来源通常是 `ma_payment_plugin_conf.config`,并附带通道维度上下文。
|
||||
* - 业务级入参(如订单号、金额、回调地址等)不要混进 `$channelConfig`,应从 `pay()` 的 `$order` 参数获取。
|
||||
*/
|
||||
abstract class BasePayment implements PayPluginInterface
|
||||
@@ -108,6 +108,12 @@ abstract class BasePayment implements PayPluginInterface
|
||||
return $this->paymentInfo['link'] ?? '';
|
||||
}
|
||||
|
||||
/** 获取版本号 */
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->paymentInfo['version'] ?? '';
|
||||
}
|
||||
|
||||
// ==================== 能力声明 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,65 +2,194 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use support\Model;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 仓储层基础父类
|
||||
* 仓储层基础类。
|
||||
*
|
||||
* 封装单表常用的 CRUD / 分页操作,具体仓储继承后可扩展业务查询。
|
||||
* 封装通用 CRUD、条件查询、加锁查询和分页查询能力。
|
||||
*/
|
||||
abstract class BaseRepository
|
||||
{
|
||||
/**
|
||||
* @var Model
|
||||
* 当前仓储绑定的模型实例。
|
||||
*/
|
||||
protected Model $model;
|
||||
|
||||
|
||||
/**
|
||||
* 构造函数,绑定模型实例。
|
||||
*/
|
||||
public function __construct(Model $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查询
|
||||
* 获取查询构造器。
|
||||
*/
|
||||
public function find(int $id, array $columns = ['*']): ?Model
|
||||
public function query()
|
||||
{
|
||||
return $this->model->newQuery()->find($id, $columns);
|
||||
return $this->model->newQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建记录
|
||||
* 按主键查询记录。
|
||||
*/
|
||||
public function find(int|string $id, array $columns = ['*']): ?Model
|
||||
{
|
||||
return $this->query()->find($id, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增记录。
|
||||
*/
|
||||
public function create(array $data): Model
|
||||
{
|
||||
return $this->model->newQuery()->create($data);
|
||||
return $this->query()->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键更新
|
||||
* 按主键更新记录。
|
||||
*/
|
||||
public function updateById(int $id, array $data): bool
|
||||
public function updateById(int|string $id, array $data): bool
|
||||
{
|
||||
return (bool) $this->model->newQuery()->whereKey($id)->update($data);
|
||||
return (bool) $this->query()->whereKey($id)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键删除
|
||||
* 按唯一键更新记录。
|
||||
*/
|
||||
public function deleteById(int $id): bool
|
||||
public function updateByKey(int|string $key, array $data): bool
|
||||
{
|
||||
return (bool) $this->model->newQuery()->whereKey($id)->delete();
|
||||
return (bool) $this->query()->whereKey($key)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分页查询示例
|
||||
* 按条件批量更新记录。
|
||||
*/
|
||||
public function updateWhere(array $where, array $data): int
|
||||
{
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
}
|
||||
|
||||
return (int) $query->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键删除记录。
|
||||
*/
|
||||
public function deleteById(int|string $id): bool
|
||||
{
|
||||
return (bool) $this->query()->whereKey($id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条件批量删除记录。
|
||||
*/
|
||||
public function deleteWhere(array $where): int
|
||||
{
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
}
|
||||
|
||||
return (int) $query->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条件获取首条记录。
|
||||
*/
|
||||
public function firstBy(array $where = [], array $columns = ['*']): ?Model
|
||||
{
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
}
|
||||
|
||||
return $query->first($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 先查后更,不存在则创建。
|
||||
*/
|
||||
public function updateOrCreate(array $where, array $data = []): Model
|
||||
{
|
||||
if ($where === []) {
|
||||
return $this->create($data);
|
||||
}
|
||||
|
||||
return Db::transaction(function () use ($where, $data): Model {
|
||||
$query = $this->query()->lockForUpdate();
|
||||
$query->where($where);
|
||||
|
||||
/** @var Model|null $model */
|
||||
$model = $query->first();
|
||||
if ($model) {
|
||||
$model->fill($data);
|
||||
$model->save();
|
||||
|
||||
return $model->refresh();
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->create(array_merge($where, $data));
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
$model = $this->firstBy($where);
|
||||
if (!$model) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$model->fill($data);
|
||||
$model->save();
|
||||
|
||||
return $model->refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条件统计数量。
|
||||
*/
|
||||
public function countBy(array $where = []): int
|
||||
{
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
}
|
||||
|
||||
return (int) $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断条件下是否存在记录。
|
||||
*/
|
||||
public function existsBy(array $where = []): bool
|
||||
{
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询。
|
||||
*
|
||||
* @param array $where ['字段' => 值],值为 null / '' 时会被忽略
|
||||
* @param array $where 条件数组,空值会被忽略
|
||||
*/
|
||||
public function paginate(array $where = [], int $page = 1, int $pageSize = 10, array $columns = ['*'])
|
||||
{
|
||||
$query = $this->model->newQuery();
|
||||
$query = $this->query();
|
||||
|
||||
if (!empty($where)) {
|
||||
$query->where($where);
|
||||
|
||||
@@ -2,23 +2,172 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use app\common\util\FormatHelper;
|
||||
use support\Db;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 业务服务层基础父类
|
||||
* 业务服务层基础类。
|
||||
*
|
||||
* 统一承载业务单号生成、时间获取和事务封装等通用能力。
|
||||
*/
|
||||
class BaseService
|
||||
{
|
||||
/**
|
||||
* 事务封装
|
||||
* 生成业务单号。
|
||||
*
|
||||
* 使用方式:
|
||||
* $this->transaction(function () { ... });
|
||||
* 适用于 biz_no / pay_no / refund_no / settle_no / notify_no / ledger_no 等场景。
|
||||
* 默认使用时间前缀 + 随机数,保证可读性和基本唯一性。
|
||||
*/
|
||||
protected function generateNo(string $prefix = ''): string
|
||||
{
|
||||
$time = FormatHelper::timestamp(time(), 'YmdHis');
|
||||
$rand = (string) random_int(100000, 999999);
|
||||
|
||||
return $prefix === '' ? $time . $rand : $prefix . $time . $rand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间字符串。
|
||||
*
|
||||
* 统一返回 `Y-m-d H:i:s` 格式,便于数据库写入和日志输出。
|
||||
*/
|
||||
protected function now(): string
|
||||
{
|
||||
return FormatHelper::timestamp(time());
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额格式化,单位为元。
|
||||
*/
|
||||
protected function formatAmount(int $amount): string
|
||||
{
|
||||
return FormatHelper::amount($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额格式化,0 时显示不限。
|
||||
*/
|
||||
protected function formatAmountOrUnlimited(int $amount): string
|
||||
{
|
||||
return FormatHelper::amountOrUnlimited($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 次数格式化,0 时显示不限。
|
||||
*/
|
||||
protected function formatCountOrUnlimited(int $count): string
|
||||
{
|
||||
return FormatHelper::countOrUnlimited($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 费率格式化,单位为百分点。
|
||||
*/
|
||||
protected function formatRate(int $basisPoints): string
|
||||
{
|
||||
return FormatHelper::rate($basisPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟格式化。
|
||||
*/
|
||||
protected function formatLatency(int $latencyMs): string
|
||||
{
|
||||
return FormatHelper::latency($latencyMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化。
|
||||
*/
|
||||
protected function formatDate(mixed $value, string $emptyText = ''): string
|
||||
{
|
||||
return FormatHelper::date($value, $emptyText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间格式化。
|
||||
*/
|
||||
protected function formatDateTime(mixed $value, string $emptyText = ''): string
|
||||
{
|
||||
return FormatHelper::dateTime($value, $emptyText);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 文本格式化。
|
||||
*/
|
||||
protected function formatJson(mixed $value, string $emptyText = ''): string
|
||||
{
|
||||
return FormatHelper::json($value, $emptyText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射表文本转换。
|
||||
*/
|
||||
protected function textFromMap(int $value, array $map, string $default = '未知'): string
|
||||
{
|
||||
return FormatHelper::textFromMap($value, $map, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口凭证明文脱敏。
|
||||
*/
|
||||
protected function maskCredentialValue(string $credentialValue, bool $maskShortValue = true): string
|
||||
{
|
||||
return FormatHelper::maskCredentialValue($credentialValue, $maskShortValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将模型或对象归一化成数组。
|
||||
*/
|
||||
protected function normalizeModel(mixed $value): ?array
|
||||
{
|
||||
return FormatHelper::normalizeModel($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务封装。
|
||||
*
|
||||
* 适合单次数据库事务,不包含自动重试逻辑。
|
||||
*
|
||||
* @param callable $callback 事务体
|
||||
* @return mixed
|
||||
*/
|
||||
protected function transaction(callable $callback)
|
||||
{
|
||||
return Db::connection()->transaction(function () use ($callback) {
|
||||
return Db::transaction(function () use ($callback) {
|
||||
return $callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持重试的事务封装。
|
||||
*
|
||||
* 适合余额冻结、扣减、状态推进和幂等写入等容易发生锁冲突的场景。
|
||||
*/
|
||||
protected function transactionRetry(callable $callback, int $attempts = 3, int $sleepMs = 50)
|
||||
{
|
||||
$attempts = max(1, $attempts);
|
||||
|
||||
beginning:
|
||||
try {
|
||||
return $this->transaction($callback);
|
||||
} catch (Throwable $e) {
|
||||
$message = strtolower($e->getMessage());
|
||||
$retryable = str_contains($message, 'deadlock')
|
||||
|| str_contains($message, 'lock wait timeout')
|
||||
|| str_contains($message, 'try restarting transaction');
|
||||
|
||||
if (--$attempts > 0 && $retryable) {
|
||||
if ($sleepMs > 0) {
|
||||
usleep($sleepMs * 1000);
|
||||
}
|
||||
|
||||
goto beginning;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user