Skip to content

Commit aa13483

Browse files
pythonHuangclaude
andcommitted
feat: 市场系统-主菜单新增市场(接口/流程/模型助手/Skills/报表五个市场),发布后平台级共享任意租户一键导入(按名称/code去重+下载计数),发布入口接入流程/助手/Skill/报表/套件五个管理页(共享发布组件)
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent c4102a1 commit aa13483

13 files changed

Lines changed: 575 additions & 2 deletions

File tree

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
using System.Text.Json;
2+
using Juggle.Application.Models.Response;
3+
using Juggle.Application.Services;
4+
using Juggle.Domain.Entities;
5+
using Juggle.Infrastructure.Persistence;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.EntityFrameworkCore;
9+
10+
namespace Juggle.Api.Controllers.Api;
11+
12+
/// <summary>
13+
/// 市场:接口/流程/模型助手/技能/报表的共享中心。
14+
/// 发布后平台级可见(TenantId=null),任意租户可导入到本地(按 code/name 去重)。
15+
/// </summary>
16+
[ApiController]
17+
[Route("api/market")]
18+
[Authorize]
19+
public class MarketController : ControllerBase
20+
{
21+
private readonly JuggleDbContext _db;
22+
private readonly ITenantAccessor _tenant;
23+
24+
public MarketController(JuggleDbContext db, ITenantAccessor tenant)
25+
{
26+
_db = db;
27+
_tenant = tenant;
28+
}
29+
30+
/// <summary>市场列表(平台级条目 + 我的条目),可按类型筛选</summary>
31+
[HttpGet("list")]
32+
public async Task<ApiResult> List([FromQuery] string? type)
33+
{
34+
var query = _db.MarketItems.Where(m => m.Deleted == 0 && m.Enabled == 1);
35+
if (!string.IsNullOrWhiteSpace(type)) query = query.Where(m => m.ItemType == type);
36+
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 }));
38+
}
39+
40+
/// <summary>我发布的市场条目(管理/下架)</summary>
41+
[HttpGet("my-list")]
42+
public async Task<ApiResult> MyList([FromQuery] string? type)
43+
{
44+
var tid = _tenant.TenantId;
45+
var query = _db.MarketItems.Where(m => m.Deleted == 0 && m.TenantId == tid);
46+
if (!string.IsNullOrWhiteSpace(type)) query = query.Where(m => m.ItemType == type);
47+
return ApiResult.Success(await query.OrderByDescending(m => m.Id).ToListAsync());
48+
}
49+
50+
/// <summary>发布到市场(内容快照 JSON 由前端按类型组装)</summary>
51+
[HttpPost("publish")]
52+
public async Task<ApiResult> Publish([FromBody] MarketPublishRequest req)
53+
{
54+
var entity = new MarketItemEntity
55+
{
56+
ItemType = req.ItemType,
57+
ItemName = req.ItemName,
58+
Description = req.Description,
59+
GroupName = req.GroupName,
60+
ContentJson = req.ContentJson,
61+
Enabled = 1,
62+
TenantId = null, // 平台级共享
63+
CreatedBy = _tenant.TenantId,
64+
CreatedAt = DateTime.Now.ToString("o")
65+
};
66+
_db.MarketItems.Add(entity);
67+
await _db.SaveChangesAsync();
68+
return ApiResult.Success(entity.Id);
69+
}
70+
71+
/// <summary>下架/删除市场条目(仅发布者或管理员)</summary>
72+
[HttpDelete("delete/{id}")]
73+
public async Task<ApiResult> Delete(long id)
74+
{
75+
var entity = await _db.MarketItems.FindAsync(id);
76+
if (entity == null) return ApiResult.Fail("条目不存在");
77+
if (entity.TenantId != null && entity.CreatedBy != _tenant.TenantId && entity.TenantId != _tenant.TenantId)
78+
return ApiResult.Fail("只能删除自己发布的条目");
79+
entity.Deleted = 1;
80+
await _db.SaveChangesAsync();
81+
return ApiResult.Success();
82+
}
83+
84+
/// <summary>从市场导入到本地(按类型创建对应实体,按 code/name 去重),下载计数 +1</summary>
85+
[HttpPost("import/{id}")]
86+
public async Task<ApiResult> Import(long id)
87+
{
88+
var item = await _db.MarketItems.FirstOrDefaultAsync(m => m.Id == id && m.Deleted == 0 && m.Enabled == 1)
89+
?? throw new Exception("市场条目不存在或已下架");
90+
try
91+
{
92+
var result = await ImportItemAsync(item);
93+
item.DownloadCount++;
94+
await _db.SaveChangesAsync();
95+
return ApiResult.Success(result);
96+
}
97+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
98+
}
99+
100+
private async Task<object> ImportItemAsync(MarketItemEntity item)
101+
{
102+
var type = item.ItemType ?? "";
103+
var tid = _tenant.TenantId;
104+
switch (type)
105+
{
106+
case "skill":
107+
{
108+
using var doc = JsonDocument.Parse(item.ContentJson ?? "{}");
109+
var root = doc.RootElement;
110+
string? Get(string p) => root.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
111+
var name = Get("skillName") ?? item.ItemName ?? "导入技能";
112+
var exists = await _db.Skills.AnyAsync(s => s.SkillName == name && s.Deleted == 0);
113+
if (exists) return new { message = $"技能「{name}」已存在,跳过" };
114+
var skill = new SkillEntity
115+
{
116+
SkillName = name,
117+
GroupName = Get("groupName") ?? item.GroupName ?? "未分组",
118+
Description = Get("description") ?? item.Description,
119+
Content = Get("content"),
120+
Enabled = 1,
121+
TenantId = tid,
122+
MarketSkillId = item.Id,
123+
CreatedAt = DateTime.Now.ToString("o")
124+
};
125+
_db.Skills.Add(skill);
126+
await _db.SaveChangesAsync();
127+
return new { message = $"技能「{name}」已导入", id = skill.Id };
128+
}
129+
case "assistant":
130+
{
131+
using var doc = JsonDocument.Parse(item.ContentJson ?? "{}");
132+
var root = doc.RootElement;
133+
string? Get(string p) => root.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
134+
var name = Get("assistantName") ?? item.ItemName ?? "导入助手";
135+
var exists = await _db.AiAssistants.AnyAsync(a => a.AssistantName == name && a.Deleted == 0);
136+
if (exists) return new { message = $"助手「{name}」已存在,跳过" };
137+
var assistant = new AiAssistantEntity
138+
{
139+
AssistantName = name,
140+
Description = Get("description") ?? item.Description,
141+
SystemPrompt = Get("systemPrompt"),
142+
InputParams = Get("inputParams"),
143+
OutputParams = Get("outputParams"),
144+
QuickPrompts = Get("quickPrompts"),
145+
Icon = Get("icon"),
146+
Enabled = 1,
147+
TenantId = tid,
148+
CreatedAt = DateTime.Now.ToString("o")
149+
};
150+
_db.AiAssistants.Add(assistant);
151+
await _db.SaveChangesAsync();
152+
return new { message = $"助手「{name}」已导入", id = assistant.Id };
153+
}
154+
case "flow":
155+
{
156+
using var doc = JsonDocument.Parse(item.ContentJson ?? "{}");
157+
var root = doc.RootElement;
158+
string? Get(string p) => root.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
159+
var name = Get("flowName") ?? item.ItemName ?? "导入流程";
160+
var exists = await _db.FlowDefinitions.AnyAsync(f => f.FlowName == name && f.Deleted == 0);
161+
if (exists) return new { message = $"流程「{name}」已存在,跳过" };
162+
var flow = new FlowDefinitionEntity
163+
{
164+
FlowKey = $"flow_{Guid.NewGuid():N}",
165+
FlowName = name,
166+
FlowDesc = Get("flowDesc") ?? item.Description,
167+
FlowType = "sync",
168+
GroupName = Get("groupName") ?? item.GroupName ?? "",
169+
FlowContent = Get("flowContent") ?? "[]",
170+
Status = 0,
171+
TenantId = tid,
172+
CreatedAt = DateTime.Now.ToString("o")
173+
};
174+
_db.FlowDefinitions.Add(flow);
175+
await _db.SaveChangesAsync();
176+
return new { message = $"流程「{name}」已导入", id = flow.Id, flowKey = flow.FlowKey };
177+
}
178+
case "report":
179+
{
180+
using var doc = JsonDocument.Parse(item.ContentJson ?? "{}");
181+
var root = doc.RootElement;
182+
string? Get(string p) => root.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
183+
var name = Get("name") ?? item.ItemName ?? "导入报表";
184+
var exists = await _db.Set<ReportEntity>().AnyAsync(r => r.Name == name && r.Deleted == 0);
185+
if (exists) return new { message = $"报表「{name}」已存在,跳过" };
186+
var report = new ReportEntity
187+
{
188+
Name = name,
189+
GroupName = Get("groupName") ?? item.GroupName ?? "",
190+
SourceType = "dataview",
191+
CustomSql = "",
192+
ParamsConfig = Get("paramsConfig") ?? "[]",
193+
LayoutJson = Get("layoutJson") ?? "{}",
194+
Status = 1,
195+
TenantId = tid,
196+
CreatedAt = DateTime.Now.ToString("o")
197+
};
198+
_db.Set<ReportEntity>().Add(report);
199+
await _db.SaveChangesAsync();
200+
return new { message = $"报表「{name}」已导入", id = report.Id };
201+
}
202+
case "api":
203+
{
204+
using var doc = JsonDocument.Parse(item.ContentJson ?? "{}");
205+
var root = doc.RootElement;
206+
var created = 0;
207+
if (root.TryGetProperty("suites", out var suitesEl) && suitesEl.ValueKind == JsonValueKind.Array)
208+
{
209+
foreach (var s in suitesEl.EnumerateArray())
210+
{
211+
var code = s.TryGetProperty("suiteCode", out var ce) ? ce.GetString() ?? "" : "";
212+
if (code.Length == 0) continue;
213+
if (await _db.Suites.AnyAsync(x => x.SuiteCode == code && x.Deleted == 0)) continue;
214+
_db.Suites.Add(new SuiteEntity
215+
{
216+
SuiteCode = code,
217+
SuiteName = s.TryGetProperty("suiteName", out var ne) ? ne.GetString() ?? code : code,
218+
SuiteDesc = s.TryGetProperty("suiteDesc", out var de) ? de.GetString() : "",
219+
TenantId = tid,
220+
CreatedAt = DateTime.Now.ToString("o")
221+
});
222+
created++;
223+
}
224+
await _db.SaveChangesAsync();
225+
}
226+
if (root.TryGetProperty("apis", out var apisEl) && apisEl.ValueKind == JsonValueKind.Array)
227+
{
228+
foreach (var a in apisEl.EnumerateArray())
229+
{
230+
var code = a.TryGetProperty("methodCode", out var ce) ? ce.GetString() ?? "" : "";
231+
if (code.Length == 0) continue;
232+
if (await _db.Apis.AnyAsync(x => x.MethodCode == code && x.Deleted == 0)) continue;
233+
_db.Apis.Add(new ApiEntity
234+
{
235+
SuiteCode = a.TryGetProperty("suiteCode", out var se) ? se.GetString() ?? "" : "",
236+
MethodCode = code,
237+
MethodName = a.TryGetProperty("methodName", out var ne) ? ne.GetString() ?? code : code,
238+
MethodDesc = a.TryGetProperty("methodDesc", out var de) ? de.GetString() : "",
239+
Url = a.TryGetProperty("url", out var ue) ? ue.GetString() ?? "" : "",
240+
RequestType = a.TryGetProperty("requestType", out var re) ? re.GetString() ?? "POST" : "POST",
241+
MethodType = "HTTP",
242+
Status = 1,
243+
TenantId = tid,
244+
CreatedAt = DateTime.Now.ToString("o")
245+
});
246+
created++;
247+
}
248+
await _db.SaveChangesAsync();
249+
}
250+
return new { message = $"接口已导入(新增 {created} 项,已存在自动跳过)" };
251+
}
252+
default:
253+
throw new Exception($"不支持的市场类型: {type}");
254+
}
255+
}
256+
}
257+
258+
public class MarketPublishRequest
259+
{
260+
public string ItemType { get; set; } = "";
261+
public string ItemName { get; set; } = "";
262+
public string Description { get; set; } = "";
263+
public string GroupName { get; set; } = "";
264+
public string ContentJson { get; set; } = "{}";
265+
}

‎Juggle.Api/Program.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ await ctx.Response.WriteAsync(
260260
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_redis_config(id INTEGER PRIMARY KEY AUTOINCREMENT, config_name TEXT, host TEXT, port TEXT, password TEXT, db TEXT, is_default INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
261261
// AI 技能表
262262
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_skill(id INTEGER PRIMARY KEY AUTOINCREMENT, skill_name TEXT, group_name TEXT, description TEXT, content TEXT, enabled INTEGER DEFAULT 1, market_skill_id INTEGER, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
263+
// 市场条目表
264+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_market_item(id INTEGER PRIMARY KEY AUTOINCREMENT, item_type TEXT, item_name TEXT, description TEXT, group_name TEXT, content_json TEXT, download_count INTEGER DEFAULT 0, enabled INTEGER DEFAULT 1, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
263265
// 知识库 4 表
264266
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_name TEXT, description TEXT, chunk_size INTEGER DEFAULT 500, chunk_overlap INTEGER DEFAULT 50, retrieve_type TEXT DEFAULT 'text', vector_model TEXT, enabled INTEGER DEFAULT 1, chunk_count INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
265267
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb_document(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_id INTEGER, doc_name TEXT, doc_type TEXT, content TEXT, status INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>
4+
/// 市场条目:接口/流程/模型助手/技能/报表的共享与导入中心。
5+
/// 发布后平台级可见(TenantId=null),任意租户可导入到本地。
6+
/// </summary>
7+
public class MarketItemEntity : BaseEntity
8+
{
9+
/// <summary>条目类型:api / flow / assistant / skill / report</summary>
10+
public string? ItemType { get; set; }
11+
12+
/// <summary>条目名称</summary>
13+
public string? ItemName { get; set; }
14+
15+
/// <summary>描述</summary>
16+
public string? Description { get; set; }
17+
18+
/// <summary>分组</summary>
19+
public string? GroupName { get; set; }
20+
21+
/// <summary>内容快照(JSON,按类型结构不同)</summary>
22+
public string? ContentJson { get; set; }
23+
24+
/// <summary>下载(导入)次数</summary>
25+
public int DownloadCount { get; set; }
26+
27+
/// <summary>是否启用:1=上架 0=下架</summary>
28+
public int Enabled { get; set; } = 1;
29+
}

‎Juggle.Infrastructure/Persistence/JuggleDbContext.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public JuggleDbContext(DbContextOptions<JuggleDbContext> options, ICurrentTenant
6767
public DbSet<KnowledgeMatchLogEntity> KnowledgeMatchLogs { get; set; } = null!;
6868
public DbSet<RedisConfigEntity> RedisConfigs { get; set; } = null!;
6969
public DbSet<SkillEntity> Skills { get; set; } = null!;
70+
public DbSet<MarketItemEntity> MarketItems { get; set; } = null!;
7071

7172
protected override void OnModelCreating(ModelBuilder modelBuilder)
7273
{
@@ -248,6 +249,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
248249
e.Property(p => p.Enabled).HasColumnName("enabled");
249250
e.Property(p => p.MarketSkillId).HasColumnName("market_skill_id");
250251
});
252+
modelBuilder.Entity<MarketItemEntity>().ToTable("t_market_item");
253+
modelBuilder.Entity<MarketItemEntity>(e => {
254+
e.Property(p => p.Id).HasColumnName("id");
255+
e.Property(p => p.Deleted).HasColumnName("deleted");
256+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
257+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
258+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
259+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
260+
e.Property(p => p.TenantId).HasColumnName("tenant_id");
261+
e.Property(p => p.ItemType).HasColumnName("item_type");
262+
e.Property(p => p.ItemName).HasColumnName("item_name");
263+
e.Property(p => p.Description).HasColumnName("description");
264+
e.Property(p => p.GroupName).HasColumnName("group_name");
265+
e.Property(p => p.ContentJson).HasColumnName("content_json");
266+
e.Property(p => p.DownloadCount).HasColumnName("download_count");
267+
e.Property(p => p.Enabled).HasColumnName("enabled");
268+
});
251269
modelBuilder.Entity<AlertRuleEntity>().ToTable("t_alert_rule");
252270
modelBuilder.Entity<AlertRecordEntity>().ToTable("t_alert_record");
253271
modelBuilder.Entity<DataViewEntity>().ToTable("t_data_view");

0 commit comments

Comments
 (0)