Appearance
Geo - Technical Map
Bám sát code thực tế của module
geovà backendgeo-api/ Hasura metadata.
Scope kỹ thuật
| Lớp | File chính | Vai trò |
|---|---|---|
| FE widgets | diva-admin/src/modules/geo/components/** | Nhập/xuất địa chỉ, province/ward select, Google Map autocomplete |
| FE logic | diva-admin/src/modules/geo/compositions/useGeo.ts | Fetch country, cache vào store, local filter |
| FE state | diva-admin/src/modules/geo/stores/useGeoStore.ts | Cache quốc gia dùng chung |
| FE types | diva-admin/src/modules/geo/types.ts | GeoJSON, Address, LatLng |
| FE GraphQL | diva-admin/src/modules/geo/graphql/geo.graphql | Query country / province / district / ward |
| Backend SQL | diva-backend/services/controller/migrations/geo/** | Schema, functions, triggers, seed data |
| Backend action service | diva-backend/services/geo-api/action/place.go | Search place, fallback Google Maps, normalize address |
FE surface
Public exports
| Export | File | Ghi chú |
|---|---|---|
AddressInput | components/index.ts | Export chính từ module |
AddressDisplay | components/index.ts | Export chính từ module |
ProvinceSelect | components/address/ProvinceSelect.tsx | Dùng ở nhiều module ngoài geo |
DistrictSelect | components/address/DistrictSelect.tsx | Có implementation thật nhưng không được AddressInput dùng mặc định |
WardSelect | components/address/WardSelect.tsx | Có cache localStorage + mapping ward cũ |
GMapAutocomplete | components/map/GMapAutocomplete.tsx | Wrapper GoogleMap + XInput |
useGeo() | compositions/useGeo.ts | Query quốc gia và filter local |
useGeoStore | stores/useGeoStore.ts | Pinia store geo |
FE components theo chức năng
| Nhóm | File | Vai trò |
|---|---|---|
| Address compose | AddressInput.tsx | Ghép ProvinceSelect + WardSelect + street input; emit Address object |
| Address display | AddressDisplay.tsx | Render địa chỉ 2 dòng |
| Province select | ProvinceSelect.tsx | Query province, filter theo keywords và province_old |
| District select | DistrictSelect.tsx | Query district theo provinceCode, reset khi province đổi |
| Ward select | WardSelect.tsx | Query ward theo provinceCode, cache localStorage, hỗ trợ ward mapping |
| Google autocomplete | GMapAutocomplete.tsx | Bọc Google Places Autocomplete vào XInput + GoogleMap |
GraphQL operations
File: diva-admin/src/modules/geo/graphql/geo.graphql
| Operation | Input | Return fields chính | Dùng ở |
|---|---|---|---|
GetWards | ward_bool_exp, ward_order_by, offset, limit | code, country_code, name, province_code, district_code, keywords, ward_mappings | WardSelect |
GetDistricts | district_bool_exp, district_order_by, offset, limit | code, country_code, name, province_code, keywords | DistrictSelect |
GetProvinces | province_bool_exp, province_order_by, offset, limit | code, country_code, name, keywords, province_old | ProvinceSelect |
GetCountry | country_bool_exp, country_order_by, offset, limit | iso, name, phone_code, flag_url | useGeo() / XInputPhone |
FE state & caching
useGeoStore
| Item | Mô tả | Reference |
|---|---|---|
| Store id | geo | useGeoStore.ts |
| State | countries: Partial<Country>[] | useGeoStore.ts |
| Seed mặc định | 1 country VN | useGeoStore.ts |
| Action | replaceCountry(items) | useGeoStore.ts |
useGeo
| Logic | Chi tiết |
|---|---|
queryCountry() | Gọi GraphQL query GetCountryDocument, requestPolicy = "network-only", rồi replace store nếu có data |
getLocalCountry() | Lấy cache từ store và lọc theo keywords |
getCountry() | Nếu cache rỗng hoặc refresh = true thì query lại trước khi lọc |
Backend data model
Core tables / views
| Object | Loại | Vai trò |
|---|---|---|
country | table | Quốc gia, mã phone, flag, keywords, boundary |
province | table | Tỉnh/thành phố hiện tại |
province_old | table | Dữ liệu địa giới cũ, map sang province mới bằng new_province_code |
district | table | Quận/huyện hiện tại |
ward | table | Phường/xã hiện tại |
ward_old | table | Dữ liệu ward cũ, map sang ward mới |
ward_mappings | table | Bảng mapping old/new ward |
place | table | Bản ghi place đã normalize từ Google Maps hoặc nhập tay |
search_place_result | view | Shape trả về của search_place |
search_subdivision_result | view | Shape trả về của search_subdivision |
user_position_status | table | Lưu online + vị trí user |
user_position_distance_status | view | Shape trả về của search_user_position_status |
timezone | table | Danh mục timezone seed |
Columns quan trọng
| Object | Cột nổi bật |
|---|---|
country | iso, iso3, name, phone_code, flag_url, boundary, keywords |
province | country_code, code, name, name_i18n, population, boundary, keywords, zip_code, disabled, code_1 |
district | country_code, province_code, code, name, name_i18n, boundary, keywords, zip_code, disabled |
ward | country_code, province_code, district_code, code, name, name_i18n, boundary, keywords, zip_code, disabled |
place | id, country_code, province_code, district_code, ward_code, name, house_number, street, position, external_place_id, postal_code, tags, tsv |
user_position_status | id, role, online, position, updated_at |
Backend functions
Hasura metadata
| File | Nội dung |
|---|---|
metadata/databases/geo/functions/functions.yaml | Include 3 function metadata files |
public_search_place.yaml | Expose function search_place |
public_search_subdivision.yaml | Expose function search_subdivision |
public_search_user_position_status.yaml | Expose function search_user_position_status |
SQL function behavior
| Function | Input | Output | Ghi chú kỹ thuật |
|---|---|---|---|
search_place | address, longitude, latitude, max_distance, current_longitude, current_latitude | SETOF search_place_result | Search place.tsv, join province/district/ward, tính ST_DistanceSphere và lọc ST_DWithin |
search_subdivision | longitude, latitude, ward_keywords, district_keywords, province_keywords, country_iso | SETOF search_subdivision_result | Match keyword hoặc boundary contains, rồi ghép ward/district/province |
search_user_position_status | online, longitude, latitude, from_time, to_time, max_distance, has_position_only | SETOF user_position_distance_status | Lọc theo status + time window + khoảng cách |
geo-api action service
searchPlace
| Step | Logic |
|---|---|
| 1 | Parse payload JSON vào searchPlaceInput |
| 2 | Validate: phải có address hoặc cặp latitude/longitude |
| 3 | Set default limit = 10, max_distance = 100 nếu không truyền |
| 4 | Query Hasura function search_place trước bằng admin client |
| 5 | Nếu DB không có kết quả và MapClient != nil, fallback sang Google Places Autocomplete + Place Details |
| 6 | Chuẩn hóa address_component sang place_insert_input rồi gọi insert vào place |
Parse địa chỉ từ Google Maps
| Thành phần Google | Map vào field |
|---|---|
street_number | HouseNumber |
route | Street |
administrative_area_level_1 | ProvinceCode |
administrative_area_level_2 / locality | DistrictCode |
administrative_area_level_3 / sublocality / sublocality_level_1 | WardCode |
country | CountryCode |
postal_code | PostalCode |
Notes về insert place
| Logic | Mô tả |
|---|---|
| Normalize | Dùng NewGeoPoint(longitude, latitude) cho position |
| Refill subdivision | Nếu đã có province code thì gọi lại search_subdivision để backfill ward/district/province theo tọa độ + keyword |
| Persist | Tạo insert_place mutation qua admin client với các object đã normalize |
Permissions
Hasura select/insert/update
| Object | Role | Quyền |
|---|---|---|
country | anonymous, customer, user | select |
province | anonymous, customer, user | select |
province_old | anonymous, customer, user | select |
district | anonymous, customer, user | select |
ward | anonymous, customer, user | select |
ward_mappings | anonymous, customer, user | select |
ward_old | anonymous, customer, user | select |
user_position_status | user | insert, select, update theo chính X-Hasura-User-Id |
user_position_distance_status | user | select theo chính X-Hasura-User-Id |
place | chưa thấy permission tách riêng trong file metadata đã đọc | cần kiểm tra thêm nếu muốn expose trực tiếp |
Rủi ro / Findings kỹ thuật
| # | Ghi chú |
|---|---|
| 1 | public_place.yaml chỉ register table place, chưa thấy permission chi tiết trong file metadata hiện tại. |
| 2 | search_subdivision có điều kiện AND d.code IS NULL OR d.code = w.district_code dễ gây precedence bug nếu không bọc ngoặc. |
| 3 | useGeo.ts filter local country chưa dùng biến formattedKeywords, đây là mismatch rõ giữa ý định sanitize và điều kiện includes(). |
| 4 | WardSelect lưu cache JSON vào localStorage mà không gắn version/dataset timestamp. |
| 5 | geo-api/action/place.go là một lớp normalize khá nặng: vừa query DB, vừa fallback Google Maps, vừa insert place. Nếu fail ở giữa chừng, caller cần xử lý partial result. |