@@ -12,6 +12,7 @@ import (
1212 galgameClient "kun-galgame-patch-api/internal/galgame/client"
1313 "kun-galgame-patch-api/internal/infrastructure/markdown"
1414 "kun-galgame-patch-api/internal/infrastructure/storage"
15+ "kun-galgame-patch-api/internal/patch/dto"
1516 "kun-galgame-patch-api/internal/patch/model"
1617 "kun-galgame-patch-api/internal/patch/repository"
1718 settingService "kun-galgame-patch-api/internal/setting/service"
@@ -860,6 +861,11 @@ func (s *PatchService) UpdateResource(ctx context.Context, resourceID, userID in
860861 }
861862
862863 galgameID := existing .GalgameID
864+ // Per-field edit diff (public-safe) — computed from existing(before) vs
865+ // update(after) BEFORE the txn overwrites `existing` below. Stored as a
866+ // PatchResourceRevision so the resource page can show 改动前 → 改动后 for
867+ // language / platform / type / note / name / size / file. Empty = no-op save.
868+ changes := diffResourceFields (existing , update )
863869 if err := s .db .Transaction (func (tx * gorm.DB ) error {
864870 if fileChanged {
865871 hist := & model.PatchResourceFileHistory {
@@ -878,6 +884,20 @@ func (s *PatchService) UpdateResource(ctx context.Context, resourceID, userID in
878884 }
879885 }
880886
887+ if len (changes ) > 0 {
888+ rev := & model.PatchResourceRevision {
889+ ResourceID : existing .ID ,
890+ Action : "updated" ,
891+ Changes : changes ,
892+ Reason : reason ,
893+ ActorID : userID ,
894+ ActorRole : actorRole ,
895+ }
896+ if err := tx .Create (rev ).Error ; err != nil {
897+ return fmt .Errorf ("write resource revision: %w" , err )
898+ }
899+ }
900+
881901 existing .Storage = update .Storage
882902 existing .Name = update .Name
883903 existing .ModelName = update .ModelName
@@ -1203,3 +1223,76 @@ func (s *PatchService) IsCommentVerifyEnabled() bool {
12031223func (s * PatchService ) IsCreatorOnlyEnabled () bool {
12041224 return s .setting .GetBool (settingService .KeyCreatorOnly )
12051225}
1226+
1227+ // GetResourceFileHistory returns the privacy-safe, paginated file-replacement
1228+ // audit for one resource. Public (any visitor, incl. anonymous): deliberately
1229+ // omits old_s3_key (internal storage key) and old_content (the old download
1230+ // links) — those stay behind the rate-limited /link endpoint. Callers see only
1231+ // when / who-role / why / old size + hash.
1232+ func (s * PatchService ) GetResourceFileHistory (resourceID , page , limit int ) ([]dto.PublicResourceFileHistoryItem , int64 , error ) {
1233+ rows , total , err := s .repo .GetResourceFileHistory (resourceID , (page - 1 )* limit , limit )
1234+ if err != nil {
1235+ return nil , 0 , err
1236+ }
1237+ items := make ([]dto.PublicResourceFileHistoryItem , 0 , len (rows ))
1238+ for _ , h := range rows {
1239+ items = append (items , dto.PublicResourceFileHistoryItem {
1240+ ID : h .ID ,
1241+ OldStorage : h .OldStorage ,
1242+ OldBlake3 : h .OldBlake3 ,
1243+ OldSize : h .OldSize ,
1244+ Reason : h .Reason ,
1245+ ActorRole : h .ActorRole ,
1246+ CreatedAt : h .CreatedAt ,
1247+ })
1248+ }
1249+ return items , total , nil
1250+ }
1251+
1252+ // diffResourceFields computes the public-safe per-field diff between the
1253+ // pre-edit (before) and post-edit (after) resource. Secrets (download link /
1254+ // s3 key / extract code / unzip password) are never emitted as raw values —
1255+ // only a single "已更新" marker. Used by UpdateResource to record a revision.
1256+ func diffResourceFields (before , after * model.PatchResource ) model.ResourceChangeList {
1257+ var ch model.ResourceChangeList
1258+ addStr := func (field , label , b , a string ) {
1259+ if b != a {
1260+ ch = append (ch , model.ResourceFieldChange {Field : field , Label : label , Before : b , After : a })
1261+ }
1262+ }
1263+ addArr := func (field , label string , b , a model.JSONArray ) {
1264+ bs , as := strings .Join (b , "、" ), strings .Join (a , "、" )
1265+ if bs != as {
1266+ ch = append (ch , model.ResourceFieldChange {Field : field , Label : label , Before : bs , After : as })
1267+ }
1268+ }
1269+ addStr ("name" , "资源名称" , before .Name , after .Name )
1270+ addStr ("size" , "文件大小" , before .Size , after .Size )
1271+ addStr ("model_name" , "AI 模型" , before .ModelName , after .ModelName )
1272+ addStr ("storage" , "存储方式" , before .Storage , after .Storage )
1273+ // blake3 故意不在此 diff:它由文件自动计算、UpdateResource 不写入它(编辑表单
1274+ // 也不回传),直接比较会让每次元数据编辑都误报 "hash → (空)"。文件替换通过
1275+ // size/storage 变化 + 下面的「已更新」标记体现,blake3 本身不参与字段 diff。
1276+ addStr ("note" , "备注" , before .Note , after .Note )
1277+ addArr ("language" , "语言" , before .Language , after .Language )
1278+ addArr ("platform" , "平台" , before .Platform , after .Platform )
1279+ addArr ("type" , "类型" , before .Type , after .Type )
1280+ if before .Code != after .Code ||
1281+ before .Password != after .Password ||
1282+ before .Content != after .Content ||
1283+ before .S3Key != after .S3Key {
1284+ ch = append (ch , model.ResourceFieldChange {
1285+ Field : "download" ,
1286+ Label : "下载文件 / 链接 / 提取码 / 密码" ,
1287+ Before : "" ,
1288+ After : "已更新" ,
1289+ })
1290+ }
1291+ return ch
1292+ }
1293+
1294+ // GetResourceRevisions returns the paginated per-field edit history for one
1295+ // resource (public; Changes are secret-free).
1296+ func (s * PatchService ) GetResourceRevisions (resourceID , page , limit int ) ([]model.PatchResourceRevision , int64 , error ) {
1297+ return s .repo .GetResourceRevisions (resourceID , (page - 1 )* limit , limit )
1298+ }
0 commit comments