@@ -20,11 +20,176 @@ public class MarketController : ControllerBase
2020{
2121 private readonly JuggleDbContext _db ;
2222 private readonly ITenantAccessor _tenant ;
23+ private readonly IHttpClientFactory _httpClientFactory ;
2324
24- public MarketController ( JuggleDbContext db , ITenantAccessor tenant )
25+ public MarketController ( JuggleDbContext db , ITenantAccessor tenant , IHttpClientFactory httpClientFactory )
2526 {
2627 _db = db ;
2728 _tenant = tenant ;
29+ _httpClientFactory = httpClientFactory ;
30+ }
31+
32+ /// <summary>官方市场 GitHub 仓库(market 目录)</summary>
33+ private const string MarketRepo = "https://raw.githubusercontent.com/pythonHuang/APIWorkflowCanvasDesignNet/main/market" ;
34+
35+ /// <summary>本地 market 目录(发现下载的文件落盘位置)</summary>
36+ private static string MarketDir => Path . Combine ( Directory . GetCurrentDirectory ( ) , "market" ) ;
37+
38+ /// <summary>发现官方市场:拉取 index.json 并把各类型 {id}.json 下载到本地 market 目录。</summary>
39+ [ HttpGet ( "discover" ) ]
40+ public async Task < ApiResult > Discover ( )
41+ {
42+ var client = _httpClientFactory . CreateClient ( ) ;
43+ client . Timeout = TimeSpan . FromSeconds ( 60 ) ;
44+ var downloaded = 0 ;
45+ var skipped = 0 ;
46+ var failed = new List < string > ( ) ;
47+
48+ string ? indexJson ;
49+ try
50+ {
51+ var indexResp = await client . GetAsync ( $ "{ MarketRepo } /index.json") ;
52+ if ( ! indexResp . IsSuccessStatusCode )
53+ return ApiResult . Fail ( $ "官方市场 index.json 获取失败({ ( int ) indexResp . StatusCode } ):{ MarketRepo } /index.json(仓库尚未建立 market 目录时请忽略)") ;
54+ indexJson = await indexResp . Content . ReadAsStringAsync ( ) ;
55+ }
56+ catch ( Exception ex )
57+ {
58+ return ApiResult . Fail ( $ "官方市场访问失败:{ ex . Message } ") ;
59+ }
60+
61+ Directory . CreateDirectory ( MarketDir ) ;
62+ await System . IO . File . WriteAllTextAsync ( Path . Combine ( MarketDir , "index.json" ) , indexJson ) ;
63+
64+ using var doc = JsonDocument . Parse ( indexJson ) ;
65+ var root = doc . RootElement ;
66+ JsonElement entries = root . ValueKind == JsonValueKind . Array ? root
67+ : root . TryGetProperty ( "items" , out var it ) ? it : default ;
68+
69+ if ( entries . ValueKind != JsonValueKind . Array )
70+ return ApiResult . Success ( new { downloaded = 0 , skipped = 0 , failed = new List < string > ( ) , message = "index.json 中没有 items 列表" } ) ;
71+
72+ foreach ( var entry in entries . EnumerateArray ( ) )
73+ {
74+ var id = entry . TryGetProperty ( "id" , out var idEl ) ? idEl . GetInt64 ( ) : 0 ;
75+ var type = entry . TryGetProperty ( "type" , out var typeEl ) ? typeEl . GetString ( ) ?? "" : "" ;
76+ var file = entry . TryGetProperty ( "file" , out var fileEl ) ? fileEl . GetString ( ) ?? "" : "" ;
77+ if ( id == 0 || string . IsNullOrWhiteSpace ( type ) ) continue ;
78+ if ( string . IsNullOrWhiteSpace ( file ) ) file = $ "{ type } /{ id } .json";
79+
80+ var targetPath = Path . Combine ( MarketDir , type , $ "{ id } .json") ;
81+ var url = $ "{ MarketRepo } /{ file } ";
82+ try
83+ {
84+ var resp = await client . GetAsync ( url ) ;
85+ if ( ! resp . IsSuccessStatusCode )
86+ {
87+ failed . Add ( $ "{ type } /{ id } .json") ;
88+ continue ;
89+ }
90+ var body = await resp . Content . ReadAsStringAsync ( ) ;
91+ Directory . CreateDirectory ( Path . GetDirectoryName ( targetPath ) ! ) ;
92+ await System . IO . File . WriteAllTextAsync ( targetPath , body ) ;
93+ downloaded ++ ;
94+ }
95+ catch { failed . Add ( $ "{ type } /{ id } .json") ; }
96+ }
97+
98+ return ApiResult . Success ( new { downloaded , skipped , failed , message = $ "发现完成:成功下载 { downloaded } 个条目到本地 market 目录" } ) ;
99+ }
100+
101+ /// <summary>本地 market 目录条目列表(含元数据与是否已导入)。</summary>
102+ [ HttpGet ( "local-files" ) ]
103+ public async Task < ApiResult > ListLocalFiles ( )
104+ {
105+ var result = new List < object > ( ) ;
106+ var imported = await _db . MarketItems . Where ( m => m . Deleted == 0 && m . MarketItemId != null ) . ToListAsync ( ) ;
107+
108+ if ( Directory . Exists ( MarketDir ) )
109+ {
110+ foreach ( var typeDir in Directory . GetDirectories ( MarketDir ) )
111+ {
112+ var type = Path . GetFileName ( typeDir ) ;
113+ foreach ( var file in Directory . GetFiles ( typeDir , "*.json" ) )
114+ {
115+ if ( Path . GetFileName ( file ) == "index.json" ) continue ;
116+ try
117+ {
118+ var json = await System . IO . File . ReadAllTextAsync ( file ) ;
119+ using var doc = JsonDocument . Parse ( json ) ;
120+ var root = doc . RootElement ;
121+ string ? Get ( string p ) => root . TryGetProperty ( p , out var v ) && v . ValueKind == JsonValueKind . String ? v . GetString ( ) : null ;
122+ var marketId = root . TryGetProperty ( "id" , out var idEl ) ? ( int ? ) idEl . GetInt32 ( ) : null ;
123+ var hit = imported . FirstOrDefault ( m => m . MarketItemId == marketId && m . ItemType == type ) ;
124+ result . Add ( new
125+ {
126+ marketId ,
127+ type ,
128+ name = Get ( "name" ) ?? Path . GetFileNameWithoutExtension ( file ) ,
129+ description = Get ( "description" ) ,
130+ icon = Get ( "icon" ) ,
131+ author = Get ( "author" ) ,
132+ version = Get ( "version" ) ,
133+ updatedAt = Get ( "updatedAt" ) ,
134+ imported = hit != null ,
135+ localItemId = hit ? . Id ,
136+ localVersion = hit ? . Version
137+ } ) ;
138+ }
139+ catch { /* 非法 JSON 跳过 */ }
140+ }
141+ }
142+ }
143+
144+ return ApiResult . Success ( result ) ;
145+ }
146+
147+ /// <summary>导入本地 market 目录条目到市场(market_item_id 相同则更新,否则新增)。</summary>
148+ [ HttpPost ( "import-file" ) ]
149+ public async Task < ApiResult > ImportFile ( [ FromBody ] MarketImportFileRequest req )
150+ {
151+ var filePath = Path . Combine ( MarketDir , req . Type , $ "{ req . MarketId } .json") ;
152+ if ( ! System . IO . File . Exists ( filePath ) ) return ApiResult . Fail ( $ "本地文件不存在: { req . Type } /{ req . MarketId } .json") ;
153+
154+ string json ;
155+ try { json = await System . IO . File . ReadAllTextAsync ( filePath ) ; }
156+ catch ( Exception ex ) { return ApiResult . Fail ( $ "读取文件失败: { ex . Message } ") ; }
157+
158+ try
159+ {
160+ using var doc = JsonDocument . Parse ( json ) ;
161+ var root = doc . RootElement ;
162+ string ? Get ( string p ) => root . TryGetProperty ( p , out var v ) && v . ValueKind == JsonValueKind . String ? v . GetString ( ) : null ;
163+
164+ var entity = await _db . MarketItems . FirstOrDefaultAsync ( m =>
165+ m . MarketItemId == req . MarketId && m . ItemType == req . Type && m . Deleted == 0 ) ;
166+ var isUpdate = entity != null ;
167+ entity ??= new MarketItemEntity { MarketItemId = req . MarketId , ItemType = req . Type , TenantId = null , Enabled = 1 } ;
168+
169+ entity . ItemName = Get ( "name" ) ?? entity . ItemName ;
170+ entity . Description = Get ( "description" ) ?? entity . Description ;
171+ entity . Icon = Get ( "icon" ) ?? entity . Icon ;
172+ entity . Author = Get ( "author" ) ?? entity . Author ;
173+ entity . Version = Get ( "version" ) ?? entity . Version ;
174+ entity . GroupName = Get ( "group" ) ?? entity . GroupName ;
175+ if ( root . TryGetProperty ( "content" , out var contentEl ) )
176+ entity . ContentJson = contentEl . ToString ( ) ;
177+ else
178+ entity . ContentJson = json ;
179+ entity . UpdatedAt = Get ( "updatedAt" ) ?? DateTime . Now . ToString ( "o" ) ;
180+
181+ if ( entity . Id == 0 ) _db . MarketItems . Add ( entity ) ;
182+ await _db . SaveChangesAsync ( ) ;
183+ return ApiResult . Success ( new
184+ {
185+ id = entity . Id ,
186+ message = isUpdate ? $ "「{ entity . ItemName } 」已更新(市场条目 id={ req . MarketId } 已存在)" : $ "「{ entity . ItemName } 」已导入市场"
187+ } ) ;
188+ }
189+ catch ( Exception ex )
190+ {
191+ return ApiResult . Fail ( $ "解析/导入失败: { ex . Message } ") ;
192+ }
28193 }
29194
30195 /// <summary>市场列表(平台级条目 + 我的条目),可按类型筛选</summary>
@@ -34,7 +199,11 @@ public async Task<ApiResult> List([FromQuery] string? type)
34199 var query = _db . MarketItems . Where ( m => m . Deleted == 0 && m . Enabled == 1 ) ;
35200 if ( ! string . IsNullOrWhiteSpace ( type ) ) query = query . Where ( m => m . ItemType == type ) ;
36201 var items = await query . OrderByDescending ( m => m . DownloadCount ) . ThenByDescending ( m => m . Id ) . ToListAsync ( ) ;
37- return ApiResult . Success ( items . Select ( m => new { m . Id , m . ItemType , m . ItemName , m . Description , m . GroupName , m . DownloadCount , m . CreatedAt } ) ) ;
202+ return ApiResult . Success ( items . Select ( m => new
203+ {
204+ m . Id , m . ItemType , m . ItemName , m . Description , m . GroupName , m . DownloadCount ,
205+ m . MarketItemId , m . Icon , m . Author , m . Version , m . UpdatedAt , m . ContentJson
206+ } ) ) ;
38207 }
39208
40209 /// <summary>我发布的市场条目(管理/下架)</summary>
@@ -263,3 +432,9 @@ public class MarketPublishRequest
263432 public string GroupName { get ; set ; } = "" ;
264433 public string ContentJson { get ; set ; } = "{}" ;
265434}
435+
436+ public class MarketImportFileRequest
437+ {
438+ public string Type { get ; set ; } = "" ;
439+ public int MarketId { get ; set ; }
440+ }
0 commit comments