mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 08:20:00 +00:00
Compare commits
40 Commits
4304092b37
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d9ae792fa | |||
| 077db1f7c6 | |||
| 8c88eb0866 | |||
| 5241d2c9c6 | |||
| 758c553512 | |||
| 0941d2bbb9 | |||
| 436047c1e8 | |||
| 7f8b17cbc5 | |||
| c0ec662a6c | |||
| 1b771167cf | |||
| 60832c5722 | |||
| d8c5c541e0 | |||
| fb7862baff | |||
| e213c57315 | |||
| 92b2fef3e0 | |||
| 4afc73f987 | |||
| 5d86afdb4d | |||
| e5145f9852 | |||
| aeeffbaa5c | |||
| b891f2ecbb | |||
| dc6d2e5626 | |||
| 328f48afe3 | |||
| ceabb9c0a2 | |||
| 1b801131d2 | |||
| ecadf71666 | |||
| 91dddc0832 | |||
| 5ce0a3147f | |||
| d64b766f04 | |||
| 2d743a2473 | |||
| e0c2395ce8 | |||
| 7865075435 | |||
| 59944d7d0b | |||
| 82b558fc66 | |||
| cf35aa8c5a | |||
| a6d7444469 | |||
| 237fc740d7 | |||
| bbb738bab9 | |||
| a96accaa63 | |||
| 779fd93a30 | |||
| 3c9450e24b |
@@ -1,31 +0,0 @@
|
|||||||
name: Laravel-Oracle-Deploy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
redeploy:
|
|
||||||
runs-on: self-hosted
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Sync and Deploy Laravel Backend
|
|
||||||
run: |
|
|
||||||
# 🎯 1. 換成你 Oracle 主機上 Laravel 專案的實際路徑
|
|
||||||
cd /home/ubuntu/apps/finance-backend || exit 1
|
|
||||||
|
|
||||||
# 2. 強制拉取 Gitea 上的最新代碼
|
|
||||||
git fetch origin main
|
|
||||||
git reset --hard origin/main
|
|
||||||
|
|
||||||
# 3. 重啟 Docker 容器並自動編譯
|
|
||||||
docker compose up -d --build --remove-orphans
|
|
||||||
|
|
||||||
# 4. (選用) 讓 Laravel 容器內部執行資料庫遷移與清除快取
|
|
||||||
# 假設你的 Laravel 容器服務名稱叫 app,如果有需要可以解開下面幾行:
|
|
||||||
# docker compose exec -T app php artisan migrate --force
|
|
||||||
# docker compose exec -T app php artisan config:cache
|
|
||||||
# docker compose exec -T app php artisan route:cache
|
|
||||||
|
|
||||||
# 5. 清理舊的 Docker 鏡像
|
|
||||||
docker image prune -f
|
|
||||||
41
.gitea/workflows/deploy.yaml
Normal file
41
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Laravel-Oracle-Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
redeploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Create .env file
|
||||||
|
run: echo "${{ secrets.ENV_FILE }}" > .env
|
||||||
|
|
||||||
|
- name: Main Deploy Backend Container
|
||||||
|
run: |
|
||||||
|
# 0. 📦 重新打包最新程式碼 (這是你原本漏掉的靈魂步驟!)
|
||||||
|
# 確保這裡的執行路徑有你的 Dockerfile
|
||||||
|
docker build -t finance_app-finance-backend:latest .
|
||||||
|
|
||||||
|
# 1. 🗑️ 刪除舊容器
|
||||||
|
docker rm -f finance-laravel-backend || true
|
||||||
|
|
||||||
|
# 2. 🚀 啟動最乾淨的新容器
|
||||||
|
docker run -d \
|
||||||
|
--name finance-laravel-backend \
|
||||||
|
--network 1panel-network \
|
||||||
|
-p 8081:8000 \
|
||||||
|
finance_app-finance-backend:latest
|
||||||
|
|
||||||
|
# 3. 🎯 終極一指通:寫入環境變數
|
||||||
|
docker exec -i finance-laravel-backend sh -c "cat > .env" << 'EOF'
|
||||||
|
${{ secrets.ENV_FILE }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. 🧹 刷新 Laravel 快取
|
||||||
|
docker exec -i finance-laravel-backend php artisan optimize:clear
|
||||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,29 +1,3 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
.env.backup
|
|
||||||
.env.production
|
|
||||||
.phpactor.json
|
|
||||||
.phpunit.result.cache
|
|
||||||
/.fleet
|
|
||||||
/.idea
|
|
||||||
/.nova
|
|
||||||
/.phpunit.cache
|
|
||||||
/.vscode
|
|
||||||
/.zed
|
|
||||||
/auth.json
|
|
||||||
/node_modules
|
|
||||||
/public/build
|
|
||||||
/public/hot
|
|
||||||
/public/storage
|
|
||||||
/storage/*.key
|
|
||||||
/storage/pail
|
|
||||||
/vendor
|
|
||||||
Homestead.json
|
|
||||||
Homestead.yaml
|
|
||||||
Thumbs.db
|
|
||||||
=======
|
|
||||||
/vendor/
|
/vendor/
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
@@ -43,10 +17,12 @@ public_html/hot
|
|||||||
|
|
||||||
storage/*.key
|
storage/*.key
|
||||||
.env
|
.env
|
||||||
|
.env.dev
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Homestead.json
|
Homestead.json
|
||||||
/.vagrant
|
/.vagrant
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
/app/public/storage
|
||||||
|
|
||||||
/public/build
|
/public/build
|
||||||
/storage/pail
|
/storage/pail
|
||||||
@@ -54,4 +30,4 @@ Homestead.json
|
|||||||
.env.production
|
.env.production
|
||||||
.phpactor.json
|
.phpactor.json
|
||||||
auth.json
|
auth.json
|
||||||
>>>>>>> 5123647b7180bb2fdc0760039c8e38d903dc15eb
|
logs/laravel.log
|
||||||
|
|||||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
FROM php:8.2-fpm
|
||||||
|
|
||||||
|
# 1. 安裝系統套件(這層幾乎不變,永遠快取)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libpq-dev \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libpng-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN docker-php-ext-install pdo pdo_pgsql pdo_mysql mbstring exif pcntl bcmath gd
|
||||||
|
|
||||||
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
# 🎯 2. 【核心魔法】只複製套件清單檔案
|
||||||
|
COPY composer.json composer.lock /var/www/
|
||||||
|
|
||||||
|
# 🎯 3. 在這裡就先執行安裝!
|
||||||
|
# 因為上面只複製了 json/lock,只要你沒裝新套件,這層 Docker 就會直接跳過、秒速套用快取!
|
||||||
|
RUN composer install --no-interaction --no-scripts --no-autoloader --no-dev
|
||||||
|
|
||||||
|
# 4. 最後才複製你天天在修改的 Laravel 原始碼(如 Controller、Routes)
|
||||||
|
COPY . /var/www
|
||||||
|
|
||||||
|
# 5. 補上自動載入優化
|
||||||
|
RUN composer dump-autoload --optimize --no-dev
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["php", "/var/www/artisan", "serve", "--host=0.0.0.0", "--port=8000"]
|
||||||
@@ -15,21 +15,42 @@ class DashboardController extends Controller
|
|||||||
$userId = Auth::id();
|
$userId = Auth::id();
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$latestMonth = $now->copy();
|
||||||
|
$latestYear = $latestMonth->year;
|
||||||
|
|
||||||
// 1. 本月基礎統計 (用一個查詢同時撈出「總金額」與「總筆數」)
|
// 1. 本月基礎統計 (用一個查詢同時撈出「總金額」與「總筆數」)
|
||||||
$monthStats = Expense::where('user_id', $userId)
|
$monthStats = Expense::where('user_id', $userId)
|
||||||
->whereYear('date', $now->year)
|
->whereYear('date', $latestYear)
|
||||||
->whereMonth('date', $now->month)
|
->whereMonth('date', $latestMonth)
|
||||||
->selectRaw('SUM(amount) as total_amount, COUNT(id) as total_count')
|
->selectRaw('SUM(amount) as total_amount, COUNT(id) as total_count')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
if (!$monthStats || $monthStats->total_amount == 0) {
|
||||||
|
// 改用 first() 確保能抓到單筆資料
|
||||||
|
$latestExpense = Expense::where('user_id', $userId)->latest('date')->first();
|
||||||
|
|
||||||
|
if ($latestExpense) {
|
||||||
|
$targetDate = Carbon::parse($latestExpense->date);
|
||||||
|
$latestYear = $targetDate->year;
|
||||||
|
$latestMonth = $targetDate->month;
|
||||||
|
|
||||||
|
// 重新撈取該月份的統計資料
|
||||||
|
$monthStats = Expense::where('user_id', $userId)
|
||||||
|
->whereYear('date', $latestYear)
|
||||||
|
->whereMonth('date', $latestMonth)
|
||||||
|
->selectRaw('SUM(amount) as total_amount, COUNT(id) as total_count')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$monthlyTotal = (float) ($monthStats->total_amount ?? 0);
|
$monthlyTotal = (float) ($monthStats->total_amount ?? 0);
|
||||||
$monthlyCount = (int) ($monthStats->total_count ?? 0);
|
$monthlyCount = (int) ($monthStats->total_count ?? 0);
|
||||||
|
|
||||||
// 2. 本月分類統計(圓餅圖)- 保持集合操作即可
|
// 2. 本月分類統計(圓餅圖)- 保持集合操作即可
|
||||||
$categoryStats = Expense::with('category')
|
$categoryStats = Expense::with('category')
|
||||||
->where('user_id', $userId)
|
->where('user_id', $userId)
|
||||||
->whereYear('date', $now->year)
|
->whereYear('date', $latestYear)
|
||||||
->whereMonth('date', $now->month)
|
->whereMonth('date', $latestMonth)
|
||||||
->get()
|
->get()
|
||||||
->groupBy(fn($e) => $e->category?->name ?? '未分類')
|
->groupBy(fn($e) => $e->category?->name ?? '未分類')
|
||||||
->map(fn($group, $name) => [
|
->map(fn($group, $name) => [
|
||||||
@@ -86,12 +107,12 @@ class DashboardController extends Controller
|
|||||||
$monthlyShare = (float) $expense->amount / $totalMonths;
|
$monthlyShare = (float) $expense->amount / $totalMonths;
|
||||||
|
|
||||||
// 跑這 6 個月的迴圈,看有沒有落在這筆費用的歸屬區間內
|
// 跑這 6 個月的迴圈,看有沒有落在這筆費用的歸屬區間內
|
||||||
foreach ($monthlyTrendData as $monthStr => $currentTotal) {
|
foreach ($monthlyTrendData as $monthStr => &$currentTotal) {
|
||||||
$currentMonthObj = Carbon::createFromFormat('Y/m', $monthStr)->startOfMonth();
|
$currentMonthObj = Carbon::createFromFormat('Y/m', $monthStr)->startOfMonth();
|
||||||
|
|
||||||
// 如果圖表上的這個月份,剛好在費用的歸屬起訖之內,就把每月平均金額灌進去!
|
// 如果圖表上的這個月份,剛好在費用的歸屬起訖之內,就把每月平均金額灌進去!
|
||||||
if ($currentMonthObj->betweenIncluded($start, $end)) {
|
if ($currentMonthObj->betweenIncluded($start, $end)) {
|
||||||
$monthlyTrendData[$monthStr] += $monthlyShare;
|
$currentTotal += $monthlyShare;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,6 @@ class GoogleAuthController extends Controller
|
|||||||
$token = $user->createToken('auth_token')->plainTextToken;
|
$token = $user->createToken('auth_token')->plainTextToken;
|
||||||
|
|
||||||
// 把 token 帶回前端
|
// 把 token 帶回前端
|
||||||
return redirect("http://localhost:5173/#/auth/callback?token={$token}&name=" . urlencode($user->name));
|
return redirect(config('app.url') . "/#/auth/callback?token={$token}&name=" . urlencode($user->name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,8 @@ class UpdateAssetPriceJob implements ShouldQueue
|
|||||||
$latestPrice = floatval($info['z'] ?? $info['o'] ?? 0);
|
$latestPrice = floatval($info['z'] ?? $info['o'] ?? 0);
|
||||||
$latestPriceDate = isset($info['d']) ? Carbon::parse($info['d'])->format('Y-m-d') : now()->format('Y-m-d');
|
$latestPriceDate = isset($info['d']) ? Carbon::parse($info['d'])->format('Y-m-d') : now()->format('Y-m-d');
|
||||||
$name = $info['n'] ?? $this->defaultName;
|
$name = $info['n'] ?? $this->defaultName;
|
||||||
|
// 忘了備份時用的
|
||||||
|
// $latestPriceDate = isset($info['d']) ? Carbon::parse($info['d'], 'Asia/Taipei')->subDay()->format('Y-m-d') : now()->subDay()->format('Y-m-d');
|
||||||
if (isset($info['y']) && $latestPrice > 0) {
|
if (isset($info['y']) && $latestPrice > 0) {
|
||||||
$changeAmount = floatval($latestPrice - $info['y']);
|
$changeAmount = floatval($latestPrice - $info['y']);
|
||||||
$changeRate = ($changeAmount / $info['y']) * 100;
|
$changeRate = ($changeAmount / $info['y']) * 100;
|
||||||
@@ -88,7 +89,9 @@ class UpdateAssetPriceJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($headerInfo) {
|
if ($headerInfo) {
|
||||||
$latestPriceDate = isset($headerInfo['navDate']) ? substr($headerInfo['navDate'], 0, 10) : now()->format('Y-m-d');
|
$latestPriceDate = isset($headerInfo['navDate']) ? Carbon::parse($headerInfo['navDate'], 'Asia/Taipei')->format('Y-m-d') : now()->format('Y-m-d');
|
||||||
|
// 忘了備份時用的
|
||||||
|
// $latestPriceDate = isset($headerInfo['navDate']) ? Carbon::parse($headerInfo['navDate'], 'Asia/Taipei')->subDay()->format('Y-m-d') : now()->subDay()->format('Y-m-d');
|
||||||
$latestPrice = floatval($headerInfo['nav'] ?? 0);
|
$latestPrice = floatval($headerInfo['nav'] ?? 0);
|
||||||
|
|
||||||
if ($latestPriceDate !== now()->format('Y-m-d')) {
|
if ($latestPriceDate !== now()->format('Y-m-d')) {
|
||||||
@@ -124,7 +127,7 @@ class UpdateAssetPriceJob implements ShouldQueue
|
|||||||
SecurityPriceHistories::updateOrCreate(
|
SecurityPriceHistories::updateOrCreate(
|
||||||
[
|
[
|
||||||
'security_id' => $security->id,
|
'security_id' => $security->id,
|
||||||
'price_date' => now()->format('Y-m-d'), // 記錄今天的日期
|
'price_date' => $latestPriceDate, // 記錄今天的日期
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'price' => $latestPrice,
|
'price' => $latestPrice,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
api: __DIR__.'/../routes/api.php',
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
|
apiPrefix: 'v1',
|
||||||
then: function () {
|
then: function () {
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
->group(base_path('routes/auth.php'));
|
->group(base_path('routes/auth.php'));
|
||||||
|
|||||||
@@ -1,38 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'paths' => ['api/*', 'v1/*', 'auth/*', 'sanctum/csrf-cookie'],
|
||||||
|
|
||||||
/*
|
// 🎯 暫時改成全放行星號,排除 cors.php 這一層的陣列比對干擾
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure your settings for cross-origin resource sharing
|
|
||||||
| or "CORS". This determines what cross-origin operations may execute
|
|
||||||
| in web browsers. You are free to adjust these settings as needed.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 🟢 修正 1:確保包含你要戳的所有 API 路徑(包含上傳)
|
|
||||||
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
|
|
||||||
|
|
||||||
// 🟢 修正 2:允許前端傳入的 HTTP 方法(GET, POST, PUT, DELETE 都要開)
|
|
||||||
'allowed_methods' => ['*'],
|
'allowed_methods' => ['*'],
|
||||||
|
'allowed_origins' => ['*'],
|
||||||
// 🟢 修正 3:精準允許你的 Vue 前端開發網址(看你前端是在 5173 還是其他埠號)
|
|
||||||
// 你也可以直接寫 ['*'] 偷懶大開,但在本地端寫特定網址更安全:
|
|
||||||
'allowed_origins' => ['http://localhost:5173', 'http://127.0.0.1:5173'],
|
|
||||||
|
|
||||||
'allowed_origins_patterns' => [],
|
'allowed_origins_patterns' => [],
|
||||||
|
|
||||||
// 🟢 修正 4:上傳 FormData 時會帶有 Content-Type 等 Header,必須全開
|
|
||||||
'allowed_headers' => ['*'],
|
'allowed_headers' => ['*'],
|
||||||
|
|
||||||
'exposed_headers' => [],
|
'exposed_headers' => [],
|
||||||
|
|
||||||
'max_age' => 0,
|
'max_age' => 0,
|
||||||
|
|
||||||
// 🟢 修正 5:非常關鍵!因為我們用了 Sanctum 驗證狀態,必須允許傳遞 Cookie/憑證
|
// 🎯 保持 true 以支援登入態 Cookie
|
||||||
'supports_credentials' => true,
|
'supports_credentials' => true,
|
||||||
|
|
||||||
];
|
];
|
||||||
35
develop.md
Normal file
35
develop.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Fin-Buddy 專案開發進度與規劃
|
||||||
|
|
||||||
|
## ✅ 已完成階段
|
||||||
|
- [x] **容器架構部署**:Laravel 與 PostgreSQL 成功運行於 `1panel-network`。
|
||||||
|
- [x] **資料庫遷移**:順利整合至 1Panel 內建資料庫容器。
|
||||||
|
- [x] **環境對齊**:修正 Port 對應 (8081:9000) 及路由快取問題,系統穩定運行。
|
||||||
|
- [x] **端到端測試**:核心 API 流程驗證通過。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ 待辦與優化清單
|
||||||
|
|
||||||
|
### 1. 系統清理與維運
|
||||||
|
- [ ] 移除舊容器與無用映像檔 (釋放空間)。
|
||||||
|
- [ ] 將 `web.php` 內危險的同步腳本遷移至 Laravel Artisan Command。
|
||||||
|
- [ ] 檢查 `.env` 安全性設定 (關閉 `APP_DEBUG`)。
|
||||||
|
- [ ] 配置 1Panel 資料庫自動備份任務。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 未來 Roadmap
|
||||||
|
|
||||||
|
| 階段 | 項目 | 目標 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **I** | **APP 化 (PWA)** | 將前端轉換為 PWA,實現手機桌面安裝與離線訪問。 |
|
||||||
|
| **I** | **記帳 Widget** | 實作快速記帳入口,點擊即錄入,簡化操作動線。 |
|
||||||
|
| **II** | **內容細化與排程** | 擴充 DB Schema,納入「信用卡結帳日」、「發票排程」邏輯。 |
|
||||||
|
| **II** | **銀行與信用卡聯動** | 實作數據同步機制 (Open Banking / 自動抓取)。 |
|
||||||
|
| **III** | **後台與權限管理** | 導入 Filament 與 Spatie,建立 API 測試後台與系統監控。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 優先級建議
|
||||||
|
1. **建議先做:** 將 `sync-all-history` 正式化為 Command,減少系統安全風險。
|
||||||
|
2. **開發策略:** 建議優先採用 **Filament** 進行後台開發,這能省去 80% 開發後台介面的時間,讓你直接專注於監控系統與 API 測試。
|
||||||
47
docker-compose.yml
Normal file
47
docker-compose.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
services:
|
||||||
|
# 🐘 1. 你的 Laravel 後端服務
|
||||||
|
finance-backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: finance-laravel-backend
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8081:9000"
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- DB_CONNECTION=pgsql
|
||||||
|
- DB_HOST=1Panel-postgresql-PXlc # 🎯 關鍵:改用資料庫的服務名稱當作 Host!
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_DATABASE=myfinance
|
||||||
|
- DB_USERNAME=myfinance_henry4682
|
||||||
|
- DB_PASSWORD=DwmQ78PB
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
depends_on:
|
||||||
|
- finance-postgres # 🎯 確保資料庫先開,後端才開
|
||||||
|
|
||||||
|
# 🐬 2. 直接在這裡把消失的 PostgreSQL 生回來!
|
||||||
|
# finance-postgres:
|
||||||
|
# image: postgres:15-alpine # 使用穩定的 15-alpine 版本
|
||||||
|
# container_name: finance-laravel-backend-postgres
|
||||||
|
# restart: always
|
||||||
|
# ports:
|
||||||
|
# - "5433:5432" # 🎯 把 5432 暴露給實體主機,這樣你本機的 pgAdmin 就能連進來了!
|
||||||
|
# environment:
|
||||||
|
# - POSTGRES_DB=myfinance
|
||||||
|
# - POSTGRES_USER=myfinance_henry4682
|
||||||
|
# - POSTGRES_PASSWORD=DwmQ78PB
|
||||||
|
# volumes:
|
||||||
|
# - finance_db_data:/var/lib/postgresql/data # 🎯 資料持久化,容器重啟資料也不會掉
|
||||||
|
# networks:
|
||||||
|
# - 1panel-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
1panel-network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
# 🎯 定義資料庫的儲存卷
|
||||||
|
volumes:
|
||||||
|
finance_db_data:
|
||||||
157
routes/web.php
157
routes/web.php
@@ -1,18 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
|
||||||
use App\Http\Controllers\ExpenseController;
|
|
||||||
use App\Http\Controllers\CategoryController;
|
|
||||||
use App\Http\Controllers\CategoryRuleController;
|
|
||||||
use App\Http\Controllers\DashboardController;
|
|
||||||
use App\Http\Controllers\InvoiceImportController;
|
|
||||||
use App\Models\InvestmentHolding;
|
|
||||||
use App\Models\Security;
|
|
||||||
use App\Models\SecurityPriceHistories;
|
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
@@ -25,151 +13,6 @@ Route::get('/', function () {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route::get('/dashboard', function () {
|
|
||||||
// return Inertia::render('Dashboard');
|
|
||||||
// })->middleware(['auth', 'verified'])->name('dashboard');
|
|
||||||
|
|
||||||
Route::get('/sync-all-history', function () {
|
|
||||||
set_time_limit(600);
|
|
||||||
|
|
||||||
// 🟢 1. 纯粹捞出持仓,不用多余的 with 关联
|
|
||||||
$holdings = InvestmentHolding::all();
|
|
||||||
$count = 0;
|
|
||||||
|
|
||||||
foreach ($holdings as $holding) {
|
|
||||||
|
|
||||||
// 🟢 2. 直接用持仓的 symbol 去匹配字典表,拿到真正需要的自增主键 id
|
|
||||||
$security = DB::table('securities')->where('code', $holding->symbol)->first();
|
|
||||||
|
|
||||||
if (!$security) {
|
|
||||||
// 如果字典表没有,顺手现场帮它开荒一笔,拿到新 id
|
|
||||||
$securityId = DB::table('securities')->insertGetId([
|
|
||||||
'code' => $holding->symbol,
|
|
||||||
'name' => $holding->name ?? $holding->symbol,
|
|
||||||
'type' => $holding->type,
|
|
||||||
'source' => $holding->type === 'stock' ? 'twse' : 'anue',
|
|
||||||
'latest_price' => floatval($holding->avg_cost ?? 0),
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$securityId = $security->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找到最早的一笔买入交易作为历史起点
|
|
||||||
$firstTx = $holding->transactions()->where('action', 'buy')->orderBy('traded_at', 'asc')->first();
|
|
||||||
if (!$firstTx) continue;
|
|
||||||
|
|
||||||
$startDate = Carbon::parse($firstTx->traded_at);
|
|
||||||
$endDate = Carbon::now();
|
|
||||||
|
|
||||||
dump("正在同步 [{$holding->type}] {$holding->symbol} - {$holding->name} 的历史资料自 {$startDate->toDateString()} 起...");
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// 股票 (Stock) 同步
|
|
||||||
// ----------------------------------------
|
|
||||||
if ($holding->type === 'stock') {
|
|
||||||
$current = $startDate->copy()->startOfMonth();
|
|
||||||
|
|
||||||
while ($current->lte($endDate)) {
|
|
||||||
$res = Http::get('https://www.twse.com.tw/rwd/zh/afterTrading/STOCK_DAY', [
|
|
||||||
'stockNo' => $holding->symbol,
|
|
||||||
'date' => $current->format('Ymd'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$data = $res->json();
|
|
||||||
|
|
||||||
if (isset($data['stat']) && $data['stat'] === 'OK' && !empty($data['data'])) {
|
|
||||||
foreach ($data['data'] as $row) {
|
|
||||||
[$y, $m, $d] = explode('/', $row[0]);
|
|
||||||
$dateStr = (1911 + (int)$y) . '-' . $m . '-' . $d;
|
|
||||||
|
|
||||||
if ($dateStr >= $startDate->toDateString()) {
|
|
||||||
$closePrice = (float) str_replace(',', '', $row[6]);
|
|
||||||
|
|
||||||
// 🟢 成功通过 $securityId 灌入历史表
|
|
||||||
DB::table('security_price_histories')->updateOrInsert(
|
|
||||||
[
|
|
||||||
'security_id' => $securityId,
|
|
||||||
'price_date' => $dateStr,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'price' => $closePrice,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$current->addMonth();
|
|
||||||
usleep(300000); // 防黑名单
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ----------------------------------------
|
|
||||||
// 基金 (Fund) 同步
|
|
||||||
// ----------------------------------------
|
|
||||||
elseif ($holding->type === 'fund') {
|
|
||||||
$res = Http::post("https://www.anuefund.com/anuefundApi/FundDetail/Price", [
|
|
||||||
'priceENUM' => 'NAVHIS',
|
|
||||||
'fundID' => $holding->symbol,
|
|
||||||
'strDate' => $startDate->format('Y-m-d'),
|
|
||||||
'endDate' => $endDate->format('Y-m-d'),
|
|
||||||
'chartFomatUse' => true,
|
|
||||||
'reload' => false
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($res->ok() && !empty($res->json('data'))) {
|
|
||||||
$pairs = json_decode($res->json('data'), true);
|
|
||||||
|
|
||||||
foreach ($pairs as $item) {
|
|
||||||
if ($item[1] === 0) continue;
|
|
||||||
|
|
||||||
$dateStr = date('Y-m-d', intval($item[0]) / 1000);
|
|
||||||
$navPrice = (float) $item[1];
|
|
||||||
|
|
||||||
// 🟢 成功通过 $securityId 灌入历史表
|
|
||||||
DB::table('security_price_histories')->updateOrInsert(
|
|
||||||
[
|
|
||||||
'security_id' => $securityId,
|
|
||||||
'price_date' => $dateStr,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'price' => $navPrice,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dump("▶ [{$holding->symbol}] 同步完成。");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "🎉 历史资料回归本地资料库成功!共写录 {$count} 笔历史价格点。";
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
|
||||||
->name('dashboard');
|
|
||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
|
||||||
Route::get('/expenses/import', [InvoiceImportController::class, 'index'])->name('expenses.import');
|
|
||||||
Route::post('/expenses/import', [InvoiceImportController::class, 'store'])->name('expenses.import.store');
|
|
||||||
Route::delete('/expenses/batch', [ExpenseController::class, 'destroyBatch'])
|
|
||||||
->name('expenses.destroyBatch');
|
|
||||||
// 注意要放在 resource 路由之前!
|
|
||||||
Route::resource('expenses', ExpenseController::class)
|
|
||||||
->only(['index', 'store', 'update', 'destroy']);
|
|
||||||
Route::resource('categories', CategoryController::class)
|
|
||||||
->only(['index', 'store', 'update', 'destroy']);
|
|
||||||
Route::resource('category-rules', CategoryRuleController::class)
|
|
||||||
->only(['store', 'destroy']);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
require __DIR__ . '/auth.php';
|
require __DIR__ . '/auth.php';
|
||||||
|
|||||||
11861
storage/logs/laravel.log
Normal file
11861
storage/logs/laravel.log
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user