diff --git a/modules/Account/app/Controllers/Api/BillController.php b/modules/Account/app/Controllers/Api/BillController.php new file mode 100644 index 0000000..07048f1 --- /dev/null +++ b/modules/Account/app/Controllers/Api/BillController.php @@ -0,0 +1,95 @@ +billService = $billService; + } + + /** + * 获取账单列表 + */ + public function index(Request $request) + { + try { + $this->data['data'] = $this->billService->getList($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 添加账单 + */ + public function add(Request $request) + { + try { + $this->data['data'] = $this->billService->add($request); + $this->data['message'] = '添加成功'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 编辑账单 + */ + public function edit(Request $request) + { + try { + $this->data['data'] = $this->billService->edit($request); + $this->data['message'] = '编辑成功'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 删除账单 + */ + public function delete(Request $request) + { + try { + $this->data['data'] = $this->billService->delete($request); + $this->data['message'] = '删除成功'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取账单详情 + */ + public function detail(Request $request) + { + try { + $this->data['data'] = $this->billService->detail($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Controllers/Api/FamilyController.php b/modules/Account/app/Controllers/Api/FamilyController.php new file mode 100644 index 0000000..098c692 --- /dev/null +++ b/modules/Account/app/Controllers/Api/FamilyController.php @@ -0,0 +1,155 @@ +familyService = $familyService; + } + + /** + * 获取家庭信息 + */ + public function info(Request $request) + { + try { + $this->data['data'] = $this->familyService->getInfo($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 创建家庭 + */ + public function create(Request $request) + { + try { + $this->data['data'] = $this->familyService->create($request); + $this->data['message'] = '创建成功'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 加入家庭 + */ + public function join(Request $request) + { + try { + $this->data['data'] = $this->familyService->join($request); + $this->data['message'] = '加入成功'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 退出家庭 + */ + public function leave(Request $request) + { + try { + $this->data['data'] = $this->familyService->leave($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取家庭邀请码 + */ + public function inviteCode(Request $request) + { + try { + $this->data['data'] = $this->familyService->getInviteCode($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 重新生成邀请码 + */ + public function regenerateInviteCode(Request $request) + { + try { + $this->data['data'] = $this->familyService->regenerateInviteCode($request); + $this->data['message'] = '邀请码已重新生成'; + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 移除家庭成员 + */ + public function removeMember(Request $request) + { + try { + $this->data['data'] = $this->familyService->removeMember($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取家庭成员列表 + */ + public function members(Request $request) + { + try { + $this->data['data'] = $this->familyService->getMembers($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 转让家主 + */ + public function transferOwner(Request $request) + { + try { + $this->data['data'] = $this->familyService->transferOwner($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Controllers/Api/StatisticsController.php b/modules/Account/app/Controllers/Api/StatisticsController.php new file mode 100644 index 0000000..ceabc30 --- /dev/null +++ b/modules/Account/app/Controllers/Api/StatisticsController.php @@ -0,0 +1,92 @@ +statisticsService = $statisticsService; + } + + /** + * 获取统计概览 + */ + public function overview(Request $request) + { + try { + $this->data['data'] = $this->statisticsService->getOverview($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取收支趋势 + */ + public function trend(Request $request) + { + try { + $this->data['data'] = $this->statisticsService->getTrend($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取分类统计 + */ + public function category(Request $request) + { + try { + $this->data['data'] = $this->statisticsService->getCategory($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取月度报表 + */ + public function monthly(Request $request) + { + try { + $this->data['data'] = $this->statisticsService->getMonthly($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } + + /** + * 获取年度报表 + */ + public function yearly(Request $request) + { + try { + $this->data['data'] = $this->statisticsService->getYearly($request); + } catch (\Throwable $th) { + $this->data['code'] = 0; + $this->data['message'] = $th->getMessage(); + } + + return response()->json($this->data); + } +} diff --git a/modules/Account/app/Models/Bill.php b/modules/Account/app/Models/Bill.php new file mode 100644 index 0000000..c95562a --- /dev/null +++ b/modules/Account/app/Models/Bill.php @@ -0,0 +1,50 @@ + 'date', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + ]; + } + + /** + * 账单所属用户 + */ + public function user() + { + return $this->belongsTo(\Modules\Member\Models\Member::class, 'user_id', 'uid'); + } + + /** + * 账单所属家庭 + */ + public function family() + { + return $this->belongsTo(Family::class); + } +} diff --git a/modules/Account/app/Models/Family.php b/modules/Account/app/Models/Family.php new file mode 100644 index 0000000..8eb2ee0 --- /dev/null +++ b/modules/Account/app/Models/Family.php @@ -0,0 +1,65 @@ + 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + ]; + } + + /** + * 家主 + */ + public function owner() + { + return $this->belongsTo(\Modules\Member\Models\Member::class, 'owner_id', 'uid'); + } + + /** + * 家庭成员 + */ + public function members() + { + return $this->belongsToMany( + \Modules\Member\Models\Member::class, + 'account_family_members', + 'family_id', + 'user_id' + )->withTimestamps(); + } + + /** + * 家庭成员关系 + */ + public function familyMembers() + { + return $this->hasMany(FamilyMember::class); + } + + /** + * 账单 + */ + public function bills() + { + return $this->hasMany(Bill::class); + } +} diff --git a/modules/Account/app/Models/FamilyMember.php b/modules/Account/app/Models/FamilyMember.php new file mode 100644 index 0000000..12b5aea --- /dev/null +++ b/modules/Account/app/Models/FamilyMember.php @@ -0,0 +1,39 @@ + 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + } + + /** + * 所属家庭 + */ + public function family() + { + return $this->belongsTo(Family::class); + } + + /** + * 成员用户 + */ + public function user() + { + return $this->belongsTo(\Modules\Member\Models\Member::class, 'user_id', 'uid'); + } +} diff --git a/modules/Account/app/Services/BillService.php b/modules/Account/app/Services/BillService.php new file mode 100644 index 0000000..742112d --- /dev/null +++ b/modules/Account/app/Services/BillService.php @@ -0,0 +1,314 @@ +user()['uid']; + $page = $request->input('page', 1); + $limit = $request->input('limit', 20); + $type = $request->input('type'); + $year = $request->input('year'); + $month = $request->input('month'); + $startDate = $request->input('start_date'); + $endDate = $request->input('end_date'); + $familyId = $request->input('family_id'); + + // 获取用户所在的家庭ID + $userFamilyId = $this->getUserFamilyId($userId); + + $query = Bill::with(['user:uid,nickname,username', 'family:id,name']); + + // 如果用户加入了家庭,且没有指定family_id,则显示家庭账单 + if ($userFamilyId && !$familyId) { + $query->where('family_id', $userFamilyId); + } elseif ($familyId) { + // 如果指定了family_id,则显示该家庭的账单 + $query->where('family_id', $familyId); + } else { + // 否则显示个人账单 + $query->where('user_id', $userId); + } + + // 年月筛选 + 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'; + } + + // 日期范围筛选 + if ($startDate) { + $query->where('bill_date', '>=', $startDate); + } + if ($endDate) { + $query->where('bill_date', '<=', $endDate); + } + + // 类型筛选 + if ($type && in_array($type, ['income', 'expense'])) { + $query->where('type', $type); + } + + $bills = $query->orderBy('bill_date', 'desc') + ->orderBy('created_at', 'desc') + ->paginate($limit, ['*'], 'page', $page); + + // 计算本月收支 + $monthExpense = Bill::query(); + $monthIncome = Bill::query(); + + // 同样的家庭查询逻辑 + if ($userFamilyId && !$familyId) { + $monthExpense->where('family_id', $userFamilyId); + $monthIncome->where('family_id', $userFamilyId); + } elseif ($familyId) { + $monthExpense->where('family_id', $familyId); + $monthIncome->where('family_id', $familyId); + } else { + $monthExpense->where('user_id', $userId); + $monthIncome->where('user_id', $userId); + } + + if ($startDate) { + $monthExpense->where('bill_date', '>=', $startDate); + $monthIncome->where('bill_date', '>=', $startDate); + } + if ($endDate) { + $monthExpense->where('bill_date', '<=', $endDate); + $monthIncome->where('bill_date', '<=', $endDate); + } + + $monthExpense->where('type', 'expense'); + $monthIncome->where('type', 'income'); + + // 转换数据格式以匹配前端 + $list = collect($bills->items())->map(function($bill) { + return [ + 'id' => $bill->id, + 'type' => $bill->type, + 'amount' => (float)$bill->amount, + 'category' => $bill->category, + 'category_id' => $this->getCategoryId($bill->category, $bill->type), + 'remark' => $bill->remark, + 'date' => $bill->bill_date, + 'bill_date' => $bill->bill_date, + 'created_at' => $bill->created_at->format('Y-m-d H:i:s'), + 'user' => $bill->user, + 'family' => $bill->family + ]; + })->toArray(); + + return [ + 'list' => $list, + 'month_expense' => (float)$monthExpense->sum('amount'), + 'month_income' => (float)$monthIncome->sum('amount'), + 'pagination' => [ + 'current_page' => $bills->currentPage(), + 'total' => $bills->total(), + 'per_page' => $bills->perPage(), + 'last_page' => $bills->lastPage() + ] + ]; + } + + /** + * 根据分类名称获取分类ID(前端使用) + */ + private function getCategoryId($categoryName, $type) + { + // 支出分类映射 + $expenseMap = [ + '餐饮' => 1, + '交通' => 2, + '购物' => 3, + '娱乐' => 4, + '医疗' => 5, + '教育' => 6, + '居住' => 7, + '其他' => 8 + ]; + + // 收入分类映射 + $incomeMap = [ + '工资' => 101, + '奖金' => 102, + '投资' => 103, + '兼职' => 104, + '其他' => 105 + ]; + + $map = $type === 'income' ? $incomeMap : $expenseMap; + return $map[$categoryName] ?? ($type === 'income' ? 105 : 8); + } + + /** + * 添加账单 + */ + public function add(Request $request) + { + $userId = auth('api')->user()['uid']; + + $data = $request->validate([ + 'type' => 'required|in:income,expense', + 'amount' => 'required|numeric|min:0.01', + 'category_id' => 'required|integer', + 'remark' => 'nullable|string|max:255', + 'date' => 'required|date', + 'family_id' => 'nullable|integer|exists:account_families,id' + ]); + + // 将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']; + + // 检查家庭权限 + if (!empty($data['family_id'])) { + $this->checkFamilyAccess($userId, $data['family_id']); + } + + $data['user_id'] = $userId; + + // 移除前端字段 + unset($data['category_id'], $data['date']); + + return Bill::create($data); + } + + /** + * 获取分类映射 + */ + private function getCategoryMap($type) + { + if ($type === 'income') { + return [ + 101 => '工资', + 102 => '奖金', + 103 => '投资', + 104 => '兼职', + 105 => '其他' + ]; + } else { + return [ + 1 => '餐饮', + 2 => '交通', + 3 => '购物', + 4 => '娱乐', + 5 => '医疗', + 6 => '教育', + 7 => '居住', + 8 => '其他' + ]; + } + } + + /** + * 编辑账单 + */ + public function edit(Request $request) + { + $userId = auth('api')->user()['uid']; + $id = $request->input('id'); + + $bill = Bill::findOrFail($id); + + // 验证权限 + if ($bill->user_id != $userId) { + throw new \Exception('无权操作此账单'); + } + + $data = $request->validate([ + 'type' => 'required|in:income,expense', + 'amount' => 'required|numeric|min:0.01', + 'category' => 'required|string|max:50', + 'remark' => 'nullable|string|max:255', + 'bill_date' => 'required|date', + 'family_id' => 'nullable|integer|exists:account_families,id' + ]); + + // 检查家庭权限 + if (!empty($data['family_id'])) { + $this->checkFamilyAccess($userId, $data['family_id']); + } + + $bill->update($data); + + return $bill; + } + + /** + * 删除账单 + */ + public function delete(Request $request) + { + $userId = auth('api')->user()['uid']; + $id = $request->input('id'); + + $bill = Bill::findOrFail($id); + + // 验证权限 + if ($bill->user_id != $userId) { + throw new \Exception('无权删除此账单'); + } + + return $bill->delete(); + } + + /** + * 获取账单详情 + */ + public function detail(Request $request) + { + $userId = auth('api')->user()['uid']; + $id = $request->input('id'); + + $bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_id']) + ->findOrFail($id); + + // 验证权限 + if ($bill->user_id != $userId && !in_array($userId, $bill->family->members->pluck('uid')->toArray())) { + throw new \Exception('无权查看此账单'); + } + + return $bill; + } + + /** + * 检查家庭访问权限 + */ + private function checkFamilyAccess($userId, $familyId) + { + $isMember = DB::table('account_family_members') + ->where('family_id', $familyId) + ->where('user_id', $userId) + ->exists(); + + if (!$isMember) { + throw new \Exception('您不是该家庭成员'); + } + } + + /** + * 获取用户所在的家庭ID + */ + private function getUserFamilyId($userId) + { + $familyMember = DB::table('account_family_members') + ->where('user_id', $userId) + ->first(); + return $familyMember ? $familyMember->family_id : null; + } +} diff --git a/modules/Account/app/Services/FamilyService.php b/modules/Account/app/Services/FamilyService.php new file mode 100644 index 0000000..e102c49 --- /dev/null +++ b/modules/Account/app/Services/FamilyService.php @@ -0,0 +1,312 @@ +user()['uid']; + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + return null; + } + + $family = Family::with(['owner:uid,nickname,username', 'members:uid,nickname,username,email']) + ->findOrFail($familyMember->family_id); + + return [ + 'id' => $family->id, + 'name' => $family->name, + 'invite_code' => $family->invite_code, + 'owner_id' => $family->owner_id, + 'is_owner' => $family->owner_id == $userId, + 'created_at' => $family->created_at->format('Y-m-d H:i:s'), + 'owner' => $family->owner, + 'members' => $family->members + ]; + } + + /** + * 创建家庭 + */ + public function create(Request $request) + { + $userId = auth('api')->user()['uid']; + + // 检查是否已加入家庭 + $existingFamily = FamilyMember::where('user_id', $userId)->first(); + if ($existingFamily) { + throw new \Exception('您已加入其他家庭,请先退出'); + } + + $data = $request->validate([ + 'name' => 'required|string|max:50' + ]); + + DB::beginTransaction(); + try { + // 生成10位邀请码 + do { + $inviteCode = strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10)); + } while (Family::where('invite_code', $inviteCode)->exists()); + + $family = Family::create([ + 'name' => $data['name'], + 'owner_id' => $userId, + 'invite_code' => $inviteCode + ]); + + // 家主自动加入家庭 + FamilyMember::create([ + 'family_id' => $family->id, + 'user_id' => $userId + ]); + + DB::commit(); + + return $family; + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + } + + /** + * 加入家庭 + */ + public function join(Request $request) + { + $userId = auth('api')->user()['uid']; + + // 检查是否已加入家庭 + $existingFamily = FamilyMember::where('user_id', $userId)->first(); + if ($existingFamily) { + throw new \Exception('您已加入其他家庭'); + } + + $data = $request->validate([ + 'invite_code' => 'required|string|size:10' + ]); + + $family = Family::where('invite_code', strtoupper($data['invite_code']))->first(); + + if (!$family) { + throw new \Exception('邀请码不存在'); + } + + // 检查是否是家主 + if ($family->owner_id == $userId) { + throw new \Exception('您已经是该家庭的家主'); + } + + // 加入家庭 + FamilyMember::create([ + 'family_id' => $family->id, + 'user_id' => $userId + ]); + + return $family; + } + + /** + * 退出家庭 + */ + public function leave(Request $request) + { + $userId = auth('api')->user()['uid']; + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + // 家主不能退出,只能转让或删除 + if ($family->owner_id == $userId) { + throw new \Exception('家主不能退出家庭,请先转让家主'); + } + + $familyMember->delete(); + + return ['message' => '已退出家庭']; + } + + /** + * 获取家庭邀请码 + */ + public function getInviteCode(Request $request) + { + $userId = auth('api')->user()['uid']; + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + return [ + 'invite_code' => $family->invite_code, + 'family_name' => $family->name + ]; + } + + /** + * 重新生成邀请码 + */ + public function regenerateInviteCode(Request $request) + { + $userId = auth('api')->user()['uid']; + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + // 只有家主可以重新生成邀请码 + if ($family->owner_id != $userId) { + throw new \Exception('只有家主可以重新生成邀请码'); + } + + // 生成新邀请码 + 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 + ]; + } + + /** + * 移除家庭成员 + */ + public function removeMember(Request $request) + { + $userId = auth('api')->user()['uid']; + + $data = $request->validate([ + 'user_id' => 'required|integer' + ]); + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + // 只有家主可以移除成员 + if ($family->owner_id != $userId) { + throw new \Exception('只有家主可以移除成员'); + } + + // 不能移除家主 + if ($family->owner_id == $data['user_id']) { + throw new \Exception('不能移除家主'); + } + + // 移除成员 + $targetMember = FamilyMember::where('family_id', $family->id) + ->where('user_id', $data['user_id']) + ->first(); + + if (!$targetMember) { + throw new \Exception('该用户不在家庭中'); + } + + $targetMember->delete(); + + return ['message' => '成员已移除']; + } + + /** + * 获取家庭成员列表 + */ + public function getMembers(Request $request) + { + $userId = auth('api')->user()['uid']; + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + $members = FamilyMember::with('user:uid,nickname,username,email') + ->where('family_id', $family->id) + ->get(); + + return $members->map(function($member) use ($family) { + return [ + 'id' => $member->user->uid, + 'name' => $member->user->nickname, + 'username' => $member->user->username, + 'is_owner' => $family->owner_id == $member->user->uid + ]; + })->toArray(); + } + + /** + * 转让家主 + */ + public function transferOwner(Request $request) + { + $userId = auth('api')->user()['uid']; + + $data = $request->validate([ + 'user_id' => 'required|integer' + ]); + + $familyMember = FamilyMember::where('user_id', $userId)->first(); + + if (!$familyMember) { + throw new \Exception('您还未加入任何家庭'); + } + + $family = Family::findOrFail($familyMember->family_id); + + // 只有当前家主可以转让 + if ($family->owner_id != $userId) { + throw new \Exception('只有家主可以转让'); + } + + // 目标用户必须在家庭中 + $targetMember = FamilyMember::where('family_id', $family->id) + ->where('user_id', $data['user_id']) + ->first(); + + if (!$targetMember) { + throw new \Exception('该用户不在家庭中'); + } + + // 转让家主 + $family->update(['owner_id' => $data['user_id']]); + + return ['message' => '家主已转让']; + } +} diff --git a/modules/Account/app/Services/StatisticsService.php b/modules/Account/app/Services/StatisticsService.php new file mode 100644 index 0000000..61201de --- /dev/null +++ b/modules/Account/app/Services/StatisticsService.php @@ -0,0 +1,231 @@ +user()['uid']; + $year = $request->input('year'); + $month = $request->input('month'); + + // 获取用户所在的家庭ID + $familyId = $this->getUserFamilyId($userId); + + // 查询当月的账单 + $query = Bill::query(); + + if ($year && $month) { + $query->whereMonth('bill_date', str_pad($month, 2, '0', STR_PAD_LEFT)) + ->whereYear('bill_date', $year); + } else { + $query->whereMonth('bill_date', date('m')) + ->whereYear('bill_date', date('Y')); + } + + if ($familyId) { + $query->where('family_id', $familyId); + } else { + $query->where('user_id', $userId); + } + + $bills = $query->get(); + + $income = (float) $bills->where('type', 'income')->sum('amount'); + $expense = (float) $bills->where('type', 'expense')->sum('amount'); + + return [ + 'income' => $income, + 'expense' => $expense, + 'balance' => $income - $expense + ]; + } + + /** + * 获取收支趋势 + */ + public function getTrend(Request $request) + { + $userId = auth('api')->user()['uid']; + $year = $request->input('year'); + $month = $request->input('month'); + $days = 7; // 最近7天 + + // 获取用户所在的家庭ID + $familyId = $this->getUserFamilyId($userId); + + $data = []; + for ($i = $days - 1; $i >= 0; $i--) { + $date = date('Y-m-d', strtotime("-$i days")); + + $query = Bill::whereDate('bill_date', $date); + + if ($familyId) { + $query->where('family_id', $familyId); + } else { + $query->where('user_id', $userId); + } + + $bills = $query->get(); + + $data[] = [ + 'date' => $date, + 'income' => (float) $bills->where('type', 'income')->sum('amount'), + 'expense' => (float) $bills->where('type', 'expense')->sum('amount') + ]; + } + + return $data; + } + + /** + * 获取分类统计 + */ + public function getCategory(Request $request) + { + $userId = auth('api')->user()['uid']; + $type = $request->input('type', 'expense'); + $year = $request->input('year'); + $month = $request->input('month'); + + // 获取用户所在的家庭ID + $familyId = $this->getUserFamilyId($userId); + + $query = Bill::where('type', $type); + + if ($year && $month) { + $query->whereMonth('bill_date', str_pad($month, 2, '0', STR_PAD_LEFT)) + ->whereYear('bill_date', $year); + } else { + $query->whereMonth('bill_date', date('m')) + ->whereYear('bill_date', date('Y')); + } + + if ($familyId) { + $query->where('family_id', $familyId); + } else { + $query->where('user_id', $userId); + } + + $bills = $query->get(); + + // 按分类汇总 + $data = $bills->groupBy('category')->map(function ($items) { + return [ + 'category_id' => $this->getCategoryId($items->first()->category, $type), + 'amount' => (float) $items->sum('amount') + ]; + })->sortByDesc('amount')->values(); + + return $data; + } + + /** + * 根据分类名称获取分类ID + */ + private function getCategoryId($categoryName, $type) + { + $expenseMap = [ + '餐饮' => 1, '交通' => 2, '购物' => 3, '娱乐' => 4, + '医疗' => 5, '教育' => 6, '居住' => 7, '其他' => 8 + ]; + $incomeMap = [ + '工资' => 101, '奖金' => 102, '投资' => 103, '兼职' => 104, '其他' => 105 + ]; + + $map = $type === 'income' ? $incomeMap : $expenseMap; + return $map[$categoryName] ?? ($type === 'income' ? 105 : 8); + } + + /** + * 获取月度报表 + */ + public function getMonthly(Request $request) + { + $userId = auth('api')->user()['uid']; + $year = $request->input('year', date('Y')); + + // 获取用户所在的家庭ID + $familyId = $this->getUserFamilyId($userId); + + $data = []; + for ($month = 1; $month <= 12; $month++) { + $query = Bill::whereMonth('bill_date', $month) + ->whereYear('bill_date', $year); + + if ($familyId) { + $query->where('family_id', $familyId); + } else { + $query->where('user_id', $userId); + } + + $bills = $query->get(); + + $data[] = [ + 'month' => sprintf('%d-%02d', $year, $month), + 'income' => (float) number_format($bills->where('type', 'income')->sum('amount'), 2), + 'expense' => (float) number_format($bills->where('type', 'expense')->sum('amount'), 2), + 'bill_count' => $bills->count() + ]; + } + + return [ + 'year' => $year, + 'data' => $data + ]; + } + + /** + * 获取年度报表 + */ + public function getYearly(Request $request) + { + $userId = auth('api')->user()['uid']; + $years = $request->input('years', 5); + + // 获取用户所在的家庭ID + $familyId = $this->getUserFamilyId($userId); + + $data = []; + for ($i = $years - 1; $i >= 0; $i--) { + $year = date('Y', strtotime("-$i years")); + + $query = Bill::whereYear('bill_date', $year); + + if ($familyId) { + $query->where('family_id', $familyId); + } else { + $query->where('user_id', $userId); + } + + $bills = $query->get(); + + $data[] = [ + 'year' => $year, + 'income' => (float) number_format($bills->where('type', 'income')->sum('amount'), 2), + 'expense' => (float) number_format($bills->where('type', 'expense')->sum('amount'), 2), + 'bill_count' => $bills->count() + ]; + } + + return $data; + } + + /** + * 获取用户所在的家庭ID + */ + private function getUserFamilyId($userId) + { + $familyMember = FamilyMember::where('user_id', $userId)->first(); + return $familyMember ? $familyMember->family_id : null; + } +} diff --git a/modules/Account/database/migrations/2025_01_18_000002_create_families_table.php b/modules/Account/database/migrations/2025_01_18_000002_create_families_table.php new file mode 100644 index 0000000..f4d0f75 --- /dev/null +++ b/modules/Account/database/migrations/2025_01_18_000002_create_families_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name')->comment('家庭名称'); + $table->unsignedBigInteger('owner_id')->comment('家主用户ID'); + $table->foreign('owner_id')->references('uid')->on('member')->onDelete('cascade'); + $table->string('invite_code', 10)->unique()->comment('邀请码'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('owner_id'); + $table->index('invite_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('account_families'); + } +}; diff --git a/modules/Account/database/migrations/2025_01_18_000003_create_bills_table.php b/modules/Account/database/migrations/2025_01_18_000003_create_bills_table.php new file mode 100644 index 0000000..233b381 --- /dev/null +++ b/modules/Account/database/migrations/2025_01_18_000003_create_bills_table.php @@ -0,0 +1,41 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->foreign('user_id')->references('uid')->on('member')->onDelete('cascade'); + $table->foreignId('family_id')->nullable()->constrained('account_families')->onDelete('set null')->comment('家庭ID'); + $table->enum('type', ['income', 'expense'])->default('expense')->comment('类型:income-收入,expense-支出'); + $table->decimal('amount', 10, 2)->comment('金额'); + $table->string('category', 50)->comment('分类'); + $table->string('remark')->nullable()->comment('备注'); + $table->date('bill_date')->comment('账单日期'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('user_id'); + $table->index('family_id'); + $table->index('type'); + $table->index('bill_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('account_bills'); + } +}; diff --git a/modules/Account/database/migrations/2025_01_18_000003_create_family_members_table.php b/modules/Account/database/migrations/2025_01_18_000003_create_family_members_table.php new file mode 100644 index 0000000..aaa4d45 --- /dev/null +++ b/modules/Account/database/migrations/2025_01_18_000003_create_family_members_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('family_id')->constrained('account_families')->onDelete('cascade')->comment('家庭ID'); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->foreign('user_id')->references('uid')->on('member')->onDelete('cascade'); + $table->timestamps(); + + $table->index('family_id'); + $table->index('user_id'); + $table->unique(['family_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('account_family_members'); + } +}; diff --git a/modules/Account/routes/api.php b/modules/Account/routes/api.php index 3ef7ea3..ff1c1ec 100644 --- a/modules/Account/routes/api.php +++ b/modules/Account/routes/api.php @@ -1,14 +1,48 @@ -// +---------------------------------------------------------------------- -use Illuminate\Support\Facades\Route; -use Modules\Account\Controllers\AccountController; -Route::middleware(['auth.check:api'])->group(function () { - Route::apiResource('account', AccountController::class)->names('account'); +use Illuminate\Support\Facades\Route; +use Modules\Account\Controllers\Api\BillController; +use Modules\Account\Controllers\Api\FamilyController; +use Modules\Account\Controllers\Api\StatisticsController; + +/* +|-------------------------------------------------------------------------- +| Account API Routes +|-------------------------------------------------------------------------- +| +| 账单、家庭、统计相关API路由 +| +*/ + +Route::middleware(['auth:api'])->group(function () { + // 账单路由 + Route::prefix('bill')->group(function () { + Route::get('list', [BillController::class, 'index']); + Route::post('add', [BillController::class, 'add']); + Route::post('edit', [BillController::class, 'edit']); + Route::post('delete', [BillController::class, 'delete']); + Route::get('detail', [BillController::class, 'detail']); + }); + + // 家庭路由 + Route::prefix('family')->group(function () { + Route::get('info', [FamilyController::class, 'info']); + Route::post('create', [FamilyController::class, 'create']); + Route::post('join', [FamilyController::class, 'join']); + Route::post('leave', [FamilyController::class, 'leave']); + Route::get('invite-code', [FamilyController::class, 'inviteCode']); + Route::post('regenerate-invite-code', [FamilyController::class, 'regenerateInviteCode']); + Route::post('remove-member', [FamilyController::class, 'removeMember']); + Route::get('members', [FamilyController::class, 'members']); + Route::post('transfer-owner', [FamilyController::class, 'transferOwner']); + }); + + // 统计路由 + Route::prefix('statistics')->group(function () { + Route::get('overview', [StatisticsController::class, 'overview']); + Route::get('trend', [StatisticsController::class, 'trend']); + Route::get('category', [StatisticsController::class, 'category']); + Route::get('monthly', [StatisticsController::class, 'monthly']); + Route::get('yearly', [StatisticsController::class, 'yearly']); + }); }); diff --git a/resources/mobile/pages/account/bill/add.vue b/resources/mobile/pages/account/bill/add.vue index 31d2de2..3f84f7c 100644 --- a/resources/mobile/pages/account/bill/add.vue +++ b/resources/mobile/pages/account/bill/add.vue @@ -33,6 +33,7 @@ v-model="form.amount" placeholder="0.00" placeholder-style="color: #ddd" + @input="onAmountInput" /> @@ -145,6 +146,16 @@ export default { onDateChange(e) { this.form.date = e.detail.value }, + onAmountInput(e) { + // 限制小数点后两位 + let value = e.detail.value + if (value.includes('.')) { + const parts = value.split('.') + if (parts[1] && parts[1].length > 2) { + this.form.amount = `${parts[0]}.${parts[1].substring(0, 2)}` + } + } + }, async handleSubmit() { // 验证 if (!this.form.amount || parseFloat(this.form.amount) <= 0) { @@ -163,6 +174,14 @@ export default { return } + if (!this.form.date) { + uni.showToast({ + title: '请选择日期', + icon: 'none' + }) + return + } + this.loading = true try { @@ -170,11 +189,11 @@ export default { type: this.form.type, amount: parseFloat(this.form.amount), category_id: this.form.categoryId, - remark: this.form.remark, + remark: this.form.remark || undefined, date: this.form.date }) - if (res.code === 200) { + if (res && res.code === 1) { uni.showToast({ title: '保存成功', icon: 'success' @@ -184,14 +203,14 @@ export default { }, 1500) } else { uni.showToast({ - title: res.message || '保存失败', + title: res?.message || '保存失败', icon: 'none' }) } } catch (error) { console.error('保存账单失败', error) uni.showToast({ - title: '保存失败,请重试', + title: error?.message || '保存失败,请重试', icon: 'none' }) } finally { diff --git a/resources/mobile/pages/account/bill/index.vue b/resources/mobile/pages/account/bill/index.vue index 49b8ad9..99fd3e0 100644 --- a/resources/mobile/pages/account/bill/index.vue +++ b/resources/mobile/pages/account/bill/index.vue @@ -48,7 +48,7 @@ - {{ date }} + {{ group.formattedDate }} 支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }} @@ -76,6 +76,8 @@