feat: 微客宝H5客户端 - 基础架构+核心页面

This commit is contained in:
li
2026-03-14 19:44:03 +08:00
parent 54d2f617bb
commit 0e974da09c
3738 changed files with 811180 additions and 21 deletions
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Customer;
use App\Models\Finance\AccountTransaction;
use App\Models\Finance\CustomerAccount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AccountController extends Controller
{
/**
* 我的账户列表
*/
public function index(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$accounts = CustomerAccount::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->get();
return $this->success($accounts);
}
/**
* 账户流水记录(分页)
*/
public function transactions(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = AccountTransaction::withoutGlobalScope('store')
->whereHas('account', fn ($q) => $q->where('customer_id', $customer->id));
if ($request->filled('account_id')) {
$query->where('customer_account_id', (int) $request->input('account_id'));
}
return $this->paginate(
$query->orderByDesc('id')->paginate(20)
);
}
}
@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Customer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AuthController extends Controller
{
/**
* 客户端登录(手机号 + 密码)
*/
public function login(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'password' => 'required|string',
]);
$customer = Customer::withoutGlobalScope('store')
->where('phone', $request->phone)
->first();
if (!$customer || !$customer->password) {
return $this->error('手机号或密码错误', 40100, 401);
}
if (!\Hash::check($request->password, $customer->password)) {
return $this->error('手机号或密码错误', 40100, 401);
}
if ($customer->status !== 1) {
return $this->error('账号已停用,请联系门店', 40101, 403);
}
// 清除旧 client token
$customer->tokens()->where('name', 'client')->delete();
$token = $customer->createToken('client')->plainTextToken;
$customer->update(['last_login_at' => now()]);
return $this->success([
'token' => $token,
'customer' => $this->formatCustomer($customer),
]);
}
/**
* 退出登录
*/
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return $this->success(null, '退出成功');
}
/**
* 获取当前客户信息
*/
public function me(Request $request): JsonResponse
{
$customer = $request->user()->load(['families', 'contracts']);
return $this->success($this->formatCustomer($customer));
}
/**
* 修改密码
*/
public function changePassword(Request $request): JsonResponse
{
$request->validate([
'old_password' => 'required|string',
'new_password' => 'required|string|min:6',
]);
$customer = $request->user();
if (!\Hash::check($request->old_password, $customer->password)) {
return $this->error('原密码不正确', 40001);
}
$customer->update(['password' => $request->new_password]);
return $this->success(null, '密码修改成功');
}
private function formatCustomer(Customer $c): array
{
return [
'id' => $c->id,
'name' => $c->name,
'phone' => $c->phone,
'wechat' => $c->wechat,
'store_id' => $c->store_id,
'expected_date' => $c->expected_date?->toDateString(),
'actual_date' => $c->actual_date?->toDateString(),
'baby_count' => $c->baby_count,
'status' => $c->status,
'families' => $c->relationLoaded('families') ? $c->families : [],
'contracts' => $c->relationLoaded('contracts') ? $c->contracts : [],
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Care\CareProfile;
use App\Models\Care\CareRecord;
use App\Models\Care\HealthMetric;
use App\Models\Crm\Customer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CareController extends Controller
{
/**
* 获取当前客户护理档案
*/
public function profile(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$profile = CareProfile::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->first();
if ($profile) {
$profile->load(['plans']);
}
return $this->success($profile);
}
/**
* 护理记录列表(分页)
*/
public function records(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$profile = CareProfile::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->first();
if (!$profile) {
return $this->paginate(
CareRecord::withoutGlobalScope('store')->whereRaw('0=1')->paginate(10)
);
}
$query = CareRecord::withoutGlobalScope('store')
->where('care_profile_id', $profile->id)
->with('nurse:id,name');
if ($request->filled('type')) {
$query->where('type', (int) $request->input('type'));
}
return $this->paginate($query->latest('recorded_at')->paginate(10));
}
/**
* 健康指标列表(最近30条)
*/
public function metrics(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$profile = CareProfile::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->first();
if (!$profile) {
return $this->success([]);
}
$query = HealthMetric::withoutGlobalScope('store')
->where('care_profile_id', $profile->id);
if ($request->filled('metric_type')) {
$query->where('metric_type', $request->input('metric_type'));
}
$list = $query->orderByDesc('measured_at')->limit(30)->get();
return $this->success($list);
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Complaint;
use App\Models\Crm\Customer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ComplaintController extends Controller
{
/**
* 我的投诉列表(分页)
*/
public function index(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = Complaint::withoutGlobalScope('store')
->where('customer_id', $customer->id);
return $this->paginate(
$query->orderByDesc('id')->paginate(10)
);
}
/**
* 提交投诉
*/
public function store(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$validated = $request->validate([
'type' => 'required|string|max:50',
'content' => 'required|string|max:1000',
'images' => 'nullable|array',
'images.*' => 'nullable|string|max:500',
]);
$complaint = Complaint::create([
'customer_id' => $customer->id,
'store_id' => $customer->store_id,
'type' => $validated['type'],
'content' => $validated['content'],
'images' => $validated['images'] ?? null,
'status' => 1,
]);
return $this->success($complaint);
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Care\CareProfile;
use App\Models\Care\CareRecord;
use App\Models\Crm\Contract;
use App\Models\Crm\Customer;
use App\Models\Meal\DailyMealPlan;
use App\Models\Nanny\NannyOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class HomeController extends Controller
{
/**
* 首页汇总数据
*/
public function index(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
// 当前客户基本信息
$customerInfo = [
'name' => $customer->name,
'phone' => $customer->phone,
'expected_date' => $customer->expected_date,
'actual_date' => $customer->actual_date,
];
// 护理档案 → 最新护理记录3条
$careProfile = CareProfile::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->first();
$careRecords = [];
if ($careProfile) {
$careRecords = CareRecord::withoutGlobalScope('store')
->where('care_profile_id', $careProfile->id)
->with('nurse:id,name')
->latest('recorded_at')
->limit(3)
->get();
}
// 今日膳食
$todayMeals = DailyMealPlan::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->whereDate('plan_date', today())
->get();
// 当前合同(最新1条)
$contract = Contract::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->latest()
->first(['id', 'contract_no', 'status', 'start_date', 'end_date', 'total_amount']);
// 月嫂信息(进行中派单)
$nannyOrder = NannyOrder::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->where('status', 2)
->latest()
->first();
if ($nannyOrder) {
$nannyOrder->load('nanny:id,name,level,phone');
}
return $this->success(compact(
'customerInfo',
'careRecords',
'todayMeals',
'contract',
'nannyOrder'
));
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Customer;
use App\Models\Meal\DailyMealPlan;
use App\Models\Meal\MealReview;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class MealController extends Controller
{
/**
* 我的用膳列表(分页)
*/
public function list(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = DailyMealPlan::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->with('review');
// 按指定日期筛选
if ($request->filled('date')) {
$query->whereDate('plan_date', $request->input('date'));
}
// 最近N天
if ($request->filled('days') && !$request->filled('date')) {
$days = (int) $request->input('days', 7);
$query->where('plan_date', '>=', Carbon::today()->subDays($days - 1));
}
return $this->paginate(
$query->orderByDesc('plan_date')->paginate(20)
);
}
/**
* 今日用膳
*/
public function today(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$list = DailyMealPlan::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->whereDate('plan_date', today())
->get();
return $this->success($list);
}
/**
* 提交膳食点评
*/
public function review(Request $request, int $planId): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$validated = $request->validate([
'rating' => 'required|integer|min:1|max:5',
'content' => 'nullable|string|max:500',
]);
// 确认该膳食计划属于当前客户
$plan = DailyMealPlan::withoutGlobalScope('store')
->where('id', $planId)
->where('customer_id', $customer->id)
->first();
if (!$plan) {
return $this->error('膳食记录不存在或无权操作', 40401, 404);
}
$review = MealReview::updateOrCreate(
['daily_meal_plan_id' => $planId],
[
'score' => $validated['rating'],
'content' => $validated['content'] ?? null,
'customer_id' => $customer->id,
]
);
return $this->success($review);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Customer;
use App\Models\Nanny\Nanny;
use App\Models\Nanny\NannyOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyController extends Controller
{
/**
* 当前派单月嫂信息
*/
public function myNanny(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$order = NannyOrder::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->whereIn('status', [2, 3])
->latest()
->first();
if (!$order) {
return $this->success(null);
}
$order->load('nanny');
return $this->success([
'order' => $order,
'nanny' => $order->nanny,
]);
}
/**
* 月嫂订单列表(分页)
*/
public function orders(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = NannyOrder::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->with('nanny:id,name,level,phone');
return $this->paginate(
$query->orderByDesc('id')->paginate(15)
);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Models\Crm\Customer;
use App\Models\Service\ServiceExecution;
use App\Models\Service\ServiceOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller
{
/**
* 我的服务订单(分页)
*/
public function orders(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = ServiceOrder::withoutGlobalScope('store')
->where('customer_id', $customer->id)
->with('package:id,name', 'item:id,name');
if ($request->filled('status')) {
$query->where('status', (int) $request->input('status'));
}
return $this->paginate(
$query->orderByDesc('id')->paginate(15)
);
}
/**
* 服务执行记录(分页)
*/
public function executions(Request $request): JsonResponse
{
/** @var Customer $customer */
$customer = $request->user();
$query = ServiceExecution::withoutGlobalScope('store')
->whereHas('order', fn ($q) => $q->where('customer_id', $customer->id))
->with('technician:id,name', 'order:id,status');
return $this->paginate(
$query->orderByDesc('executed_at')->paginate(15)
);
}
}
+16 -11
View File
@@ -4,30 +4,35 @@ namespace App\Models\Crm;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class Customer extends Model
class Customer extends Authenticatable
{
use BelongsToStore, SoftDeletes;
use BelongsToStore, SoftDeletes, HasApiTokens;
protected $fillable = [
'store_id', 'lead_id', 'name', 'phone', 'id_card', 'wechat',
'birthday', 'expected_date', 'actual_date', 'baby_count',
'tags', 'status', 'owner_id', 'remark',
'store_id', 'lead_id', 'name', 'phone', 'password', 'open_id',
'id_card', 'wechat', 'birthday', 'expected_date', 'actual_date',
'baby_count', 'tags', 'status', 'owner_id', 'remark', 'last_login_at',
];
protected $hidden = ['password'];
protected function casts(): array
{
return [
'birthday' => 'date',
'birthday' => 'date',
'expected_date' => 'date',
'actual_date' => 'date',
'baby_count' => 'integer',
'tags' => 'array',
'status' => 'integer',
'actual_date' => 'date',
'baby_count' => 'integer',
'tags' => 'array',
'status' => 'integer',
'last_login_at' => 'datetime',
'password' => 'hashed',
];
}
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->string('password')->nullable()->after('phone')->comment('客户端登录密码');
$table->string('open_id', 100)->nullable()->unique()->after('password')->comment('微信 openid');
$table->timestamp('last_login_at')->nullable()->after('open_id');
});
}
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn(['password', 'open_id', 'last_login_at']);
});
}
};
+55
View File
@@ -399,3 +399,58 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
Route::put('articles/{kbArticle}/offline', [KbArticleController::class, 'offline']);
});
});
// ===========================================================================
// 客户端(微客宝 H5)路由 prefix: /api/v1/client/
// ===========================================================================
Route::prefix('client')->group(function () {
// ----- 公开路由 -----
Route::post('auth/login', [\App\Http\Controllers\Client\AuthController::class, 'login']);
// ----- 需要认证(Customer Sanctum token-----
Route::middleware('auth:sanctum')->group(function () {
// 认证
Route::post('auth/logout', [\App\Http\Controllers\Client\AuthController::class, 'logout']);
Route::get('auth/me', [\App\Http\Controllers\Client\AuthController::class, 'me']);
Route::post('auth/change-password', [\App\Http\Controllers\Client\AuthController::class, 'changePassword']);
// 首页汇总
Route::get('home', [\App\Http\Controllers\Client\HomeController::class, 'index']);
// 护理
Route::prefix('care')->group(function () {
Route::get('profile', [\App\Http\Controllers\Client\CareController::class, 'profile']);
Route::get('records', [\App\Http\Controllers\Client\CareController::class, 'records']);
Route::get('metrics', [\App\Http\Controllers\Client\CareController::class, 'metrics']);
});
// 膳食
Route::prefix('meal')->group(function () {
Route::get('list', [\App\Http\Controllers\Client\MealController::class, 'list']);
Route::get('today', [\App\Http\Controllers\Client\MealController::class, 'today']);
Route::post('review/{planId}', [\App\Http\Controllers\Client\MealController::class, 'review']);
});
// 财务账户
Route::prefix('account')->group(function () {
Route::get('/', [\App\Http\Controllers\Client\AccountController::class, 'index']);
Route::get('transactions', [\App\Http\Controllers\Client\AccountController::class, 'transactions']);
});
// 服务订单
Route::prefix('service')->group(function () {
Route::get('orders', [\App\Http\Controllers\Client\ServiceController::class, 'orders']);
Route::get('executions', [\App\Http\Controllers\Client\ServiceController::class, 'executions']);
});
// 月嫂
Route::prefix('nanny')->group(function () {
Route::get('my', [\App\Http\Controllers\Client\NannyController::class, 'myNanny']);
Route::get('orders', [\App\Http\Controllers\Client\NannyController::class, 'orders']);
});
// 投诉反馈
Route::get('complaints', [\App\Http\Controllers\Client\ComplaintController::class, 'index']);
Route::post('complaints', [\App\Http\Controllers\Client\ComplaintController::class, 'store']);
});
});