This commit is contained in:
技术老胡
2026-04-01 14:30:50 +08:00
parent 0eee3b92c2
commit d34bd386ec
15 changed files with 542 additions and 186 deletions

View File

@@ -116,18 +116,18 @@ class ChannelController extends BaseController
}
$channelData = [
'merchant_id' => $merchantId,
'merchant_app_id' => $merchantAppId,
'mer_id' => $merchantId,
'app_id' => $merchantAppId,
'chan_code' => $channelCode !== '' ? $channelCode : 'CH' . date('YmdHis') . mt_rand(1000, 9999),
'chan_name' => $channelName,
'plugin_code' => $pluginCode,
'method_id' => (int)$method->id,
'config_json' => array_merge($configJson, [
'pay_type_id' => (int)$method->id,
'config' => array_merge($configJson, [
'enabled_products' => is_array($enabledProducts) ? array_values($enabledProducts) : [],
]),
'split_ratio' => isset($data['split_ratio']) ? (float)$data['split_ratio'] : 100,
'chan_cost' => isset($data['channel_cost']) ? (float)$data['channel_cost'] : 0,
'chan_mode' => trim((string)($data['channel_mode'] ?? 'wallet')) ?: 'wallet',
'chan_mode' => in_array(strtolower(trim((string)($data['channel_mode'] ?? 'wallet'))), ['1', 'direct', 'merchant'], true) ? 1 : 0,
'daily_limit' => isset($data['daily_limit']) ? (float)$data['daily_limit'] : 0,
'daily_cnt' => isset($data['daily_count']) ? (int)$data['daily_count'] : 0,
'min_amount' => isset($data['min_amount']) && $data['min_amount'] !== '' ? (float)$data['min_amount'] : null,
@@ -648,4 +648,4 @@ class ChannelController extends BaseController
}
return $summary;
}
}
}

View File

@@ -34,9 +34,8 @@ class MerchantAppController extends BaseController
$packageMap = $this->buildPackageMap();
$items = [];
foreach ($paginator->items() as $row) {
$item = (array)$row;
$config = $this->getConfigObject($this->appConfigKey((int)($item['id'] ?? 0)));
$packageCode = trim((string)($config['package_code'] ?? ''));
$item = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
$packageCode = trim((string)($item['package_code'] ?? ''));
$item['package_code'] = $packageCode;
$item['package_name'] = $packageCode !== '' ? ($packageMap[$packageCode] ?? $packageCode) : '';
$items[] = $item;
@@ -62,7 +61,7 @@ class MerchantAppController extends BaseController
return $this->fail('app not found', 404);
}
return $this->success($row);
return $this->success(method_exists($row, 'toArray') ? $row->toArray() : (array)$row);
}
public function configDetail(Request $request)
@@ -77,7 +76,8 @@ class MerchantAppController extends BaseController
return $this->fail('app not found', 404);
}
$config = array_merge($this->defaultAppConfig(), $this->getConfigObject($this->appConfigKey($id)));
$appRow = method_exists($app, 'toArray') ? $app->toArray() : (array)$app;
$config = $this->buildAppConfig($appRow);
return $this->success([
'app' => $app,
'config' => $config,
@@ -122,11 +122,12 @@ class MerchantAppController extends BaseController
}
$update = [
'merchant_id' => $merchantId,
'mer_id' => $merchantId,
'api_type' => $apiType,
'app_id' => $appId,
'app_code' => $appId,
'app_name' => $appName,
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
];
if (!empty($data['app_secret'])) {
@@ -142,12 +143,14 @@ class MerchantAppController extends BaseController
$secret = !empty($data['app_secret']) ? (string)$data['app_secret'] : $this->generateSecret();
$this->merchantAppRepository->create([
'merchant_id' => $merchantId,
'mer_id' => $merchantId,
'api_type' => $apiType,
'app_id' => $appId,
'app_code' => $appId,
'app_secret' => $secret,
'app_name' => $appName,
'status' => $status,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
@@ -236,8 +239,9 @@ class MerchantAppController extends BaseController
}
}
$stored = array_merge($this->defaultAppConfig(), $this->getConfigObject($this->appConfigKey($id)), $config);
$this->systemConfigService->setValue($this->appConfigKey($id), $stored);
$updateData = $config;
$updateData['updated_at'] = date('Y-m-d H:i:s');
$this->merchantAppRepository->updateById($id, $updateData);
return $this->success(null, 'saved');
}
@@ -248,42 +252,6 @@ class MerchantAppController extends BaseController
return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
}
private function appConfigKey(int $appId): string
{
return 'merchant_app_config_' . $appId;
}
private function defaultAppConfig(): array
{
return [
'package_code' => '',
'notify_url' => '',
'return_url' => '',
'callback_mode' => 'server',
'sign_type' => 'md5',
'order_expire_minutes' => 30,
'callback_retry_limit' => 6,
'ip_whitelist' => '',
'amount_min' => 0,
'amount_max' => 0,
'daily_limit' => 0,
'notify_enabled' => 1,
'remark' => '',
'updated_at' => '',
];
}
private function getConfigObject(string $configKey): array
{
$raw = $this->systemConfigService->getValue($configKey, '{}');
if (!is_string($raw) || $raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
private function getConfigEntries(string $configKey): array
{
$raw = $this->systemConfigService->getValue($configKey, '[]');
@@ -299,6 +267,26 @@ class MerchantAppController extends BaseController
return array_values(array_filter($decoded, 'is_array'));
}
private function buildAppConfig(array $app): array
{
return [
'package_code' => trim((string)($app['package_code'] ?? '')),
'notify_url' => trim((string)($app['notify_url'] ?? '')),
'return_url' => trim((string)($app['return_url'] ?? '')),
'callback_mode' => trim((string)($app['callback_mode'] ?? 'server')) ?: 'server',
'sign_type' => trim((string)($app['sign_type'] ?? 'md5')) ?: 'md5',
'order_expire_minutes' => (int)($app['order_expire_minutes'] ?? 30),
'callback_retry_limit' => (int)($app['callback_retry_limit'] ?? 6),
'ip_whitelist' => trim((string)($app['ip_whitelist'] ?? '')),
'amount_min' => (string)($app['amount_min'] ?? '0.00'),
'amount_max' => (string)($app['amount_max'] ?? '0.00'),
'daily_limit' => (string)($app['daily_limit'] ?? '0.00'),
'notify_enabled' => (int)($app['notify_enabled'] ?? 1),
'remark' => trim((string)($app['remark'] ?? '')),
'updated_at' => (string)($app['updated_at'] ?? ''),
];
}
private function buildPackageMap(): array
{
$map = [];

View File

@@ -25,18 +25,15 @@ class MerchantController extends BaseController
'status' => $request->get('status', ''),
'merchant_no' => trim((string)$request->get('merchant_no', '')),
'merchant_name' => trim((string)$request->get('merchant_name', '')),
'email' => trim((string)$request->get('email', '')),
'balance' => trim((string)$request->get('balance', '')),
];
$paginator = $this->merchantRepository->searchPaginate($filters, $page, $pageSize);
$groupMap = $this->buildGroupMap();
$items = [];
foreach ($paginator->items() as $row) {
$item = (array)$row;
$profile = $this->getConfigObject($this->merchantProfileKey((int)($item['id'] ?? 0)));
$groupCode = trim((string)($profile['group_code'] ?? ''));
$item['group_code'] = $groupCode;
$item['group_name'] = $groupCode !== '' ? ($groupMap[$groupCode] ?? $groupCode) : '';
$items[] = $item;
$item = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
$items[] = $this->normalizeMerchantRow($item);
}
return $this->success([
@@ -59,7 +56,8 @@ class MerchantController extends BaseController
return $this->fail('merchant not found', 404);
}
return $this->success($row);
$merchant = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
return $this->success($this->normalizeMerchantRow($merchant));
}
public function profileDetail(Request $request)
@@ -74,10 +72,10 @@ class MerchantController extends BaseController
return $this->fail('merchant not found', 404);
}
$profile = array_merge($this->defaultMerchantProfile(), $this->getConfigObject($this->merchantProfileKey($id)));
$merchantRow = method_exists($merchant, 'toArray') ? $merchant->toArray() : (array)$merchant;
return $this->success([
'merchant' => $merchant,
'profile' => $profile,
'merchant' => $this->normalizeMerchantRow($merchantRow),
'profile' => $this->buildMerchantProfile($merchantRow),
]);
}
@@ -88,23 +86,24 @@ class MerchantController extends BaseController
$merchantNo = trim((string)($data['merchant_no'] ?? ''));
$merchantName = trim((string)($data['merchant_name'] ?? ''));
$fundsMode = trim((string)($data['funds_mode'] ?? 'direct'));
$balance = max(0, (float)($data['balance'] ?? 0));
$email = trim((string)($data['email'] ?? $data['notify_email'] ?? ''));
$status = (int)($data['status'] ?? 1);
$remark = trim((string)($data['remark'] ?? ''));
if ($merchantNo === '' || $merchantName === '') {
return $this->fail('merchant_no and merchant_name are required', 400);
}
if (!in_array($fundsMode, ['direct', 'wallet', 'hybrid'], true)) {
return $this->fail('invalid funds_mode', 400);
}
if ($id > 0) {
$this->merchantRepository->updateById($id, [
'merchant_no' => $merchantNo,
'merchant_name' => $merchantName,
'funds_mode' => $fundsMode,
'balance' => $balance,
'email' => $email,
'status' => $status,
'remark' => $remark,
'updated_at' => date('Y-m-d H:i:s'),
]);
} else {
$exists = $this->merchantRepository->findByMerchantNo($merchantNo);
@@ -115,8 +114,12 @@ class MerchantController extends BaseController
$this->merchantRepository->create([
'merchant_no' => $merchantNo,
'merchant_name' => $merchantName,
'funds_mode' => $fundsMode,
'balance' => $balance,
'email' => $email,
'status' => $status,
'remark' => $remark,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
@@ -148,46 +151,17 @@ class MerchantController extends BaseController
return $this->fail('merchant not found', 404);
}
$riskLevel = trim((string)$request->post('risk_level', 'standard'));
$settlementCycle = trim((string)$request->post('settlement_cycle', 't1'));
if (!in_array($riskLevel, ['low', 'standard', 'high'], true)) {
return $this->fail('invalid risk_level', 400);
}
if (!in_array($settlementCycle, ['d0', 't1', 'manual'], true)) {
return $this->fail('invalid settlement_cycle', 400);
}
$merchantRow = method_exists($merchant, 'toArray') ? $merchant->toArray() : (array)$merchant;
$profile = [
'group_code' => trim((string)$request->post('group_code', '')),
'contact_name' => trim((string)$request->post('contact_name', '')),
'contact_phone' => trim((string)$request->post('contact_phone', '')),
'notify_email' => trim((string)$request->post('notify_email', '')),
'callback_domain' => trim((string)$request->post('callback_domain', '')),
'callback_ip_whitelist' => trim((string)$request->post('callback_ip_whitelist', '')),
'risk_level' => $riskLevel,
'single_limit' => max(0, (float)$request->post('single_limit', 0)),
'daily_limit' => max(0, (float)$request->post('daily_limit', 0)),
'settlement_cycle' => $settlementCycle,
'tech_support' => trim((string)$request->post('tech_support', '')),
'email' => trim((string)$request->post('email', $request->post('notify_email', ''))),
'remark' => trim((string)$request->post('remark', '')),
'updated_at' => date('Y-m-d H:i:s'),
'balance' => max(0, (float)$request->post('balance', $merchantRow['balance'] ?? 0)),
];
if ($profile['group_code'] !== '') {
$groupExists = false;
foreach ($this->getConfigEntries('merchant_groups') as $group) {
if (($group['group_code'] ?? '') === $profile['group_code']) {
$groupExists = true;
break;
}
}
if (!$groupExists) {
return $this->fail('group_code not found', 400);
}
}
$stored = array_merge($this->defaultMerchantProfile(), $this->getConfigObject($this->merchantProfileKey($merchantId)), $profile);
$this->systemConfigService->setValue($this->merchantProfileKey($merchantId), $stored);
$updateData = $profile;
$updateData['updated_at'] = date('Y-m-d H:i:s');
$this->merchantRepository->updateById($merchantId, $updateData);
return $this->success(null, 'saved');
}
@@ -198,9 +172,9 @@ class MerchantController extends BaseController
$pageSize = (int)$request->get('page_size', 10);
$filters = $this->buildOpFilters($request);
$summaryQuery = Db::table('ma_merchant as m')
->leftJoin('ma_merchant_app as ma', 'ma.merchant_id', '=', 'm.id')
->leftJoin('ma_pay_channel as pc', 'pc.merchant_id', '=', 'm.id')
$summaryQuery = Db::table('ma_mer as m')
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id')
->leftJoin('ma_pay_channel as pc', 'pc.mer_id', '=', 'm.id')
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
$join->on('o.merchant_id', '=', 'm.id');
if (!empty($filters['created_from'])) {
@@ -225,9 +199,9 @@ class MerchantController extends BaseController
)
->first();
$listQuery = Db::table('ma_merchant as m')
->leftJoin('ma_merchant_app as ma', 'ma.merchant_id', '=', 'm.id')
->leftJoin('ma_pay_channel as pc', 'pc.merchant_id', '=', 'm.id')
$listQuery = Db::table('ma_mer as m')
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id')
->leftJoin('ma_pay_channel as pc', 'pc.mer_id', '=', 'm.id')
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
$join->on('o.merchant_id', '=', 'm.id');
if (!empty($filters['created_from'])) {
@@ -241,7 +215,7 @@ class MerchantController extends BaseController
$paginator = $listQuery
->selectRaw(
'm.id, m.merchant_no, m.merchant_name, m.funds_mode, m.status, m.created_at,
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at,
COUNT(DISTINCT ma.id) AS app_count,
COUNT(DISTINCT CASE WHEN ma.status = 1 THEN ma.id END) AS active_app_count,
COUNT(DISTINCT pc.id) AS channel_count,
@@ -252,7 +226,7 @@ class MerchantController extends BaseController
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.fee ELSE 0 END), 0) AS fee_amount,
MAX(o.created_at) AS last_order_at'
)
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.funds_mode', 'm.status', 'm.created_at')
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at')
->orderByDesc('m.id')
->paginate($pageSize, ['*'], 'page', $page);
@@ -280,7 +254,7 @@ class MerchantController extends BaseController
$pageSize = (int)$request->get('page_size', 10);
$filters = $this->buildOpFilters($request);
$summaryQuery = Db::table('ma_merchant as m')
$summaryQuery = Db::table('ma_mer as m')
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
$join->on('o.merchant_id', '=', 'm.id');
if (!empty($filters['created_from'])) {
@@ -303,7 +277,7 @@ class MerchantController extends BaseController
)
->first();
$listQuery = Db::table('ma_merchant as m')
$listQuery = Db::table('ma_mer as m')
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
$join->on('o.merchant_id', '=', 'm.id');
if (!empty($filters['created_from'])) {
@@ -317,7 +291,7 @@ class MerchantController extends BaseController
$paginator = $listQuery
->selectRaw(
'm.id, m.merchant_no, m.merchant_name, m.funds_mode, m.status, m.created_at,
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at,
COUNT(DISTINCT CASE WHEN o.status = 1 THEN o.id END) AS success_order_count,
COUNT(DISTINCT CASE WHEN o.status = 0 THEN o.id END) AS pending_order_count,
COUNT(DISTINCT CASE WHEN o.notify_stat = 0 THEN o.id END) AS notify_pending_orders,
@@ -327,7 +301,7 @@ class MerchantController extends BaseController
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS net_amount,
MAX(o.pay_at) AS last_pay_at'
)
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.funds_mode', 'm.status', 'm.created_at')
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at')
->orderByRaw('COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) DESC')
->paginate($pageSize, ['*'], 'page', $page);
@@ -354,7 +328,7 @@ class MerchantController extends BaseController
$auditStatus = trim((string)$request->get('audit_status', ''));
$keyword = trim((string)$request->get('keyword', ''));
$summaryQuery = Db::table('ma_merchant as m');
$summaryQuery = Db::table('ma_mer as m');
if ($keyword !== '') {
$summaryQuery->where(function ($query) use ($keyword) {
$query->where('m.merchant_no', 'like', '%' . $keyword . '%')
@@ -375,8 +349,8 @@ class MerchantController extends BaseController
)
->first();
$listQuery = Db::table('ma_merchant as m')
->leftJoin('ma_merchant_app as ma', 'ma.merchant_id', '=', 'm.id');
$listQuery = Db::table('ma_mer as m')
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id');
if ($keyword !== '') {
$listQuery->where(function ($query) use ($keyword) {
$query->where('m.merchant_no', 'like', '%' . $keyword . '%')
@@ -391,12 +365,12 @@ class MerchantController extends BaseController
$paginator = $listQuery
->selectRaw(
'm.id, m.merchant_no, m.merchant_name, m.funds_mode, m.status, m.created_at, m.updated_at,
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at, m.updated_at,
COUNT(DISTINCT ma.id) AS app_count,
COUNT(DISTINCT CASE WHEN ma.status = 1 THEN ma.id END) AS active_app_count,
COUNT(DISTINCT CASE WHEN ma.status = 0 THEN ma.id END) AS disabled_app_count'
)
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.funds_mode', 'm.status', 'm.created_at', 'm.updated_at')
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at', 'm.updated_at')
->orderBy('m.status', 'asc')
->orderByDesc('m.id')
->paginate($pageSize, ['*'], 'page', $page);
@@ -433,11 +407,11 @@ class MerchantController extends BaseController
$status = $action === 'approve' ? 1 : 0;
Db::connection()->transaction(function () use ($id, $status) {
Db::table('ma_merchant')->where('id', $id)->update([
Db::table('ma_mer')->where('id', $id)->update([
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
]);
Db::table('ma_merchant_app')->where('merchant_id', $id)->update([
Db::table('ma_pay_app')->where('mer_id', $id)->update([
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
]);
@@ -733,6 +707,8 @@ class MerchantController extends BaseController
'merchant_id' => (int)$request->get('merchant_id', 0),
'status' => (string)$request->get('status', ''),
'keyword' => trim((string)$request->get('keyword', '')),
'email' => trim((string)$request->get('email', '')),
'balance' => trim((string)$request->get('balance', '')),
'created_from' => trim((string)$request->get('created_from', '')),
'created_to' => trim((string)$request->get('created_to', '')),
];
@@ -749,9 +725,16 @@ class MerchantController extends BaseController
if (!empty($filters['keyword'])) {
$query->where(function ($builder) use ($filters) {
$builder->where('m.merchant_no', 'like', '%' . $filters['keyword'] . '%')
->orWhere('m.merchant_name', 'like', '%' . $filters['keyword'] . '%');
->orWhere('m.merchant_name', 'like', '%' . $filters['keyword'] . '%')
->orWhere('m.email', 'like', '%' . $filters['keyword'] . '%');
});
}
if (!empty($filters['email'])) {
$query->where('m.email', 'like', '%' . $filters['email'] . '%');
}
if (isset($filters['balance']) && $filters['balance'] !== '') {
$query->where('m.balance', (string)$filters['balance']);
}
}
private function getConfigEntries(string $configKey): array
@@ -863,6 +846,31 @@ class MerchantController extends BaseController
return $map;
}
private function normalizeMerchantRow(array $merchant): array
{
$merchant['merchant_no'] = trim((string)($merchant['merchant_no'] ?? ''));
$merchant['merchant_name'] = trim((string)($merchant['merchant_name'] ?? ''));
$merchant['balance'] = (string)($merchant['balance'] ?? '0.00');
$merchant['email'] = trim((string)($merchant['email'] ?? ''));
$merchant['remark'] = trim((string)($merchant['remark'] ?? ''));
$merchant['status'] = (int)($merchant['status'] ?? 1);
$merchant['created_at'] = (string)($merchant['created_at'] ?? '');
$merchant['updated_at'] = (string)($merchant['updated_at'] ?? '');
return $merchant;
}
private function buildMerchantProfile(array $merchant): array
{
return [
'merchant_no' => trim((string)($merchant['merchant_no'] ?? '')),
'merchant_name' => trim((string)($merchant['merchant_name'] ?? '')),
'balance' => (string)($merchant['balance'] ?? '0.00'),
'email' => trim((string)($merchant['email'] ?? '')),
'status' => (int)($merchant['status'] ?? 1),
'remark' => trim((string)($merchant['remark'] ?? '')),
];
}
private function getConfigObject(string $configKey): array
{
$raw = $this->systemConfigService->getValue($configKey, '{}');

View File

@@ -54,8 +54,8 @@ class PayMethodController extends BaseController
if ($id > 0) {
$this->methodRepository->updateById($id, [
'method_code' => $code,
'method_name' => $name,
'type' => $code,
'name' => $name,
'icon' => $icon,
'sort' => $sort,
'status' => $status,
@@ -66,8 +66,8 @@ class PayMethodController extends BaseController
return $this->fail('支付方式编码已存在', 400);
}
$this->methodRepository->create([
'method_code' => $code,
'method_name' => $name,
'type' => $code,
'name' => $name,
'icon' => $icon,
'sort' => $sort,
'status' => $status,

View File

@@ -9,19 +9,25 @@ use app\common\base\BaseModel;
*/
class Merchant extends BaseModel
{
protected $table = 'ma_merchant';
protected $table = 'ma_mer';
protected $fillable = [
'merchant_no',
'merchant_name',
'balance',
'email',
'funds_mode',
'status',
'remark',
'extra',
];
public $timestamps = true;
protected $casts = [
'balance' => 'decimal:2',
'status' => 'integer',
'extra' => 'array',
];
}

View File

@@ -9,22 +9,65 @@ use app\common\base\BaseModel;
*/
class MerchantApp extends BaseModel
{
protected $table = 'ma_merchant_app';
protected $table = 'ma_pay_app';
protected $fillable = [
'merchant_id',
'mer_id',
'api_type',
'app_id',
'app_code',
'app_secret',
'app_name',
'status',
'remark',
'package_code',
'notify_url',
'return_url',
'callback_mode',
'sign_type',
'order_expire_minutes',
'callback_retry_limit',
'ip_whitelist',
'amount_min',
'amount_max',
'daily_limit',
'notify_enabled',
'extra',
];
public $timestamps = true;
protected $appends = ['merchant_id', 'app_id'];
protected $casts = [
'merchant_id' => 'integer',
'mer_id' => 'integer',
'order_expire_minutes' => 'integer',
'callback_retry_limit' => 'integer',
'amount_min' => 'decimal:2',
'amount_max' => 'decimal:2',
'daily_limit' => 'decimal:2',
'notify_enabled' => 'integer',
'status' => 'integer',
'extra' => 'array',
];
public function getMerchantIdAttribute()
{
return $this->attributes['mer_id'] ?? null;
}
public function setMerchantIdAttribute($value): void
{
$this->attributes['mer_id'] = (int)$value;
}
public function getAppIdAttribute()
{
return $this->attributes['app_code'] ?? null;
}
public function setAppIdAttribute($value): void
{
$this->attributes['app_code'] = (string)$value;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace app\models;
use app\common\base\BaseModel;
/**
* 商户后台用户模型
*/
class MerchantUser extends BaseModel
{
protected $table = 'ma_mer_user';
protected $fillable = [
'mer_id',
'username',
'password',
'nick_name',
'avatar',
'mobile',
'email',
'role_code',
'is_owner',
'status',
'login_ip',
'login_at',
];
public $timestamps = true;
protected $appends = ['merchant_id'];
protected $casts = [
'mer_id' => 'integer',
'is_owner' => 'integer',
'status' => 'integer',
'login_at' => 'datetime',
];
protected $hidden = ['password'];
public function getMerchantIdAttribute()
{
return $this->attributes['mer_id'] ?? null;
}
public function setMerchantIdAttribute($value): void
{
$this->attributes['mer_id'] = (int)$value;
}
}

View File

@@ -14,13 +14,13 @@ class PaymentChannel extends BaseModel
protected $table = 'ma_pay_channel';
protected $fillable = [
'merchant_id',
'merchant_app_id',
'mer_id',
'app_id',
'chan_code',
'chan_name',
'plugin_code',
'method_id',
'config_json',
'pay_type_id',
'config',
'split_ratio',
'chan_cost',
'chan_mode',
@@ -34,13 +34,16 @@ class PaymentChannel extends BaseModel
public $timestamps = true;
protected $appends = ['merchant_id', 'merchant_app_id', 'method_id', 'config_json'];
protected $casts = [
'merchant_id' => 'integer',
'merchant_app_id' => 'integer',
'method_id' => 'integer',
'config_json' => 'array',
'mer_id' => 'integer',
'app_id' => 'integer',
'pay_type_id' => 'integer',
'config' => 'array',
'split_ratio' => 'decimal:2',
'chan_cost' => 'decimal:2',
'chan_mode' => 'integer',
'daily_limit' => 'decimal:2',
'daily_cnt' => 'integer',
'min_amount' => 'decimal:2',
@@ -51,7 +54,7 @@ class PaymentChannel extends BaseModel
public function getConfigArray(): array
{
return $this->config_json ?? [];
return $this->config ?? [];
}
public function getEnabledProducts(): array
@@ -59,4 +62,44 @@ class PaymentChannel extends BaseModel
$config = $this->getConfigArray();
return $config['enabled_products'] ?? [];
}
public function getMerchantIdAttribute()
{
return $this->attributes['mer_id'] ?? null;
}
public function setMerchantIdAttribute($value): void
{
$this->attributes['mer_id'] = (int)$value;
}
public function getMerchantAppIdAttribute()
{
return $this->attributes['app_id'] ?? null;
}
public function setMerchantAppIdAttribute($value): void
{
$this->attributes['app_id'] = (int)$value;
}
public function getMethodIdAttribute()
{
return $this->attributes['pay_type_id'] ?? null;
}
public function setMethodIdAttribute($value): void
{
$this->attributes['pay_type_id'] = (int)$value;
}
public function getConfigJsonAttribute()
{
return $this->attributes['config'] ?? [];
}
public function setConfigJsonAttribute($value): void
{
$this->attributes['config'] = $value;
}
}

View File

@@ -11,11 +11,11 @@ use app\common\base\BaseModel;
*/
class PaymentMethod extends BaseModel
{
protected $table = 'ma_pay_method';
protected $table = 'ma_pay_type';
protected $fillable = [
'method_code',
'method_name',
'type',
'name',
'icon',
'sort',
'status',
@@ -23,8 +23,30 @@ class PaymentMethod extends BaseModel
public $timestamps = true;
protected $appends = ['method_code', 'method_name'];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
];
public function getMethodCodeAttribute()
{
return $this->attributes['type'] ?? null;
}
public function setMethodCodeAttribute($value): void
{
$this->attributes['type'] = (string)$value;
}
public function getMethodNameAttribute()
{
return $this->attributes['name'] ?? null;
}
public function setMethodNameAttribute($value): void
{
$this->attributes['name'] = (string)$value;
}
}

View File

@@ -21,7 +21,7 @@ class MerchantAppRepository extends BaseRepository
public function findByAppId(string $appId): ?MerchantApp
{
return $this->model->newQuery()
->where('app_id', $appId)
->where('app_code', $appId)
->where('status', 1)
->first();
}
@@ -32,19 +32,30 @@ class MerchantAppRepository extends BaseRepository
public function findByMerchantAndApp(int $merchantId, int $appId): ?MerchantApp
{
return $this->model->newQuery()
->where('merchant_id', $merchantId)
->where('mer_id', $merchantId)
->where('id', $appId)
->where('status', 1)
->first();
}
/**
* 根据商户ID和应用IDapp_id查询
*/
public function findByMerchantAndAppId(int $merchantId, string $appId): ?MerchantApp
{
return $this->model->newQuery()
->where('mer_id', $merchantId)
->where('app_code', $appId)
->first();
}
/**
* 后台按 app_id 查询(不过滤状态)
*/
public function findAnyByAppId(string $appId): ?MerchantApp
{
return $this->model->newQuery()
->where('app_id', $appId)
->where('app_code', $appId)
->first();
}
@@ -56,13 +67,13 @@ class MerchantAppRepository extends BaseRepository
$query = $this->model->newQuery();
if (!empty($filters['merchant_id'])) {
$query->where('merchant_id', (int)$filters['merchant_id']);
$query->where('mer_id', (int)$filters['merchant_id']);
}
if (($filters['status'] ?? '') !== '' && $filters['status'] !== null) {
$query->where('status', (int)$filters['status']);
}
if (!empty($filters['app_id'])) {
$query->where('app_id', 'like', '%' . $filters['app_id'] . '%');
$query->where('app_code', 'like', '%' . $filters['app_id'] . '%');
}
if (!empty($filters['app_name'])) {
$query->where('app_name', 'like', '%' . $filters['app_name'] . '%');
@@ -70,6 +81,15 @@ class MerchantAppRepository extends BaseRepository
if (!empty($filters['api_type'])) {
$query->where('api_type', (string)$filters['api_type']);
}
if (!empty($filters['package_code'])) {
$query->where('package_code', (string)$filters['package_code']);
}
if (($filters['notify_enabled'] ?? '') !== '' && $filters['notify_enabled'] !== null) {
$query->where('notify_enabled', (int)$filters['notify_enabled']);
}
if (!empty($filters['callback_mode'])) {
$query->where('callback_mode', (string)$filters['callback_mode']);
}
$query->orderByDesc('id');

View File

@@ -41,6 +41,12 @@ class MerchantRepository extends BaseRepository
if (!empty($filters['merchant_name'])) {
$query->where('merchant_name', 'like', '%' . $filters['merchant_name'] . '%');
}
if (!empty($filters['email'])) {
$query->where('email', 'like', '%' . $filters['email'] . '%');
}
if (isset($filters['balance']) && $filters['balance'] !== '') {
$query->where('balance', (string)$filters['balance']);
}
$query->orderByDesc('id');

View File

@@ -15,9 +15,9 @@ class PaymentChannelRepository extends BaseRepository
public function findAvailableChannel(int $merchantId, int $merchantAppId, int $methodId): ?PaymentChannel
{
return $this->model->newQuery()
->where('merchant_id', $merchantId)
->where('merchant_app_id', $merchantAppId)
->where('method_id', $methodId)
->where('mer_id', $merchantId)
->where('app_id', $merchantAppId)
->where('pay_type_id', $methodId)
->where('status', 1)
->orderBy('sort', 'asc')
->first();
@@ -51,13 +51,13 @@ class PaymentChannelRepository extends BaseRepository
$query = $this->model->newQuery();
if (!empty($filters['merchant_id'])) {
$query->where('merchant_id', (int)$filters['merchant_id']);
$query->where('mer_id', (int)$filters['merchant_id']);
}
if (!empty($filters['merchant_app_id'])) {
$query->where('merchant_app_id', (int)$filters['merchant_app_id']);
$query->where('app_id', (int)$filters['merchant_app_id']);
}
if (!empty($filters['method_id'])) {
$query->where('method_id', (int)$filters['method_id']);
$query->where('pay_type_id', (int)$filters['method_id']);
}
if (($filters['status'] ?? '') !== '' && $filters['status'] !== null) {
$query->where('status', (int)$filters['status']);

View File

@@ -27,7 +27,7 @@ class PaymentMethodRepository extends BaseRepository
public function findByCode(string $methodCode): ?PaymentMethod
{
return $this->model->newQuery()
->where('method_code', $methodCode)
->where('type', $methodCode)
->where('status', 1)
->first();
}
@@ -38,7 +38,7 @@ class PaymentMethodRepository extends BaseRepository
public function findAnyByCode(string $methodCode): ?PaymentMethod
{
return $this->model->newQuery()
->where('method_code', $methodCode)
->where('type', $methodCode)
->first();
}
@@ -53,10 +53,10 @@ class PaymentMethodRepository extends BaseRepository
$query->where('status', (int)$filters['status']);
}
if (!empty($filters['method_code'])) {
$query->where('method_code', 'like', '%' . $filters['method_code'] . '%');
$query->where('type', 'like', '%' . $filters['method_code'] . '%');
}
if (!empty($filters['method_name'])) {
$query->where('method_name', 'like', '%' . $filters['method_name'] . '%');
$query->where('name', 'like', '%' . $filters['method_name'] . '%');
}
$query->orderBy('sort', 'asc')->orderByDesc('id');

View File

@@ -0,0 +1,169 @@
-- Current schema alignment for the updated merchant / merchant-app model.
-- Target DB: MySQL 5.7+
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- =========================================================
-- 1. Expand ma_mer for admin + merchant backend scenarios
-- =========================================================
ALTER TABLE `ma_mer`
ADD COLUMN `merchant_short_name` varchar(60) NOT NULL DEFAULT '' COMMENT '商户简称' AFTER `merchant_name`,
ADD COLUMN `merchant_type` varchar(20) NOT NULL DEFAULT 'company' COMMENT '商户类型company/individual/other' AFTER `merchant_short_name`,
ADD COLUMN `group_code` varchar(32) NOT NULL DEFAULT '' COMMENT '商户分组编码' AFTER `merchant_type`,
ADD COLUMN `legal_name` varchar(100) NOT NULL DEFAULT '' COMMENT '法人姓名' AFTER `funds_mode`,
ADD COLUMN `contact_name` varchar(50) NOT NULL DEFAULT '' COMMENT '联系人姓名' AFTER `legal_name`,
ADD COLUMN `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系人手机号' AFTER `contact_name`,
ADD COLUMN `contact_email` varchar(100) NOT NULL DEFAULT '' COMMENT '联系人邮箱' AFTER `contact_phone`,
ADD COLUMN `website` varchar(255) NOT NULL DEFAULT '' COMMENT '商户官网' AFTER `contact_email`,
ADD COLUMN `province` varchar(50) NOT NULL DEFAULT '' COMMENT '省份' AFTER `website`,
ADD COLUMN `city` varchar(50) NOT NULL DEFAULT '' COMMENT '城市' AFTER `province`,
ADD COLUMN `address` varchar(255) NOT NULL DEFAULT '' COMMENT '详细地址' AFTER `city`,
ADD COLUMN `callback_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '回调域名' AFTER `address`,
ADD COLUMN `callback_ip_whitelist` text COMMENT '回调IP白名单' AFTER `callback_domain`,
ADD COLUMN `risk_level` varchar(20) NOT NULL DEFAULT 'standard' COMMENT '风控等级low/standard/high' AFTER `callback_ip_whitelist`,
ADD COLUMN `settlement_mode` varchar(20) NOT NULL DEFAULT 'auto' COMMENT '结算方式auto/manual' AFTER `risk_level`,
ADD COLUMN `settlement_cycle` varchar(20) NOT NULL DEFAULT 't1' COMMENT '结算周期d0/t1/manual' AFTER `settlement_mode`,
ADD COLUMN `settlement_account_name` varchar(100) NOT NULL DEFAULT '' COMMENT '结算账户名' AFTER `settlement_cycle`,
ADD COLUMN `settlement_account_no` varchar(100) NOT NULL DEFAULT '' COMMENT '结算账户号' AFTER `settlement_account_name`,
ADD COLUMN `settlement_bank_name` varchar(100) NOT NULL DEFAULT '' COMMENT '结算银行名称' AFTER `settlement_account_no`,
ADD COLUMN `settlement_bank_branch` varchar(100) NOT NULL DEFAULT '' COMMENT '结算支行名称' AFTER `settlement_bank_name`,
ADD COLUMN `single_limit` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '单笔限额(元)' AFTER `settlement_bank_branch`,
ADD COLUMN `daily_limit` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '日限额(元)' AFTER `single_limit`,
ADD COLUMN `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注' AFTER `daily_limit`,
ADD COLUMN `extra` json DEFAULT NULL COMMENT '扩展字段(JSON)' AFTER `remark`;
ALTER TABLE `ma_mer`
ADD KEY `idx_group_code` (`group_code`),
ADD KEY `idx_contact_phone` (`contact_phone`),
ADD KEY `idx_status` (`status`);
-- =========================================================
-- 2. Expand ma_pay_app for merchant app backend settings
-- =========================================================
ALTER TABLE `ma_pay_app`
ADD COLUMN `package_code` varchar(32) NOT NULL DEFAULT '' COMMENT '商户套餐编码' AFTER `app_name`,
ADD COLUMN `notify_url` varchar(255) NOT NULL DEFAULT '' COMMENT '异步通知地址' AFTER `package_code`,
ADD COLUMN `return_url` varchar(255) NOT NULL DEFAULT '' COMMENT '同步跳转地址' AFTER `notify_url`,
ADD COLUMN `callback_mode` varchar(20) NOT NULL DEFAULT 'server' COMMENT '回调模式server/server+page/manual' AFTER `return_url`,
ADD COLUMN `sign_type` varchar(20) NOT NULL DEFAULT 'md5' COMMENT '签名方式' AFTER `callback_mode`,
ADD COLUMN `order_expire_minutes` int(11) NOT NULL DEFAULT 30 COMMENT '订单超时时间(分钟)' AFTER `sign_type`,
ADD COLUMN `callback_retry_limit` int(11) NOT NULL DEFAULT 6 COMMENT '回调重试次数' AFTER `order_expire_minutes`,
ADD COLUMN `ip_whitelist` text COMMENT 'IP白名单' AFTER `callback_retry_limit`,
ADD COLUMN `amount_min` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '单笔最小金额' AFTER `ip_whitelist`,
ADD COLUMN `amount_max` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '单笔最大金额' AFTER `amount_min`,
ADD COLUMN `daily_limit` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '日限额' AFTER `amount_max`,
ADD COLUMN `notify_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否开启通知0-否,1-是' AFTER `daily_limit`,
ADD COLUMN `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注' AFTER `notify_enabled`,
ADD COLUMN `extra` json DEFAULT NULL COMMENT '扩展字段(JSON)' AFTER `remark`;
ALTER TABLE `ma_pay_app`
ADD KEY `idx_api_type` (`api_type`),
ADD KEY `idx_status` (`status`);
-- =========================================================
-- 3. Merchant backend user table
-- =========================================================
CREATE TABLE IF NOT EXISTS `ma_mer_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`mer_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '商户ID',
`username` varchar(50) NOT NULL DEFAULT '' COMMENT '登录账号',
`password` varchar(255) DEFAULT NULL COMMENT '登录密码hash',
`nick_name` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
`avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像地址',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱',
`role_code` varchar(32) NOT NULL DEFAULT 'owner' COMMENT '角色编码',
`is_owner` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否商户主账号0-否,1-是',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态0-禁用,1-启用',
`login_ip` varchar(45) NOT NULL DEFAULT '' COMMENT '最后登录IP',
`login_at` datetime DEFAULT NULL COMMENT '最后登录时间',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_mer_id` (`mer_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户后台用户表';
-- =========================================================
-- 4. Compatibility views for legacy code paths
-- =========================================================
DROP VIEW IF EXISTS `ma_merchant`;
CREATE VIEW `ma_merchant` AS
SELECT
`id`,
`merchant_no`,
`merchant_name`,
`merchant_short_name`,
`merchant_type`,
`group_code`,
`funds_mode`,
`legal_name`,
`contact_name`,
`contact_phone`,
`contact_email`,
`website`,
`province`,
`city`,
`address`,
`callback_domain`,
`callback_ip_whitelist`,
`risk_level`,
`settlement_mode`,
`settlement_cycle`,
`settlement_account_name`,
`settlement_account_no`,
`settlement_bank_name`,
`settlement_bank_branch`,
`single_limit`,
`daily_limit`,
`status`,
`remark`,
`extra`,
`created_at`,
`updated_at`
FROM `ma_mer`;
DROP VIEW IF EXISTS `ma_merchant_app`;
CREATE VIEW `ma_merchant_app` AS
SELECT
`id`,
`mer_id` AS `merchant_id`,
`api_type`,
`app_code` AS `app_id`,
`app_secret`,
`app_name`,
`package_code`,
`notify_url`,
`return_url`,
`callback_mode`,
`sign_type`,
`order_expire_minutes`,
`callback_retry_limit`,
`ip_whitelist`,
`amount_min`,
`amount_max`,
`daily_limit`,
`notify_enabled`,
`status`,
`remark`,
`extra`,
`created_at`,
`updated_at`
FROM `ma_pay_app`;
DROP VIEW IF EXISTS `ma_pay_method`;
CREATE VIEW `ma_pay_method` AS
SELECT
`id`,
`type` AS `method_code`,
`name` AS `method_name`,
`icon`,
`sort`,
`status`,
`created_at`,
`updated_at`
FROM `ma_pay_type`;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -78,13 +78,13 @@ ON DUPLICATE KEY UPDATE
-- 7) 支付通道(为测试商户 M001 / 应用 1001 初始化拉卡拉通道)
INSERT INTO `ma_pay_channel` (
`merchant_id`,
`merchant_app_id`,
`mer_id`,
`app_id`,
`chan_code`,
`chan_name`,
`plugin_code`,
`method_id`,
`config_json`,
`pay_type_id`,
`config`,
`split_ratio`,
`chan_cost`,
`chan_mode`,
@@ -98,16 +98,16 @@ INSERT INTO `ma_pay_channel` (
`updated_at`
)
SELECT
m.id AS merchant_id,
app.id AS merchant_app_id,
m.id AS mer_id,
app.id AS app_id,
'lakala_alipay' AS chan_code,
'拉卡拉-支付宝' AS chan_name,
'lakala' AS plugin_code,
pm.id AS method_id,
JSON_OBJECT('notify_url', 'https://example.com/notify') AS config_json,
pm.id AS pay_type_id,
JSON_OBJECT('notify_url', 'https://example.com/notify') AS config,
100.00 AS split_ratio,
0.00 AS chan_cost,
'wallet' AS chan_mode,
0 AS chan_mode,
0.00 AS daily_limit,
0 AS daily_cnt,
0.01 AS min_amount,
@@ -122,8 +122,8 @@ JOIN `ma_pay_method` pm ON pm.method_code = 'alipay'
ON DUPLICATE KEY UPDATE
`chan_name` = VALUES(`chan_name`),
`plugin_code` = VALUES(`plugin_code`),
`method_id` = VALUES(`method_id`),
`config_json` = VALUES(`config_json`),
`pay_type_id` = VALUES(`pay_type_id`),
`config` = VALUES(`config`),
`split_ratio` = VALUES(`split_ratio`),
`chan_cost` = VALUES(`chan_cost`),
`chan_mode` = VALUES(`chan_mode`),
@@ -136,13 +136,13 @@ ON DUPLICATE KEY UPDATE
`updated_at` = NOW();
INSERT INTO `ma_pay_channel` (
`merchant_id`,
`merchant_app_id`,
`mer_id`,
`app_id`,
`chan_code`,
`chan_name`,
`plugin_code`,
`method_id`,
`config_json`,
`pay_type_id`,
`config`,
`split_ratio`,
`chan_cost`,
`chan_mode`,
@@ -156,16 +156,16 @@ INSERT INTO `ma_pay_channel` (
`updated_at`
)
SELECT
m.id AS merchant_id,
app.id AS merchant_app_id,
m.id AS mer_id,
app.id AS app_id,
'lakala_wechat' AS chan_code,
'拉卡拉-微信支付' AS chan_name,
'lakala' AS plugin_code,
pm.id AS method_id,
JSON_OBJECT('notify_url', 'https://example.com/notify') AS config_json,
pm.id AS pay_type_id,
JSON_OBJECT('notify_url', 'https://example.com/notify') AS config,
100.00 AS split_ratio,
0.00 AS chan_cost,
'wallet' AS chan_mode,
0 AS chan_mode,
0.00 AS daily_limit,
0 AS daily_cnt,
0.01 AS min_amount,
@@ -180,8 +180,8 @@ JOIN `ma_pay_method` pm ON pm.method_code = 'wechat'
ON DUPLICATE KEY UPDATE
`chan_name` = VALUES(`chan_name`),
`plugin_code` = VALUES(`plugin_code`),
`method_id` = VALUES(`method_id`),
`config_json` = VALUES(`config_json`),
`pay_type_id` = VALUES(`pay_type_id`),
`config` = VALUES(`config`),
`split_ratio` = VALUES(`split_ratio`),
`chan_cost` = VALUES(`chan_cost`),
`chan_mode` = VALUES(`chan_mode`),
@@ -191,4 +191,4 @@ ON DUPLICATE KEY UPDATE
`max_amount` = VALUES(`max_amount`),
`status` = VALUES(`status`),
`sort` = VALUES(`sort`),
`updated_at` = NOW();
`updated_at` = NOW();