Commit cb1ed766 authored by vicotor's avatar vicotor

add code

parents
Pipeline #893 failed with stages
.idea
.vscode
-- WARNING: This schema is for context only and is not meant to be run.
-- Table order and constraints may not be valid for execution.
CREATE TABLE invitation (
user_id uuid NOT NULL,
inviter_id uuid,
wallet_address character varying NOT NULL,
real_name character varying NOT NULL DEFAULT ''::character varying,
x_name character varying,
wechat_name character varying,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp without time zone,
is_signed boolean DEFAULT false,
reward integer DEFAULT 0,
CONSTRAINT invitation_pkey PRIMARY KEY (user_id),
CONSTRAINT invitation_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id),
CONSTRAINT invitation_inviter_id_fkey FOREIGN KEY (inviter_id) REFERENCES auth.users(id)
);
DROP TABLE IF EXISTS sign_config;
CREATE TABLE IF NOT EXISTS sign_config (
id SERIAL PRIMARY KEY,
qr_code_secret TEXT NOT NULL,
venue_latitude DECIMAL(10,8) NOT NULL,
venue_longitude DECIMAL(11,8) NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
radius_km DECIMAL(5,2) DEFAULT 1.0,
reward_base_amount DECIMAL(10,0) DEFAULT 10,
reward_max_amount DECIMAL(10,0) DEFAULT 1000,
created_at TIMESTAMP DEFAULT NOW()
);
-- 签到记录表
CREATE TABLE IF NOT EXISTS sign_logs (
id SERIAL PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
qr_code TEXT NOT NULL,
user_latitude DECIMAL(10,8) NOT NULL,
user_longitude DECIMAL(11,8) NOT NULL,
distance_km DECIMAL(8,3),
is_successful BOOLEAN DEFAULT false,
error_reason TEXT,
signed_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO sign_config (
qr_code_secret,
venue_latitude,
venue_longitude,
start_time,
end_time,
radius_km,
reward_base_amount,
reward_max_amount
) VALUES (
'meeting_checkin_2024_08_29_abcdefghijklmnopqrstuvwxyz123456789012',
22.2835121, --pier 1929
114.1743664,
'2025-08-28 11:30:00', -- UTC 时间,对应北京时间 19:30
'2025-08-29 12:30:00', -- UTC 时间,对应北京时间 20:30
1.0,
10,
1000
);
supabase init
supabase start
supabase functions new <func-name>
supabase login
supabase functions serve
supabase functions deploy <func-name>
This diff is collapsed.
main
\ No newline at end of file
v2.39.2
\ No newline at end of file
This diff is collapsed.
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
// supabase/functions/sign/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'jsr:@supabase/supabase-js@2';
console.log("Sign function started")
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
};
// Haversine公式计算距离
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371 // 地球半径(千米)
const dLat = (lat2 - lat1) * Math.PI / 180
const dLon = (lon2 - lon1) * Math.PI / 180
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: corsHeaders
});
}
try {
// 验证参数有效性
const { user_id, qr_code, latitude, longitude } = await req.json()
if (!user_id || !qr_code || typeof latitude !== 'number' || typeof longitude !== 'number') {
console.error('invalid parameters')
return new Response(JSON.stringify({
error: 'Invalid parameters'
}), {
status: 400,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
}
// 获取用户认证信息
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
console.error('no auth header')
return new Response(JSON.stringify({
error: 'No Authorization'
}), {
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
}
// 创建Supabase客户端
const supabaseClient = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
global: {
headers: {
Authorization: authHeader
}
}
});
// 获取当前用户
const { data: { user }, error: userError } = await supabaseClient.auth.getUser();
if (userError || !user) {
console.error('Auth error:', userError)
return new Response(JSON.stringify({
error: 'Auth Fail',
details: userError?.message
}), {
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
}
console.info(`Sign request from user ${user.id}: QR=${qr_code}, Lat=${latitude}, Lon=${longitude}`)
if (user.id !== user_id) {
console.error(`User ID mismatch: ${user.id} !== ${user_id}`)
return new Response(JSON.stringify({
error: 'User ID mismatch'
}), {
status: 403,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
}
// console.log(`Sign request from user ${user_id}: QR=${qr_code}, Lat=${latitude}, Lon=${longitude}`)
// 获取签到配置
const { data: config, error: configError } = await supabaseClient
.from('sign_config')
.select('*')
.order('created_at', { ascending: false })
.limit(1)
.single()
if (configError || !config) {
console.error('get sign config error:', configError)
return new Response(JSON.stringify({
success: false,
message: 'not found config'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 验证时间范围
const now = new Date()
const beijingTime = new Date(now.getTime() + (8 * 60 * 60 * 1000))
const startTime = new Date(config.start_time)
const endTime = new Date(config.end_time)
console.info(`Current Beijing time: ${beijingTime.toISOString()}, Sign time range: ${startTime.toISOString()} - ${endTime.toISOString()}`)
if (beijingTime < startTime || beijingTime > endTime) {
console.error('Not in sign time range')
return new Response(JSON.stringify({
success: false,
message: 'not in sign time range'
}), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 从 invitation 表中 获取用户签到状态.
const { data: userInfo, error: getError } = await supabaseClient
.from('invitation')
.select('*')
.eq('user_id', user_id)
.limit(1)
.single()
if (getError) {
console.error('Get sign status error:', getError)
return new Response(JSON.stringify({
success: false,
message: 'User sign status fetch failed'
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// console.log(`Fetched user info: ${JSON.stringify(userInfo)}`)
if (userInfo.is_signed) {
console.error('User has already signed in', user.id)
return new Response(JSON.stringify({
success: true,
message: 'You have already signed in'
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 验证二维码
if (qr_code !== config.qr_code_secret) {
console.error(`QR code mismatch: ${qr_code} !== ${config.qr_code_secret}`)
return new Response(JSON.stringify({
success: false,
message: 'Invalid QR code'
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 验证定位距离
const distance = calculateDistance(
config.venue_latitude,
config.venue_longitude,
latitude,
longitude
)
if (distance > config.radius_km) {
console.error(`User ${user_id} is too far: ${distance.toFixed(2)} km away, limit is ${config.radius_km} km`)
return new Response(JSON.stringify({
success: false,
message: `You are ${distance.toFixed(2)} kilometers away from the venue, exceeding the ${config.radius_km}-kilometer range.`
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// Fetch the count of invitations where inviter_id matches the user's ID
const { count: count, error: countError } = await supabaseClient
.from('invitation')
.select('user_id', {count:'exact'})
.eq('inviter_id', user_id);
const invitedCount = count || 0;
if (countError) {
console.error('Error fetching invitation count:', countError);
return new Response(JSON.stringify({
success: false,
message: 'Failed to fetch invitation count'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
console.info(`Invitation count for user ${user_id}: ${invitedCount}`);
// 验证邀请人数
if ((invitedCount || 0) <= 1) {
console.error(`Not enough invitations: ${invitedCount} (minimum 2 required)`)
return new Response(JSON.stringify({
success: false,
message: 'Minimum 2 invitations required to sign in'
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 计算奖励金额
let rewardAmount = config.reward_base_amount * invitedCount
if (rewardAmount > config.reward_max_amount) {
rewardAmount = config.reward_max_amount
}
// 更新用户签到状态
const { error: updateError } = await supabaseClient
.from('invitation')
.update({
is_signed: true,
reward: rewardAmount
})
.eq('user_id', user_id)
if (updateError) {
console.error('Update error:', updateError)
return new Response(JSON.stringify({
success: false,
message: 'Sign in failed, please try again'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// 记录签到日志
await supabaseClient
.from('sign_logs')
.insert({
user_id: user_id,
qr_code: qr_code,
user_latitude: latitude,
user_longitude: longitude,
distance_km: distance,
is_successful: true,
error_reason: null
})
console.info(`Sign successful for user ${user_id}, reward: ${rewardAmount}`)
return new Response(JSON.stringify({
success: true,
message: 'sign success',
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} catch (error) {
console.error('Function error:', error)
return new Response(JSON.stringify({
success: false,
message: 'Internal error'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})
\ No newline at end of file
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
// supabase/functions/sign/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'jsr:@supabase/supabase-js@2';
console.log("Sign function started")
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
};
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: corsHeaders
});
}
try {
// 创建Supabase客户端
// const supabaseClient = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
// });
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(JSON.stringify({
error: 'No Authorization'
}), {
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
}
console.log('auth header found')
// 创建Supabase客户端
const supabaseClient = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
global: {
headers: {
Authorization: authHeader
}
}
});
// 获取签到配置
const { data: config, error: configError } = await supabaseClient
.from('sign_config')
.select('*')
.order('created_at', { ascending: false })
.limit(1)
.single()
if (configError || !config) {
console.error('Config error:', configError)
return new Response(JSON.stringify({
success: false,
message: 'not found sign config'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
return new Response(JSON.stringify({
starttime: config.start_time,
endtime: config.end_time,
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} catch (error) {
console.error('Function error:', error)
return new Response(JSON.stringify({
success: false,
message: 'Internal error'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment