This commit is contained in:
2026-01-18 20:17:59 +08:00
parent 7e05f5e76f
commit de9c14f070
23 changed files with 1825 additions and 71 deletions

View File

@@ -0,0 +1,95 @@
<?php
namespace Modules\Account\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\BaseController;
use Modules\Account\Services\BillService;
class BillController extends BaseController
{
protected $billService;
public function __construct(BillService $billService)
{
$this->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);
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Modules\Account\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\BaseController;
use Modules\Account\Services\FamilyService;
class FamilyController extends BaseController
{
protected $familyService;
public function __construct(FamilyService $familyService)
{
$this->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);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Modules\Account\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\BaseController;
use Modules\Account\Services\StatisticsService;
class StatisticsController extends BaseController
{
protected $statisticsService;
public function __construct(StatisticsService $statisticsService)
{
$this->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);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Modules\Account\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Bill extends Model
{
use SoftDeletes;
protected $table = 'account_bills';
protected $fillable = [
'user_id',
'family_id',
'type',
'amount',
'category',
'remark',
'bill_date'
];
protected $dateFormat = 'Y-m-d H:i:s';
protected function casts(): array
{
return [
'bill_date' => '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);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Modules\Account\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Family extends Model
{
use SoftDeletes;
protected $table = 'account_families';
protected $fillable = [
'name',
'owner_id',
'invite_code'
];
protected $dateFormat = 'Y-m-d H:i:s';
protected function casts(): array
{
return [
'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 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);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Modules\Account\Models;
use Illuminate\Database\Eloquent\Model;
class FamilyMember extends Model
{
protected $table = 'account_family_members';
protected $fillable = [
'family_id',
'user_id'
];
protected $dateFormat = 'Y-m-d H:i:s';
protected function casts(): array
{
return [
'created_at' => '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');
}
}

View File

@@ -0,0 +1,314 @@
<?php
namespace Modules\Account\Services;
use Illuminate\Http\Request;
use Modules\Account\Models\Bill;
use Illuminate\Support\Facades\DB;
class BillService
{
/**
* 获取账单列表
*/
public function getList(Request $request)
{
$userId = auth('api')->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;
}
}

View File

@@ -0,0 +1,312 @@
<?php
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 FamilyService
{
/**
* 获取家庭信息
*/
public function getInfo(Request $request)
{
$userId = auth('api')->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' => '家主已转让'];
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Modules\Account\Services;
use Illuminate\Http\Request;
use Modules\Account\Models\Bill;
use Modules\Account\Models\FamilyMember;
use Illuminate\Support\Facades\DB;
class StatisticsService
{
/**
* 获取统计概览
*/
public function getOverview(Request $request)
{
$userId = auth('api')->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;
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('account_families', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('account_bills', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('account_family_members', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -1,14 +1,48 @@
<?php
// +----------------------------------------------------------------------
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
// +----------------------------------------------------------------------
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']);
});
});