first commit
This commit is contained in:
134
app/admin/controller/AdminUserController.php
Normal file
134
app/admin/controller/AdminUserController.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\AdminUser;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class AdminUserController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
|
||||
$query = AdminUser::with('roles');
|
||||
|
||||
if ($username = $request->get('username')) {
|
||||
$query->where('username', 'like', "%{$username}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$username = trim($request->post('username', ''));
|
||||
$password = $request->post('password', '');
|
||||
$roleIds = $request->post('role_ids', []);
|
||||
|
||||
if (!$username || !$password) {
|
||||
return jsonResponse(null, '用户名和密码必填', 400);
|
||||
}
|
||||
|
||||
if (AdminUser::where('username', $username)->exists()) {
|
||||
return jsonResponse(null, '用户名已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin = AdminUser::create([
|
||||
'username' => $username,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'status' => (int)$request->post('status', 1),
|
||||
'is_super' => (int)$request->post('is_super', 0),
|
||||
]);
|
||||
|
||||
if (!empty($roleIds)) {
|
||||
$admin->roles()->sync($roleIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '创建成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$admin = AdminUser::find($id);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
$username = trim($request->post('username', ''));
|
||||
if ($username && $username !== $admin->username) {
|
||||
if (AdminUser::where('username', $username)->exists()) {
|
||||
return jsonResponse(null, '用户名已存在', 400);
|
||||
}
|
||||
$admin->username = $username;
|
||||
}
|
||||
|
||||
$password = $request->post('password');
|
||||
if ($password) {
|
||||
$admin->password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
if ($request->post('status') !== null) {
|
||||
$admin->status = (int)$request->post('status');
|
||||
}
|
||||
|
||||
if ($request->post('is_super') !== null) {
|
||||
$admin->is_super = (int)$request->post('is_super');
|
||||
}
|
||||
|
||||
$roleIds = $request->post('role_ids');
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin->save();
|
||||
if (is_array($roleIds)) {
|
||||
$admin->roles()->sync($roleIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
if ($id === 1) {
|
||||
return jsonResponse(null, '超级管理员不可删除', 403);
|
||||
}
|
||||
$admin = AdminUser::find($id);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin->roles()->detach();
|
||||
$admin->delete();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '删除成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '删除失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/admin/controller/AuthController.php
Normal file
64
app/admin/controller/AuthController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\AdminUser;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function login(Request $request)
|
||||
{
|
||||
$username = trim((string)$request->post('username', ''));
|
||||
$password = (string)$request->post('password', '');
|
||||
if ($username === '' || $password === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
$admin = AdminUser::where('username', $username)->first();
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '账号或密码错误', 401);
|
||||
}
|
||||
if (intval($admin->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
if (!password_verify($password, $admin->password_hash)) {
|
||||
return jsonResponse(null, '账号或密码错误', 401);
|
||||
}
|
||||
|
||||
$token = AuthService::issueAdminToken($admin);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'admin' => $admin
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
$admin = $request->admin;
|
||||
$permissions = [];
|
||||
if (intval($admin->is_super) === 1) {
|
||||
$permissions = ['*'];
|
||||
} else {
|
||||
$admin->loadMissing(['roles.permissions']);
|
||||
$map = [];
|
||||
foreach ($admin->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$map[$permission->code] = true;
|
||||
}
|
||||
}
|
||||
$permissions = array_keys($map);
|
||||
}
|
||||
return jsonResponse([
|
||||
'admin' => $admin,
|
||||
'permissions' => $permissions
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
AuthService::revokeAdminToken($request->token ?? null);
|
||||
return jsonResponse(null, '已退出登录');
|
||||
}
|
||||
}
|
||||
|
||||
26
app/admin/controller/DashboardController.php
Normal file
26
app/admin/controller/DashboardController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
public function stat(Request $request)
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$stat = [
|
||||
'today_orders' => Order::where('created_at', '>=', $today . ' 00:00:00')->count(),
|
||||
'wait_receive_orders' => Order::whereIn('status', ['shipping', 'wait_receive'])->count(),
|
||||
'inspecting_orders' => Order::where('status', 'inspecting')->count(),
|
||||
'today_reports' => Report::where('created_at', '>=', $today . ' 00:00:00')->count(),
|
||||
'total_users' => User::count(),
|
||||
'total_amount' => Order::where('status', 'finished')->sum('total_price') ?? 0
|
||||
];
|
||||
|
||||
return jsonResponse($stat);
|
||||
}
|
||||
}
|
||||
85
app/admin/controller/OrderController.php
Normal file
85
app/admin/controller/OrderController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
|
||||
class OrderController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$status = $request->get('status', 'all');
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = Order::query();
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($orderNo = $request->get('order_no')) {
|
||||
$query->where('order_no', 'like', "%{$orderNo}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = (int)$request->get('id');
|
||||
$order = Order::with(['logs'])->find($id);
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse($order);
|
||||
}
|
||||
|
||||
public function receive(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$order = Order::find($id);
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
\app\common\service\OrderFlowService::adminReceive($order, $request->admin->id);
|
||||
return jsonResponse(null, '确认收件成功,已进入鉴定状态');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function returnShip(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$expressCompany = trim((string)$request->post('express_company', ''));
|
||||
$expressNo = trim((string)$request->post('express_no', ''));
|
||||
|
||||
$order = Order::find($id);
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
\app\common\service\OrderFlowService::adminReturnShip($order, $request->admin->id, $expressCompany, $expressNo);
|
||||
return jsonResponse(null, '回寄信息已提交');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
app/admin/controller/PermissionController.php
Normal file
94
app/admin/controller/PermissionController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Permission;
|
||||
|
||||
class PermissionController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
// 返回树形结构或者普通列表
|
||||
$permissions = Permission::orderBy('sort', 'asc')->get();
|
||||
return jsonResponse($permissions);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
$parent_id = (int)$request->post('parent_id', 0);
|
||||
$type = (int)$request->post('type', 1); // 1菜单 2按钮
|
||||
$sort = (int)$request->post('sort', 0);
|
||||
|
||||
if (!$name || !$code) {
|
||||
return jsonResponse(null, '名称和代码必填', 400);
|
||||
}
|
||||
|
||||
if (Permission::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '代码已存在', 400);
|
||||
}
|
||||
|
||||
$permission = Permission::create([
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'parent_id' => $parent_id,
|
||||
'type' => $type,
|
||||
'sort' => $sort,
|
||||
]);
|
||||
|
||||
return jsonResponse($permission, '创建成功');
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return jsonResponse(null, '权限不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
|
||||
if ($name) $permission->name = $name;
|
||||
if ($code && $code !== $permission->code) {
|
||||
if (Permission::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '代码已存在', 400);
|
||||
}
|
||||
$permission->code = $code;
|
||||
}
|
||||
|
||||
if ($request->post('parent_id') !== null) {
|
||||
$permission->parent_id = (int)$request->post('parent_id');
|
||||
}
|
||||
if ($request->post('type') !== null) {
|
||||
$permission->type = (int)$request->post('type');
|
||||
}
|
||||
if ($request->post('sort') !== null) {
|
||||
$permission->sort = (int)$request->post('sort');
|
||||
}
|
||||
|
||||
$permission->save();
|
||||
return jsonResponse(null, '更新成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return jsonResponse(null, '权限不存在', 404);
|
||||
}
|
||||
|
||||
if (Permission::where('parent_id', $id)->exists()) {
|
||||
return jsonResponse(null, '存在子权限,不可删除', 400);
|
||||
}
|
||||
|
||||
$permission->delete();
|
||||
// Remove from role_permissions
|
||||
\Illuminate\Database\Capsule\Manager::table('role_permissions')->where('permission_id', $id)->delete();
|
||||
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
}
|
||||
114
app/admin/controller/ReportController.php
Normal file
114
app/admin/controller/ReportController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
use app\common\service\OrderFlowService;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class ReportController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = Report::with(['order', 'inspector']);
|
||||
|
||||
if ($reportNo = $request->get('report_no')) {
|
||||
$query->where('report_no', 'like', "%{$reportNo}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = (int)$request->get('id');
|
||||
$report = Report::with(['order.logs', 'inspector'])->find($id);
|
||||
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '报告不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse($report);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$conclusion = trim($request->post('conclusion', '')); // REAL, FAKE, DOUBT
|
||||
$level = trim($request->post('level', ''));
|
||||
$flawsJson = $request->post('flaws_json', []);
|
||||
$imagesJson = $request->post('images_json', []);
|
||||
|
||||
if (!in_array($conclusion, ['REAL', 'FAKE', 'DOUBT'])) {
|
||||
return jsonResponse(null, '鉴定结论不合法', 400);
|
||||
}
|
||||
|
||||
if (empty($imagesJson)) {
|
||||
return jsonResponse(null, '必须上传证据图片', 400);
|
||||
}
|
||||
|
||||
$order = Order::find($orderId);
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
if ($order->status !== 'inspecting') {
|
||||
return jsonResponse(null, '订单当前状态不可出具报告', 400);
|
||||
}
|
||||
|
||||
if (Report::where('order_id', $orderId)->exists()) {
|
||||
return jsonResponse(null, '该订单已出具报告,不可重复出具', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$reportNo = 'R' . date('YmdHis') . rand(1000, 9999);
|
||||
$verifyCode = bin2hex(random_bytes(8)); // 16字符防伪码
|
||||
|
||||
$report = Report::create([
|
||||
'report_no' => $reportNo,
|
||||
'order_id' => $orderId,
|
||||
'conclusion' => $conclusion,
|
||||
'level' => $level,
|
||||
'flaws_json' => $flawsJson,
|
||||
'images_json' => $imagesJson,
|
||||
'inspector_id' => $request->admin->id,
|
||||
'verify_code' => $verifyCode
|
||||
]);
|
||||
|
||||
// 扭转订单状态
|
||||
$order->status = 'finished';
|
||||
$order->save();
|
||||
|
||||
$conclusionMap = [
|
||||
'REAL' => '正品',
|
||||
'FAKE' => '仿品',
|
||||
'DOUBT' => '存疑'
|
||||
];
|
||||
$conclusionText = $conclusionMap[$conclusion] ?? '未知';
|
||||
|
||||
OrderFlowService::addLog($orderId, 'report_generated', '报告已出具', "鉴定结论:{$conclusionText}", 'admin', $request->admin->id);
|
||||
|
||||
DB::commit();
|
||||
return jsonResponse($report, '报告出具成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '出具报告失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
app/admin/controller/RoleController.php
Normal file
140
app/admin/controller/RoleController.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Role;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class RoleController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
|
||||
$query = Role::with('permissions');
|
||||
|
||||
if ($name = $request->get('name')) {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
public function all(Request $request)
|
||||
{
|
||||
$roles = Role::all();
|
||||
return jsonResponse($roles);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
$description = trim($request->post('description', ''));
|
||||
$permissionIds = $request->post('permission_ids', []);
|
||||
|
||||
if (!$name || !$code) {
|
||||
return jsonResponse(null, '角色名称和编码必填', 400);
|
||||
}
|
||||
|
||||
if (Role::where('name', $name)->exists()) {
|
||||
return jsonResponse(null, '角色名称已存在', 400);
|
||||
}
|
||||
if (Role::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '角色编码已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role = Role::create([
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
if (!empty($permissionIds)) {
|
||||
$role->permissions()->sync($permissionIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '创建成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$role = Role::find($id);
|
||||
if (!$role) {
|
||||
return jsonResponse(null, '角色不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim($request->post('name', ''));
|
||||
if ($name && $name !== $role->name) {
|
||||
if (Role::where('name', $name)->exists()) {
|
||||
return jsonResponse(null, '角色名称已存在', 400);
|
||||
}
|
||||
$role->name = $name;
|
||||
}
|
||||
|
||||
$code = trim($request->post('code', ''));
|
||||
if ($code && $code !== $role->code) {
|
||||
if (Role::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '角色编码已存在', 400);
|
||||
}
|
||||
$role->code = $code;
|
||||
}
|
||||
|
||||
if ($request->post('description') !== null) {
|
||||
$role->description = trim($request->post('description'));
|
||||
}
|
||||
|
||||
$permissionIds = $request->post('permission_ids');
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role->save();
|
||||
if (is_array($permissionIds)) {
|
||||
$role->permissions()->sync($permissionIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$role = Role::find($id);
|
||||
if (!$role) {
|
||||
return jsonResponse(null, '角色不存在', 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role->permissions()->detach();
|
||||
$role->delete();
|
||||
DB::table('admin_roles')->where('role_id', $id)->delete();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '删除成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '删除失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/admin/controller/UploadController.php
Normal file
37
app/admin/controller/UploadController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class UploadController
|
||||
{
|
||||
public function image(Request $request)
|
||||
{
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
return jsonResponse(null, '仅支持图片文件', 400);
|
||||
}
|
||||
|
||||
$dir = public_path() . '/upload/images/' . date('Ymd');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$filename = uniqid() . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
|
||||
$url = '/upload/images/' . date('Ymd') . '/' . $filename;
|
||||
|
||||
return jsonResponse([
|
||||
'url' => $url,
|
||||
'name' => $file->getUploadName(),
|
||||
], '上传成功');
|
||||
}
|
||||
}
|
||||
53
app/admin/controller/UserController.php
Normal file
53
app/admin/controller/UserController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = User::query();
|
||||
|
||||
if ($mobile = $request->get('mobile')) {
|
||||
$query->where('mobile', 'like', "%{$mobile}%");
|
||||
}
|
||||
|
||||
if ($nickname = $request->get('nickname')) {
|
||||
$query->where('nickname', 'like', "%{$nickname}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$status = (int)$request->post('status');
|
||||
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
$user->status = $status === 1 ? 1 : 0;
|
||||
$user->save();
|
||||
|
||||
return jsonResponse(null, '更新状态成功');
|
||||
}
|
||||
}
|
||||
136
app/admin/controller/WechatAppController.php
Normal file
136
app/admin/controller/WechatAppController.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\WechatApp;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class WechatAppController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
if ($page < 1) $page = 1;
|
||||
if ($limit < 1) $limit = 15;
|
||||
|
||||
$query = WechatApp::query();
|
||||
if (($name = trim((string)$request->get('name', ''))) !== '') {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
if (($appId = trim((string)$request->get('app_id', ''))) !== '') {
|
||||
$query->where('app_id', 'like', "%{$appId}%");
|
||||
}
|
||||
if (($type = trim((string)$request->get('type', ''))) !== '') {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
if ($request->get('status') !== null && $request->get('status') !== '') {
|
||||
$query->where('status', (int)$request->get('status'));
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->select([
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'app_id',
|
||||
'status',
|
||||
'remark',
|
||||
'created_at',
|
||||
])
|
||||
->selectRaw("IF(app_secret IS NULL OR app_secret = '', 0, 1) as has_secret")
|
||||
->orderByDesc('id')
|
||||
->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim((string)$request->post('name', ''));
|
||||
$type = trim((string)$request->post('type', 'h5'));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$appSecret = trim((string)$request->post('app_secret', ''));
|
||||
$status = (int)$request->post('status', 1);
|
||||
$remark = trim((string)$request->post('remark', ''));
|
||||
|
||||
if ($name === '' || $appId === '') {
|
||||
return jsonResponse(null, '名称和AppID必填', 400);
|
||||
}
|
||||
if (!in_array($type, ['h5', 'mini'], true)) {
|
||||
return jsonResponse(null, '类型不合法', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$row = WechatApp::create([
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $appSecret ?: null,
|
||||
'status' => $status ? 1 : 0,
|
||||
'remark' => $remark ?: null,
|
||||
]);
|
||||
return jsonResponse($row, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatApp::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '应用不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim((string)$request->post('name', $row->name));
|
||||
$type = trim((string)$request->post('type', $row->type));
|
||||
$appId = trim((string)$request->post('app_id', $row->app_id));
|
||||
$appSecret = trim((string)$request->post('app_secret', ''));
|
||||
$status = (int)$request->post('status', $row->status);
|
||||
$remark = trim((string)$request->post('remark', $row->remark ?? ''));
|
||||
|
||||
if ($name === '' || $appId === '') {
|
||||
return jsonResponse(null, '名称和AppID必填', 400);
|
||||
}
|
||||
if (!in_array($type, ['h5', 'mini'], true)) {
|
||||
return jsonResponse(null, '类型不合法', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$row->name = $name;
|
||||
$row->type = $type;
|
||||
$row->app_id = $appId;
|
||||
if ($appSecret !== '') {
|
||||
$row->app_secret = $appSecret;
|
||||
}
|
||||
$row->status = $status ? 1 : 0;
|
||||
$row->remark = $remark ?: null;
|
||||
$row->save();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatApp::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '应用不存在', 404);
|
||||
}
|
||||
$row->delete();
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
}
|
||||
|
||||
336
app/admin/controller/WechatMerchantController.php
Normal file
336
app/admin/controller/WechatMerchantController.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\WechatMerchant;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class WechatMerchantController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
if ($page < 1) $page = 1;
|
||||
if ($limit < 1) $limit = 15;
|
||||
|
||||
$query = WechatMerchant::query();
|
||||
|
||||
if (($name = trim((string)$request->get('name', ''))) !== '') {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
if (($mchId = trim((string)$request->get('mch_id', ''))) !== '') {
|
||||
$query->where('mch_id', 'like', "%{$mchId}%");
|
||||
}
|
||||
if ($request->get('status') !== null && $request->get('status') !== '') {
|
||||
$query->where('status', (int)$request->get('status'));
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->select([
|
||||
'id',
|
||||
'name',
|
||||
'mode',
|
||||
'mch_id',
|
||||
'app_id',
|
||||
'sub_mch_id',
|
||||
'sub_app_id',
|
||||
'service_provider',
|
||||
'serial_no',
|
||||
'notify_url',
|
||||
'is_default',
|
||||
'status',
|
||||
'remark',
|
||||
'apiclient_cert_path',
|
||||
'apiclient_key_path',
|
||||
'created_at',
|
||||
])
|
||||
->selectRaw("IF(api_v3_key IS NULL OR api_v3_key = '', 0, 1) as has_api_v3_key")
|
||||
->selectRaw("IF((private_key_pem IS NULL OR private_key_pem = '') AND (apiclient_key_path IS NULL OR apiclient_key_path = ''), 0, 1) as has_private_key")
|
||||
->selectRaw("IF(apiclient_cert_path IS NULL OR apiclient_cert_path = '', 0, 1) as has_apiclient_cert")
|
||||
->orderByDesc('is_default')
|
||||
->orderByDesc('id')
|
||||
->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim((string)$request->post('name', ''));
|
||||
$mode = trim((string)$request->post('mode', 'direct'));
|
||||
$mchId = trim((string)$request->post('mch_id', ''));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$subMchId = trim((string)$request->post('sub_mch_id', ''));
|
||||
$subAppId = trim((string)$request->post('sub_app_id', ''));
|
||||
$serviceProvider = trim((string)$request->post('service_provider', ''));
|
||||
$serialNo = trim((string)$request->post('serial_no', ''));
|
||||
$apiV3Key = trim((string)$request->post('api_v3_key', ''));
|
||||
$privateKeyPem = trim((string)$request->post('private_key_pem', ''));
|
||||
$notifyUrl = trim((string)$request->post('notify_url', ''));
|
||||
$remark = trim((string)$request->post('remark', ''));
|
||||
$status = (int)$request->post('status', 1);
|
||||
$isDefault = (int)$request->post('is_default', 0) ? 1 : 0;
|
||||
|
||||
if ($name === '' || $mchId === '') {
|
||||
return jsonResponse(null, '名称和商户号必填', 400);
|
||||
}
|
||||
if (!in_array($mode, ['direct', 'service_provider', 'third_party'], true)) {
|
||||
return jsonResponse(null, '商户类型不合法', 400);
|
||||
}
|
||||
if ($mode === 'service_provider' && $subMchId === '') {
|
||||
return jsonResponse(null, '服务商模式必须填写子商户号', 400);
|
||||
}
|
||||
|
||||
if ($this->existsConflict(0, $mode, $mchId, $subMchId, $appId, $subAppId)) {
|
||||
return jsonResponse(null, '该商户配置已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
if ($isDefault === 1) {
|
||||
WechatMerchant::where('is_default', 1)->update(['is_default' => 0]);
|
||||
}
|
||||
$row = WechatMerchant::create([
|
||||
'name' => $name,
|
||||
'mode' => $mode,
|
||||
'mch_id' => $mchId,
|
||||
'app_id' => $appId ?: null,
|
||||
'serial_no' => $serialNo ?: null,
|
||||
'api_v3_key' => $apiV3Key ?: null,
|
||||
'private_key_pem' => $privateKeyPem ?: null,
|
||||
'notify_url' => $notifyUrl ?: null,
|
||||
'sub_mch_id' => $subMchId ?: null,
|
||||
'sub_app_id' => $subAppId ?: null,
|
||||
'service_provider' => $serviceProvider ?: null,
|
||||
'remark' => $remark ?: null,
|
||||
'status' => $status ? 1 : 0,
|
||||
'is_default' => $isDefault,
|
||||
]);
|
||||
DB::commit();
|
||||
return jsonResponse($row, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim((string)$request->post('name', $row->name));
|
||||
$mode = trim((string)$request->post('mode', $row->mode));
|
||||
$mchId = trim((string)$request->post('mch_id', $row->mch_id));
|
||||
$appId = trim((string)$request->post('app_id', $row->app_id ?? ''));
|
||||
$subMchId = trim((string)$request->post('sub_mch_id', $row->sub_mch_id ?? ''));
|
||||
$subAppId = trim((string)$request->post('sub_app_id', $row->sub_app_id ?? ''));
|
||||
$serviceProvider = trim((string)$request->post('service_provider', $row->service_provider ?? ''));
|
||||
$serialNo = trim((string)$request->post('serial_no', $row->serial_no ?? ''));
|
||||
$apiV3Key = trim((string)$request->post('api_v3_key', ''));
|
||||
$privateKeyPem = trim((string)$request->post('private_key_pem', ''));
|
||||
$notifyUrl = trim((string)$request->post('notify_url', $row->notify_url ?? ''));
|
||||
$remark = trim((string)$request->post('remark', $row->remark ?? ''));
|
||||
$status = (int)$request->post('status', $row->status);
|
||||
$isDefault = $request->post('is_default') !== null ? ((int)$request->post('is_default') ? 1 : 0) : (int)$row->is_default;
|
||||
|
||||
if ($name === '' || $mchId === '') {
|
||||
return jsonResponse(null, '名称和商户号必填', 400);
|
||||
}
|
||||
if (!in_array($mode, ['direct', 'service_provider', 'third_party'], true)) {
|
||||
return jsonResponse(null, '商户类型不合法', 400);
|
||||
}
|
||||
if ($mode === 'service_provider' && $subMchId === '') {
|
||||
return jsonResponse(null, '服务商模式必须填写子商户号', 400);
|
||||
}
|
||||
|
||||
if ($this->existsConflict($id, $mode, $mchId, $subMchId, $appId, $subAppId)) {
|
||||
return jsonResponse(null, '该商户配置已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
if ($isDefault === 1) {
|
||||
WechatMerchant::where('is_default', 1)->where('id', '<>', $id)->update(['is_default' => 0]);
|
||||
}
|
||||
$row->name = $name;
|
||||
$row->mode = $mode;
|
||||
$row->mch_id = $mchId;
|
||||
$row->app_id = $appId ?: null;
|
||||
$row->serial_no = $serialNo ?: null;
|
||||
if ($apiV3Key !== '') {
|
||||
$row->api_v3_key = $apiV3Key;
|
||||
}
|
||||
if ($privateKeyPem !== '') {
|
||||
$row->private_key_pem = $privateKeyPem;
|
||||
}
|
||||
$row->notify_url = $notifyUrl ?: null;
|
||||
$row->sub_mch_id = $subMchId ?: null;
|
||||
$row->sub_app_id = $subAppId ?: null;
|
||||
$row->service_provider = $serviceProvider ?: null;
|
||||
$row->remark = $remark ?: null;
|
||||
$row->status = $status ? 1 : 0;
|
||||
$row->is_default = $isDefault;
|
||||
$row->save();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
if ((int)$row->is_default === 1) {
|
||||
return jsonResponse(null, '默认商户号不可删除', 400);
|
||||
}
|
||||
$row->delete();
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
|
||||
public function uploadApiclientCert(Request $request)
|
||||
{
|
||||
return $this->uploadPem($request, 'apiclient_cert');
|
||||
}
|
||||
|
||||
public function uploadApiclientKey(Request $request)
|
||||
{
|
||||
return $this->uploadPem($request, 'apiclient_key');
|
||||
}
|
||||
|
||||
public function uploadApiV3Key(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
/** @var UploadFile|null $file */
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$size = $file->getSize();
|
||||
if ($size === false || $size > 1024) {
|
||||
return jsonResponse(null, '文件过大', 400);
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$content = is_string($content) ? $content : '';
|
||||
$key = preg_replace('/\s+/', '', $content);
|
||||
$key = is_string($key) ? trim($key) : '';
|
||||
if ($key === '' || strlen($key) !== 32) {
|
||||
return jsonResponse(null, 'APIv3 Key 格式不正确(应为32位)', 400);
|
||||
}
|
||||
|
||||
$row->api_v3_key = $key;
|
||||
$row->save();
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $row->id,
|
||||
'has_api_v3_key' => 1,
|
||||
], '上传成功');
|
||||
}
|
||||
|
||||
private function uploadPem(Request $request, string $type)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
/** @var UploadFile|null $file */
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if ($ext !== 'pem') {
|
||||
return jsonResponse(null, '仅支持 pem 文件', 400);
|
||||
}
|
||||
|
||||
$dir = runtime_path() . '/wechatpay/merchants/' . $row->id;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0700, true);
|
||||
}
|
||||
$filename = $type === 'apiclient_cert' ? 'apiclient_cert.pem' : 'apiclient_key.pem';
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
@chmod($path, 0600);
|
||||
|
||||
if ($type === 'apiclient_cert') {
|
||||
$row->apiclient_cert_path = $path;
|
||||
try {
|
||||
$certPem = file_get_contents($path);
|
||||
$x509 = openssl_x509_read($certPem);
|
||||
if ($x509) {
|
||||
$info = openssl_x509_parse($x509);
|
||||
$serialHex = $info['serialNumberHex'] ?? '';
|
||||
if (is_string($serialHex) && $serialHex !== '') {
|
||||
$row->serial_no = $row->serial_no ?: strtoupper($serialHex);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$row->apiclient_key_path = $path;
|
||||
}
|
||||
$row->save();
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $row->id,
|
||||
'serial_no' => $row->serial_no,
|
||||
'has_apiclient_cert' => $row->apiclient_cert_path ? 1 : 0,
|
||||
'has_private_key' => ($row->private_key_pem || $row->apiclient_key_path) ? 1 : 0,
|
||||
], '上传成功');
|
||||
}
|
||||
|
||||
private function existsConflict(int $id, string $mode, string $mchId, string $subMchId, string $appId, string $subAppId): bool
|
||||
{
|
||||
$query = WechatMerchant::query()->where('mode', $mode)->where('mch_id', $mchId);
|
||||
if ($mode === 'service_provider') {
|
||||
$query->where('sub_mch_id', $subMchId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('sub_mch_id')->orWhere('sub_mch_id', '');
|
||||
});
|
||||
}
|
||||
if ($appId !== '') {
|
||||
$query->where('app_id', $appId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('app_id')->orWhere('app_id', '');
|
||||
});
|
||||
}
|
||||
if ($subAppId !== '') {
|
||||
$query->where('sub_app_id', $subAppId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('sub_app_id')->orWhere('sub_app_id', '');
|
||||
});
|
||||
}
|
||||
if ($id > 0) {
|
||||
$query->where('id', '<>', $id);
|
||||
}
|
||||
return $query->exists();
|
||||
}
|
||||
}
|
||||
35
app/admin/middleware/AuthMiddleware.php
Normal file
35
app/admin/middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$token = $this->getBearerToken($request);
|
||||
$admin = AuthService::getAdminByToken($token);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
$request->admin = $admin;
|
||||
$request->token = $token;
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
protected function getBearerToken(Request $request): ?string
|
||||
{
|
||||
$authorization = $request->header('authorization');
|
||||
if (!$authorization) {
|
||||
return null;
|
||||
}
|
||||
if (stripos($authorization, 'Bearer ') === 0) {
|
||||
return trim(substr($authorization, 7));
|
||||
}
|
||||
return trim($authorization);
|
||||
}
|
||||
}
|
||||
|
||||
39
app/admin/middleware/PermissionMiddleware.php
Normal file
39
app/admin/middleware/PermissionMiddleware.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
class PermissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$admin = $request->admin ?? null;
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
if (intval($admin->is_super) === 1) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$route = $request->route;
|
||||
$permissionCode = $route ? $route->getName() : null;
|
||||
if (!$permissionCode) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$admin->loadMissing(['roles.permissions']);
|
||||
$codes = [];
|
||||
foreach ($admin->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$codes[$permission->code] = true;
|
||||
}
|
||||
}
|
||||
if (!isset($codes[$permissionCode])) {
|
||||
return jsonResponse(null, '无权限', 403);
|
||||
}
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
|
||||
49
app/api/controller/AuthController.php
Normal file
49
app/api/controller/AuthController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function login(Request $request)
|
||||
{
|
||||
$mobile = trim((string)$request->post('mobile', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($mobile === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
if (!preg_match('/^\d{11}$/', $mobile)) {
|
||||
return jsonResponse(null, '手机号格式错误', 400);
|
||||
}
|
||||
|
||||
$user = User::firstOrCreate(
|
||||
['mobile' => $mobile],
|
||||
['nickname' => '用户' . substr($mobile, -4), 'status' => 1]
|
||||
);
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
return jsonResponse([
|
||||
'user' => $request->user
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
AuthService::revokeUserToken($request->token ?? null);
|
||||
return jsonResponse(null, '已退出登录');
|
||||
}
|
||||
}
|
||||
|
||||
132
app/api/controller/OrderController.php
Normal file
132
app/api/controller/OrderController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\service\OrderFlowService;
|
||||
use app\common\service\PaymentService;
|
||||
use app\common\model\Order;
|
||||
|
||||
class OrderController
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
$params = $request->post();
|
||||
$userId = $request->user->id;
|
||||
|
||||
try {
|
||||
$order = OrderFlowService::createOrder($params, $userId);
|
||||
|
||||
return jsonResponse([
|
||||
'order_id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'pay_amount' => $order->total_price
|
||||
], '下单成功');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, '下单失败: ' . $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$status = $request->get('status', 'all');
|
||||
$userId = $request->user->id;
|
||||
|
||||
$query = Order::where('user_id', $userId);
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$orders = $query->orderBy('id', 'desc')->get();
|
||||
|
||||
return jsonResponse(['items' => $orders, 'total' => $orders->count()]);
|
||||
}
|
||||
|
||||
public function detail(Request $request, $id)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
$order = Order::with(['logs'])->where('id', $id)->where('user_id', $userId)->first();
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
$timeline = [];
|
||||
$isFirst = true;
|
||||
foreach ($order->logs as $log) {
|
||||
$timeline[] = [
|
||||
'title' => $log->title,
|
||||
'time' => $log->created_at->format('Y-m-d H:i:s'),
|
||||
'desc' => $log->description,
|
||||
'is_current' => $isFirst,
|
||||
'is_done' => true
|
||||
];
|
||||
$isFirst = false;
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'category' => $order->category,
|
||||
'service_type' => $order->service_type,
|
||||
'status' => $order->status,
|
||||
'is_fast' => (bool)$order->is_fast,
|
||||
'express_company' => $order->express_company,
|
||||
'express_no' => $order->express_no,
|
||||
'timeline' => $timeline
|
||||
]);
|
||||
}
|
||||
|
||||
public function pay(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$userId = $request->user->id;
|
||||
$payType = trim((string)$request->post('pay_type', 'jsapi'));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($payType === 'native') {
|
||||
$pay = PaymentService::createWechatNativePay($order);
|
||||
return jsonResponse($pay, '支付发起成功');
|
||||
}
|
||||
|
||||
if ($appId === '') {
|
||||
return jsonResponse(null, '缺少 app_id,无法发起 JSAPI 支付', 400);
|
||||
}
|
||||
|
||||
$openid = trim((string)$request->post('openid', ''));
|
||||
$pay = PaymentService::createWechatJsapiPay($order, $appId, $openid);
|
||||
return jsonResponse($pay, '支付发起成功');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function ship(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$expressCompany = trim($request->post('express_company', ''));
|
||||
$expressNo = trim($request->post('express_no', ''));
|
||||
|
||||
if (!$expressCompany || !$expressNo) {
|
||||
return jsonResponse(null, '物流信息不完整', 400);
|
||||
}
|
||||
|
||||
$userId = $request->user->id;
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
OrderFlowService::userShip($order, $expressCompany, $expressNo);
|
||||
return jsonResponse(null, '发货信息已提交');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
app/api/controller/PayController.php
Normal file
103
app/api/controller/PayController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\PaymentTransaction;
|
||||
use app\common\model\WechatMerchant;
|
||||
use app\common\service\OrderFlowService;
|
||||
use app\common\service\WechatPayV3Client;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class PayController
|
||||
{
|
||||
public function wechatNotify(Request $request)
|
||||
{
|
||||
$body = (string)$request->rawBody();
|
||||
|
||||
$timestamp = (string)$request->header('Wechatpay-Timestamp', '');
|
||||
$nonce = (string)$request->header('Wechatpay-Nonce', '');
|
||||
$signature = (string)$request->header('Wechatpay-Signature', '');
|
||||
if ($timestamp === '' || $nonce === '' || $signature === '') {
|
||||
return json(['code' => 'FAIL', 'message' => 'missing headers'], 400);
|
||||
}
|
||||
|
||||
$merchants = WechatMerchant::where('status', 1)->get();
|
||||
if ($merchants->count() === 0) {
|
||||
return json(['code' => 'FAIL', 'message' => 'no merchant'], 500);
|
||||
}
|
||||
|
||||
$client = new WechatPayV3Client($merchants->first());
|
||||
$ok = $client->verifyPlatformSignature($timestamp, $nonce, $body, $signature);
|
||||
if (!$ok) {
|
||||
return json(['code' => 'FAIL', 'message' => 'invalid signature'], 400);
|
||||
}
|
||||
|
||||
$payload = json_decode($body, true) ?: [];
|
||||
$resource = $payload['resource'] ?? null;
|
||||
if (!is_array($resource)) {
|
||||
return json(['code' => 'FAIL', 'message' => 'invalid body'], 400);
|
||||
}
|
||||
|
||||
$decrypt = null;
|
||||
$matchedMerchant = null;
|
||||
foreach ($merchants as $m) {
|
||||
$apiV3Key = (string)($m->api_v3_key ?? '');
|
||||
if ($apiV3Key === '') continue;
|
||||
try {
|
||||
$decrypt = $client->decryptNotifyResource($resource, $apiV3Key);
|
||||
$matchedMerchant = $m;
|
||||
break;
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
if (!$decrypt || !$matchedMerchant) {
|
||||
return json(['code' => 'FAIL', 'message' => 'decrypt failed'], 400);
|
||||
}
|
||||
|
||||
$outTradeNo = (string)($decrypt['out_trade_no'] ?? '');
|
||||
$tradeState = (string)($decrypt['trade_state'] ?? '');
|
||||
if ($outTradeNo === '') {
|
||||
return json(['code' => 'FAIL', 'message' => 'missing out_trade_no'], 400);
|
||||
}
|
||||
|
||||
$tx = PaymentTransaction::where('out_trade_no', $outTradeNo)->first();
|
||||
if (!$tx) {
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
|
||||
if ($tradeState !== 'SUCCESS') {
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx->status = 'paid';
|
||||
$tx->paid_at = date('Y-m-d H:i:s');
|
||||
$tx->raw_json = $decrypt;
|
||||
$tx->save();
|
||||
|
||||
$order = Order::find($tx->order_id);
|
||||
if ($order) {
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paid';
|
||||
$order->pay_merchant_id = (int)$matchedMerchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
if (!$order->pay_time) {
|
||||
$order->pay_time = date('Y-m-d H:i:s');
|
||||
}
|
||||
$order->save();
|
||||
|
||||
OrderFlowService::payOrder($order);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return json(['code' => 'FAIL', 'message' => 'server error'], 500);
|
||||
}
|
||||
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
}
|
||||
|
||||
70
app/api/controller/ReportController.php
Normal file
70
app/api/controller/ReportController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Report;
|
||||
use app\common\model\Order;
|
||||
|
||||
class ReportController
|
||||
{
|
||||
// 获取C端自己的报告
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->get('order_id');
|
||||
$userId = $request->user->id;
|
||||
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
$report = Report::with(['inspector'])->where('order_id', $orderId)->first();
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '报告尚未出具', 404);
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'report_no' => $report->report_no,
|
||||
'conclusion' => $report->conclusion,
|
||||
'level' => $report->level,
|
||||
'flaws' => $report->flaws_json,
|
||||
'images' => $report->images_json,
|
||||
'verify_code' => $report->verify_code,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
'inspector' => [
|
||||
'name' => $report->inspector->nickname ?? $report->inspector->username,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// 公开验证防伪码 (无需登录)
|
||||
public function verify(Request $request)
|
||||
{
|
||||
$code = trim($request->get('code', ''));
|
||||
if (!$code) {
|
||||
return jsonResponse(null, '防伪码不能为空', 400);
|
||||
}
|
||||
|
||||
$report = Report::with(['order', 'inspector'])->where('verify_code', $code)->first();
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '无效的防伪码或报告不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'report_no' => $report->report_no,
|
||||
'conclusion' => $report->conclusion,
|
||||
'level' => $report->level,
|
||||
'flaws' => $report->flaws_json,
|
||||
'images' => $report->images_json,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
'order' => [
|
||||
'category' => $report->order->category,
|
||||
'brand' => $report->order->brand,
|
||||
'model' => $report->order->model,
|
||||
],
|
||||
'inspector' => [
|
||||
'name' => $report->inspector->nickname ?? $report->inspector->username,
|
||||
]
|
||||
], '验证成功,该报告真实有效');
|
||||
}
|
||||
}
|
||||
37
app/api/controller/UploadController.php
Normal file
37
app/api/controller/UploadController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class UploadController
|
||||
{
|
||||
public function image(Request $request)
|
||||
{
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
return jsonResponse(null, '仅支持图片文件', 400);
|
||||
}
|
||||
|
||||
$dir = public_path() . '/upload/images/' . date('Ymd');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$filename = uniqid() . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
|
||||
$url = '/upload/images/' . date('Ymd') . '/' . $filename;
|
||||
|
||||
return jsonResponse([
|
||||
'url' => $url,
|
||||
'name' => $file->getUploadName(),
|
||||
], '上传成功');
|
||||
}
|
||||
}
|
||||
49
app/api/controller/UserController.php
Normal file
49
app/api/controller/UserController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
use app\common\model\User;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function stat(Request $request)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
|
||||
$totalOrders = Order::where('user_id', $userId)->count();
|
||||
$totalReports = Report::whereHas('order', function ($query) use ($userId) {
|
||||
$query->where('user_id', $userId);
|
||||
})->count();
|
||||
|
||||
return jsonResponse([
|
||||
'total_orders' => $totalOrders,
|
||||
'total_reports' => $totalReports
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInfo(Request $request)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
$user = User::find($userId);
|
||||
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '用户异常', 404);
|
||||
}
|
||||
|
||||
$nickname = trim($request->post('nickname', ''));
|
||||
$avatar = trim($request->post('avatar', ''));
|
||||
|
||||
if ($nickname) {
|
||||
$user->nickname = $nickname;
|
||||
}
|
||||
if ($avatar) {
|
||||
$user->avatar = $avatar;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return jsonResponse($user, '更新成功');
|
||||
}
|
||||
}
|
||||
205
app/api/controller/WechatAuthController.php
Normal file
205
app/api/controller/WechatAuthController.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\model\WechatApp;
|
||||
use app\common\model\UserWechatIdentity;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class WechatAuthController
|
||||
{
|
||||
public function appList(Request $request)
|
||||
{
|
||||
$type = trim((string)$request->get('type', ''));
|
||||
$query = WechatApp::query()->where('status', 1);
|
||||
if ($type !== '') {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
$list = $query->select(['id', 'name', 'type', 'app_id'])->orderByDesc('id')->get();
|
||||
return jsonResponse($list);
|
||||
}
|
||||
|
||||
public function miniLogin(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($appId === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$app = $this->getApp($appId, 'mini');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
$secret = (string)($app->app_secret ?? '');
|
||||
if ($secret === '') {
|
||||
return jsonResponse(null, '未配置 app_secret', 400);
|
||||
}
|
||||
|
||||
$url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . urlencode($appId) .
|
||||
'&secret=' . urlencode($secret) .
|
||||
'&js_code=' . urlencode($code) .
|
||||
'&grant_type=authorization_code';
|
||||
$res = $this->httpGetJson($url);
|
||||
$openid = isset($res['openid']) ? trim((string)$res['openid']) : '';
|
||||
$unionid = isset($res['unionid']) ? trim((string)$res['unionid']) : '';
|
||||
if ($openid === '') {
|
||||
$msg = $res['errmsg'] ?? '获取 openid 失败';
|
||||
return jsonResponse(null, (string)$msg, 400);
|
||||
}
|
||||
|
||||
$user = $this->resolveUserByWechatIdentity($appId, $openid, $unionid, 'mini');
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$this->upsertIdentity($user->id, $appId, $openid, $unionid, 'mini');
|
||||
$user->openid = $openid;
|
||||
$user->save();
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
'openid' => $openid,
|
||||
'app_id' => $appId,
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function h5Login(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($appId === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$app = $this->getApp($appId, 'h5');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
$secret = (string)($app->app_secret ?? '');
|
||||
if ($secret === '') {
|
||||
return jsonResponse(null, '未配置 app_secret', 400);
|
||||
}
|
||||
|
||||
$url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' . urlencode($appId) .
|
||||
'&secret=' . urlencode($secret) .
|
||||
'&code=' . urlencode($code) .
|
||||
'&grant_type=authorization_code';
|
||||
$res = $this->httpGetJson($url);
|
||||
$openid = isset($res['openid']) ? trim((string)$res['openid']) : '';
|
||||
$unionid = isset($res['unionid']) ? trim((string)$res['unionid']) : '';
|
||||
if ($openid === '') {
|
||||
$msg = $res['errmsg'] ?? '获取 openid 失败';
|
||||
return jsonResponse(null, (string)$msg, 400);
|
||||
}
|
||||
|
||||
$user = $this->resolveUserByWechatIdentity($appId, $openid, $unionid, 'h5');
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$this->upsertIdentity($user->id, $appId, $openid, $unionid, 'h5');
|
||||
$user->openid = $openid;
|
||||
$user->save();
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
'openid' => $openid,
|
||||
'app_id' => $appId,
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function h5AuthorizeUrl(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->get('app_id', ''));
|
||||
$redirectUri = trim((string)$request->get('redirect_uri', ''));
|
||||
$scope = trim((string)$request->get('scope', 'snsapi_base'));
|
||||
$state = trim((string)$request->get('state', ''));
|
||||
|
||||
if ($appId === '' || $redirectUri === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
if (!in_array($scope, ['snsapi_base', 'snsapi_userinfo'], true)) {
|
||||
return jsonResponse(null, 'scope 不合法', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->getApp($appId, 'h5');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' . urlencode($appId) .
|
||||
'&redirect_uri=' . urlencode($redirectUri) .
|
||||
'&response_type=code&scope=' . urlencode($scope) .
|
||||
'&state=' . urlencode($state) .
|
||||
'#wechat_redirect';
|
||||
|
||||
return jsonResponse(['url' => $url]);
|
||||
}
|
||||
|
||||
private function getApp(string $appId, string $type): WechatApp
|
||||
{
|
||||
$row = WechatApp::where('app_id', $appId)->where('status', 1)->first();
|
||||
if (!$row) {
|
||||
throw new \RuntimeException('AppID 未配置或已停用');
|
||||
}
|
||||
if ((string)$row->type !== $type) {
|
||||
throw new \RuntimeException('AppID 类型不匹配');
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function httpGetJson(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$body = curl_exec($ch);
|
||||
if ($body === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('微信接口请求失败: ' . $err);
|
||||
}
|
||||
curl_close($ch);
|
||||
return json_decode($body, true) ?: [];
|
||||
}
|
||||
|
||||
private function resolveUserByWechatIdentity(string $appId, string $openid, string $unionid, string $scene): User
|
||||
{
|
||||
if ($unionid !== '') {
|
||||
$identity = UserWechatIdentity::where('unionid', $unionid)->first();
|
||||
if ($identity) {
|
||||
$user = User::find($identity->user_id);
|
||||
if ($user) return $user;
|
||||
}
|
||||
}
|
||||
|
||||
$identity = UserWechatIdentity::where('app_id', $appId)->where('openid', $openid)->first();
|
||||
if ($identity) {
|
||||
$user = User::find($identity->user_id);
|
||||
if ($user) return $user;
|
||||
}
|
||||
|
||||
return User::create([
|
||||
'openid' => $openid,
|
||||
'nickname' => $scene === 'mini' ? '小程序用户' : '微信用户',
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function upsertIdentity(int $userId, string $appId, string $openid, string $unionid, string $scene): void
|
||||
{
|
||||
UserWechatIdentity::updateOrCreate(
|
||||
['user_id' => $userId, 'app_id' => $appId],
|
||||
['openid' => $openid, 'unionid' => $unionid ?: null, 'scene' => $scene]
|
||||
);
|
||||
}
|
||||
}
|
||||
35
app/api/middleware/AuthMiddleware.php
Normal file
35
app/api/middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace app\api\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$token = $this->getBearerToken($request);
|
||||
$user = AuthService::getUserByToken($token);
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
$request->user = $user;
|
||||
$request->token = $token;
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
protected function getBearerToken(Request $request): ?string
|
||||
{
|
||||
$authorization = $request->header('authorization');
|
||||
if (!$authorization) {
|
||||
return null;
|
||||
}
|
||||
if (stripos($authorization, 'Bearer ') === 0) {
|
||||
return trim(substr($authorization, 7));
|
||||
}
|
||||
return trim($authorization);
|
||||
}
|
||||
}
|
||||
|
||||
24
app/common/exception/BusinessException.php
Normal file
24
app/common/exception/BusinessException.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace app\common\exception;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Exception\ExceptionHandler;
|
||||
|
||||
class BusinessException extends Exception
|
||||
{
|
||||
protected $data;
|
||||
|
||||
public function __construct(string $message = "", int $code = 400, $data = null, Throwable $previous = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
15
app/common/model/AdminToken.php
Normal file
15
app/common/model/AdminToken.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminToken extends Model
|
||||
{
|
||||
protected $table = 'admin_tokens';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['token_hash', 'updated_at'];
|
||||
protected $casts = [
|
||||
'expired_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
17
app/common/model/AdminUser.php
Normal file
17
app/common/model/AdminUser.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminUser extends Model
|
||||
{
|
||||
protected $table = 'admin_users';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['password_hash', 'updated_at'];
|
||||
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'admin_roles', 'admin_id', 'role_id');
|
||||
}
|
||||
}
|
||||
|
||||
20
app/common/model/Order.php
Normal file
20
app/common/model/Order.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
protected $table = 'orders';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
// 隐藏时间戳等不必要字段,保持前端接口整洁
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
// 关联订单流转日志 (时间轴)
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(OrderLog::class, 'order_id', 'id')->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
11
app/common/model/OrderLog.php
Normal file
11
app/common/model/OrderLog.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OrderLog extends Model
|
||||
{
|
||||
protected $table = 'order_logs';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/common/model/PaymentTransaction.php
Normal file
15
app/common/model/PaymentTransaction.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaymentTransaction extends Model
|
||||
{
|
||||
protected $table = 'payment_transactions';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
protected $casts = [
|
||||
'raw_json' => 'array',
|
||||
];
|
||||
}
|
||||
12
app/common/model/Permission.php
Normal file
12
app/common/model/Permission.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
protected $table = 'permissions';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
26
app/common/model/Report.php
Normal file
26
app/common/model/Report.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
protected $table = 'reports';
|
||||
protected $guarded = [];
|
||||
|
||||
// Cast JSON fields automatically
|
||||
protected $casts = [
|
||||
'flaws_json' => 'array',
|
||||
'images_json' => 'array',
|
||||
];
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class, 'order_id');
|
||||
}
|
||||
|
||||
public function inspector()
|
||||
{
|
||||
return $this->belongsTo(AdminUser::class, 'inspector_id');
|
||||
}
|
||||
}
|
||||
17
app/common/model/Role.php
Normal file
17
app/common/model/Role.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
protected $table = 'roles';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany(Permission::class, 'role_permissions', 'role_id', 'permission_id');
|
||||
}
|
||||
}
|
||||
|
||||
12
app/common/model/User.php
Normal file
12
app/common/model/User.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected $table = 'users';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
15
app/common/model/UserToken.php
Normal file
15
app/common/model/UserToken.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserToken extends Model
|
||||
{
|
||||
protected $table = 'user_tokens';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['token_hash', 'updated_at'];
|
||||
protected $casts = [
|
||||
'expired_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
12
app/common/model/UserWechatIdentity.php
Normal file
12
app/common/model/UserWechatIdentity.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserWechatIdentity extends Model
|
||||
{
|
||||
protected $table = 'user_wechat_identities';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
12
app/common/model/WechatApp.php
Normal file
12
app/common/model/WechatApp.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WechatApp extends Model
|
||||
{
|
||||
protected $table = 'wechat_apps';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at', 'app_secret'];
|
||||
}
|
||||
|
||||
11
app/common/model/WechatMerchant.php
Normal file
11
app/common/model/WechatMerchant.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WechatMerchant extends Model
|
||||
{
|
||||
protected $table = 'wechat_merchants';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at', 'api_v3_key', 'private_key_pem', 'apiclient_cert_path', 'apiclient_key_path'];
|
||||
}
|
||||
90
app/common/service/AuthService.php
Normal file
90
app/common/service/AuthService.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserToken;
|
||||
use app\common\model\AdminUser;
|
||||
use app\common\model\AdminToken;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public static function issueUserToken(User $user): string
|
||||
{
|
||||
$ttl = intval(getenv('USER_TOKEN_TTL') ?: 604800);
|
||||
$token = generateToken();
|
||||
$hash = hashToken($token);
|
||||
|
||||
UserToken::create([
|
||||
'user_id' => $user->id,
|
||||
'token_hash' => $hash,
|
||||
'expired_at' => $ttl > 0 ? Carbon::now()->addSeconds($ttl) : null,
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function getUserByToken(?string $token): ?User
|
||||
{
|
||||
if (!$token) {
|
||||
return null;
|
||||
}
|
||||
$hash = hashToken($token);
|
||||
$row = UserToken::where('token_hash', $hash)->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ($row->expired_at && $row->expired_at->lt(Carbon::now())) {
|
||||
return null;
|
||||
}
|
||||
return User::find($row->user_id);
|
||||
}
|
||||
|
||||
public static function revokeUserToken(?string $token): void
|
||||
{
|
||||
if (!$token) {
|
||||
return;
|
||||
}
|
||||
UserToken::where('token_hash', hashToken($token))->delete();
|
||||
}
|
||||
|
||||
public static function issueAdminToken(AdminUser $admin): string
|
||||
{
|
||||
$ttl = intval(getenv('ADMIN_TOKEN_TTL') ?: 86400);
|
||||
$token = generateToken();
|
||||
$hash = hashToken($token);
|
||||
|
||||
AdminToken::create([
|
||||
'admin_id' => $admin->id,
|
||||
'token_hash' => $hash,
|
||||
'expired_at' => $ttl > 0 ? Carbon::now()->addSeconds($ttl) : null,
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function getAdminByToken(?string $token): ?AdminUser
|
||||
{
|
||||
if (!$token) {
|
||||
return null;
|
||||
}
|
||||
$hash = hashToken($token);
|
||||
$row = AdminToken::where('token_hash', $hash)->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ($row->expired_at && $row->expired_at->lt(Carbon::now())) {
|
||||
return null;
|
||||
}
|
||||
return AdminUser::find($row->admin_id);
|
||||
}
|
||||
|
||||
public static function revokeAdminToken(?string $token): void
|
||||
{
|
||||
if (!$token) {
|
||||
return;
|
||||
}
|
||||
AdminToken::where('token_hash', hashToken($token))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
126
app/common/service/OrderFlowService.php
Normal file
126
app/common/service/OrderFlowService.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\OrderLog;
|
||||
use Exception;
|
||||
|
||||
class OrderFlowService
|
||||
{
|
||||
/**
|
||||
* 模拟创建订单事务
|
||||
*/
|
||||
public static function createOrder(array $params, int $userId)
|
||||
{
|
||||
$orderNo = 'AXY' . date('YmdHis') . rand(100, 999);
|
||||
|
||||
// 1. 创建主订单
|
||||
$order = Order::create([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $userId,
|
||||
'category' => $params['category'] ?? '奢品包袋',
|
||||
'service_type' => $params['service_type'] ?? '真伪鉴定',
|
||||
'brand' => $params['brand'] ?? '未知品牌',
|
||||
'model' => $params['model'] ?? '',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
'is_fast' => $params['is_fast'] ?? 0,
|
||||
'total_price' => $params['total_price'] ?? 49.00,
|
||||
'status' => 'wait_pay',
|
||||
]);
|
||||
|
||||
// 2. 写入初始流转日志
|
||||
self::addLog($order->id, 'create', '订单已创建', '等待用户支付', 'user', $userId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟支付成功
|
||||
*/
|
||||
public static function payOrder(Order $order)
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$order->status = 'shipping'; // 待寄送
|
||||
$order->pay_time = date('Y-m-d H:i:s');
|
||||
$order->pay_status = 'paid';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'pay_success', '支付成功', '请尽快寄出物品', 'user', $order->user_id);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户填写物流并发货
|
||||
*/
|
||||
public static function userShip(Order $order, string $expressCompany, string $expressNo)
|
||||
{
|
||||
if ($order->status !== 'shipping') {
|
||||
throw new Exception("当前状态不允许发货");
|
||||
}
|
||||
|
||||
$order->express_company = $expressCompany;
|
||||
$order->express_no = $expressNo;
|
||||
$order->status = 'wait_receive'; // 等待平台收件(可复用为在途)
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'user_ship', '物品已寄出', "物流公司: {$expressCompany}, 单号: {$expressNo}", 'user', $order->user_id);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台确认收件并开始鉴定
|
||||
*/
|
||||
public static function adminReceive(Order $order, int $adminId)
|
||||
{
|
||||
if ($order->status !== 'wait_receive' && $order->status !== 'shipping') {
|
||||
throw new Exception("当前状态不允许收件");
|
||||
}
|
||||
|
||||
$order->status = 'inspecting';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'admin_receive', '平台已收件', '物品已入库,即将开始鉴定', 'admin', $adminId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
public static function adminReturnShip(Order $order, int $adminId, string $expressCompany, string $expressNo)
|
||||
{
|
||||
if ($order->status !== 'finished') {
|
||||
throw new Exception("当前状态不允许回寄");
|
||||
}
|
||||
if ($expressCompany === '' || $expressNo === '') {
|
||||
throw new Exception("回寄物流信息不完整");
|
||||
}
|
||||
|
||||
$order->return_express_company = $expressCompany;
|
||||
$order->return_express_no = $expressNo;
|
||||
$order->return_ship_time = date('Y-m-d H:i:s');
|
||||
$order->status = 'return_shipping';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'return_ship', '已回寄', "物流公司: {$expressCompany}, 单号: {$expressNo}", 'admin', $adminId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入时间轴日志
|
||||
*/
|
||||
public static function addLog(int $orderId, string $actionType, string $title, string $desc = '', string $operatorType = 'system', int $operatorId = 0)
|
||||
{
|
||||
return OrderLog::create([
|
||||
'order_id' => $orderId,
|
||||
'action_type' => $actionType,
|
||||
'title' => $title,
|
||||
'description' => $desc,
|
||||
'operator_type' => $operatorType,
|
||||
'operator_id' => $operatorId
|
||||
]);
|
||||
}
|
||||
}
|
||||
255
app/common/service/PaymentService.php
Normal file
255
app/common/service/PaymentService.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\PaymentTransaction;
|
||||
use app\common\model\WechatMerchant;
|
||||
use app\common\model\UserWechatIdentity;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class PaymentService
|
||||
{
|
||||
public static function createWechatJsapiPay(Order $order, string $appId, string $openid = ''): array
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
throw new \RuntimeException('订单状态不正确');
|
||||
}
|
||||
|
||||
$merchant = self::selectWechatMerchantForJsapi($order, $appId);
|
||||
if (!$merchant) {
|
||||
throw new \RuntimeException('未配置可用的微信商户号');
|
||||
}
|
||||
|
||||
$notifyUrl = (string)($merchant->notify_url ?? '');
|
||||
if ($notifyUrl === '') {
|
||||
$notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: '');
|
||||
}
|
||||
if ($notifyUrl === '') {
|
||||
throw new \RuntimeException('回调地址未配置');
|
||||
}
|
||||
|
||||
$outTradeNo = self::genOutTradeNo($order);
|
||||
$amountFen = (int)round(((float)$order->total_price) * 100);
|
||||
$description = '安心验-鉴定订单 ' . $order->order_no;
|
||||
|
||||
$openid = trim($openid);
|
||||
if ($openid === '') {
|
||||
$identity = UserWechatIdentity::where('user_id', $order->user_id)->where('app_id', $appId)->first();
|
||||
if ($identity) {
|
||||
$openid = (string)$identity->openid;
|
||||
}
|
||||
}
|
||||
if ($openid === '') {
|
||||
throw new \RuntimeException('缺少 openid,无法发起 JSAPI 支付');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx = PaymentTransaction::create([
|
||||
'order_id' => $order->id,
|
||||
'channel' => 'wechat',
|
||||
'merchant_id' => $merchant->id,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'amount' => $order->total_price,
|
||||
'status' => 'created',
|
||||
]);
|
||||
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paying';
|
||||
$order->pay_merchant_id = $merchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
$order->save();
|
||||
|
||||
$client = new WechatPayV3Client($merchant);
|
||||
$resp = $client->createJsapiTransaction($outTradeNo, $description, $amountFen, $notifyUrl, $openid);
|
||||
|
||||
$tx->prepay_id = $resp['prepay_id'] ?? null;
|
||||
$tx->raw_json = $resp;
|
||||
$tx->save();
|
||||
|
||||
if (!$tx->prepay_id) {
|
||||
throw new \RuntimeException('微信支付下单失败:缺少 prepay_id');
|
||||
}
|
||||
|
||||
$payParams = $client->buildJsapiPayParams($appId, $tx->prepay_id);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'channel' => 'wechat',
|
||||
'pay_type' => 'jsapi',
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'merchant' => [
|
||||
'id' => $merchant->id,
|
||||
'name' => $merchant->name,
|
||||
'mode' => $merchant->mode,
|
||||
'mch_id' => $merchant->mch_id,
|
||||
],
|
||||
'pay_params' => $payParams,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function createWechatNativePay(Order $order): array
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
throw new \RuntimeException('订单状态不正确');
|
||||
}
|
||||
|
||||
$merchant = self::selectWechatMerchant($order);
|
||||
if (!$merchant) {
|
||||
throw new \RuntimeException('未配置可用的微信商户号');
|
||||
}
|
||||
|
||||
$notifyUrl = (string)($merchant->notify_url ?? '');
|
||||
if ($notifyUrl === '') {
|
||||
$notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: '');
|
||||
}
|
||||
if ($notifyUrl === '') {
|
||||
throw new \RuntimeException('回调地址未配置');
|
||||
}
|
||||
|
||||
$outTradeNo = self::genOutTradeNo($order);
|
||||
$amountFen = (int)round(((float)$order->total_price) * 100);
|
||||
$description = '安心验-鉴定订单 ' . $order->order_no;
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx = PaymentTransaction::create([
|
||||
'order_id' => $order->id,
|
||||
'channel' => 'wechat',
|
||||
'merchant_id' => $merchant->id,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'amount' => $order->total_price,
|
||||
'status' => 'created',
|
||||
]);
|
||||
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paying';
|
||||
$order->pay_merchant_id = $merchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
$order->save();
|
||||
|
||||
$client = new WechatPayV3Client($merchant);
|
||||
$resp = $client->createNativeTransaction($outTradeNo, $description, $amountFen, $notifyUrl);
|
||||
|
||||
$tx->prepay_id = $resp['prepay_id'] ?? null;
|
||||
$tx->code_url = $resp['code_url'] ?? null;
|
||||
$tx->raw_json = $resp;
|
||||
$tx->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'channel' => 'wechat',
|
||||
'pay_type' => 'native',
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'code_url' => $tx->code_url,
|
||||
'merchant' => [
|
||||
'id' => $merchant->id,
|
||||
'name' => $merchant->name,
|
||||
'mode' => $merchant->mode,
|
||||
'mch_id' => $merchant->mch_id,
|
||||
],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function selectWechatMerchant(Order $order): ?WechatMerchant
|
||||
{
|
||||
$list = WechatMerchant::where('status', 1)
|
||||
->whereIn('mode', ['direct', 'service_provider'])
|
||||
->whereNotNull('serial_no')->where('serial_no', '<>', '')
|
||||
->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', '');
|
||||
})->orWhere(function ($q2) {
|
||||
$q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', '');
|
||||
});
|
||||
})
|
||||
->orderByDesc('is_default')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
if ($list->count() === 0) return null;
|
||||
if ($list->count() === 1) return $list->first();
|
||||
|
||||
$default = $list->firstWhere('is_default', 1);
|
||||
if ($default) return $default;
|
||||
|
||||
$idx = abs(crc32((string)$order->order_no)) % $list->count();
|
||||
return $list->values()->get($idx);
|
||||
}
|
||||
|
||||
public static function selectWechatMerchantForJsapi(Order $order, string $appId): ?WechatMerchant
|
||||
{
|
||||
$appId = trim($appId);
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('缺少 app_id');
|
||||
}
|
||||
|
||||
$list = WechatMerchant::where('status', 1)
|
||||
->whereIn('mode', ['direct', 'service_provider'])
|
||||
->whereNotNull('serial_no')->where('serial_no', '<>', '')
|
||||
->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', '');
|
||||
})->orWhere(function ($q2) {
|
||||
$q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', '');
|
||||
});
|
||||
})
|
||||
->get()
|
||||
->filter(function ($m) use ($appId) {
|
||||
$mode = (string)($m->mode ?? '');
|
||||
if ($mode === 'direct') {
|
||||
return (string)($m->app_id ?? '') === $appId;
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
$subAppId = (string)($m->sub_app_id ?? '');
|
||||
if ($subAppId !== '') return $subAppId === $appId;
|
||||
return (string)($m->app_id ?? '') === $appId;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
->values();
|
||||
|
||||
if ($list->count() === 0) return null;
|
||||
if ($list->count() === 1) return $list->first();
|
||||
|
||||
$default = $list->firstWhere('is_default', 1);
|
||||
if ($default) return $default;
|
||||
|
||||
$idx = abs(crc32((string)$order->order_no)) % $list->count();
|
||||
return $list->get($idx);
|
||||
}
|
||||
|
||||
private static function genOutTradeNo(Order $order): string
|
||||
{
|
||||
return 'AXY' . date('YmdHis') . $order->id . random_int(1000, 9999);
|
||||
}
|
||||
|
||||
private static function resolveJsapiAppId(WechatMerchant $merchant): string
|
||||
{
|
||||
$mode = (string)($merchant->mode ?? 'direct');
|
||||
$appId = (string)($merchant->app_id ?? '');
|
||||
$subAppId = (string)($merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') throw new \RuntimeException('商户 AppID 未配置');
|
||||
return $appId;
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
if ($subAppId !== '') return $subAppId;
|
||||
if ($appId === '') throw new \RuntimeException('服务商 AppID 未配置');
|
||||
return $appId;
|
||||
}
|
||||
throw new \RuntimeException('该商户类型暂不支持 JSAPI');
|
||||
}
|
||||
}
|
||||
276
app/common/service/WechatPayV3Client.php
Normal file
276
app/common/service/WechatPayV3Client.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\WechatMerchant;
|
||||
|
||||
class WechatPayV3Client
|
||||
{
|
||||
private WechatMerchant $merchant;
|
||||
|
||||
public function __construct(WechatMerchant $merchant)
|
||||
{
|
||||
$this->merchant = $merchant;
|
||||
}
|
||||
|
||||
public function createNativeTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array
|
||||
{
|
||||
$body = $this->buildPayBody($outTradeNo, $description, $amountFen, $notifyUrl);
|
||||
$resp = $this->request('POST', '/v3/pay/transactions/native', $body);
|
||||
|
||||
$data = json_decode($resp['body'], true) ?: [];
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
return $data;
|
||||
}
|
||||
$message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']);
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
public function createJsapiTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array
|
||||
{
|
||||
$body = $this->buildJsapiPayBody($outTradeNo, $description, $amountFen, $notifyUrl, $openid);
|
||||
$resp = $this->request('POST', '/v3/pay/transactions/jsapi', $body);
|
||||
|
||||
$data = json_decode($resp['body'], true) ?: [];
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
return $data;
|
||||
}
|
||||
$message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']);
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
public function buildJsapiPayParams(string $appId, string $prepayId): array
|
||||
{
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$serialNo = (string)($this->merchant->serial_no ?? '');
|
||||
$privateKeyPem = (string)($this->merchant->private_key_pem ?? '');
|
||||
if ($privateKeyPem === '') {
|
||||
$privateKeyPem = $this->loadKeyPemFromFile();
|
||||
}
|
||||
if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') {
|
||||
throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)');
|
||||
}
|
||||
|
||||
$timeStamp = (string)time();
|
||||
$nonceStr = bin2hex(random_bytes(16));
|
||||
$package = 'prepay_id=' . $prepayId;
|
||||
|
||||
$signStr = $appId . "\n" . $timeStamp . "\n" . $nonceStr . "\n" . $package . "\n";
|
||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
||||
if (!$privateKey) {
|
||||
throw new \RuntimeException('私钥格式错误');
|
||||
}
|
||||
openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
|
||||
return [
|
||||
'appId' => $appId,
|
||||
'timeStamp' => $timeStamp,
|
||||
'nonceStr' => $nonceStr,
|
||||
'package' => $package,
|
||||
'signType' => 'RSA',
|
||||
'paySign' => base64_encode($signature),
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyPlatformSignature(string $timestamp, string $nonce, string $body, string $signature): bool
|
||||
{
|
||||
$certPath = getenv('WECHATPAY_PLATFORM_CERT_PATH') ?: '';
|
||||
if ($certPath === '' || !file_exists($certPath)) {
|
||||
throw new \RuntimeException('平台证书未配置');
|
||||
}
|
||||
$cert = file_get_contents($certPath);
|
||||
$publicKey = openssl_pkey_get_public($cert);
|
||||
if (!$publicKey) {
|
||||
throw new \RuntimeException('平台证书读取失败');
|
||||
}
|
||||
$message = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
|
||||
$ok = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256);
|
||||
return $ok === 1;
|
||||
}
|
||||
|
||||
public function decryptNotifyResource(array $resource, string $apiV3Key): array
|
||||
{
|
||||
$ciphertext = (string)($resource['ciphertext'] ?? '');
|
||||
$nonce = (string)($resource['nonce'] ?? '');
|
||||
$aad = (string)($resource['associated_data'] ?? '');
|
||||
if ($ciphertext === '' || $nonce === '') {
|
||||
throw new \RuntimeException('回调报文不完整');
|
||||
}
|
||||
|
||||
$cipherRaw = base64_decode($ciphertext);
|
||||
$tag = substr($cipherRaw, -16);
|
||||
$data = substr($cipherRaw, 0, -16);
|
||||
$plain = openssl_decrypt($data, 'aes-256-gcm', $apiV3Key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
|
||||
if ($plain === false) {
|
||||
throw new \RuntimeException('回调报文解密失败');
|
||||
}
|
||||
return json_decode($plain, true) ?: [];
|
||||
}
|
||||
|
||||
private function buildPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array
|
||||
{
|
||||
$mode = (string)($this->merchant->mode ?? 'direct');
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$appId = (string)($this->merchant->app_id ?? '');
|
||||
$subMchId = (string)($this->merchant->sub_mch_id ?? '');
|
||||
$subAppId = (string)($this->merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('商户 AppID 未配置');
|
||||
}
|
||||
return [
|
||||
'appid' => $appId,
|
||||
'mchid' => $mchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
];
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
if ($appId === '' || $subMchId === '') {
|
||||
throw new \RuntimeException('服务商模式配置不完整');
|
||||
}
|
||||
$body = [
|
||||
'sp_appid' => $appId,
|
||||
'sp_mchid' => $mchId,
|
||||
'sub_mchid' => $subMchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
];
|
||||
if ($subAppId !== '') {
|
||||
$body['sub_appid'] = $subAppId;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
throw new \RuntimeException('第三方支付模式未配置下单方式');
|
||||
}
|
||||
|
||||
private function buildJsapiPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array
|
||||
{
|
||||
$openid = trim($openid);
|
||||
if ($openid === '') {
|
||||
throw new \RuntimeException('openid 不能为空');
|
||||
}
|
||||
|
||||
$mode = (string)($this->merchant->mode ?? 'direct');
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$appId = (string)($this->merchant->app_id ?? '');
|
||||
$subMchId = (string)($this->merchant->sub_mch_id ?? '');
|
||||
$subAppId = (string)($this->merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('商户 AppID 未配置');
|
||||
}
|
||||
return [
|
||||
'appid' => $appId,
|
||||
'mchid' => $mchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
'payer' => ['openid' => $openid],
|
||||
];
|
||||
}
|
||||
|
||||
if ($mode === 'service_provider') {
|
||||
if ($appId === '' || $subMchId === '') {
|
||||
throw new \RuntimeException('服务商模式配置不完整');
|
||||
}
|
||||
$body = [
|
||||
'sp_appid' => $appId,
|
||||
'sp_mchid' => $mchId,
|
||||
'sub_mchid' => $subMchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
'payer' => [],
|
||||
];
|
||||
if ($subAppId !== '') {
|
||||
$body['sub_appid'] = $subAppId;
|
||||
$body['payer']['sub_openid'] = $openid;
|
||||
} else {
|
||||
$body['payer']['sp_openid'] = $openid;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('第三方支付模式未配置下单方式');
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, array $body): array
|
||||
{
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$serialNo = (string)($this->merchant->serial_no ?? '');
|
||||
$privateKeyPem = (string)($this->merchant->private_key_pem ?? '');
|
||||
if ($privateKeyPem === '') {
|
||||
$privateKeyPem = $this->loadKeyPemFromFile();
|
||||
}
|
||||
|
||||
if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') {
|
||||
throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)');
|
||||
}
|
||||
|
||||
$bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE);
|
||||
$timestamp = (string)time();
|
||||
$nonceStr = bin2hex(random_bytes(16));
|
||||
|
||||
$signStr = $method . "\n" . $path . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $bodyJson . "\n";
|
||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
||||
if (!$privateKey) {
|
||||
throw new \RuntimeException('私钥格式错误');
|
||||
}
|
||||
openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
|
||||
$authorization = sprintf(
|
||||
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"',
|
||||
$mchId,
|
||||
$nonceStr,
|
||||
$timestamp,
|
||||
$serialNo,
|
||||
$signature
|
||||
);
|
||||
|
||||
$ch = curl_init('https://api.mch.weixin.qq.com' . $path);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json',
|
||||
'Authorization: ' . $authorization,
|
||||
'User-Agent: anxinyan-webman',
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyJson);
|
||||
$respBody = curl_exec($ch);
|
||||
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($respBody === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('微信支付请求失败: ' . $err);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'body' => $respBody,
|
||||
];
|
||||
}
|
||||
|
||||
private function loadKeyPemFromFile(): string
|
||||
{
|
||||
$path = (string)($this->merchant->apiclient_key_path ?? '');
|
||||
if ($path === '') return '';
|
||||
$real = realpath($path);
|
||||
$base = realpath(runtime_path() . '/wechatpay/merchants');
|
||||
if (!$real || !$base) return '';
|
||||
if (strpos($real, $base) !== 0) return '';
|
||||
if (!is_file($real)) return '';
|
||||
$pem = file_get_contents($real);
|
||||
return is_string($pem) ? $pem : '';
|
||||
}
|
||||
}
|
||||
42
app/controller/IndexController.php
Normal file
42
app/controller/IndexController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return <<<EOF
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<iframe
|
||||
src="https://www.workerman.net/wellcome"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
|
||||
></iframe>
|
||||
EOF;
|
||||
}
|
||||
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
}
|
||||
4
app/functions.php
Normal file
4
app/functions.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
46
app/middleware/Cors.php
Normal file
46
app/middleware/Cors.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
class Cors implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$origin = (string)$request->header('origin', '');
|
||||
$allow = trim((string)(getenv('CORS_ALLOW_ORIGINS') ?: '*'));
|
||||
$allowOrigin = '';
|
||||
|
||||
if ($allow === '*') {
|
||||
$allowOrigin = '*';
|
||||
} else {
|
||||
$allowList = array_values(array_filter(array_map('trim', explode(',', $allow))));
|
||||
if ($origin !== '' && in_array($origin, $allowList, true)) {
|
||||
$allowOrigin = $origin;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Access-Control-Allow-Methods' => 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
|
||||
'Access-Control-Max-Age' => '86400',
|
||||
];
|
||||
|
||||
if ($allowOrigin !== '') {
|
||||
$headers['Access-Control-Allow-Origin'] = $allowOrigin;
|
||||
if ($allowOrigin !== '*') {
|
||||
$headers['Access-Control-Allow-Credentials'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
if (strtoupper($request->method()) === 'OPTIONS') {
|
||||
return response('', 204)->withHeaders($headers);
|
||||
}
|
||||
|
||||
$response = $handler($request);
|
||||
return $response->withHeaders($headers);
|
||||
}
|
||||
}
|
||||
|
||||
42
app/middleware/StaticFile.php
Normal file
42
app/middleware/StaticFile.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class StaticFile implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// Access to files beginning with. Is prohibited
|
||||
if (strpos($request->path(), '/.') !== false) {
|
||||
return response('<h1>403 forbidden</h1>', 403);
|
||||
}
|
||||
/** @var Response $response */
|
||||
$response = $handler($request);
|
||||
// Add cross domain HTTP header
|
||||
/*$response->withHeaders([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);*/
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
29
app/model/Test.php
Normal file
29
app/model/Test.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Test extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'test';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
}
|
||||
10
app/process/Http.php
Normal file
10
app/process/Http.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
|
||||
}
|
||||
305
app/process/Monitor.php
Normal file
305
app/process/Monitor.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class FileMonitor
|
||||
* @package process
|
||||
*/
|
||||
class Monitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $paths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $loadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $ppid = 0;
|
||||
|
||||
/**
|
||||
* Pause monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pause(): void
|
||||
{
|
||||
file_put_contents(static::lockFile(), time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resume(): void
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file(static::lockFile())) {
|
||||
unlink(static::lockFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether monitor is paused
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPaused(): bool
|
||||
{
|
||||
clearstatcache();
|
||||
return file_exists(static::lockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock file
|
||||
* @return string
|
||||
*/
|
||||
protected static function lockFile(): string
|
||||
{
|
||||
return runtime_path('monitor.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMonitor constructor.
|
||||
* @param $monitorDir
|
||||
* @param $monitorExtensions
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($monitorDir, $monitorExtensions, array $options = [])
|
||||
{
|
||||
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
|
||||
static::resume();
|
||||
$this->paths = (array)$monitorDir;
|
||||
$this->extensions = $monitorExtensions;
|
||||
foreach (get_included_files() as $index => $file) {
|
||||
$this->loadedFiles[$file] = $index;
|
||||
if (strpos($file, 'webman-framework/src/support/App.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
$disableFunctions = explode(',', ini_get('disable_functions'));
|
||||
if (in_array('exec', $disableFunctions, true)) {
|
||||
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
|
||||
} else {
|
||||
if ($options['enable_file_monitor'] ?? true) {
|
||||
Timer::add(1, function () {
|
||||
$this->checkAllFilesChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
|
||||
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
|
||||
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $monitorDir
|
||||
* @return bool
|
||||
*/
|
||||
public function checkFilesChange($monitorDir): bool
|
||||
{
|
||||
static $lastMtime, $tooManyFilesCheck;
|
||||
if (!$lastMtime) {
|
||||
$lastMtime = time();
|
||||
}
|
||||
clearstatcache();
|
||||
if (!is_dir($monitorDir)) {
|
||||
if (!is_file($monitorDir)) {
|
||||
return false;
|
||||
}
|
||||
$iterator = [new SplFileInfo($monitorDir)];
|
||||
} else {
|
||||
// recursive traversal directory
|
||||
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
|
||||
$iterator = new RecursiveIteratorIterator($dirIterator);
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($iterator as $file) {
|
||||
$count ++;
|
||||
/** @var SplFileInfo $file */
|
||||
if (is_dir($file->getRealPath())) {
|
||||
continue;
|
||||
}
|
||||
// check mtime
|
||||
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
|
||||
$lastMtime = $file->getMTime();
|
||||
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
|
||||
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
|
||||
continue;
|
||||
}
|
||||
$var = 0;
|
||||
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
|
||||
if ($var) {
|
||||
continue;
|
||||
}
|
||||
// send SIGUSR1 signal to master process for reload
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
if ($masterPid = $this->getMasterPid()) {
|
||||
echo $file . " updated and reload\n";
|
||||
posix_kill($masterPid, SIGUSR1);
|
||||
} else {
|
||||
echo "Master process has gone away and can not reload\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
echo $file . " updated and reload\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$tooManyFilesCheck && $count > 1000) {
|
||||
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
|
||||
$tooManyFilesCheck = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMasterPid(): int
|
||||
{
|
||||
if ($this->ppid === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
|
||||
echo "Master process has gone away\n";
|
||||
return $this->ppid = 0;
|
||||
}
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return $this->ppid;
|
||||
}
|
||||
$cmdline = "/proc/$this->ppid/cmdline";
|
||||
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
|
||||
// Process not exist
|
||||
$this->ppid = 0;
|
||||
}
|
||||
return $this->ppid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function checkAllFilesChange(): bool
|
||||
{
|
||||
if (static::isPaused()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->paths as $path) {
|
||||
if ($this->checkFilesChange($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $memoryLimit
|
||||
* @return void
|
||||
*/
|
||||
public function checkMemory($memoryLimit): void
|
||||
{
|
||||
if (static::isPaused() || $memoryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
$masterPid = $this->getMasterPid();
|
||||
if ($masterPid <= 0) {
|
||||
echo "Master process has gone away\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
|
||||
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
|
||||
return;
|
||||
}
|
||||
foreach (explode(' ', $children) as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$statusFile = "/proc/$pid/status";
|
||||
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
|
||||
continue;
|
||||
}
|
||||
$mem = 0;
|
||||
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
|
||||
$mem = $match[1];
|
||||
}
|
||||
$mem = (int)($mem / 1024);
|
||||
if ($mem >= $memoryLimit) {
|
||||
posix_kill($pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory limit
|
||||
* @param $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
protected function getMemoryLimit($memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === 0) {
|
||||
return 0;
|
||||
}
|
||||
$usePhpIni = false;
|
||||
if (!$memoryLimit) {
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$usePhpIni = true;
|
||||
}
|
||||
|
||||
if ($memoryLimit == -1) {
|
||||
return 0;
|
||||
}
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
|
||||
$memoryLimit = (int)$memoryLimit;
|
||||
if ($unit === 'g') {
|
||||
$memoryLimit = 1024 * $memoryLimit;
|
||||
} else if ($unit === 'k') {
|
||||
$memoryLimit = ($memoryLimit / 1024);
|
||||
} else if ($unit === 'm') {
|
||||
$memoryLimit = (int)($memoryLimit);
|
||||
} else if ($unit === 't') {
|
||||
$memoryLimit = (1024 * 1024 * $memoryLimit);
|
||||
} else {
|
||||
$memoryLimit = ($memoryLimit / (1024 * 1024));
|
||||
}
|
||||
if ($memoryLimit < 50) {
|
||||
$memoryLimit = 50;
|
||||
}
|
||||
if ($usePhpIni) {
|
||||
$memoryLimit = (0.8 * $memoryLimit);
|
||||
}
|
||||
return (int)$memoryLimit;
|
||||
}
|
||||
|
||||
}
|
||||
14
app/view/index/view.html
Normal file
14
app/view/index/view.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user