# ๐Ÿ”จ doc_mall ๆจกๅ—ๆŠ€ๆœฏๅฎž็Žฐๆ‹†่งฃ ## ๐Ÿ“‹ ็›ฎๅฝ• 1. [ๆ•ดไฝ“ๆžถๆž„](#ๆ•ดไฝ“ๆžถๆž„) 2. [ๆ•ฐๆฎๅบ“ๅฑ‚ๅฎž็Žฐ](#ๆ•ฐๆฎๅบ“ๅฑ‚ๅฎž็Žฐ) 3. [ๅŽ็ซฏ/APIๅฑ‚ๅฎž็Žฐ](#ๅŽ็ซฏapiๅฑ‚ๅฎž็Žฐ) 4. [ๅ‰็ซฏๅฎž็Žฐ](#ๅ‰็ซฏๅฎž็Žฐ) 5. [ๆ•ฐๆฎๆตๆœบๅˆถ](#ๆ•ฐๆฎๆตๆœบๅˆถ) 6. [ไธšๅŠก้€ป่พ‘ๅฎž็Žฐ](#ไธšๅŠก้€ป่พ‘ๅฎž็Žฐ) 7. [ๅฎ‰ๅ…จๆœบๅˆถ](#ๅฎ‰ๅ…จๆœบๅˆถ) 8. [ๆ€ง่ƒฝไผ˜ๅŒ–](#ๆ€ง่ƒฝไผ˜ๅŒ–) --- ## ไธ€ใ€ๆ•ดไฝ“ๆžถๆž„ ### 1.1 ๆŠ€ๆœฏๆ ˆๆžถๆž„ๅ›พ ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๅ‰็ซฏๅฑ‚ (uni-app-x) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๆถˆ่ดน่€…็ซฏ โ”‚ โ”‚ ๅ•†ๅฎถ็ซฏ โ”‚ โ”‚ ้…้€็ซฏ โ”‚ โ”‚ ็ฎก็†็ซฏ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Supabase ๅฎขๆˆท็ซฏๅฐ่ฃ… โ”‚ โ”‚ โ”‚ โ”‚ (AkSupa.uts) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ HTTPS REST API โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ API ๅฑ‚ (PostgREST) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ่‡ชๅŠจ็”Ÿๆˆ REST API โ”‚ โ”‚ โ”‚ โ”‚ - GET /rest/v1/ml_products โ”‚ โ”‚ โ”‚ โ”‚ - POST /rest/v1/ml_orders โ”‚ โ”‚ โ”‚ โ”‚ - RPC /rest/v1/rpc/calculate_cart_total โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ่ฎค่ฏๅฑ‚ (Supabase Auth) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ JWT Token ่ฎค่ฏ โ”‚ โ”‚ โ”‚ โ”‚ - ็”จๆˆท็™ปๅฝ•/ๆณจๅ†Œ โ”‚ โ”‚ โ”‚ โ”‚ - Token ๅˆทๆ–ฐ โ”‚ โ”‚ โ”‚ โ”‚ - ๆƒ้™้ชŒ่ฏ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๆƒ้™ๅฑ‚ (RLS - Row Level Security) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ่กŒ็บงๅฎ‰ๅ…จ็ญ–็•ฅ โ”‚ โ”‚ โ”‚ โ”‚ - ็”จๆˆทๆ•ฐๆฎ้š”็ฆป โ”‚ โ”‚ โ”‚ โ”‚ - ๅ•†ๅฎถๆƒ้™ๆŽงๅˆถ โ”‚ โ”‚ โ”‚ โ”‚ - ๅ…ฌๅผ€ๆ•ฐๆฎ่ฎฟ้—ฎ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๆ•ฐๆฎๅบ“ๅฑ‚ (PostgreSQL) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ่กจ็ป“ๆž„ โ”‚ โ”‚ ่งฆๅ‘ๅ™จ โ”‚ โ”‚ ๅ‡ฝๆ•ฐ โ”‚ โ”‚ ่ง†ๅ›พ โ”‚ โ”‚ โ”‚ โ”‚ ็ดขๅผ• โ”‚ โ”‚ RLS็ญ–็•ฅ โ”‚ โ”‚ ๅบๅˆ— โ”‚ โ”‚ ๆ‰ฉๅฑ• โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### 1.2 ๆ ธๅฟƒ่ฎพ่ฎกๆจกๅผ #### BaaS (Backend as a Service) ๆจกๅผ - **็‰น็‚น**: ไฝฟ็”จ Supabase ไฝœไธบๅŽ็ซฏๆœๅŠก๏ผŒๆ— ้œ€่‡ชๅปบๅŽ็ซฏๆœๅŠกๅ™จ - **ไผ˜ๅŠฟ**: - ่‡ชๅŠจ็”Ÿๆˆ REST API - ๅ†…็ฝฎ่ฎค่ฏๅ’Œๆƒ้™็ณป็ปŸ - ๅฎžๆ—ถๆ•ฐๆฎๅŒๆญฅ - ๅ‡ๅฐ‘ๅŽ็ซฏๅผ€ๅ‘ๅทฅไฝœ้‡ #### ๆ•ฐๆฎๅบ“ไผ˜ๅ…ˆ (Database-First) ๆจกๅผ - **ๆต็จ‹**: ๆ•ฐๆฎๅบ“่ฎพ่ฎก โ†’ ่‡ชๅŠจ็”Ÿๆˆ API โ†’ ๅ‰็ซฏ่ฐƒ็”จ - **ๅฎž็Žฐ**: 1. ่ฎพ่ฎกๆ•ฐๆฎๅบ“่กจ็ป“ๆž„ 2. PostgREST ่‡ชๅŠจ็”Ÿๆˆ REST API 3. ๅ‰็ซฏ้€š่ฟ‡ Supabase ๅฎขๆˆท็ซฏ่ฐƒ็”จ #### ็ฑปๅž‹้ฉฑๅŠจๅผ€ๅ‘ (Type-Driven Development) - **ๅฎž็Žฐ**: TypeScript/UTS ็ฑปๅž‹ๅฎšไน‰ - **ๆ–‡ไปถ**: `types/mall-types.uts` - **ไผ˜ๅŠฟ**: ็ฑปๅž‹ๅฎ‰ๅ…จใ€ไปฃ็ ๆ็คบใ€ๅ‡ๅฐ‘้”™่ฏฏ --- ## ไบŒใ€ๆ•ฐๆฎๅบ“ๅฑ‚ๅฎž็Žฐ ### 2.1 ่กจ็ป“ๆž„่ฎพ่ฎก #### 2.1.1 ๆ ธๅฟƒ่กจ็ป“ๆž„ **็”จๆˆทๆ‰ฉๅฑ•่กจ** (`ml_user_profiles`) ```sql CREATE TABLE public.ml_user_profiles ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE, status INTEGER DEFAULT 1, -- 1:ๆญฃๅธธ 2:ๅ†ป็ป“ 3:ๆณจ้”€ 4:ๅพ…ๅฎกๆ ธ real_name VARCHAR(100), -- ็œŸๅฎžๅง“ๅ id_card VARCHAR(32), -- ่บซไปฝ่ฏๅท credit_score INTEGER DEFAULT 100, -- ไฟก็”จๅˆ†ๆ•ฐ 0-1000 verification_status INTEGER DEFAULT 0, -- ่ฎค่ฏ็Šถๆ€ verification_data JSONB DEFAULT '{}', -- ่ฎค่ฏ็›ธๅ…ณๆ•ฐๆฎ preferences JSONB DEFAULT '{}', -- ็”จๆˆทๅๅฅฝ่ฎพ็ฝฎ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); ``` **ๅ•†ๅ“่กจ** (`ml_products`) ```sql CREATE TABLE public.ml_products ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), cid SERIAL UNIQUE NOT NULL, -- SEOๅ‹ๅฅฝ็š„่‡ชๅขžID merchant_id UUID NOT NULL REFERENCES public.ak_users(id), category_id UUID NOT NULL REFERENCES public.ml_categories(id), brand_id UUID REFERENCES public.ml_brands(id), product_code VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(500) NOT NULL, description TEXT, main_image_url TEXT, image_urls JSONB DEFAULT '[]', -- ๅ•†ๅ“ๅ›พ็‰‡ๆ•ฐ็ป„ base_price DECIMAL(12,2) NOT NULL, total_stock INTEGER DEFAULT 0, available_stock INTEGER DEFAULT 0, status INTEGER DEFAULT 1, -- 1:ไธŠๆžถ 2:ไธ‹ๆžถ 3:่‰็จฟ 4:ๅˆ ้™ค view_count INTEGER DEFAULT 0, sale_count INTEGER DEFAULT 0, rating_avg DECIMAL(3,2) DEFAULT 0.00, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); ``` **่ฎขๅ•่กจ** (`ml_orders`) ```sql CREATE TABLE public.ml_orders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), cid SERIAL UNIQUE NOT NULL, order_no VARCHAR(50) UNIQUE NOT NULL, user_id UUID NOT NULL REFERENCES public.ak_users(id), merchant_id UUID NOT NULL REFERENCES public.ak_users(id), product_amount DECIMAL(12,2) NOT NULL DEFAULT 0, discount_amount DECIMAL(12,2) DEFAULT 0, shipping_fee DECIMAL(12,2) DEFAULT 0, total_amount DECIMAL(12,2) NOT NULL, shipping_address JSONB NOT NULL, -- ๆ”ถ่ดงๅœฐๅ€JSON order_status INTEGER DEFAULT 1, -- 1:ๅพ…ๆ”ฏไป˜ 2:ๅพ…ๅ‘่ดง 3:ๅพ…ๆ”ถ่ดง 4:ๅทฒๅฎŒๆˆ payment_status INTEGER DEFAULT 1, -- 1:ๆœชๆ”ฏไป˜ 2:ๅทฒๆ”ฏไป˜ 3:้ƒจๅˆ†้€€ๆฌพ 4:ๅ…จ้ข้€€ๆฌพ shipping_status INTEGER DEFAULT 1, -- 1:ๆœชๅ‘่ดง 2:ๅทฒๅ‘่ดง 3:่ฟ่พ“ไธญ 4:ๅทฒ้€่พพ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); ``` ### 2.2 ็ดขๅผ•่ฎพ่ฎก #### 2.2.1 ๆ€ง่ƒฝไผ˜ๅŒ–็ดขๅผ• ```sql -- ๅ•†ๅ“่กจ็ดขๅผ• CREATE INDEX idx_ml_products_merchant ON public.ml_products(merchant_id); CREATE INDEX idx_ml_products_category ON public.ml_products(category_id); CREATE INDEX idx_ml_products_status ON public.ml_products(status); CREATE INDEX idx_ml_products_cid ON public.ml_products(cid); -- SEOๆŸฅ่ฏข CREATE INDEX idx_ml_products_created ON public.ml_products(created_at DESC); -- ่ฎขๅ•่กจ็ดขๅผ• CREATE INDEX idx_ml_orders_user ON public.ml_orders(user_id); CREATE INDEX idx_ml_orders_merchant ON public.ml_orders(merchant_id); CREATE INDEX idx_ml_orders_status ON public.ml_orders(order_status); CREATE INDEX idx_ml_orders_created ON public.ml_orders(created_at DESC); -- JSONB GIN็ดขๅผ•๏ผˆ็”จไบŽJSONๅญ—ๆฎตๆŸฅ่ฏข๏ผ‰ CREATE INDEX idx_ml_products_images_gin ON public.ml_products USING GIN(image_urls); CREATE INDEX idx_ml_orders_address_gin ON public.ml_orders USING GIN(shipping_address); ``` ### 2.3 ่งฆๅ‘ๅ™จๅฎž็Žฐ #### 2.3.1 ่‡ชๅŠจๆ›ดๆ–ฐๆ—ถ้—ดๆˆณ ```sql -- ่งฆๅ‘ๅ™จๅ‡ฝๆ•ฐ CREATE OR REPLACE FUNCTION public.update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- ๅบ”็”จๅˆฐ่กจ CREATE TRIGGER trigger_ml_user_profiles_updated_at BEFORE UPDATE ON public.ml_user_profiles FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); ``` #### 2.3.2 ็กฎไฟๅ”ฏไธ€้ป˜่ฎคๅœฐๅ€ ```sql CREATE OR REPLACE FUNCTION public.ensure_single_default_address() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_default = TRUE THEN UPDATE public.ml_user_addresses SET is_default = FALSE WHERE user_id = NEW.user_id AND id != NEW.id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_ensure_single_default_address BEFORE INSERT OR UPDATE ON public.ml_user_addresses FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address(); ``` #### 2.3.3 ่‡ชๅŠจๆ›ดๆ–ฐๅ•†ๅ“ๅบ“ๅญ˜ ```sql CREATE OR REPLACE FUNCTION public.update_product_stock() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN -- ๆ’ๅ…ฅ่ฎขๅ•ๅ•†ๅ“ๆ—ถ๏ผŒๅ‡ๅฐ‘ๅบ“ๅญ˜ UPDATE public.ml_products SET available_stock = available_stock - NEW.quantity WHERE id = NEW.product_id; ELSIF TG_OP = 'DELETE' THEN -- ๅˆ ้™ค่ฎขๅ•ๅ•†ๅ“ๆ—ถ๏ผŒๆขๅคๅบ“ๅญ˜ UPDATE public.ml_products SET available_stock = available_stock + OLD.quantity WHERE id = OLD.product_id; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_product_stock AFTER INSERT OR DELETE ON public.ml_order_items FOR EACH ROW EXECUTE FUNCTION public.update_product_stock(); ``` ### 2.4 ๆ•ฐๆฎๅบ“ๅ‡ฝๆ•ฐๅฎž็Žฐ #### 2.4.1 ไธšๅŠกๅ‡ฝๆ•ฐ **็”Ÿๆˆ่ฎขๅ•ๅท** ```sql CREATE OR REPLACE FUNCTION public.generate_order_no() RETURNS TEXT AS $$ DECLARE order_no TEXT; BEGIN order_no := 'ML' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(NEXTVAL('ml_order_seq')::TEXT, 6, '0'); RETURN order_no; END; $$ LANGUAGE plpgsql; ``` **่ฎก็ฎ—่ดญ็‰ฉ่ฝฆๆ€ป้‡‘้ข** ```sql CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID) RETURNS DECIMAL AS $$ DECLARE total_amount DECIMAL(12,2) := 0; BEGIN SELECT COALESCE(SUM( CASE WHEN c.sku_id IS NOT NULL THEN s.price * c.quantity ELSE p.base_price * c.quantity END ), 0) INTO total_amount FROM public.ml_shopping_cart c LEFT JOIN public.ml_product_skus s ON c.sku_id = s.id LEFT JOIN public.ml_products p ON c.product_id = p.id WHERE c.user_id = p_user_id AND c.selected = TRUE AND p.status = 1 AND (s.id IS NULL OR s.status = 1); RETURN total_amount; END; $$ LANGUAGE plpgsql; ``` **่Žทๅ–็”จๆˆท้ป˜่ฎคๅœฐๅ€** ```sql CREATE OR REPLACE FUNCTION public.get_user_default_address(p_user_id UUID) RETURNS TABLE ( id UUID, receiver_name VARCHAR, receiver_phone VARCHAR, address_detail TEXT ) AS $$ BEGIN RETURN QUERY SELECT a.id, a.receiver_name, a.receiver_phone, a.address_detail FROM public.ml_user_addresses a WHERE a.user_id = p_user_id AND a.is_default = TRUE AND a.status = 1 LIMIT 1; END; $$ LANGUAGE plpgsql; ``` ### 2.5 RLS ่กŒ็บงๅฎ‰ๅ…จ็ญ–็•ฅ #### 2.5.1 ็”จๆˆทๆ•ฐๆฎ้š”็ฆป ```sql -- ็”จๆˆทๅช่ƒฝ่ฎฟ้—ฎ่‡ชๅทฑ็š„ๆ•ฐๆฎ CREATE POLICY ml_user_profiles_select_policy ON public.ml_user_profiles FOR SELECT USING ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id) ); CREATE POLICY ml_user_profiles_update_policy ON public.ml_user_profiles FOR UPDATE USING ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id) ); ``` #### 2.5.2 ๅ•†ๅ“ๆƒ้™ๆŽงๅˆถ ```sql -- ๆ‰€ๆœ‰ไบบๅฏๆŸฅ็œ‹ๅทฒไธŠๆžถๅ•†ๅ“ CREATE POLICY ml_products_select_policy ON public.ml_products FOR SELECT USING (status = 1); -- ๅ•†ๅฎถๅช่ƒฝ็ฎก็†่‡ชๅทฑ็š„ๅ•†ๅ“ CREATE POLICY ml_products_insert_policy ON public.ml_products FOR INSERT WITH CHECK ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id) ); CREATE POLICY ml_products_update_policy ON public.ml_products FOR UPDATE USING ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id) ); ``` #### 2.5.3 ่ฎขๅ•ๆƒ้™ๆŽงๅˆถ ```sql -- ็”จๆˆทๅ’Œๅ•†ๅฎถ้ƒฝๅฏๆŸฅ็œ‹็›ธๅ…ณ่ฎขๅ• CREATE POLICY ml_orders_select_policy ON public.ml_orders FOR SELECT USING ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id) OR auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id) ); -- ๅชๆœ‰็”จๆˆท่ƒฝๅˆ›ๅปบ่ฎขๅ• CREATE POLICY ml_orders_insert_policy ON public.ml_orders FOR INSERT WITH CHECK ( auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id) ); ``` --- ## ไธ‰ใ€ๅŽ็ซฏ/APIๅฑ‚ๅฎž็Žฐ ### 3.1 PostgREST ่‡ชๅŠจ็”Ÿๆˆ API #### 3.1.1 REST API ่‡ชๅŠจ็”Ÿๆˆๆœบๅˆถ Supabase ๅŸบไบŽ PostgREST ่‡ชๅŠจไธบๆฏไธช่กจ็”Ÿๆˆ REST API๏ผš **ๆŸฅ่ฏข API** ``` GET /rest/v1/ml_products GET /rest/v1/ml_products?id=eq.{uuid} GET /rest/v1/ml_products?status=eq.1&limit=20 GET /rest/v1/ml_products?base_price=gte.100&base_price=lte.500 ``` **ๆ’ๅ…ฅ API** ``` POST /rest/v1/ml_orders Content-Type: application/json { "user_id": "uuid", "merchant_id": "uuid", "total_amount": 100.00 } ``` **ๆ›ดๆ–ฐ API** ``` PATCH /rest/v1/ml_products?id=eq.{uuid} Content-Type: application/json { "status": 2 } ``` **ๅˆ ้™ค API** ``` DELETE /rest/v1/ml_user_favorites?id=eq.{uuid} ``` **RPC ๅ‡ฝๆ•ฐ่ฐƒ็”จ** ``` POST /rest/v1/rpc/calculate_cart_total Content-Type: application/json { "p_user_id": "uuid" } ``` ### 3.2 ๆŸฅ่ฏขๆ“ไฝœ็ฌฆๆ”ฏๆŒ PostgREST ๆ”ฏๆŒไธฐๅฏŒ็š„ๆŸฅ่ฏขๆ“ไฝœ็ฌฆ๏ผš ```typescript // ็ญ‰ไบŽ ?status=eq.1 // ไธ็ญ‰ไบŽ ?status=neq.2 // ๅคงไบŽ/ๅฐไบŽ ?base_price=gte.100&base_price=lte.500 // ๆจก็ณŠๆŸฅ่ฏข ?name=ilike.%ๅ•†ๅ“% // IN ๆŸฅ่ฏข ?category_id=in.(id1,id2,id3) // IS NULL ?deleted_at=is.null // JSONB ๆŸฅ่ฏข ?preferences->theme=eq.dark ``` ### 3.3 ๅˆ†้กตๅ’ŒๆŽ’ๅบ ```typescript // ๅˆ†้กต ?limit=20&offset=0 // ๆˆ–ไฝฟ็”จ Range ๅคด Range: 0-19 // ๆŽ’ๅบ ?order=created_at.desc ?order=base_price.asc,created_at.desc // ่ฎกๆ•ฐ Prefer: count=exact ``` --- ## ๅ››ใ€ๅ‰็ซฏๅฎž็Žฐ ### 4.1 ้กน็›ฎ็ป“ๆž„ ``` pages/mall/ โ”œโ”€โ”€ consumer/ # ๆถˆ่ดน่€…็ซฏ โ”‚ โ”œโ”€โ”€ index.uvue # ้ฆ–้กต โ”‚ โ”œโ”€โ”€ product-detail.uvue # ๅ•†ๅ“่ฏฆๆƒ… โ”‚ โ”œโ”€โ”€ cart.uvue # ่ดญ็‰ฉ่ฝฆ โ”‚ โ”œโ”€โ”€ checkout.uvue # ็ป“็ฎ—้กต โ”‚ โ”œโ”€โ”€ orders.uvue # ่ฎขๅ•ๅˆ—่กจ โ”‚ โ””โ”€โ”€ subscription/ # ่ฎข้˜…็›ธๅ…ณ โ”œโ”€โ”€ merchant/ # ๅ•†ๅฎถ็ซฏ โ”‚ โ”œโ”€โ”€ index.uvue # ๅ•†ๅฎถ้ฆ–้กต โ”‚ โ””โ”€โ”€ product-detail.uvue โ”œโ”€โ”€ delivery/ # ้…้€็ซฏ โ”œโ”€โ”€ admin/ # ็ฎก็†็ซฏ โ””โ”€โ”€ service/ # ๅฎขๆœ็ซฏ components/supadb/ โ”œโ”€โ”€ aksupa.uts # Supabase ๅฎขๆˆท็ซฏๅฐ่ฃ… โ”œโ”€โ”€ aksupainstance.uts # ๅ…จๅฑ€ๅ•ไพ‹ โ””โ”€โ”€ aksuparealtime.uts # ๅฎžๆ—ถ่ฎข้˜… types/ โ””โ”€โ”€ mall-types.uts # ็ฑปๅž‹ๅฎšไน‰ ``` ### 4.2 Supabase ๅฎขๆˆท็ซฏๅฐ่ฃ… #### 4.2.1 ๅฎขๆˆท็ซฏๅˆๅง‹ๅŒ– **ๆ–‡ไปถ**: `components/supadb/aksupainstance.uts` ```typescript import AkSupa from './aksupa.uts' import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts' // ๅˆ›ๅปบๅ…จๅฑ€ Supabase ๅฎขๆˆท็ซฏๅฎžไพ‹ const supa = new AkSupa(SUPA_URL, SUPA_KEY) // ่‡ชๅŠจ็™ปๅฝ• (ๅผ€ๅ‘็Žฏๅขƒ) const supaReady: Promise = (async () => { try { await supa.signIn('test@example.com', 'password') return true } catch (err) { console.error('Supabase auto sign-in failed', err) return false } })() export { supaReady } export default supa ``` #### 4.2.2 API ่ฐƒ็”จๅฐ่ฃ… **ๆ–‡ไปถ**: `components/supadb/aksupa.uts` ```typescript export class AkSupa { baseUrl: string apikey: string constructor(baseUrl: string, apikey: string) { this.baseUrl = baseUrl this.apikey = apikey } // ๆŸฅ่ฏขๆ•ฐๆฎ async select( table: string, filter?: string | null, options?: AkSupaSelectOptions ): Promise> { let url = this.baseUrl + '/rest/v1/' + table let headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}` } as UTSJSONObject // ๆž„ๅปบๆŸฅ่ฏขๅ‚ๆ•ฐ let params: string[] = [] if (options?.limit) params.push(`limit=${options.limit}`) if (options?.order) params.push(`order=${encodeURIComponent(options.order)}`) if (filter) params.push(filter) if (params.length > 0) { url += '?' + params.join('&') } return await this.requestWithAutoRefresh({ url, method: 'GET', headers }) } // ๆ’ๅ…ฅๆ•ฐๆฎ async insert( table: string, row: UTSJSONObject | Array ): Promise> { const url = this.baseUrl + '/rest/v1/' + table const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}`, Prefer: 'return=representation' } as UTSJSONObject return await this.requestWithAutoRefresh({ url, method: 'POST', headers, data: row }) } // ๆ›ดๆ–ฐๆ•ฐๆฎ async update( table: string, filter: UTSJSONObject, data: UTSJSONObject ): Promise> { const filterStr = buildSupabaseFilterQuery(filter) const url = this.baseUrl + '/rest/v1/' + table + '?' + filterStr const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}`, Prefer: 'return=representation' } as UTSJSONObject return await this.requestWithAutoRefresh({ url, method: 'PATCH', headers, data }) } // ่ฐƒ็”จ RPC ๅ‡ฝๆ•ฐ async rpc( functionName: string, params: UTSJSONObject ): Promise> { const url = `${this.baseUrl}/rest/v1/rpc/${functionName}` const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}` } as UTSJSONObject return await this.requestWithAutoRefresh({ url, method: 'POST', headers, data: params }) } } ``` ### 4.3 ้กต้ขๅฎž็Žฐ็คบไพ‹ #### 4.3.1 ๅ•†ๅ“ๅˆ—่กจ้กต้ข **ๆ–‡ไปถ**: `pages/mall/consumer/index.uvue` ```typescript ``` #### 4.3.2 ๅ•†ๅ“่ฏฆๆƒ…้กต้ข ```typescript // ๅŠ ่ฝฝๅ•†ๅ“่ฏฆๆƒ… const loadProductDetail = async (productId: string) => { try { // ๆŸฅ่ฏขๅ•†ๅ“ไฟกๆฏ const productRes = await supa.select('ml_products', `id=eq.${productId}`, { single: true } ) if (productRes.success && productRes.data) { product.value = productRes.data as ProductType } // ๆŸฅ่ฏขๅ•†ๅ“SKU const skuRes = await supa.select('ml_product_skus', { product_id: productId }) if (skuRes.success && skuRes.data) { productSkus.value = skuRes.data as Array } // ๆŸฅ่ฏขๅ•†ๅฎถไฟกๆฏ const merchantRes = await supa.select('ml_shops', { merchant_id: product.value.merchant_id }, { single: true }) if (merchantRes.success && merchantRes.data) { merchant.value = merchantRes.data as MerchantType } } catch (err) { console.error('ๅŠ ่ฝฝๅ•†ๅ“่ฏฆๆƒ…ๅคฑ่ดฅ:', err) } } ``` #### 4.3.3 ่ดญ็‰ฉ่ฝฆๆ“ไฝœ ```typescript // ๆทปๅŠ ๅˆฐ่ดญ็‰ฉ่ฝฆ const addToCart = async (product: ProductType, skuId?: string, quantity: number = 1) => { try { const userId = getCurrentUserId() if (!userId) { uni.showToast({ title: '่ฏทๅ…ˆ็™ปๅฝ•', icon: 'error' }) return } // ๆฃ€ๆŸฅ่ดญ็‰ฉ่ฝฆไธญๆ˜ฏๅฆๅทฒๅญ˜ๅœจ const existingRes = await supa.select('ml_shopping_cart', { user_id: userId, product_id: product.id, sku_id: skuId || null }, { single: true }) if (existingRes.success && existingRes.data) { // ๆ›ดๆ–ฐๆ•ฐ้‡ await supa.update('ml_shopping_cart', { id: existingRes.data.id }, { quantity: existingRes.data.quantity + quantity } ) } else { // ๆ–ฐๅขž่ดญ็‰ฉ่ฝฆ้กน await supa.insert('ml_shopping_cart', { user_id: userId, product_id: product.id, sku_id: skuId || null, quantity: quantity, selected: true }) } uni.showToast({ title: 'ๅทฒๆทปๅŠ ๅˆฐ่ดญ็‰ฉ่ฝฆ', icon: 'success' }) loadCartCount() } catch (err) { console.error('ๆทปๅŠ ๅˆฐ่ดญ็‰ฉ่ฝฆๅคฑ่ดฅ:', err) uni.showToast({ title: 'ๆ“ไฝœๅคฑ่ดฅ', icon: 'error' }) } } ``` #### 4.3.4 ๅˆ›ๅปบ่ฎขๅ• ```typescript // ๅˆ›ๅปบ่ฎขๅ• const createOrder = async (cartItems: Array, address: UserAddressType) => { try { // 1. ่ฎก็ฎ—่ฎขๅ•ๆ€ป้‡‘้ข๏ผˆไฝฟ็”จๆ•ฐๆฎๅบ“ๅ‡ฝๆ•ฐ๏ผ‰ const totalRes = await supa.rpc('calculate_cart_total', { p_user_id: getCurrentUserId() }) if (!totalRes.success) { throw new Error('่ฎก็ฎ—่ฎขๅ•้‡‘้ขๅคฑ่ดฅ') } const totalAmount = totalRes.data as number // 2. ็”Ÿๆˆ่ฎขๅ•ๅท const orderNoRes = await supa.rpc('generate_order_no') if (!orderNoRes.success) { throw new Error('็”Ÿๆˆ่ฎขๅ•ๅทๅคฑ่ดฅ') } const orderNo = orderNoRes.data as string // 3. ๅˆ›ๅปบ่ฎขๅ• const orderRes = await supa.insert('ml_orders', { order_no: orderNo, user_id: getCurrentUserId(), merchant_id: cartItems[0].product.merchant_id, product_amount: totalAmount, discount_amount: 0, shipping_fee: 0, total_amount: totalAmount, shipping_address: { receiver_name: address.receiver_name, receiver_phone: address.receiver_phone, province: address.province, city: address.city, district: address.district, address_detail: address.address_detail }, order_status: 1, // ๅพ…ๆ”ฏไป˜ payment_status: 1, shipping_status: 1 }) if (!orderRes.success) { throw new Error('ๅˆ›ๅปบ่ฎขๅ•ๅคฑ่ดฅ') } const orderId = orderRes.data.id // 4. ๅˆ›ๅปบ่ฎขๅ•ๅ•†ๅ“้กน const orderItems = cartItems.map(item => ({ order_id: orderId, product_id: item.product_id, sku_id: item.sku_id || null, product_name: item.product.name, price: item.sku?.price || item.product.price, quantity: item.quantity, total_amount: (item.sku?.price || item.product.price) * item.quantity })) await supa.insert('ml_order_items', orderItems) // 5. ๆธ…็ฉบ่ดญ็‰ฉ่ฝฆ for (const item of cartItems) { await supa.delete('ml_shopping_cart', { id: item.id }) } uni.showToast({ title: '่ฎขๅ•ๅˆ›ๅปบๆˆๅŠŸ', icon: 'success' }) // ่ทณ่ฝฌๅˆฐ่ฎขๅ•่ฏฆๆƒ… uni.navigateTo({ url: `/pages/mall/consumer/order-detail?orderId=${orderId}` }) } catch (err) { console.error('ๅˆ›ๅปบ่ฎขๅ•ๅคฑ่ดฅ:', err) uni.showToast({ title: 'ๅˆ›ๅปบ่ฎขๅ•ๅคฑ่ดฅ', icon: 'error' }) } } ``` ### 4.4 ๅฎžๆ—ถๆ•ฐๆฎๅŒๆญฅ #### 4.4.1 ่ฎข้˜…่ฎขๅ•็Šถๆ€ๆ›ดๆ–ฐ ```typescript import { AkSupaRealtime } from '@/components/supadb/aksuparealtime.uts' // ่ฎข้˜…่ฎขๅ•็Šถๆ€ๆ›ดๆ–ฐ const subscribeOrderStatus = (orderId: string, callback: (payload: any) => void) => { const realtime = new AkSupaRealtime(WS_URL, SUPA_KEY) realtime.subscribe('ml_orders', { filter: `id=eq.${orderId}`, event: 'UPDATE', callback: (payload) => { console.log('่ฎขๅ•็Šถๆ€ๆ›ดๆ–ฐ:', payload) callback(payload) } }) } // ไฝฟ็”จ onMounted(() => { subscribeOrderStatus(orderId, (payload) => { // ๆ›ดๆ–ฐ่ฎขๅ•็Šถๆ€ order.value.order_status = payload.new.order_status }) }) ``` --- ## ไบ”ใ€ๆ•ฐๆฎๆตๆœบๅˆถ ### 5.1 ๆ•ฐๆฎๆตๅ‘ๅ›พ ``` ็”จๆˆทๆ“ไฝœ โ”‚ โ–ผ ๅ‰็ซฏ้กต้ข (uni-app-x) โ”‚ โ–ผ Supabase ๅฎขๆˆท็ซฏ (AkSupa) โ”‚ โ–ผ HTTP Request โ”‚ โ”œโ”€ Headers: apikey, Authorization (JWT Token) โ”‚ โ–ผ PostgREST API โ”‚ โ”œโ”€ ่งฃๆž่ฏทๆฑ‚ โ”œโ”€ ้ชŒ่ฏ JWT Token โ”œโ”€ ๅบ”็”จ RLS ็ญ–็•ฅ โ”‚ โ–ผ PostgreSQL ๆ•ฐๆฎๅบ“ โ”‚ โ”œโ”€ ๆ‰ง่กŒ SQL ๆŸฅ่ฏข โ”œโ”€ ่งฆๅ‘่งฆๅ‘ๅ™จ โ”œโ”€ ๆ‰ง่กŒๅ‡ฝๆ•ฐ โ”‚ โ–ผ ่ฟ”ๅ›žๆ•ฐๆฎ โ”‚ โ–ผ PostgREST ๆ ผๅผๅŒ–ๅ“ๅบ” โ”‚ โ–ผ HTTP Response โ”‚ โ”œโ”€ JSON ๆ•ฐๆฎ โ”œโ”€ ็Šถๆ€็  โ”‚ โ–ผ Supabase ๅฎขๆˆท็ซฏๅค„็† โ”‚ โ”œโ”€ ่งฃๆžๅ“ๅบ” โ”œโ”€ ้”™่ฏฏๅค„็† โ”‚ โ–ผ ๅ‰็ซฏ้กต้ขๆ›ดๆ–ฐ UI ``` ### 5.2 ่ฎค่ฏๆต็จ‹ ``` 1. ็”จๆˆท็™ปๅฝ• โ”‚ โ–ผ 2. Supabase Auth ้ชŒ่ฏ โ”‚ โ”œโ”€ ้ชŒ่ฏ้‚ฎ็ฎฑ/ๅฏ†็  โ”œโ”€ ็”Ÿๆˆ JWT Token โ”‚ โ–ผ 3. ๅญ˜ๅ‚จ Token โ”‚ โ”œโ”€ ๆœฌๅœฐๅญ˜ๅ‚จ (uni.setStorageSync) โ”‚ โ–ผ 4. ๅŽ็ปญ่ฏทๆฑ‚่‡ชๅŠจๆบๅธฆ Token โ”‚ โ”œโ”€ Authorization: Bearer โ”‚ โ–ผ 5. PostgREST ้ชŒ่ฏ Token โ”‚ โ”œโ”€ ่งฃๆž JWT โ”œโ”€ ่Žทๅ– auth.uid() โ”‚ โ–ผ 6. RLS ็ญ–็•ฅๅบ”็”จ โ”‚ โ”œโ”€ ๆ นๆฎ auth.uid() ่ฟ‡ๆปคๆ•ฐๆฎ ``` ### 5.3 ๆƒ้™ๆŽงๅˆถๆต็จ‹ ``` ่ฏทๆฑ‚ๅˆฐ่พพ PostgREST โ”‚ โ–ผ ่งฃๆž JWT Token โ”‚ โ”œโ”€ ่Žทๅ– auth.uid() โ”‚ โ–ผ ๆŸฅๆ‰พ RLS ็ญ–็•ฅ โ”‚ โ”œโ”€ SELECT ็ญ–็•ฅ โ†’ USING ๅญๅฅ โ”œโ”€ INSERT ็ญ–็•ฅ โ†’ WITH CHECK ๅญๅฅ โ”œโ”€ UPDATE ็ญ–็•ฅ โ†’ USING + WITH CHECK โ”œโ”€ DELETE ็ญ–็•ฅ โ†’ USING ๅญๅฅ โ”‚ โ–ผ ๅบ”็”จ็ญ–็•ฅๆกไปถ โ”‚ โ”œโ”€ ็”จๆˆทๅช่ƒฝ่ฎฟ้—ฎ่‡ชๅทฑ็š„ๆ•ฐๆฎ โ”œโ”€ ๅ•†ๅฎถๅช่ƒฝ็ฎก็†่‡ชๅทฑ็š„ๅ•†ๅ“ โ”œโ”€ ๅ…ฌๅผ€ๆ•ฐๆฎๆ‰€ๆœ‰ไบบๅฏๆŸฅ็œ‹ โ”‚ โ–ผ ๆ‰ง่กŒ SQL ๆŸฅ่ฏข โ”‚ โ”œโ”€ ่‡ชๅŠจๆทปๅŠ  WHERE ๆกไปถ โ”‚ โ–ผ ่ฟ”ๅ›ž่ฟ‡ๆปคๅŽ็š„ๆ•ฐๆฎ ``` --- ## ๅ…ญใ€ไธšๅŠก้€ป่พ‘ๅฎž็Žฐ ### 6.1 ๅ•†ๅ“็ฎก็†ๆต็จ‹ #### 6.1.1 ๅ•†ๅ“ไธŠๆžถๆต็จ‹ ```typescript // ๅ•†ๅฎถไธŠๆžถๅ•†ๅ“ const publishProduct = async (productData: any) => { // 1. ้ชŒ่ฏๅ•†ๅฎถๆƒ้™ const merchantRes = await supa.select('ml_user_profiles', { user_id: getCurrentUserId(), user_type: 2, // ๅ•†ๅฎถ verification_status: 1 // ๅทฒ่ฎค่ฏ }, { single: true }) if (!merchantRes.success || !merchantRes.data) { throw new Error('ๆ‚จ่ฟ˜ไธๆ˜ฏ่ฎค่ฏๅ•†ๅฎถ') } // 2. ๅˆ›ๅปบๅ•†ๅ“ const productRes = await supa.insert('ml_products', { merchant_id: getCurrentUserId(), category_id: productData.category_id, name: productData.name, description: productData.description, base_price: productData.price, total_stock: productData.stock, available_stock: productData.stock, status: 1, // ไธŠๆžถ ...productData }) // 3. ๅˆ›ๅปบๅ•†ๅ“SKU if (productData.skus && productData.skus.length > 0) { const skus = productData.skus.map((sku: any) => ({ product_id: productRes.data.id, sku_code: sku.sku_code, specifications: sku.specifications, price: sku.price, stock: sku.stock })) await supa.insert('ml_product_skus', skus) } return productRes.data } ``` ### 6.2 ่ฎขๅ•ๅค„็†ๆต็จ‹ #### 6.2.1 ่ฎขๅ•็Šถๆ€ๆต่ฝฌ ```typescript // ่ฎขๅ•็Šถๆ€ๆžšไธพ const ORDER_STATUS = { PENDING_PAYMENT: 1, // ๅพ…ๆ”ฏไป˜ PAID: 2, // ๅทฒๆ”ฏไป˜ SHIPPED: 3, // ๅทฒๅ‘่ดง DELIVERED: 4, // ๅทฒ้€่พพ COMPLETED: 5, // ๅทฒๅฎŒๆˆ CANCELLED: 6, // ๅทฒๅ–ๆถˆ REFUNDING: 7, // ้€€ๆฌพไธญ REFUNDED: 8 // ๅทฒ้€€ๆฌพ } // ๆ›ดๆ–ฐ่ฎขๅ•็Šถๆ€ const updateOrderStatus = async (orderId: string, newStatus: number) => { const updateData: any = { order_status: newStatus, updated_at: new Date().toISOString() } // ๆ นๆฎ็Šถๆ€่ฎพ็ฝฎ็›ธๅบ”ๆ—ถ้—ดๆˆณ switch (newStatus) { case ORDER_STATUS.PAID: updateData.paid_at = new Date().toISOString() updateData.payment_status = 2 // ๅทฒๆ”ฏไป˜ break case ORDER_STATUS.SHIPPED: updateData.shipped_at = new Date().toISOString() updateData.shipping_status = 2 // ๅทฒๅ‘่ดง break case ORDER_STATUS.DELIVERED: updateData.delivered_at = new Date().toISOString() updateData.shipping_status = 4 // ๅทฒ้€่พพ break case ORDER_STATUS.COMPLETED: updateData.completed_at = new Date().toISOString() break } await supa.update('ml_orders', { id: orderId }, updateData) } ``` ### 6.3 ๅบ“ๅญ˜็ฎก็†ๆœบๅˆถ #### 6.3.1 ๅบ“ๅญ˜ๆ‰ฃๅ‡ๆต็จ‹ ```typescript // ไธ‹ๅ•ๆ—ถๆ‰ฃๅ‡ๅบ“ๅญ˜ const deductStock = async (orderItems: Array) => { for (const item of orderItems) { if (item.sku_id) { // ๆ‰ฃๅ‡ SKU ๅบ“ๅญ˜ const skuRes = await supa.select('ml_product_skus', { id: item.sku_id }, { single: true }) if (skuRes.data.stock < item.quantity) { throw new Error(`ๅ•†ๅ“ ${item.product_name} ๅบ“ๅญ˜ไธ่ถณ`) } await supa.update('ml_product_skus', { id: item.sku_id }, { stock: skuRes.data.stock - item.quantity } ) } else { // ๆ‰ฃๅ‡ๅ•†ๅ“ๆ€ปๅบ“ๅญ˜ const productRes = await supa.select('ml_products', { id: item.product_id }, { single: true }) if (productRes.data.available_stock < item.quantity) { throw new Error(`ๅ•†ๅ“ ${item.product_name} ๅบ“ๅญ˜ไธ่ถณ`) } await supa.update('ml_products', { id: item.product_id }, { available_stock: productRes.data.available_stock - item.quantity, sale_count: productRes.data.sale_count + item.quantity } ) } } } ``` --- ## ไธƒใ€ๅฎ‰ๅ…จๆœบๅˆถ ### 7.1 ๅคšๅฑ‚ๅฎ‰ๅ…จ้˜ฒๆŠค ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 1. ็ฝ‘็ปœๅฑ‚ๅฎ‰ๅ…จ โ”‚ โ”‚ - HTTPS ๅŠ ๅฏ†ไผ ่พ“ โ”‚ โ”‚ - SSL/TLS ่ฏไนฆ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 2. ่ฎค่ฏๅฑ‚ๅฎ‰ๅ…จ โ”‚ โ”‚ - JWT Token ้ชŒ่ฏ โ”‚ โ”‚ - Token ่ฟ‡ๆœŸๆœบๅˆถ โ”‚ โ”‚ - ่‡ชๅŠจๅˆทๆ–ฐ Token โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 3. ๆƒ้™ๅฑ‚ๅฎ‰ๅ…จ (RLS) โ”‚ โ”‚ - ่กŒ็บงๅฎ‰ๅ…จ็ญ–็•ฅ โ”‚ โ”‚ - ๅŸบไบŽ็”จๆˆท่ง’่‰ฒ็š„ๆƒ้™ๆŽงๅˆถ โ”‚ โ”‚ - ๆ•ฐๆฎ้š”็ฆป โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 4. ๅบ”็”จๅฑ‚ๅฎ‰ๅ…จ โ”‚ โ”‚ - ๅ‰็ซฏๆ•ฐๆฎ้ชŒ่ฏ โ”‚ โ”‚ - ่พ“ๅ…ฅ่ฟ‡ๆปคๅ’Œ่ฝฌไน‰ โ”‚ โ”‚ - ้˜ฒๆญข SQL ๆณจๅ…ฅ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### 7.2 ๆ•ฐๆฎ้ชŒ่ฏๆœบๅˆถ #### 7.2.1 ๆ•ฐๆฎๅบ“็บฆๆŸ ```sql -- CHECK ็บฆๆŸ CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4)) CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7)) CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000) -- ๅค–้”ฎ็บฆๆŸ REFERENCES public.ak_users(id) ON DELETE CASCADE REFERENCES public.ml_products(id) ON DELETE CASCADE -- ๅ”ฏไธ€็บฆๆŸ UNIQUE(product_code) UNIQUE(order_no) UNIQUE(user_id, product_id, sku_id) -- ่ดญ็‰ฉ่ฝฆๅ”ฏไธ€ๆ€ง ``` #### 7.2.2 ๅ‰็ซฏ้ชŒ่ฏ ```typescript // ่กจๅ•้ชŒ่ฏ const validateOrderData = (orderData: any): boolean => { if (!orderData.user_id) { uni.showToast({ title: '็”จๆˆทIDไธ่ƒฝไธบ็ฉบ', icon: 'error' }) return false } if (!orderData.total_amount || orderData.total_amount <= 0) { uni.showToast({ title: '่ฎขๅ•้‡‘้ขๆ— ๆ•ˆ', icon: 'error' }) return false } if (!orderData.shipping_address) { uni.showToast({ title: '่ฏท้€‰ๆ‹ฉๆ”ถ่ดงๅœฐๅ€', icon: 'error' }) return false } return true } ``` --- ## ๅ…ซใ€ๆ€ง่ƒฝไผ˜ๅŒ– ### 8.1 ๆ•ฐๆฎๅบ“ไผ˜ๅŒ– #### 8.1.1 ็ดขๅผ•ไผ˜ๅŒ– ```sql -- ๅคๅˆ็ดขๅผ•๏ผˆ้’ˆๅฏนๅธธ็”จๆŸฅ่ฏข็ป„ๅˆ๏ผ‰ CREATE INDEX idx_ml_products_status_created ON ml_products(status, created_at DESC); CREATE INDEX idx_ml_orders_user_status ON ml_orders(user_id, order_status); -- ้ƒจๅˆ†็ดขๅผ•๏ผˆๅช็ดขๅผ•ๆดป่ทƒๆ•ฐๆฎ๏ผ‰ CREATE INDEX idx_ml_products_active ON ml_products(merchant_id, category_id) WHERE status = 1; ``` #### 8.1.2 ๆŸฅ่ฏขไผ˜ๅŒ– ```typescript // ๅชๆŸฅ่ฏข้œ€่ฆ็š„ๅญ—ๆฎต const res = await supa.select('ml_products', null, { columns: 'id,name,base_price,main_image_url', // ๅชๆŸฅ่ฏขๅฟ…่ฆๅญ—ๆฎต limit: 20 }) // ไฝฟ็”จ่ง†ๅ›พ็ฎ€ๅŒ–ๅคๆ‚ๆŸฅ่ฏข const res = await supa.select('ml_products_detail_view', { status: 1 }, { limit: 20 }) ``` ### 8.2 ๅ‰็ซฏไผ˜ๅŒ– #### 8.2.1 ๅˆ†้กตๅŠ ่ฝฝ ```typescript // ๅˆ†้กตๅŠ ่ฝฝๅ•†ๅ“ๅˆ—่กจ const loadProducts = async (loadMore: boolean = false) => { const currentPage = loadMore ? page.value + 1 : 1 const res = await supa .from('ml_products') .select('*') .eq('status', 1) .order('created_at', { ascending: false }) .range((currentPage - 1) * pageSize.value, currentPage * pageSize.value - 1) // ่ฟฝๅŠ ๆˆ–ๆ›ฟๆขๆ•ฐๆฎ if (loadMore) { productList.value.push(...res.data) } else { productList.value = res.data } } ``` #### 8.2.2 ๆ•ฐๆฎ็ผ“ๅญ˜ ```typescript // ไฝฟ็”จๆœฌๅœฐ็ผ“ๅญ˜ๅ‡ๅฐ‘่ฏทๆฑ‚ const getCachedProducts = async (categoryId: string) => { const cacheKey = `products_${categoryId}` const cached = uni.getStorageSync(cacheKey) if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5ๅˆ†้’Ÿๅ†…ไฝฟ็”จ็ผ“ๅญ˜ return cached.data } // ไปŽๆœๅŠกๅ™จ่Žทๅ– const res = await supa.select('ml_products', { category_id: categoryId, status: 1 }) // ๆ›ดๆ–ฐ็ผ“ๅญ˜ uni.setStorageSync(cacheKey, { data: res.data, timestamp: Date.now() }) return res.data } ``` ### 8.3 ๅฎžๆ—ถๅŒๆญฅไผ˜ๅŒ– ```typescript // ๅช่ฎข้˜…ๅฟ…่ฆ็š„ๆ•ฐๆฎๅ˜ๆ›ด const subscribeOrderUpdates = (orderId: string) => { realtime.subscribe('ml_orders', { filter: `id=eq.${orderId}`, // ๅช่ฎข้˜…็‰นๅฎš่ฎขๅ• event: 'UPDATE', // ๅช็›‘ๅฌๆ›ดๆ–ฐไบ‹ไปถ callback: handleOrderUpdate }) } // ๅŠๆ—ถๅ–ๆถˆ่ฎข้˜… onUnmounted(() => { realtime.unsubscribe('ml_orders') }) ``` --- ## ไนใ€ๅผ€ๅ‘ๅทฅไฝœๆต ### 9.1 ๅผ€ๅ‘ๆต็จ‹ ``` 1. ๆ•ฐๆฎๅบ“่ฎพ่ฎก โ”‚ โ”œโ”€ ่ฎพ่ฎก่กจ็ป“ๆž„ โ”œโ”€ ่ฎพ่ฎก็ดขๅผ• โ”œโ”€ ่ฎพ่ฎก RLS ็ญ–็•ฅ โ”œโ”€ ่ฎพ่ฎก่งฆๅ‘ๅ™จ โ””โ”€ ่ฎพ่ฎกๅ‡ฝๆ•ฐ โ”‚ โ–ผ 2. ๆ‰ง่กŒๆ•ฐๆฎๅบ“่„šๆœฌ โ”‚ โ”œโ”€ complete_mall_database.sql โ”‚ โ–ผ 3. ๅฎšไน‰็ฑปๅž‹ โ”‚ โ”œโ”€ types/mall-types.uts โ”‚ โ–ผ 4. ๅผ€ๅ‘ๅ‰็ซฏ้กต้ข โ”‚ โ”œโ”€ ้กต้ข็ป„ไปถ โ”œโ”€ API ่ฐƒ็”จ โ””โ”€ ไธšๅŠก้€ป่พ‘ โ”‚ โ–ผ 5. ๆต‹่ฏ•้ชŒ่ฏ โ”‚ โ”œโ”€ ๅŠŸ่ƒฝๆต‹่ฏ• โ”œโ”€ ๆƒ้™ๆต‹่ฏ• โ””โ”€ ๆ€ง่ƒฝๆต‹่ฏ• ``` ### 9.2 ่ฐƒ่ฏ•ๆŠ€ๅทง ```typescript // 1. ๅฏ็”จ่ฏฆ็ป†ๆ—ฅๅฟ— console.log('่ฏทๆฑ‚ๅ‚ๆ•ฐ:', { table: 'ml_products', filter: filter, options: options }) // 2. ๆฃ€ๆŸฅๅ“ๅบ” console.log('API ๅ“ๅบ”:', { success: res.success, status: res.status, data: res.data, error: res.error }) // 3. ้ชŒ่ฏๆƒ้™ const session = await supa.getSession() console.log('ๅฝ“ๅ‰็”จๆˆท:', session.user) console.log('JWT Token:', AkReq.getToken()) ``` --- ## ๐Ÿ“š ็›ธๅ…ณๆ–‡ๆกฃ - [ๆจกๅ—ๅˆ†ๆžๆŠฅๅ‘Š](./MODULE_ANALYSIS.md) - [ๅ‰ๅŽ็ซฏ่”่ฐƒๆŒ‡ๅ—](./FRONTEND_BACKEND_DEBUGGING.md) - [ๆ•ฐๆฎๅบ“ๅˆ›ๅปบๆŠฅๅ‘Š](./database/database_creation_report.md) - [ๅฎŒๆ•ด้ƒจ็ฝฒๆŒ‡ๅ—](./database/complete_deployment_guide.md) --- **็”Ÿๆˆๆ—ถ้—ด**: 2025ๅนด1ๆœˆ **็‰ˆๆœฌ**: v1.0 **็Šถๆ€**: โœ… ๅฎŒๆ•ดๆŠ€ๆœฏๅฎž็Žฐๆ‹†่งฃ