diff --git a/.gitignore b/.gitignore
index 62b24f7..72d2a79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@
 ##
 ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
 
-flower.db
+flower.db*
 TestCase/
 
 # User-specific files
diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs
index bcf696d..7c1e268 100644
--- a/Server/Controller/BaseController.cs
+++ b/Server/Controller/BaseController.cs
@@ -239,7 +239,8 @@ public abstract partial class BaseController : ControllerBase
     /// <param name="uid">用户唯一 id</param>
     /// <param name="fid">花草唯一 id</param>
     /// <param name="file">文件对象</param>
-    protected async Task WriteToFile(int uid, int fid, FileResult file)
+    /// <param name="token">取消令牌</param>
+    protected async Task WriteToFile(int uid, int fid, FileResult file, CancellationToken token = default)
     {
         var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString());
         if (!Directory.Exists(directory))
@@ -247,7 +248,7 @@ public abstract partial class BaseController : ControllerBase
             Directory.CreateDirectory(directory);
         }
         var path = Path.Combine(directory, file.Path);
-        await System.IO.File.WriteAllBytesAsync(path, file.Content);
+        await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
     }
 }
 
diff --git a/Server/Controller/BaseController.sqlite.cs b/Server/Controller/BaseController.sqlite.cs
index 1c6e257..abdb3b5 100644
--- a/Server/Controller/BaseController.sqlite.cs
+++ b/Server/Controller/BaseController.sqlite.cs
@@ -1,11 +1,31 @@
 using Blahblah.FlowerStory.Server.Data.Model;
 using Microsoft.EntityFrameworkCore;
-using System.Security.Cryptography;
 
 namespace Blahblah.FlowerStory.Server.Controller;
 
 partial class BaseController
 {
+    /// <summary>
+    /// 执行事务
+    /// </summary>
+    /// <param name="executor">执行代理</param>
+    /// <param name="token">取消令牌</param>
+    /// <exception cref="ArgumentNullException">执行代理为 null</exception>
+    protected async Task ExecuteTransaction(Func<CancellationToken, Task> executor, CancellationToken token = default)
+    {
+        if (executor == null) throw new ArgumentNullException(nameof(executor));
+        using var trans = await database.Database.BeginTransactionAsync(token);
+        try
+        {
+            await executor(token);
+            await trans.CommitAsync(token);
+        }
+        catch
+        {
+            await trans.RollbackAsync(token);
+            throw;
+        }
+    }
 
     /// <summary>
     /// 根据 uid 获取用户对象
@@ -50,4 +70,14 @@ partial class BaseController
     {
         return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}");
     }
+
+    /// <summary>
+    /// 添加照片项
+    /// </summary>
+    /// <param name="item">照片对象</param>
+    /// <returns></returns>
+    protected int AddPhotoItem(PhotoItem item)
+    {
+        return database.Database.ExecuteSql($"INSERT INTO \"photos\"(\"fid\",\"rid\",\"filetype\",\"filename\",\"path\",\"dateupload\") VALUES({item.FlowerId},{item.RecordId},{item.FileType},{item.FileName},{item.Path},{item.DateUploadUnixTime})");
+    }
 }
diff --git a/Server/Controller/EventApiController.cs b/Server/Controller/EventApiController.cs
index d4e546c..64a14bd 100644
--- a/Server/Controller/EventApiController.cs
+++ b/Server/Controller/EventApiController.cs
@@ -95,10 +95,7 @@ public class EventApiController : BaseController
 
         if (includePhoto == true)
         {
-            foreach (var r in records)
-            {
-                r.Photos = database.Photos.Where(p => p.RecordId == r.Id).ToList();
-            }
+            records = records.Include(r => r.Photos);
         }
 
         return Ok(records.ToArray());
@@ -334,9 +331,9 @@ public class EventApiController : BaseController
     /// </remarks>
     /// <param name="id">事件唯一 id</param>
     /// <param name="photo">图片</param>
-    /// <returns>修改成功则返回 HTTP 204</returns>
-    /// <response code="204">修改成功</response>
-    /// <response code="400">照片格式非法</response>
+    /// <returns>添加成功则返回 HTTP 204</returns>
+    /// <response code="204">添加成功</response>
+    /// <response code="400">图片格式非法</response>
     /// <response code="401">未找到登录会话或已过期</response>
     /// <response code="403">用户已禁用</response>
     /// <response code="404">未找到关联用户或者关联的事件</response>
@@ -351,7 +348,7 @@ public class EventApiController : BaseController
     [HttpPost]
     [Consumes("multipart/form-data")]
     [RequestSizeLimit(15 * 1024 * 1024)]
-    public async Task<ActionResult> UploadCovers([Required][FromQuery] int id, [Required] IFormFile photo)
+    public async Task<ActionResult> UploadPhoto([Required][FromQuery] int id, [Required] IFormFile photo)
     {
         var (result, user) = CheckPermission();
         if (result != null)
@@ -391,7 +388,7 @@ public class EventApiController : BaseController
 
             try
             {
-                await WriteToFile(user.Id, id, file);
+                await WriteToFile(user.Id, record.FlowerId, file);
             }
             catch (Exception ex)
             {
@@ -405,6 +402,107 @@ public class EventApiController : BaseController
         return NoContent();
     }
 
+    /// <summary>
+    /// 批量添加事件关联照片,总大小限制 75MB
+    /// </summary>
+    /// <remarks>
+    /// 请求示例:
+    /// 
+    ///     POST /api/event/add_photos
+    ///     Authorization: authorization id
+    /// 
+    /// 参数:
+    /// 
+    ///     id: int
+    ///     photos: IFormFile[]
+    /// 
+    /// </remarks>
+    /// <param name="id">事件唯一 id</param>
+    /// <param name="photos">图片集</param>
+    /// <returns>添加成功则返回 HTTP 204</returns>
+    /// <response code="204">添加成功</response>
+    /// <response code="400">图片参数非法或图片格式非法</response>
+    /// <response code="401">未找到登录会话或已过期</response>
+    /// <response code="403">用户已禁用</response>
+    /// <response code="404">未找到关联用户或者关联的事件</response>
+    /// <response code="413">提交正文过大</response>
+    [Route("add_photos", Name = "addEventPhotos")]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
+    [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+    [HttpPost]
+    [Consumes("multipart/form-data")]
+    // 5 photos
+    [RequestSizeLimit(75 * 1024 * 1024)]
+    public async Task<ActionResult> UploadPhotos([Required][FromQuery] int id, [Required] IFormFile[] photos)
+    {
+        var (result, user) = CheckPermission();
+        if (result != null)
+        {
+            return result;
+        }
+        if (user == null)
+        {
+            return NotFound();
+        }
+
+        if (photos == null || photos.Length == 0)
+        {
+            SaveDatabase();
+            return BadRequest();
+        }
+
+        var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id);
+        if (record == null)
+        {
+            SaveDatabase();
+            return NotFound(id);
+        }
+
+        SaveDatabase();
+
+        try
+        {
+            await ExecuteTransaction(async token =>
+            {
+                foreach (var photo in photos)
+                {
+                    if (photo.Length > 0)
+                    {
+                        var file = WrapFormFile(photo) ?? throw new BadHttpRequestException(photo?.FileName ?? string.Empty);
+
+                        var p = new PhotoItem
+                        {
+                            FlowerId = record.FlowerId,
+                            RecordId = id,
+                            FileType = file.FileType,
+                            FileName = file.Filename,
+                            Path = file.Path,
+                            DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
+                        };
+                        AddPhotoItem(p);
+
+                        await WriteToFile(user.Id, record.FlowerId, file, token);
+                    }
+                }
+            });
+        }
+        catch (BadHttpRequestException bex)
+        {
+            return BadRequest(bex.Message);
+        }
+        catch (Exception ex)
+        {
+            return Problem(ex.ToString(), "api/event/add_photos");
+            // TODO: Logger
+        }
+
+        return NoContent();
+    }
+
     /// <summary>
     /// 获取事件关联的照片列表
     /// </summary>