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',
];
}