From 1d5930dc4c407b36eeb179fb73081cb431ef10b8 Mon Sep 17 00:00:00 2001 From: molong Date: Mon, 19 Jan 2026 14:52:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8E=E7=AB=AF=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Account/app/Controllers/Admin/Bill.php | 101 +++++ .../Account/app/Controllers/Admin/Family.php | 152 +++++++ .../app/Controllers/Admin/Statistics.php | 101 +++++ .../Account/app/Services/AdminBillService.php | 311 +++++++++++++ .../app/Services/AdminFamilyService.php | 422 ++++++++++++++++++ .../app/Services/AdminStatisticsService.php | 303 +++++++++++++ .../seeders/AccountDatabaseSeeder.php | 33 +- modules/Account/routes/admin.php | 28 +- 8 files changed, 1448 insertions(+), 3 deletions(-) create mode 100644 modules/Account/app/Controllers/Admin/Bill.php create mode 100644 modules/Account/app/Controllers/Admin/Family.php create mode 100644 modules/Account/app/Controllers/Admin/Statistics.php create mode 100644 modules/Account/app/Services/AdminBillService.php create mode 100644 modules/Account/app/Services/AdminFamilyService.php create mode 100644 modules/Account/app/Services/AdminStatisticsService.php diff --git a/modules/Account/app/Controllers/Admin/Bill.php b/modules/Account/app/Controllers/Admin/Bill.php new file mode 100644 index 0000000..8f36814 --- /dev/null +++ b/modules/Account/app/Controllers/Admin/Bill.php @@ -0,0 +1,101 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Controllers\Admin; + +use Illuminate\Http\Request; +use App\Http\Controllers\BaseController; +use Modules\Account\Services\AdminBillService; + +class Bill extends BaseController { + + /** + * @title 账单列表 + * + * @param Request $request + * @param BillService $service + * @return void + */ + public function index(Request $request, AdminBillService $service) { + try { + $this->data['data'] = $service->getDataList($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 添加账单 + * + * @param Request $request + * @param BillService $service + * @return void + */ + public function add(Request $request, AdminBillService $service) { + try { + $this->data['data'] = $service->create($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 编辑账单 + * + * @param Request $request + * @param BillService $service + * @return void + */ + public function edit(Request $request, AdminBillService $service) { + try { + $this->data['data'] = $service->update($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 删除账单 + * + * @param Request $request + * @param BillService $service + * @return void + */ + public function delete(Request $request, AdminBillService $service) { + try { + $this->data['data'] = $service->delete($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 账单详情 + * + * @param Request $request + * @param BillService $service + * @return void + */ + public function detail(Request $request, AdminBillService $service) { + try { + $this->data['data'] = $service->detail($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Controllers/Admin/Family.php b/modules/Account/app/Controllers/Admin/Family.php new file mode 100644 index 0000000..fa97f82 --- /dev/null +++ b/modules/Account/app/Controllers/Admin/Family.php @@ -0,0 +1,152 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Controllers\Admin; + +use Illuminate\Http\Request; +use App\Http\Controllers\BaseController; +use Modules\Account\Services\AdminFamilyService; + +class Family extends BaseController { + + /** + * @title 家庭列表 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function index(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->getDataList($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 添加家庭 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function add(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->create($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 编辑家庭 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function edit(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->update($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 删除家庭 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function delete(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->delete($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 家庭详情 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function detail(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->detail($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 家庭成员列表 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function members(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->getMembers($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 添加家庭成员 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function addMember(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->addMember($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 移除家庭成员 + * + * @param Request $request + * @param AdminFamilyService $service + * @return void + */ + public function removeMember(Request $request, AdminFamilyService $service) { + try { + $this->data['data'] = $service->removeMember($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Controllers/Admin/Statistics.php b/modules/Account/app/Controllers/Admin/Statistics.php new file mode 100644 index 0000000..468c8af --- /dev/null +++ b/modules/Account/app/Controllers/Admin/Statistics.php @@ -0,0 +1,101 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Controllers\Admin; + +use Illuminate\Http\Request; +use App\Http\Controllers\BaseController; +use Modules\Account\Services\AdminStatisticsService; + +class Statistics extends BaseController { + + /** + * @title 概览统计 + * + * @param Request $request + * @param AdminStatisticsService $service + * @return void + */ + public function overview(Request $request, AdminStatisticsService $service) { + try { + $this->data['data'] = $service->getOverview($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 趋势统计 + * + * @param Request $request + * @param AdminStatisticsService $service + * @return void + */ + public function trend(Request $request, AdminStatisticsService $service) { + try { + $this->data['data'] = $service->getTrend($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 分类统计 + * + * @param Request $request + * @param AdminStatisticsService $service + * @return void + */ + public function category(Request $request, AdminStatisticsService $service) { + try { + $this->data['data'] = $service->getCategoryStatistics($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 用户统计 + * + * @param Request $request + * @param AdminStatisticsService $service + * @return void + */ + public function user(Request $request, AdminStatisticsService $service) { + try { + $this->data['data'] = $service->getUserStatistics($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } + + /** + * @title 家庭统计 + * + * @param Request $request + * @param AdminStatisticsService $service + * @return void + */ + public function family(Request $request, AdminStatisticsService $service) { + try { + $this->data['data'] = $service->getFamilyStatistics($request); + } catch (\Exception $e) { + $this->data['code'] = 0; + $this->data['message'] = $e->getMessage(); + } + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Services/AdminBillService.php b/modules/Account/app/Services/AdminBillService.php new file mode 100644 index 0000000..9857904 --- /dev/null +++ b/modules/Account/app/Services/AdminBillService.php @@ -0,0 +1,311 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Services; + +use Illuminate\Http\Request; +use Modules\Account\Models\Bill; + +class AdminBillService { + + /** + * @title 获取账单列表 + * + * @param Request $request + * @return void + */ + public function getDataList($request) { + $map = []; + + // 用户筛选 + if ($request->filled('user_id')) { + $map[] = ['user_id', '=', $request->input('user_id')]; + } + + // 家庭筛选 + if ($request->filled('family_id')) { + $map[] = ['family_id', '=', $request->input('family_id')]; + } + + // 类型筛选 + if ($request->filled('type') && in_array($request->input('type'), ['income', 'expense'])) { + $map[] = ['type', '=', $request->input('type')]; + } + + // 分类筛选 + if ($request->filled('category')) { + $map[] = ['category', 'like', '%' . $request->input('category') . '%']; + } + + // 支付方式筛选 + if ($request->filled('payment_method')) { + $map[] = ['payment_method', '=', $request->input('payment_method')]; + } + + // 备注搜索 + if ($request->filled('remark')) { + $map[] = ['remark', 'like', '%' . $request->input('remark') . '%']; + } + + // 日期范围筛选 + if ($request->filled('start_date')) { + $map[] = ['bill_date', '>=', $request->input('start_date')]; + } + if ($request->filled('end_date')) { + $map[] = ['bill_date', '<=', $request->input('end_date')]; + } + + // 年月筛选 + if ($request->filled('year') && $request->filled('month')) { + $startDate = $request->input('year') . '-' . str_pad($request->input('month'), 2, '0', STR_PAD_LEFT) . '-01'; + $endDate = $request->input('year') . '-' . str_pad($request->input('month'), 2, '0', STR_PAD_LEFT) . '-31'; + // 清除之前的日期条件 + $map = array_filter($map, function($item) { + return !isset($item[0]) || !in_array($item[0], ['bill_date']); + }); + $map[] = ['bill_date', '>=', $startDate]; + $map[] = ['bill_date', '<=', $endDate]; + } + + $query = Bill::with(['user:uid,nickname,username', 'family:id,name']); + + $query->where($map); + + // 排序 + $query->orderBy($request->input('order', 'bill_date'), $request->input('sort', 'desc')); + + // 分页 + if ($request->filled('page')) { + $data = [ + 'total' => $query->count(), + 'page' => $request->input('page', 1), + 'data' => $query->offset($request->input('offset', 0)) + ->limit($request->input('limit', 10)) + ->get(), + ]; + } else { + $data = $query->get(); + } + + // 计算统计数据 + if ($request->filled('page')) { + $statsQuery = Bill::query()->where($map); + $totalExpense = (clone $statsQuery)->where('type', 'expense')->sum('amount'); + $totalIncome = (clone $statsQuery)->where('type', 'income')->sum('amount'); + $data['statistics'] = [ + 'total_expense' => (float)$totalExpense, + 'total_income' => (float)$totalIncome, + 'total_balance' => (float)($totalIncome - $totalExpense), + 'total_count' => $data['total'] + ]; + } + + return $data; + } + + /** + * @title 添加账单 + * + * @param Request $request + * @return void + */ + public function create($request) { + $request->validate([ + 'user_id' => 'required|integer', + 'type' => 'required|in:income,expense', + 'amount' => 'required|numeric|min:0.01', + 'category_id' => 'required|integer', + 'payment_method' => 'nullable|string|in:微信,支付宝,银行卡,现金,其他', + 'remark' => 'nullable|string|max:255', + 'date' => 'required|date', + 'family_id' => 'nullable|integer|exists:account_families,id' + ], [ + 'user_id.required' => '请选择用户', + 'type.required' => '请选择账单类型', + 'amount.required' => '请输入金额', + 'category_id.required' => '请选择分类', + 'date.required' => '请选择日期' + ]); + + $data = $request->all(); + + // 将category_id转换为category字符串 + $categoryMap = $this->getCategoryMap($data['type']); + if (!isset($categoryMap[$data['category_id']])) { + throw new \Exception('分类不存在'); + } + + $data['category'] = $categoryMap[$data['category_id']]; + $data['bill_date'] = $data['date']; + + // 移除前端字段 + unset($data['category_id'], $data['date']); + + return Bill::create($data); + } + + /** + * @title 更新账单 + * + * @param Request $request + * @return void + */ + public function update($request) { + $request->validate([ + 'id' => 'required|integer', + 'user_id' => 'required|integer', + 'type' => 'required|in:income,expense', + 'amount' => 'required|numeric|min:0.01', + 'category_id' => 'required|integer', + 'payment_method' => 'nullable|string|in:微信,支付宝,银行卡,现金,其他', + 'remark' => 'nullable|string|max:255', + 'date' => 'required|date', + 'family_id' => 'nullable|integer|exists:account_families,id' + ], [ + 'id.required' => '请提供账单ID', + 'user_id.required' => '请选择用户', + 'type.required' => '请选择账单类型', + 'amount.required' => '请输入金额', + 'category_id.required' => '请选择分类', + 'date.required' => '请选择日期' + ]); + + $bill = Bill::findOrFail($request->input('id')); + $data = $request->all(); + + // 将category_id转换为category字符串 + $categoryMap = $this->getCategoryMap($data['type']); + if (!isset($categoryMap[$data['category_id']])) { + throw new \Exception('分类不存在'); + } + + $data['category'] = $categoryMap[$data['category_id']]; + $data['bill_date'] = $data['date']; + + // 移除前端字段 + unset($data['category_id'], $data['date'], $data['id']); + + $bill->update($data); + + return $bill; + } + + /** + * @title 删除账单 + * + * @param Request $request + * @return void + */ + public function delete($request) { + if ($request->filled('id')) { + try { + $bill = Bill::findOrFail($request->input('id')); + $bill->delete(); + } catch (\Throwable $th) { + throw new \Exception("账单不存在!", 1); + } + } + + if ($request->filled('ids')) { + try { + $bill = Bill::whereIn('id', $request->input('ids')); + $bill->delete(); + } catch (\Throwable $th) { + throw new \Exception($th->getMessage(), 1); + } + } + + return $bill; + } + + /** + * @title 账单详情 + * + * @param Request $request + * @return void + */ + public function detail($request) { + $id = $request->input('id'); + + $bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_id']) + ->findOrFail($id); + + return $bill; + } + + /** + * @title 获取分类映射 + * + * @param string $type + * @return array + */ + private function getCategoryMap($type) { + if ($type === 'income') { + return [ + 101 => '工资', + 102 => '奖金', + 103 => '投资', + 104 => '兼职', + 105 => '其他' + ]; + } else { + return [ + 1 => '餐饮', + 2 => '交通', + 3 => '购物', + 4 => '娱乐', + 5 => '医疗', + 6 => '教育', + 7 => '居住', + 8 => '其他' + ]; + } + } + + /** + * @title 获取分类列表 + * + * @return array + */ + public function getCategoryList() { + return [ + 'expense' => [ + ['id' => 1, 'name' => '餐饮'], + ['id' => 2, 'name' => '交通'], + ['id' => 3, 'name' => '购物'], + ['id' => 4, 'name' => '娱乐'], + ['id' => 5, 'name' => '医疗'], + ['id' => 6, 'name' => '教育'], + ['id' => 7, 'name' => '居住'], + ['id' => 8, 'name' => '其他'] + ], + 'income' => [ + ['id' => 101, 'name' => '工资'], + ['id' => 102, 'name' => '奖金'], + ['id' => 103, 'name' => '投资'], + ['id' => 104, 'name' => '兼职'], + ['id' => 105, 'name' => '其他'] + ] + ]; + } + + /** + * @title 获取支付方式列表 + * + * @return array + */ + public function getPaymentMethods() { + return [ + ['value' => '微信', 'label' => '微信'], + ['value' => '支付宝', 'label' => '支付宝'], + ['value' => '银行卡', 'label' => '银行卡'], + ['value' => '现金', 'label' => '现金'], + ['value' => '其他', 'label' => '其他'] + ]; + } +} diff --git a/modules/Account/app/Services/AdminFamilyService.php b/modules/Account/app/Services/AdminFamilyService.php new file mode 100644 index 0000000..bb55582 --- /dev/null +++ b/modules/Account/app/Services/AdminFamilyService.php @@ -0,0 +1,422 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Services; + +use Illuminate\Http\Request; +use Modules\Account\Models\Family; +use Modules\Account\Models\FamilyMember; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; + +class AdminFamilyService { + + /** + * @title 获取家庭列表 + * + * @param Request $request + * @return void + */ + public function getDataList($request) { + $map = []; + + // 名称搜索 + if ($request->filled('name')) { + $map[] = ['name', 'like', '%' . $request->input('name') . '%']; + } + + // 家主筛选 + if ($request->filled('owner_id')) { + $map[] = ['owner_id', '=', $request->input('owner_id')]; + } + + // 邀请码筛选 + if ($request->filled('invite_code')) { + $map[] = ['invite_code', '=', $request->input('invite_code')]; + } + + // 日期范围筛选 + if ($request->filled('start_date')) { + $map[] = ['created_at', '>=', $request->input('start_date')]; + } + if ($request->filled('end_date')) { + $map[] = ['created_at', '<=', $request->input('end_date')]; + } + + $query = Family::with(['owner:uid,nickname,username', 'members:uid,nickname,username']); + + $query->where($map); + + // 排序 + $query->orderBy($request->input('order', 'created_at'), $request->input('sort', 'desc')); + + // 分页 + if ($request->filled('page')) { + $data = [ + 'total' => $query->count(), + 'page' => $request->input('page', 1), + 'data' => $query->offset($request->input('offset', 0)) + ->limit($request->input('limit', 10)) + ->get(), + ]; + } else { + $data = $query->get(); + } + + // 添加成员数量统计 + if ($request->filled('page')) { + foreach ($data['data'] as $family) { + $family->member_count = $family->members->count(); + } + } + + return $data; + } + + /** + * @title 创建家庭 + * + * @param Request $request + * @return void + */ + public function create($request) { + $request->validate([ + 'name' => 'required|string|max:50', + 'owner_id' => 'required|integer' + ], [ + 'name.required' => '请输入家庭名称', + 'owner_id.required' => '请选择家主' + ]); + + $data = $request->all(); + + // 生成10位邀请码 + do { + $inviteCode = strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10)); + } while (Family::where('invite_code', $inviteCode)->exists()); + + $data['invite_code'] = $inviteCode; + + DB::beginTransaction(); + try { + $family = Family::create($data); + + // 家主自动加入家庭 + FamilyMember::create([ + 'family_id' => $family->id, + 'user_id' => $data['owner_id'] + ]); + + DB::commit(); + + return $family; + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + } + + /** + * @title 更新家庭 + * + * @param Request $request + * @return void + */ + public function update($request) { + $request->validate([ + 'id' => 'required|integer', + 'name' => 'required|string|max:50', + 'owner_id' => 'required|integer' + ], [ + 'id.required' => '请提供家庭ID', + 'name.required' => '请输入家庭名称', + 'owner_id.required' => '请选择家主' + ]); + + $family = Family::findOrFail($request->input('id')); + $data = $request->all(); + + unset($data['id']); + + $family->update($data); + + return $family; + } + + /** + * @title 删除家庭 + * + * @param Request $request + * @return void + */ + public function delete($request) { + if ($request->filled('id')) { + try { + $family = Family::findOrFail($request->input('id')); + DB::beginTransaction(); + try { + // 删除家庭成员 + FamilyMember::where('family_id', $family->id)->delete(); + // 删除家庭 + $family->delete(); + DB::commit(); + } catch (\Throwable $th) { + DB::rollBack(); + throw $th; + } + } catch (\Throwable $th) { + throw new \Exception("家庭不存在!", 1); + } + } + + if ($request->filled('ids')) { + try { + $families = Family::whereIn('id', $request->input('ids'))->get(); + DB::beginTransaction(); + try { + foreach ($families as $family) { + // 删除家庭成员 + FamilyMember::where('family_id', $family->id)->delete(); + } + // 删除家庭 + $families->each->delete(); + DB::commit(); + } catch (\Throwable $th) { + DB::rollBack(); + throw $th; + } + } catch (\Throwable $th) { + throw new \Exception($th->getMessage(), 1); + } + } + + return $family; + } + + /** + * @title 家庭详情 + * + * @param Request $request + * @return void + */ + public function detail($request) { + $id = $request->input('id'); + + $family = Family::with(['owner:uid,nickname,username', 'members:uid,nickname,username,email']) + ->findOrFail($id); + + // 添加成员数量 + $family->member_count = $family->members->count(); + + // 添加统计信息 + $billCount = DB::table('account_bills')->where('family_id', $family->id)->count(); + $totalExpense = DB::table('account_bills') + ->where('family_id', $family->id) + ->where('type', 'expense') + ->sum('amount'); + $totalIncome = DB::table('account_bills') + ->where('family_id', $family->id) + ->where('type', 'income') + ->sum('amount'); + + $family->statistics = [ + 'bill_count' => $billCount, + 'total_expense' => (float)$totalExpense, + 'total_income' => (float)$totalIncome, + 'total_balance' => (float)($totalIncome - $totalExpense) + ]; + + return $family; + } + + /** + * @title 获取家庭成员列表 + * + * @param Request $request + * @return void + */ + public function getMembers($request) { + $familyId = $request->input('family_id'); + + if (!$familyId) { + throw new \Exception('请提供家庭ID'); + } + + $family = Family::findOrFail($familyId); + + $members = FamilyMember::with('user:uid,nickname,username,email') + ->where('family_id', $familyId) + ->get(); + + return [ + 'family_id' => $familyId, + 'family_name' => $family->name, + 'owner_id' => $family->owner_id, + 'members' => $members->map(function($member) use ($family) { + return [ + 'id' => $member->id, + 'user_id' => $member->user_id, + 'name' => $member->user->nickname ?? $member->user->username, + 'username' => $member->user->username, + 'email' => $member->user->email, + 'is_owner' => $family->owner_id == $member->user_id, + 'joined_at' => $member->created_at + ]; + })->toArray(), + 'total' => $members->count() + ]; + } + + /** + * @title 添加家庭成员 + * + * @param Request $request + * @return void + */ + public function addMember($request) { + $request->validate([ + 'family_id' => 'required|integer', + 'user_id' => 'required|integer' + ], [ + 'family_id.required' => '请提供家庭ID', + 'user_id.required' => '请选择用户' + ]); + + $familyId = $request->input('family_id'); + $userId = $request->input('user_id'); + + // 检查家庭是否存在 + $family = Family::findOrFail($familyId); + + // 检查用户是否已在家庭中 + $existingMember = FamilyMember::where('family_id', $familyId) + ->where('user_id', $userId) + ->first(); + + if ($existingMember) { + throw new \Exception('该用户已在家庭中'); + } + + // 检查用户是否已在其他家庭中 + $userInOtherFamily = FamilyMember::where('user_id', $userId)->first(); + if ($userInOtherFamily) { + throw new \Exception('该用户已在其他家庭中'); + } + + // 添加家庭成员 + FamilyMember::create([ + 'family_id' => $familyId, + 'user_id' => $userId + ]); + + return ['message' => '成员已添加']; + } + + /** + * @title 移除家庭成员 + * + * @param Request $request + * @return void + */ + public function removeMember($request) { + $request->validate([ + 'family_id' => 'required|integer', + 'user_id' => 'required|integer' + ], [ + 'family_id.required' => '请提供家庭ID', + 'user_id.required' => '请提供用户ID' + ]); + + $familyId = $request->input('family_id'); + $userId = $request->input('user_id'); + + // 检查家庭是否存在 + $family = Family::findOrFail($familyId); + + // 不能移除家主 + if ($family->owner_id == $userId) { + throw new \Exception('不能移除家主,请先转让家主'); + } + + // 移除成员 + $member = FamilyMember::where('family_id', $familyId) + ->where('user_id', $userId) + ->first(); + + if (!$member) { + throw new \Exception('该用户不在家庭中'); + } + + $member->delete(); + + return ['message' => '成员已移除']; + } + + /** + * @title 重新生成邀请码 + * + * @param Request $request + * @return void + */ + public function regenerateInviteCode($request) { + $familyId = $request->input('family_id'); + + if (!$familyId) { + throw new \Exception('请提供家庭ID'); + } + + $family = Family::findOrFail($familyId); + + // 生成新邀请码 + do { + $inviteCode = strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10)); + } while (Family::where('invite_code', $inviteCode)->exists()); + + $family->update(['invite_code' => $inviteCode]); + + return [ + 'invite_code' => $inviteCode, + 'family_name' => $family->name + ]; + } + + /** + * @title 转让家主 + * + * @param Request $request + * @return void + */ + public function transferOwner($request) { + $request->validate([ + 'family_id' => 'required|integer', + 'user_id' => 'required|integer' + ], [ + 'family_id.required' => '请提供家庭ID', + 'user_id.required' => '请提供用户ID' + ]); + + $familyId = $request->input('family_id'); + $userId = $request->input('user_id'); + + $family = Family::findOrFail($familyId); + + // 目标用户必须在家庭中 + $targetMember = FamilyMember::where('family_id', $familyId) + ->where('user_id', $userId) + ->first(); + + if (!$targetMember) { + throw new \Exception('该用户不在家庭中'); + } + + // 转让家主 + $family->update(['owner_id' => $userId]); + + return ['message' => '家主已转让']; + } +} diff --git a/modules/Account/app/Services/AdminStatisticsService.php b/modules/Account/app/Services/AdminStatisticsService.php new file mode 100644 index 0000000..61b6d59 --- /dev/null +++ b/modules/Account/app/Services/AdminStatisticsService.php @@ -0,0 +1,303 @@ + +// +---------------------------------------------------------------------- +namespace Modules\Account\Services; + +use Illuminate\Http\Request; +use Modules\Account\Models\Bill; +use Modules\Account\Models\Family; +use Modules\Account\Models\FamilyMember; +use Illuminate\Support\Facades\DB; + +class AdminStatisticsService { + + /** + * @title 获取概览统计 + * + * @param Request $request + * @return array + */ + public function getOverview($request) { + $year = $request->input('year', date('Y')); + $month = $request->input('month', date('m')); + $startDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01'; + $endDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-31'; + + // 总账单数 + $totalBills = Bill::count(); + + // 当月账单数 + $monthBills = Bill::whereBetween('bill_date', [$startDate, $endDate])->count(); + + // 总收入 + $totalIncome = (float)Bill::where('type', 'income')->sum('amount'); + + // 总支出 + $totalExpense = (float)Bill::where('type', 'expense')->sum('amount'); + + // 当月收入 + $monthIncome = (float)Bill::where('type', 'income') + ->whereBetween('bill_date', [$startDate, $endDate]) + ->sum('amount'); + + // 当月支出 + $monthExpense = (float)Bill::where('type', 'expense') + ->whereBetween('bill_date', [$startDate, $endDate]) + ->sum('amount'); + + // 总用户数 + $totalUsers = DB::table('auth_users')->count(); + + // 总家庭数 + $totalFamilies = Family::count(); + + // 活跃用户数(本月有记账的用户) + $activeUsers = Bill::whereBetween('bill_date', [$startDate, $endDate]) + ->distinct('user_id') + ->count('user_id'); + + // 活跃家庭数(本月有记账的家庭) + $activeFamilies = Bill::whereBetween('bill_date', [$startDate, $endDate]) + ->whereNotNull('family_id') + ->distinct('family_id') + ->count('family_id'); + + return [ + 'total_bills' => $totalBills, + 'month_bills' => $monthBills, + 'total_income' => $totalIncome, + 'total_expense' => $totalExpense, + 'total_balance' => $totalIncome - $totalExpense, + 'month_income' => $monthIncome, + 'month_expense' => $monthExpense, + 'month_balance' => $monthIncome - $monthExpense, + 'total_users' => $totalUsers, + 'total_families' => $totalFamilies, + 'active_users' => $activeUsers, + 'active_families' => $activeFamilies + ]; + } + + /** + * @title 获取趋势统计 + * + * @param Request $request + * @return array + */ + public function getTrend($request) { + $type = $request->input('type', 'month'); // month 或 year + $year = $request->input('year', date('Y')); + $months = $request->input('months', 12); + $years = $request->input('years', 5); + + if ($type === 'month') { + // 月度趋势 + $data = []; + for ($i = $months - 1; $i >= 0; $i--) { + $date = date('Y-m', strtotime("-$i months")); + $startDate = $date . '-01'; + $endDate = date('Y-m-t', strtotime($date)); + + $income = (float)Bill::where('type', 'income') + ->whereBetween('bill_date', [$startDate, $endDate]) + ->sum('amount'); + + $expense = (float)Bill::where('type', 'expense') + ->whereBetween('bill_date', [$startDate, $endDate]) + ->sum('amount'); + + $count = Bill::whereBetween('bill_date', [$startDate, $endDate])->count(); + + $data[] = [ + 'date' => $date, + 'income' => $income, + 'expense' => $expense, + 'balance' => $income - $expense, + 'count' => $count + ]; + } + } else { + // 年度趋势 + $data = []; + for ($i = $years - 1; $i >= 0; $i--) { + $currentYear = date('Y') - $i; + + $income = (float)Bill::where('type', 'income') + ->whereYear('bill_date', $currentYear) + ->sum('amount'); + + $expense = (float)Bill::where('type', 'expense') + ->whereYear('bill_date', $currentYear) + ->sum('amount'); + + $count = Bill::whereYear('bill_date', $currentYear)->count(); + + $data[] = [ + 'year' => $currentYear, + 'income' => $income, + 'expense' => $expense, + 'balance' => $income - $expense, + 'count' => $count + ]; + } + } + + return $data; + } + + /** + * @title 获取分类统计 + * + * @param Request $request + * @return array + */ + public function getCategoryStatistics($request) { + $type = $request->input('type', 'expense'); // income 或 expense + $year = $request->input('year', date('Y')); + $month = $request->input('month'); + + $query = Bill::where('type', $type); + + if ($year && $month) { + $startDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01'; + $endDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-31'; + $query->whereBetween('bill_date', [$startDate, $endDate]); + } else { + $query->whereYear('bill_date', $year); + } + + $bills = $query->get(); + + // 按分类汇总 + $data = $bills->groupBy('category')->map(function ($items) { + return [ + 'category' => $items->first()->category, + 'count' => $items->count(), + 'amount' => (float)$items->sum('amount') + ]; + })->sortByDesc('amount')->values(); + + // 计算总金额和占比 + $totalAmount = $data->sum('amount'); + $data = $data->map(function ($item) use ($totalAmount) { + $item['percentage'] = $totalAmount > 0 ? round($item['amount'] / $totalAmount * 100, 2) : 0; + return $item; + }); + + return [ + 'type' => $type, + 'total_amount' => (float)$totalAmount, + 'total_count' => $data->sum('count'), + 'categories' => $data + ]; + } + + /** + * @title 获取用户统计 + * + * @param Request $request + * @return array + */ + public function getUserStatistics($request) { + $limit = $request->input('limit', 20); + $type = $request->input('type', 'expense'); // income 或 expense + $year = $request->input('year', date('Y')); + $month = $request->input('month'); + + $query = Bill::select('user_id') + ->selectRaw('COUNT(*) as bill_count') + ->selectRaw('SUM(CASE WHEN type = "income" THEN amount ELSE 0 END) as total_income') + ->selectRaw('SUM(CASE WHEN type = "expense" THEN amount ELSE 0 END) as total_expense') + ->selectRaw('SUM(CASE WHEN type = "income" THEN amount ELSE 0 END) - SUM(CASE WHEN type = "expense" THEN amount ELSE 0 END) as balance') + ->with('user:uid,nickname,username'); + + if ($year && $month) { + $startDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01'; + $endDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-31'; + $query->whereBetween('bill_date', [$startDate, $endDate]); + } else { + $query->whereYear('bill_date', $year); + } + + $users = $query->groupBy('user_id') + ->orderBy($type === 'income' ? 'total_income' : 'total_expense', 'desc') + ->limit($limit) + ->get(); + + return [ + 'type' => $type, + 'users' => $users->map(function ($user) { + return [ + 'user_id' => $user->user_id, + 'name' => $user->user->nickname ?? $user->user->username, + 'username' => $user->user->username, + 'bill_count' => $user->bill_count, + 'total_income' => (float)$user->total_income, + 'total_expense' => (float)$user->total_expense, + 'balance' => (float)$user->balance + ]; + }) + ]; + } + + /** + * @title 获取家庭统计 + * + * @param Request $request + * @return array + */ + public function getFamilyStatistics($request) { + $limit = $request->input('limit', 20); + $type = $request->input('type', 'expense'); // income 或 expense + $year = $request->input('year', date('Y')); + $month = $request->input('month'); + + $query = Bill::select('family_id') + ->selectRaw('COUNT(*) as bill_count') + ->selectRaw('SUM(CASE WHEN type = "income" THEN amount ELSE 0 END) as total_income') + ->selectRaw('SUM(CASE WHEN type = "expense" THEN amount ELSE 0 END) as total_expense') + ->selectRaw('SUM(CASE WHEN type = "income" THEN amount ELSE 0 END) - SUM(CASE WHEN type = "expense" THEN amount ELSE 0 END) as balance') + ->whereNotNull('family_id') + ->with('family:id,name,owner_id'); + + if ($year && $month) { + $startDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01'; + $endDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-31'; + $query->whereBetween('bill_date', [$startDate, $endDate]); + } else { + $query->whereYear('bill_date', $year); + } + + $families = $query->groupBy('family_id') + ->orderBy($type === 'income' ? 'total_income' : 'total_expense', 'desc') + ->limit($limit) + ->get(); + + // 获取成员数量 + $familyIds = $families->pluck('family_id')->toArray(); + $memberCounts = FamilyMember::whereIn('family_id', $familyIds) + ->selectRaw('family_id, COUNT(*) as member_count') + ->groupBy('family_id') + ->pluck('member_count', 'family_id'); + + return [ + 'type' => $type, + 'families' => $families->map(function ($family) use ($memberCounts) { + return [ + 'family_id' => $family->family_id, + 'name' => $family->family->name, + 'member_count' => $memberCounts[$family->family_id] ?? 0, + 'bill_count' => $family->bill_count, + 'total_income' => (float)$family->total_income, + 'total_expense' => (float)$family->total_expense, + 'balance' => (float)$family->balance + ]; + }) + ]; + } +} diff --git a/modules/Account/database/seeders/AccountDatabaseSeeder.php b/modules/Account/database/seeders/AccountDatabaseSeeder.php index 3f04079..167be8d 100644 --- a/modules/Account/database/seeders/AccountDatabaseSeeder.php +++ b/modules/Account/database/seeders/AccountDatabaseSeeder.php @@ -9,6 +9,7 @@ namespace Modules\Account\Database\Seeders; use Illuminate\Database\Seeder; +use App\Services\Auth\MenuService; class AccountDatabaseSeeder extends Seeder { @@ -17,6 +18,36 @@ class AccountDatabaseSeeder extends Seeder */ public function run(): void { - // $this->call([]); + $this->addAccountMenu(); } + /** + * @title 记账模块菜单导入 + * + * @return void + */ + public function addAccountMenu(){ + $menuService = app(MenuService::class); + + $menuService->importMenu([ + ['title' => '账单', 'name' => 'account', 'path' => '/account', 'component' => 'account', 'type' => 'menu', 'children' => [ + ['title' => '账单管理', 'name' => 'account.lists', 'path' => '/account/lists', 'component' => 'account/lists', 'type' => 'menu', 'children' => [ + ['title' => '查看账单', 'name' => 'account.bill.view', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '添加账单', 'name' => 'account.bill.add', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '编辑账单', 'name' => 'account.bill.edit', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '删除账单', 'name' => 'account.bill.delete', 'path' => '', 'component' => '', 'type' => 'button'], + ] + ], + ['title' => '家庭管理', 'name' => 'account.family', 'path' => '/account/family', 'component' => 'account/family', 'type' => 'menu', 'children' => [ + ['title' => '查看家庭', 'name' => 'account.family.view', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '添加家庭', 'name' => 'account.family.add', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '编辑家庭', 'name' => 'account.family.edit', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '删除家庭', 'name' => 'account.family.delete', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '管理成员', 'name' => 'account.family.manage', 'path' => '', 'component' => '', 'type' => 'button'], + ]], + ['title' => '统计报表', 'name' => 'account.statistics', 'path' => '/account/statistics', 'component' => 'account/statistics', 'type' => 'menu', 'children' => [ + ['title' => '查看统计', 'name' => 'account.statistics.view', 'path' => '', 'component' => '', 'type' => 'button'], + ['title' => '导出报表', 'name' => 'account.statistics.export', 'path' => '', 'component' => '', 'type' => 'button'], + ]], + ]]], 0); + } } diff --git a/modules/Account/routes/admin.php b/modules/Account/routes/admin.php index 45b19aa..1819c27 100644 --- a/modules/Account/routes/admin.php +++ b/modules/Account/routes/admin.php @@ -7,8 +7,32 @@ // | Author: molong // +---------------------------------------------------------------------- use Illuminate\Support\Facades\Route; -use Modules\Account\Controllers\AccountController; +use Modules\Account\Controllers\Admin\Bill; +use Modules\Account\Controllers\Admin\Family; +use Modules\Account\Controllers\Admin\Statistics; Route::middleware(['auth.check:admin'])->group(function () { - Route::apiResource('account', AccountController::class)->names('account'); + // 账单管理 + Route::get('account/bill', [Bill::class, 'index'])->name('account.bill.index'); + Route::post('account/bill', [Bill::class, 'add'])->name('account.bill.add'); + Route::put('account/bill', [Bill::class, 'edit'])->name('account.bill.edit'); + Route::delete('account/bill', [Bill::class, 'delete'])->name('account.bill.delete'); + Route::get('account/bill/detail', [Bill::class, 'detail'])->name('account.bill.detail'); + + // 家庭管理 + Route::get('account/family', [Family::class, 'index'])->name('account.family.index'); + Route::post('account/family', [Family::class, 'add'])->name('account.family.add'); + Route::put('account/family', [Family::class, 'edit'])->name('account.family.edit'); + Route::delete('account/family', [Family::class, 'delete'])->name('account.family.delete'); + Route::get('account/family/detail', [Family::class, 'detail'])->name('account.family.detail'); + Route::get('account/family/members', [Family::class, 'members'])->name('account.family.members'); + Route::post('account/family/member', [Family::class, 'addMember'])->name('account.family.addMember'); + Route::delete('account/family/member', [Family::class, 'removeMember'])->name('account.family.removeMember'); + + // 统计报表 + Route::get('account/statistics/overview', [Statistics::class, 'overview'])->name('account.statistics.overview'); + Route::get('account/statistics/trend', [Statistics::class, 'trend'])->name('account.statistics.trend'); + Route::get('account/statistics/category', [Statistics::class, 'category'])->name('account.statistics.category'); + Route::get('account/statistics/user', [Statistics::class, 'user'])->name('account.statistics.user'); + Route::get('account/statistics/family', [Statistics::class, 'family'])->name('account.statistics.family'); });