565 字
3 分鐘
LINE LIFF 打卡系統開發實戰

專案背景#

為連鎖門市開發行動打卡系統,讓員工透過 LINE 官方帳號即可完成上下班打卡,同時整合 Odoo 人資模組進行出勤管理。

技術架構#

┌─────────────────────────────────────┐
│ LINE 官方帳號 │
│ (LIFF 應用程式) │
└─────────────────┬───────────────────┘
┌───────▼───────┐
│ LIFF SDK │
│ + Svelte 5 │
└───────┬───────┘
┌───────▼───────┐
│ REST API │
│ (Odoo 17) │
└───────┬───────┘
┌───────▼───────┐
│ PostgreSQL │
│ 出勤記錄 │
└───────────────┘

核心功能#

1. GPS 定位打卡#

// 取得使用者位置
async function getCurrentPosition() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
lat: position.coords.latitude,
lng: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
reject,
{ enableHighAccuracy: true, timeout: 10000 }
);
});
}
// 驗證是否在門市範圍內
function isWithinStore(userPos, storePos, radiusMeters = 100) {
const distance = calculateDistance(userPos, storePos);
return distance <= radiusMeters;
}

2. LIFF 整合#

import liff from '@line/liff';
async function initializeLiff() {
await liff.init({ liffId: import.meta.env.VITE_LIFF_ID });
if (!liff.isLoggedIn()) {
liff.login();
return;
}
const profile = await liff.getProfile();
return {
lineUserId: profile.userId,
displayName: profile.displayName,
pictureUrl: profile.pictureUrl
};
}

3. 打卡流程#

啟動 LIFF
┌─────────────┐
│ LINE 登入 │
│ 取得 userId │
└──────┬──────┘
┌─────────────┐
│ 取得 GPS │
│ 位置資訊 │
└──────┬──────┘
┌─────────────┐ 否
│ 門市範圍內?│────────► 顯示錯誤提示
└──────┬──────┘
│ 是
┌─────────────┐
│ 呼叫 API │
│ 記錄打卡 │
└──────┬──────┘
┌─────────────┐
│ 顯示結果 │
│ 打卡成功 │
└─────────────┘

Svelte 5 前端實作#

<script>
import { onMount } from 'svelte';
let status = $state('loading');
let location = $state(null);
let store = $state(null);
onMount(async () => {
await initializeLiff();
location = await getCurrentPosition();
store = await getNearestStore(location);
});
async function handleCheckIn() {
status = 'processing';
const result = await fetch('/api/attendance/check-in', {
method: 'POST',
body: JSON.stringify({
storeId: store.id,
latitude: location.lat,
longitude: location.lng
})
});
status = result.ok ? 'success' : 'error';
}
</script>
<main class="container">
{#if status === 'loading'}
<p>載入中...</p>
{:else if status === 'ready'}
<button onclick={handleCheckIn}>
打卡 - {store.name}
</button>
{:else if status === 'success'}
<p>✅ 打卡成功!</p>
{/if}
</main>

Odoo 後端 API#

class AttendanceController(http.Controller):
@http.route('/api/attendance/check-in', type='json', auth='public')
def check_in(self, **kwargs):
line_user_id = kwargs.get('line_user_id')
store_id = kwargs.get('store_id')
latitude = kwargs.get('latitude')
longitude = kwargs.get('longitude')
# 查詢員工
employee = request.env['hr.employee'].sudo().search([
('line_user_id', '=', line_user_id)
], limit=1)
if not employee:
return {'success': False, 'error': '員工未綁定'}
# 建立打卡記錄
attendance = request.env['hr.attendance'].sudo().create({
'employee_id': employee.id,
'check_in': fields.Datetime.now(),
'store_id': store_id,
'latitude': latitude,
'longitude': longitude,
})
return {'success': True, 'attendance_id': attendance.id}

專案成效#

指標導入前導入後
打卡方式紙本簽到LINE 行動打卡
統計作業3 天/月即時自動
代打問題常發生GPS 驗證杜絕
員工滿意度60%95%

技術重點#

  • LINE LIFF SDK 整合
  • Svelte 5 響應式開發
  • GPS Geolocation API
  • Odoo REST API 設計

部分資訊可能已經過時