From 32464cb49447344a121e2f5a1a82037d5477f9e4 Mon Sep 17 00:00:00 2001
From: Tsanie Lily <tsorgy@gmail.com>
Date: Wed, 5 Jul 2023 17:33:22 +0800
Subject: [PATCH] add latitude & longitude

---
 Server/Controller/BaseController.cs           |  43 ++-
 Server/Controller/EventApiController.cs       |   8 +-
 Server/Controller/FlowerApiController.cs      |  59 ++-
 .../Controller/FlowerApiController.structs.cs |  21 +-
 Server/Controller/ImageController.cs          |   8 +-
 Server/Controller/UserApiController.cs        |   6 +
 Server/Data/Model/FlowerItem.cs               |  20 +-
 Server/Data/Model/ILocation.cs                |  17 +
 Server/Data/Model/PhotoItem.cs                |   5 +-
 Server/Data/Model/RecordItem.cs               |  14 +-
 ...5083734_Add-Latitude-Longitude.Designer.cs | 335 ++++++++++++++++++
 .../20230705083734_Add-Latitude-Longitude.cs  |  58 +++
 .../Migrations/FlowerDatabaseModelSnapshot.cs |  16 +
 Server/Program.cs                             |   2 +-
 14 files changed, 581 insertions(+), 31 deletions(-)
 create mode 100644 Server/Data/Model/ILocation.cs
 create mode 100644 Server/Migrations/20230705083734_Add-Latitude-Longitude.Designer.cs
 create mode 100644 Server/Migrations/20230705083734_Add-Latitude-Longitude.cs

diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs
index a43a0e7..2a30ed2 100644
--- a/Server/Controller/BaseController.cs
+++ b/Server/Controller/BaseController.cs
@@ -193,7 +193,7 @@ public abstract partial class BaseController : ControllerBase
     /// </summary>
     /// <param name="file">来自请求的文件</param>
     /// <returns>文件结果对象</returns>
-    protected FileResult? WrapFormFile(IFormFile file)
+    protected static FileResult? WrapFormFile(IFormFile file)
     {
         if (file == null)
         {
@@ -235,11 +235,10 @@ public abstract partial class BaseController : ControllerBase
     /// <summary>
     /// 写入文件到用户的花草目录中
     /// </summary>
-    /// <param name="uid">用户唯一 id</param>
     /// <param name="fid">花草唯一 id</param>
     /// <param name="file">文件对象</param>
     /// <param name="token">取消令牌</param>
-    protected async Task WriteToFile(int uid, int fid, FileResult file, CancellationToken token = default)
+    protected static async Task WriteToFile(int fid, FileResult file, CancellationToken token = default)
     {
         var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
         if (!Directory.Exists(directory))
@@ -253,11 +252,10 @@ public abstract partial class BaseController : ControllerBase
     /// <summary>
     /// 删除花草下的文件
     /// </summary>
-    /// <param name="uid">用户唯一 id</param>
     /// <param name="fid">花草唯一 id</param>
     /// <param name="path">文件路径</param>
     /// <returns>返回是否已删除</returns>
-    protected bool DeleteFile(int uid, int fid, string path)
+    protected static bool DeleteFile(int fid, string path)
     {
         var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
         if (Directory.Exists(directory))
@@ -271,6 +269,41 @@ public abstract partial class BaseController : ControllerBase
         }
         return false;
     }
+
+    private const double EarthRadius = 6378137;
+
+    private static double Radius(double degree)
+    {
+        return degree * Math.PI / 180.0;
+    }
+
+    /// <summary>
+    /// 获取两个经纬度之间的距离
+    /// </summary>
+    /// <param name="lat1">纬度1</param>
+    /// <param name="lon1">经度1</param>
+    /// <param name="lat2">纬度2</param>
+    /// <param name="lon2">经度2</param>
+    /// <returns></returns>
+    protected static double GetDistance(double lat1, double lon1, double lat2, double lon2)
+    {
+        double rlat1 = Radius(lat1);
+        double rlat2 = Radius(lat2);
+        return EarthRadius * Math.Acos(
+            Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Cos(Radius(lon1) - Radius(lon2)) +
+            Math.Sin(rlat1) * Math.Sin(rlat2));
+    }
+
+    /// <inheritdoc/>
+    protected static double GetDistance2(double lat1, double lon1, double lat2, double lon2)
+    {
+        double rlat1 = Radius(lat1);
+        double rlat2 = Radius(lat2);
+        double a = rlat1 - rlat2;
+        double b = Radius(lon1) - Radius(lon2);
+        double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) + Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Pow(Math.Sin(b / 2), 2)));
+        return s * EarthRadius;
+    }
 }
 
 /// <summary>
diff --git a/Server/Controller/EventApiController.cs b/Server/Controller/EventApiController.cs
index 17e7a03..694b1dd 100644
--- a/Server/Controller/EventApiController.cs
+++ b/Server/Controller/EventApiController.cs
@@ -389,7 +389,7 @@ public class EventApiController : BaseController
                     };
                     AddPhotoItem(p);
 
-                    await WriteToFile(user.Id, record.FlowerId, file, token);
+                    await WriteToFile(record.FlowerId, file, token);
                 });
             }
             catch (Exception ex)
@@ -483,7 +483,7 @@ public class EventApiController : BaseController
                         };
                         AddPhotoItem(p);
 
-                        await WriteToFile(user.Id, record.FlowerId, file, token);
+                        await WriteToFile(record.FlowerId, file, token);
                     }
                 }
             });
@@ -555,7 +555,7 @@ public class EventApiController : BaseController
 
         if (photo.Record != null)
         {
-            DeleteFile(user.Id, photo.Record.FlowerId, photo.Path);
+            DeleteFile(photo.Record.FlowerId, photo.Path);
         }
 
         return NoContent();
@@ -613,7 +613,7 @@ public class EventApiController : BaseController
         {
             if (photo.Record != null)
             {
-                DeleteFile(user.Id, photo.Record.FlowerId, photo.Path);
+                DeleteFile(photo.Record.FlowerId, photo.Path);
             }
         }
 
diff --git a/Server/Controller/FlowerApiController.cs b/Server/Controller/FlowerApiController.cs
index db471c5..129a9f1 100644
--- a/Server/Controller/FlowerApiController.cs
+++ b/Server/Controller/FlowerApiController.cs
@@ -2,6 +2,7 @@
 using Blahblah.FlowerStory.Server.Data.Model;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
 using System.ComponentModel.DataAnnotations;
 using System.Runtime.InteropServices;
 
@@ -38,6 +39,9 @@ public class FlowerApiController : BaseController
     ///     cfrom: decimal?
     ///     cto: decimal?
     ///     photo: bool?
+    ///     lon: double?
+    ///     lat: double?
+    ///     distance: int?
     ///     p: int?
     ///     size: int?
     /// 
@@ -49,6 +53,9 @@ public class FlowerApiController : BaseController
     /// <param name="costFrom">开销最小值</param>
     /// <param name="costTo">开销最大值</param>
     /// <param name="includePhoto">是否包含封面图片</param>
+    /// <param name="latitude">纬度</param>
+    /// <param name="longitude">经度</param>
+    /// <param name="distance">距离(米)</param>
     /// <param name="page">页数</param>
     /// <param name="pageSize">分页大小</param>
     /// <returns>会话有效则返回符合条件的花草集</returns>
@@ -63,7 +70,7 @@ public class FlowerApiController : BaseController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [HttpGet]
     [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
-    public ActionResult<FlowerItem[]> GetFlowers(
+    public ActionResult<FlowerResult> GetFlowers(
         [FromQuery(Name = "cid")] int? categoryId,
         [FromQuery] string? key,
         [FromQuery(Name = "from")] long? buyFrom,
@@ -71,6 +78,9 @@ public class FlowerApiController : BaseController
         [FromQuery(Name = "cfrom")] decimal? costFrom,
         [FromQuery(Name = "cto")] decimal? costTo,
         [FromQuery(Name = "photo")] bool? includePhoto,
+        [FromQuery(Name = "lat")] double? latitude,
+        [FromQuery(Name = "lon")] double? longitude,
+        [FromQuery] int? distance,
         [FromQuery(Name = "p")] int? page = 0,
         [FromQuery(Name = "size")] int? pageSize = 20)
     {
@@ -86,7 +96,7 @@ public class FlowerApiController : BaseController
 
         SaveDatabase();
 
-        var flowers = database.Flowers.Where(f => f.OwnerId == user.Id);
+        IEnumerable<FlowerItem> flowers = database.Flowers.Where(f => f.OwnerId == user.Id);
         if (categoryId != null)
         {
             flowers = flowers.Where(f => f.CategoryId == categoryId);
@@ -115,6 +125,20 @@ public class FlowerApiController : BaseController
             flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo);
         }
 
+        if (distance != null && latitude != null && longitude != null)
+        {
+            flowers = flowers.Where(f => f.Latitude != null && f.Longitude != null)
+                .AsEnumerable()
+                .Where(f =>
+                {
+                    var d = GetDistance(latitude.Value, longitude.Value, f.Latitude ?? 0, f.Longitude ?? 0);
+                    f.Distance = (int)d;
+                    return d <= distance;
+                });
+        }
+
+        int count = flowers.Count();
+
         var size = pageSize ?? 20;
         var p = page ?? 0;
         flowers = flowers.OrderByDescending(f => f.DateBuyUnixTime).Skip(p * size).Take(size);
@@ -128,12 +152,16 @@ public class FlowerApiController : BaseController
                         r.FlowerId == f.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList();
                 foreach (var photo in f.Photos)
                 {
-                    photo.Url = $"{ImageController.BaseUrl}/photo/flower/{f.Id}/{photo.Path}";
+                    photo.Url = $"photo/flower/{f.Id}/{photo.Path}";
                 }
             }
         }
 
-        return Ok(flowers.ToArray());
+        return Ok(new FlowerResult
+        {
+            Flowers = flowers.ToArray(),
+            Count = count
+        });
     }
 
     /// <summary>
@@ -194,7 +222,7 @@ public class FlowerApiController : BaseController
                     r.FlowerId == item.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList();
             foreach (var photo in item.Photos)
             {
-                photo.Url = $"{ImageController.BaseUrl}/photo/flower/{item.Id}/{photo.Path}";
+                photo.Url = $"photo/flower/{item.Id}/{photo.Path}";
             }
         }
 
@@ -393,7 +421,7 @@ public class FlowerApiController : BaseController
                     };
                     AddPhotoItem(cover);
 
-                    await WriteToFile(user.Id, item.Id, file, token);
+                    await WriteToFile(item.Id, file, token);
                 });
             }
             catch (Exception ex)
@@ -491,11 +519,14 @@ public class FlowerApiController : BaseController
             }
             else
             {
-                var photo = database.Photos.Where(p => p.RecordId == record.Id).SingleOrDefault();
-                if (photo != null)
+                var photos = database.Photos.Where(p => p.RecordId == record.Id).ToList();
+                if (photos.Count > 0)
                 {
                     database.Photos.Where(p => p.RecordId == record.Id).ExecuteDelete();
-                    DeleteFile(user.Id, update.Id, photo.Path);
+                    foreach (var photo in photos)
+                    {
+                        DeleteFile(update.Id, photo.Path);
+                    }
                 }
             }
             SaveDatabase();
@@ -515,7 +546,7 @@ public class FlowerApiController : BaseController
                     };
                     AddPhotoItem(cover);
 
-                    await WriteToFile(user.Id, update.Id, file, token);
+                    await WriteToFile(update.Id, file, token);
                 });
             }
             catch (Exception ex)
@@ -524,8 +555,12 @@ public class FlowerApiController : BaseController
                 // TODO: Logger
             }
         }
+        else
+        {
+            SaveDatabase();
+        }
 
-        return Ok(user);
+        return Ok(flower);
     }
 
     /// <summary>
@@ -620,7 +655,7 @@ public class FlowerApiController : BaseController
                     };
                     AddPhotoItem(cover);
 
-                    await WriteToFile(user.Id, id, file, token);
+                    await WriteToFile(id, file, token);
                 });
             }
             catch (Exception ex)
diff --git a/Server/Controller/FlowerApiController.structs.cs b/Server/Controller/FlowerApiController.structs.cs
index fa502a0..62268ba 100644
--- a/Server/Controller/FlowerApiController.structs.cs
+++ b/Server/Controller/FlowerApiController.structs.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Mvc;
+using Blahblah.FlowerStory.Server.Data.Model;
+using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
 
 namespace Blahblah.FlowerStory.Server.Controller;
@@ -58,5 +59,21 @@ public record FlowerUpdateParameter : FlowerParameter
     /// </summary>
     [Required]
     [FromForm(Name = "id")]
-    public int Id { get; set; }
+    public int Id { get; init; }
 }
+
+/// <summary>
+/// 花草结果对象
+/// </summary>
+public record FlowerResult
+{
+    /// <summary>
+    /// 花草列表
+    /// </summary>
+    public required FlowerItem[] Flowers { get; init; }
+
+    /// <summary>
+    /// 花草总数
+    /// </summary>
+    public int Count { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Controller/ImageController.cs b/Server/Controller/ImageController.cs
index be5f074..a640db9 100644
--- a/Server/Controller/ImageController.cs
+++ b/Server/Controller/ImageController.cs
@@ -1,6 +1,7 @@
 using Blahblah.FlowerStory.Server.Data;
 using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
+using System.Net;
 
 namespace Blahblah.FlowerStory.Server.Controller;
 
@@ -11,9 +12,6 @@ namespace Blahblah.FlowerStory.Server.Controller;
 [Route("photo")]
 public class ImageController : BaseController
 {
-    /// <inheritdoc/>
-    public const string BaseUrl = "https://flower.tsanie.org";
-
     /// <inheritdoc/>
     public ImageController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
     {
@@ -66,6 +64,8 @@ public class ImageController : BaseController
     ///     GET /photo/flower/{fid}/{name}
     /// 
     /// </remarks>
+    /// <param name="fid">花草唯一 id</param>
+    /// <param name="name">照片名称</param>
     /// <returns>认证通过则显示花草照片</returns>
     /// <response code="200">返回花草照片</response>
     /// <response code="401">认证失败</response>
@@ -98,7 +98,7 @@ public class ImageController : BaseController
             return Forbid();
         }
 #endif
-        var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString(), name);
+        var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString(), WebUtility.UrlEncode(name));
         if (System.IO.File.Exists(path))
         {
             var data = await System.IO.File.ReadAllBytesAsync(path);
diff --git a/Server/Controller/UserApiController.cs b/Server/Controller/UserApiController.cs
index 4ab8fc5..2f81e30 100644
--- a/Server/Controller/UserApiController.cs
+++ b/Server/Controller/UserApiController.cs
@@ -119,6 +119,7 @@ public partial class UserApiController : BaseController
     [Route("logout", Name = "logout")]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpPost]
     public ActionResult Logout()
     {
@@ -161,6 +162,7 @@ public partial class UserApiController : BaseController
     [Route("register", Name = "register")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpPost]
     [Consumes("application/json")]
     public ActionResult<UserItem> Register([FromBody] UserParameter user)
@@ -212,6 +214,7 @@ public partial class UserApiController : BaseController
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpGet]
     [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
     public ActionResult<UserItem> Profile()
@@ -260,6 +263,7 @@ public partial class UserApiController : BaseController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpPut]
     [Consumes("application/json")]
     public ActionResult<UserItem> Update([FromBody] UpdateParameter update)
@@ -314,6 +318,7 @@ public partial class UserApiController : BaseController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpPut]
     [Consumes("multipart/form-data")]
     [RequestSizeLimit(5 * 1024 * 1024)]
@@ -366,6 +371,7 @@ public partial class UserApiController : BaseController
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
+    [ProducesErrorResponseType(typeof(ErrorResponse))]
     [HttpDelete]
     public ActionResult RemoveAvatar()
     {
diff --git a/Server/Data/Model/FlowerItem.cs b/Server/Data/Model/FlowerItem.cs
index aa0a424..2ef6950 100644
--- a/Server/Data/Model/FlowerItem.cs
+++ b/Server/Data/Model/FlowerItem.cs
@@ -8,7 +8,7 @@ namespace Blahblah.FlowerStory.Server.Data.Model;
 /// 花草对象
 /// </summary>
 [Table("flowers")]
-public class FlowerItem
+public class FlowerItem : ILocation
 {
     /// <summary>
     /// 自增 id,主键
@@ -83,4 +83,22 @@ public class FlowerItem
     /// 封面相关照片
     /// </summary>
     public ICollection<PhotoItem>? Photos { get; set; }
+
+    /// <summary>
+    /// 纬度
+    /// </summary>
+    [Column("latitude")]
+    public double? Latitude { get; set; }
+
+    /// <summary>
+    /// 经度
+    /// </summary>
+    [Column("longitude")]
+    public double? Longitude { get; set; }
+
+    /// <summary>
+    /// 距离(米)
+    /// </summary>
+    [NotMapped]
+    public int? Distance { get; set; }
 }
diff --git a/Server/Data/Model/ILocation.cs b/Server/Data/Model/ILocation.cs
new file mode 100644
index 0000000..1800124
--- /dev/null
+++ b/Server/Data/Model/ILocation.cs
@@ -0,0 +1,17 @@
+namespace Blahblah.FlowerStory.Server.Data.Model;
+
+/// <summary>
+/// 实体位置接口
+/// </summary>
+public interface ILocation
+{
+    /// <summary>
+    /// 纬度
+    /// </summary>
+    double? Latitude { get; }
+
+    /// <summary>
+    /// 经度
+    /// </summary>
+    double? Longitude { get; }
+}
diff --git a/Server/Data/Model/PhotoItem.cs b/Server/Data/Model/PhotoItem.cs
index 72516a2..f09c2c5 100644
--- a/Server/Data/Model/PhotoItem.cs
+++ b/Server/Data/Model/PhotoItem.cs
@@ -82,6 +82,9 @@ public class PhotoItem
     [JsonIgnore]
     public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime);
 
+    /// <summary>
+    /// 前端显示的 URL
+    /// </summary>
     [NotMapped]
-    public string Url { get; set; }
+    public string? Url { get; set; }
 }
diff --git a/Server/Data/Model/RecordItem.cs b/Server/Data/Model/RecordItem.cs
index fe201b7..0d28af8 100644
--- a/Server/Data/Model/RecordItem.cs
+++ b/Server/Data/Model/RecordItem.cs
@@ -8,7 +8,7 @@ namespace Blahblah.FlowerStory.Server.Data.Model;
 /// 记录对象
 /// </summary>
 [Table("records")]
-public class RecordItem
+public class RecordItem : ILocation
 {
     /// <summary>
     /// 自增 id,主键
@@ -90,4 +90,16 @@ public class RecordItem
     /// 事件关联照片
     /// </summary>
     public ICollection<PhotoItem>? Photos { get; set; }
+
+    /// <summary>
+    /// 纬度
+    /// </summary>
+    [Column("latitude")]
+    public double? Latitude { get; set; }
+
+    /// <summary>
+    /// 经度
+    /// </summary>
+    [Column("longitude")]
+    public double? Longitude { get; set; }
 }
diff --git a/Server/Migrations/20230705083734_Add-Latitude-Longitude.Designer.cs b/Server/Migrations/20230705083734_Add-Latitude-Longitude.Designer.cs
new file mode 100644
index 0000000..3da48ea
--- /dev/null
+++ b/Server/Migrations/20230705083734_Add-Latitude-Longitude.Designer.cs
@@ -0,0 +1,335 @@
+// <auto-generated />
+using System;
+using Blahblah.FlowerStory.Server.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Blahblah.FlowerStory.Server.Migrations
+{
+    [DbContext(typeof(FlowerDatabase))]
+    [Migration("20230705083734_Add-Latitude-Longitude")]
+    partial class AddLatitudeLongitude
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("fid");
+
+                    b.Property<int>("CategoryId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("categoryid");
+
+                    b.Property<decimal?>("Cost")
+                        .HasColumnType("real")
+                        .HasColumnName("cost");
+
+                    b.Property<long>("DateBuyUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("datebuy")
+                        .HasAnnotation("Relational:JsonPropertyName", "dateBuy");
+
+                    b.Property<double?>("LastLatitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("latitude");
+
+                    b.Property<double?>("LastLongitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("longitude");
+
+                    b.Property<string>("Memo")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("memo");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uid");
+
+                    b.Property<string>("Purchase")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("purchase");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("OwnerId");
+
+                    b.ToTable("flowers");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pid");
+
+                    b.Property<long>("DateUploadUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("dateupload")
+                        .HasAnnotation("Relational:JsonPropertyName", "dateUpload");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("filename");
+
+                    b.Property<string>("FileType")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("filetype");
+
+                    b.Property<int>("FlowerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("fid");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<int>("RecordId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("FlowerId");
+
+                    b.HasIndex("RecordId");
+
+                    b.ToTable("photos");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rid");
+
+                    b.Property<int?>("ByUserId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("byuid");
+
+                    b.Property<string>("ByUserName")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("byname");
+
+                    b.Property<long>("DateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("date")
+                        .HasAnnotation("Relational:JsonPropertyName", "date");
+
+                    b.Property<int>("EventId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("eid");
+
+                    b.Property<int>("FlowerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("fid");
+
+                    b.Property<double?>("Latitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("latitude");
+
+                    b.Property<double?>("Longitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("longitude");
+
+                    b.Property<string>("Memo")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("memo");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("FlowerId");
+
+                    b.HasIndex("OwnerId");
+
+                    b.ToTable("records");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("tid");
+
+                    b.Property<long>("ActiveDateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("activedate")
+                        .HasAnnotation("Relational:JsonPropertyName", "activeDate");
+
+                    b.Property<string>("ClientAgent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("clientagent");
+
+                    b.Property<string>("ClientApp")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("clientapp");
+
+                    b.Property<string>("DeviceId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deviceid");
+
+                    b.Property<long>("ExpireDateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("expiredate")
+                        .HasAnnotation("Relational:JsonPropertyName", "expireDate");
+
+                    b.Property<int>("ExpireSeconds")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("expiresecs");
+
+                    b.Property<long>("LogonDateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("logondate")
+                        .HasAnnotation("Relational:JsonPropertyName", "logonDate");
+
+                    b.Property<int>("UserId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uid");
+
+                    b.Property<string>("VerifyCode")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("verifycode");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("tokens");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uid");
+
+                    b.Property<long?>("ActiveDateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("activedate");
+
+                    b.Property<byte[]>("Avatar")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("avatar");
+
+                    b.Property<string>("Email")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("email");
+
+                    b.Property<int>("Level")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("level");
+
+                    b.Property<string>("Mobile")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("mobile");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.Property<string>("Password")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("password");
+
+                    b.Property<long>("RegisterDateUnixTime")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("regdate")
+                        .HasAnnotation("Relational:JsonPropertyName", "registerDate");
+
+                    b.Property<string>("UserId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("id");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("users");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
+                {
+                    b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner")
+                        .WithMany()
+                        .HasForeignKey("OwnerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Owner");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b =>
+                {
+                    b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower")
+                        .WithMany("Photos")
+                        .HasForeignKey("FlowerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Blahblah.FlowerStory.Server.Data.Model.RecordItem", "Record")
+                        .WithMany("Photos")
+                        .HasForeignKey("RecordId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Flower");
+
+                    b.Navigation("Record");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
+                {
+                    b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower")
+                        .WithMany()
+                        .HasForeignKey("FlowerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner")
+                        .WithMany()
+                        .HasForeignKey("OwnerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Flower");
+
+                    b.Navigation("Owner");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
+                {
+                    b.Navigation("Photos");
+                });
+
+            modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
+                {
+                    b.Navigation("Photos");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Server/Migrations/20230705083734_Add-Latitude-Longitude.cs b/Server/Migrations/20230705083734_Add-Latitude-Longitude.cs
new file mode 100644
index 0000000..4e5458b
--- /dev/null
+++ b/Server/Migrations/20230705083734_Add-Latitude-Longitude.cs
@@ -0,0 +1,58 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Blahblah.FlowerStory.Server.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddLatitudeLongitude : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<double>(
+                name: "latitude",
+                table: "records",
+                type: "REAL",
+                nullable: true);
+
+            migrationBuilder.AddColumn<double>(
+                name: "longitude",
+                table: "records",
+                type: "REAL",
+                nullable: true);
+
+            migrationBuilder.AddColumn<double>(
+                name: "latitude",
+                table: "flowers",
+                type: "REAL",
+                nullable: true);
+
+            migrationBuilder.AddColumn<double>(
+                name: "longitude",
+                table: "flowers",
+                type: "REAL",
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "latitude",
+                table: "records");
+
+            migrationBuilder.DropColumn(
+                name: "longitude",
+                table: "records");
+
+            migrationBuilder.DropColumn(
+                name: "latitude",
+                table: "flowers");
+
+            migrationBuilder.DropColumn(
+                name: "longitude",
+                table: "flowers");
+        }
+    }
+}
diff --git a/Server/Migrations/FlowerDatabaseModelSnapshot.cs b/Server/Migrations/FlowerDatabaseModelSnapshot.cs
index bfa546e..ac35087 100644
--- a/Server/Migrations/FlowerDatabaseModelSnapshot.cs
+++ b/Server/Migrations/FlowerDatabaseModelSnapshot.cs
@@ -37,6 +37,14 @@ namespace Blahblah.FlowerStory.Server.Migrations
                         .HasColumnName("datebuy")
                         .HasAnnotation("Relational:JsonPropertyName", "dateBuy");
 
+                    b.Property<double?>("LastLatitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("latitude");
+
+                    b.Property<double?>("LastLongitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("longitude");
+
                     b.Property<string>("Memo")
                         .HasColumnType("TEXT")
                         .HasColumnName("memo");
@@ -133,6 +141,14 @@ namespace Blahblah.FlowerStory.Server.Migrations
                         .HasColumnType("INTEGER")
                         .HasColumnName("fid");
 
+                    b.Property<double?>("Latitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("latitude");
+
+                    b.Property<double?>("Longitude")
+                        .HasColumnType("REAL")
+                        .HasColumnName("longitude");
+
                     b.Property<string>("Memo")
                         .HasColumnType("TEXT")
                         .HasColumnName("memo");
diff --git a/Server/Program.cs b/Server/Program.cs
index f6cc671..2752f73 100644
--- a/Server/Program.cs
+++ b/Server/Program.cs
@@ -11,7 +11,7 @@ public class Program
     /// <inheritdoc/>
     public const string ProjectName = "Flower Story";
     /// <inheritdoc/>
-    public const string Version = "0.4.626";
+    public const string Version = "0.5.705";
 
     /// <inheritdoc/>
     public static void Main(string[] args)