ABP VNext 与 Neo4j:构建基于图数据库的高效关系查询

news2025/6/7 22:51:22

ABP VNext 与 Neo4j:构建基于图数据库的高效关系查询 🚀

在社交网络、权限图谱、推荐系统等应用场景中,关系链深度和复杂度远超传统关系型数据库的表达能力。本文基于 ABP VNext 框架,集成 Neo4j 图数据库,构建一套高效、可复现、可维护的复杂关系查询系统,并实现推荐的性能与封装模式:GraphClientWrapper、NodeMapper、CypherBuilder。


预备知识 🛠

  • ABP CLI(abp)使用,以及 ABP VNext 项目结构。
  • Neo4jClient 库使用方式。
  • C# 反射与表达式树基础(若要深入了解 NodeMapper 与 CypherBuilder 的实现细节)。

📚 目录

  • ABP VNext 与 Neo4j:构建基于图数据库的高效关系查询 🚀
    • 预备知识 🛠
    • 一、项目环境配置
      • 1. 创建 ABP VNext 项目 🏗
      • 2. Docker 启动 Neo4j 🐳
      • 3. 配置文件(appsettings.json)示例 ⚙️
    • 二、配置文件与依赖注入
      • 1. 在 appsettings.json 中集中管理 Neo4j 连接 🔐
      • 2. Neo4jClient 初始化类(Neo4jInitializer) 🚀
      • 3. 在 Program.cs 中注册 Neo4jClient 🧩
      • 4. DI 初始化流程示意 🌀
    • 三、基础模型与 Repository 实现
      • 1. UserNode 节点模型 📦
      • 2. IUserGraphRepository 接口定义 🔍
      • 3. UserGraphRepository 实现 🎯
    • 四、索引与约束
      • 1. 执行时机与注意事项 ⏱
    • 五、性能与维护最佳实践
      • 1. 参数化查询与分页示例 📊
      • 2. 日志追踪与慢查询监控 📈
      • 3. 事务管理建议 🛡
    • 六、推荐封装模式
      • 1. GraphClientWrapper:连接复用与延迟事务 🧮
      • 2. NodeMapper:增强版类型映射(支持可空类型) ✨
      • 3. CypherBuilder:链式构建与参数化支持 🚧
    • 七、社交系统示例
      • 1. Neo4j CLI/Cypher 初始化脚本 📝
      • 2. 在代码中使用 Repository 查询示例 🔍


一、项目环境配置

1. 创建 ABP VNext 项目 🏗

# 使用 ABP CLI 创建一个 Web 应用模板
abp new AbpNeo4jDemo -t app

提示:执行前需安装 ABP CLI(dotnet tool install -g Volo.Abp.Cli),并确保版本与 ABP VNext 兼容,否则可能出现模板下载失败的问题。


2. Docker 启动 Neo4j 🐳

为了方便本地开发与测试,推荐使用 Docker 启动 Neo4j:

docker run \
  --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -v $PWD/neo4j/data:/data \
  -v $PWD/neo4j/logs:/logs \
  -e NEO4J_AUTH=neo4j/test123 \
  neo4j:5.11.0
  • 📌 neo4j:5.11.0:指定 Neo4j 版本,避免使用 latest 标签出现兼容性问题。
  • 📌 -v $PWD/neo4j/data:/data-v $PWD/neo4j/logs:/logs:将数据与日志目录映射到主机,实现数据持久化。
  • 🔑 NEO4J_AUTH=neo4j/test123:用户名 neo4j,密码 test123

注意:容器删除后,数据仍会保留在主机映射目录,避免丢失。


3. 配置文件(appsettings.json)示例 ⚙️

建议把 Neo4j 的连接信息放到 appsettings.json,避免硬编码:

{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=AbpNeo4jDemo;User Id=sa;Password=YourStrong!Passw0rd;"
  },
  "Neo4j": {
    "Uri": "bolt://localhost:7687",
    "User": "neo4j",
    "Password": "test123"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Neo4jClient": "Warning",
      "Microsoft": "Warning"
    }
  }
}
  • 🔒 在生产环境可使用环境变量(例如 NEO4J__PASSWORD)来覆盖敏感信息,避免明文存储。

二、配置文件与依赖注入

1. 在 appsettings.json 中集中管理 Neo4j 连接 🔐

在项目中创建一个配置类 Neo4jOptions,用于映射配置段:

public class Neo4jOptions
{
    public const string SectionName = "Neo4j";
    public string Uri { get; set; }
    public string User { get; set; }
    public string Password { get; set; }
}

然后在 Program.cs 中绑定该配置段:

var builder = WebApplication.CreateBuilder(args);

// 绑定 Neo4j 配置到 Neo4jOptions
builder.Services.Configure<Neo4jOptions>(
    builder.Configuration.GetSection(Neo4jOptions.SectionName)
);

2. Neo4jClient 初始化类(Neo4jInitializer) 🚀

为了避免在 DI 注册时直接 async/await,可封装一个初始化类,在参数配置准备好后再建立连接:

using Microsoft.Extensions.Options;
using Neo4jClient;

public static class Neo4jInitializer
{
    /// <summary>
    /// 同步初始化 BoltGraphClient 并返回已连接的实例。
    /// </summary>
    public static IGraphClient Init(IOptions<Neo4jOptions> options)
    {
        var cfg = options.Value;
        var client = new BoltGraphClient(cfg.Uri, cfg.User, cfg.Password);

        // 同步等待连接完成。若 Neo4j 未就绪会阻塞启动,建议确保先启动数据库。
        client.ConnectAsync().GetAwaiter().GetResult();
        return client;
    }
}

优化建议

  • 若担心启动阻塞,可改为“延迟连接”模式,或者在 IHostedService 中异步检测并重连,避免主线程长时间等待。

3. 在 Program.cs 中注册 Neo4jClient 🧩

Program.cs 中,将已连接的 IGraphClient 注册为 Singleton,并确保 ABP 框架初始化:

using Neo4jClient;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

// 1. 绑定 Neo4j 配置(前面已配置)
builder.Services.Configure<Neo4jOptions>(
    builder.Configuration.GetSection(Neo4jOptions.SectionName)
);

// 2. 注册 Neo4jClient:先解析配置,再调用初始化方法
builder.Services.AddSingleton<IGraphClient>(sp =>
{
    var options = sp.GetRequiredService<IOptions<Neo4jOptions>>();
    return Neo4jInitializer.Init(options);
});

// 3. 注册 Repository 层
builder.Services.AddScoped<IUserGraphRepository, UserGraphRepository>();

// 4. 注册 GraphClientWrapper(延迟事务管理)
builder.Services.AddScoped<IGraphClientWrapper, GraphClientWrapper>();

// 5. 注册 ABP 应用模块
builder.Services.AddApplication<AbpNeo4jDemoModule>();

var app = builder.Build();

// 🚀 必须在 UseRouting 之前调用
app.InitializeApplication();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints => 
{
    endpoints.MapControllers();
});

app.Run();

4. DI 初始化流程示意 🌀

应用启动 Program.cs
绑定配置 Configure
解析 IOptions
调用 Neo4jInitializer.Init(options)
创建 BoltGraphClient 实例
执行 ConnectAsync() 同步等待
连接成功,返回 IGraphClient
注册到 DI 容器 AddSingleton
后续 Repository/Wrapper 注入并使用 IGraphClient
ABP InitializeApplication 完成模块初始化

三、基础模型与 Repository 实现

1. UserNode 节点模型 📦

using Newtonsoft.Json;

public class UserNode
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("email")]
    public string Email { get; set; }

    // 若后续需要存储创建时间、ID 等属性,可再扩展:
    // [JsonProperty("createdAt")]
    // public DateTime CreatedAt { get; set; }
}
  • ✨ 通过 [JsonProperty] 特性,确保 C# 属性与 Neo4j 节点属性精确映射。

2. IUserGraphRepository 接口定义 🔍

public interface IUserGraphRepository
{
    /// <summary>
    /// 获取指定用户名的一度好友列表(最多 limit 条)。
    /// </summary>
    Task<(List<UserNode> Friends, long TotalCount)> GetFriendsPagedAsync(
        string name, int skip, int limit);

    /// <summary>
    /// 创建用户及好友关系(事务演示)。
    /// </summary>
    Task CreateUserWithFriendAsync(
        string userName, string userEmail, 
        string friendName, string friendEmail);
}
  • 🔄 将分页与事务示例合并到接口定义中,更贴近真实业务需求。

3. UserGraphRepository 实现 🎯

using Neo4jClient;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class UserGraphRepository : IUserGraphRepository
{
    private readonly ITransactionalGraphClient _txnClient;
    private readonly IGraphClient _client;
    private readonly ILogger<UserGraphRepository> _logger;

    public UserGraphRepository(IGraphClient client, ILogger<UserGraphRepository> logger)
    {
        _client = client;
        _logger = logger;

        // 尝试转换为 ITransactionalGraphClient
        _txnClient = client as ITransactionalGraphClient;
    }

    /// <summary>
    /// 分页获取用户一度好友。
    /// </summary>
    public async Task<(List<UserNode> Friends, long TotalCount)> GetFriendsPagedAsync(
        string name, int skip, int limit)
    {
        if (string.IsNullOrWhiteSpace(name) || skip < 0 || limit <= 0)
        {
            _logger.LogWarning("GetFriendsPagedAsync 参数无效:name={Name}, skip={Skip}, limit={Limit}", name, skip, limit);
            return (new List<UserNode>(), 0);
        }

        try
        {
            // 1. 计算总数
            var countResult = await _client.Cypher
                .Match("(u:User)-[:FRIENDS_WITH]->(f:User)")
                .Where((UserNode u) => u.Name == name)
                .Return(f => f.Count())
                .ResultsAsync;
            var totalCount = countResult.SingleOrDefault();

            // 2. 分页查询,并按好友姓名排序
            var friends = await _client.Cypher
                .Match("(u:User)-[:FRIENDS_WITH]->(f:User)")
                .Where((UserNode u) => u.Name == name)
                .OrderBy("f.name")
                .Skip(skip)
                .Limit(limit)
                .Return(f => f.As<UserNode>())
                .ResultsAsync;

            return (friends.ToList(), totalCount);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "分页查询用户 {Name} 好友失败", name);
            return (new List<UserNode>(), 0);
        }
    }

    /// <summary>
    /// 在一个事务中创建用户与好友关系示例。
    /// </summary>
    public async Task CreateUserWithFriendAsync(
        string userName, string userEmail, 
        string friendName, string friendEmail)
    {
        if (_txnClient == null)
        {
            throw new InvalidOperationException("IGraphClient 未实现 ITransactionalGraphClient,无法使用事务");
        }

        // 唯一标识避免重复创建
        await _txnClient.Cypher
            .Merge("(u:User {name: $userName})")
            .OnCreate()
            .Set("u.email = $userEmail")
            .WithParams(new { userName, userEmail })
            .ExecuteWithoutResultsAsync();

        await _txnClient.Cypher
            .Merge("(f:User {name: $friendName})")
            .OnCreate()
            .Set("f.email = $friendEmail")
            .WithParams(new { friendName, friendEmail })
            .ExecuteWithoutResultsAsync();

        using var tx = await _txnClient.BeginTransactionAsync();
        try
        {
            await _txnClient.Cypher
                .Match("(u:User {name: $userName})", "(f:User {name: $friendName})")
                .WithParams(new { userName, friendName })
                .Merge("(u)-[:FRIENDS_WITH {since: date()}]->(f)")
                .ExecuteWithoutResultsAsync();

            await tx.CommitAsync();
        }
        catch (Exception ex)
        {
            await tx.RollbackAsync();
            _logger.LogError(ex, "创建用户 {UserName} 与好友 {FriendName} 关系失败", userName, friendName);
            throw;
        }
    }
}
  • 🔑 核心:
    • 使用 MERGE 代替 CREATE,实现幂等创建节点与关系。
    • 在分页查询中加入 OrderBy("f.name"),保证分页稳定性。
    • 全程使用异步方法,避免同步等待(sync-over-async)带来的线程阻塞问题。

四、索引与约束

为了保证查询性能并避免重复节点,建议在 Neo4j 中提前创建索引与约束。使用 Neo4j 5.x+ 语法,保证幂等性:

// 创建 User 节点的 name 属性索引(仅在不存在时创建)
CREATE INDEX IF NOT EXISTS user_name_index FOR (u:User) ON (u.name);

// 创建 User 节点的 email 属性唯一约束(仅在不存在时创建)
CREATE CONSTRAINT IF NOT EXISTS user_email_unique FOR (u:User) REQUIRE u.email IS UNIQUE;

1. 执行时机与注意事项 ⏱

  • 执行时机

    • 可在项目启动时通过 IHostedService 检测并执行,也可在 CI/CD 环境中统一执行初始化脚本。
    • 建议将以上 DDL 脚本保存到独立文件(如 init.cypher),并与应用部署流程绑定,保证环境一致。
  • 查询索引状态

    CALL db.indexes();
    
    • 可查看索引/约束的状态(ONLINE、POPULATING、FAILED 等),确保已就绪后再执行业务查询。

五、性能与维护最佳实践

1. 参数化查询与分页示例 📊

在实际业务中,需要根据不同场景做分页或多条件筛选。示例代码如上所示,已在 GetFriendsPagedAsync 中展示。

  • 关键点
    1. Where((UserNode u) => u.Name == name) 自动生成参数化 Cypher,避免注入风险。
    2. OrderBy("f.name") 保证分页结果稳定,有利于前端分页。
    3. Skip(skip).Limit(limit) 实现分页。

2. 日志追踪与慢查询监控 📈

对关键查询或写操作,建议记录耗时与结果量,便于定位性能瓶颈。示例:

public async Task<List<UserNode>> GetFriendsWithLoggingAsync(string name)
{
    var sw = System.Diagnostics.Stopwatch.StartNew();
    try
    {
        var result = await _client.Cypher
            .Match("(u:User)-[:FRIENDS_WITH]->(f:User)")
            .Where((UserNode u) => u.Name == name)
            .Return(f => f.As<UserNode>())
            .ResultsAsync;

        sw.Stop();
        _logger.LogInformation("查询用户 {Name} 好友 耗时 {Elapsed} ms,共 {Count} 条", 
            name, sw.ElapsedMilliseconds, result.Count());
        return result.ToList();
    }
    catch (Exception ex)
    {
        sw.Stop();
        _logger.LogError(ex, "查询用户 {Name} 好友 异常 耗时 {Elapsed} ms", 
            name, sw.ElapsedMilliseconds);
        return new List<UserNode>();
    }
}
  • ⚠️ 可将“慢查询阈值”配置化(如 200 ms),若超过阈值触发告警或埋点。

3. 事务管理建议 🛡

在多步写操作时,务必使用显式事务,保证一致性与原子性。前文 CreateUserWithFriendAsync 方法中已演示。

  • 要点
    1. 使用 Merge() 而不是直接 Create(),保证幂等性。
    2. 在事务内顺序执行节点与关系创建,最后 CommitAsync()。若任何步骤失败,RollbackAsync()
    3. 避免在事务中执行长时间查询或阻塞操作,以减少锁冲突。

六、推荐封装模式

1. GraphClientWrapper:连接复用与延迟事务 🧮

using Neo4jClient;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

public interface IGraphClientWrapper : IDisposable
{
    IGraphClient Client { get; }
    ITransactionalGraphClient TransactionalClient { get; }
    Task BeginTransactionAsync();
    Task CommitAsync();
    Task RollbackAsync();
}

public class GraphClientWrapper : IGraphClientWrapper
{
    private readonly IGraphClient _client;
    private readonly ILogger<GraphClientWrapper> _logger;
    private ITransaction _transaction;

    public IGraphClient Client => _client;
    public ITransactionalGraphClient TransactionalClient => _client as ITransactionalGraphClient;

    public GraphClientWrapper(IGraphClient client, ILogger<GraphClientWrapper> logger)
    {
        _client = client;
        _logger = logger;
    }

    /// <summary>
    /// 延迟创建事务,直到真正需要写操作时调用。
    /// </summary>
    public async Task BeginTransactionAsync()
    {
        if (_transaction != null) return;

        if (_client is ITransactionalGraphClient txnClient)
        {
            _transaction = await txnClient.BeginTransactionAsync();
        }
        else
        {
            throw new InvalidOperationException("IGraphClient 未实现 ITransactionalGraphClient,无法开启事务");
        }
    }

    public async Task CommitAsync()
    {
        if (_transaction == null) return;
        await _transaction.CommitAsync();
        await _transaction.DisposeAsync();
        _transaction = null;
    }

    public async Task RollbackAsync()
    {
        if (_transaction == null) return;
        await _transaction.RollbackAsync();
        await _transaction.DisposeAsync();
        _transaction = null;
    }

    public void Dispose()
    {
        if (_transaction != null)
        {
            try
            {
                _transaction.RollbackAsync().GetAwaiter().GetResult();
                _transaction.DisposeAsync().GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "GraphClientWrapper Dispose 时回滚事务失败");
            }
        }
    }
}
  • 🔑 设计要点:
    1. 构造函数不直接开启事务,只有在调用 BeginTransactionAsync() 时才创建。
    2. 提供 CommitAsync() / RollbackAsync() 方法,业务调用方根据需要自行控制事务边界。
    3. Dispose() 中检查是否有未提交事务,若有则回滚,保证不会因忘记提交导致脏数据。

2. NodeMapper:增强版类型映射(支持可空类型) ✨

using System;
using System.Collections.Generic;
using System.Reflection;

public static class NodeMapper<T> where T : class, new()
{
    /// <summary>
    /// 将 Neo4jClient 返回的属性字典映射到模型对象 T。
    /// 支持大小写忽略与 Nullable 类型转换。
    /// </summary>
    public static T Map(IDictionary<string, object> props)
    {
        var obj = new T();
        var type = typeof(T);
        var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;

        foreach (var kv in props)
        {
            try
            {
                var propInfo = type.GetProperty(kv.Key, flags);
                if (propInfo == null) continue;

                var targetType = propInfo.PropertyType;
                var value = kv.Value;
                if (value == null)
                {
                    propInfo.SetValue(obj, null);
                    continue;
                }

                // 如果是 Nullable<T>,获取底层类型
                var underlying = Nullable.GetUnderlyingType(targetType);
                if (underlying != null)
                {
                    var nonNullVal = Convert.ChangeType(value, underlying);
                    propInfo.SetValue(obj, nonNullVal);
                }
                else
                {
                    var converted = Convert.ChangeType(value, targetType);
                    propInfo.SetValue(obj, converted);
                }
            }
            catch
            {
                // 忽略映射失败的属性或记录日志
                continue;
            }
        }
        return obj;
    }
}
  • 🌟 优化点:
    • 忽略属性名称大小写差异。
    • 支持将数据库返回的数值转换为 Nullable 类型(如 int?DateTime?)。
    • 映射失败时不抛异常,以保证健壮性。

3. CypherBuilder:链式构建与参数化支持 🚧

using Neo4jClient;
using Neo4jClient.Cypher;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class CypherBuilder
{
    private readonly IGraphClient _client;
    private ICypherFluentQuery _query;

    public CypherBuilder(IGraphClient client)
    {
        _client = client;
        Reset();
    }

    /// <summary>
    /// 重置内部查询,开始一个全新的 Cypher 构建
    /// </summary>
    private void Reset()
    {
        _query = _client.Cypher;
    }

    /// <summary>
    /// 添加 MATCH 子句,开始新的查询分支
    /// </summary>
    public CypherBuilder StartMatch(string pattern)
    {
        Reset();
        _query = _query.Match(pattern);
        return this;
    }

    /// <summary>
    /// 在现有查询上追加 MATCH 子句(不重置)
    /// </summary>
    public CypherBuilder AppendMatch(string pattern)
    {
        _query = _query.Match(pattern);
        return this;
    }

    /// <summary>
    /// 添加 WHERE 子句(支持参数化)
    /// 示例:Where("u.name = $name", new { name = "Alice" })
    /// </summary>
    public CypherBuilder Where(string condition, object parameters = null)
    {
        if (parameters != null)
        {
            _query = _query.Where(condition, parameters);
        }
        else
        {
            _query = _query.Where(condition);
        }
        return this;
    }

    /// <summary>
    /// 添加 ORDER BY 子句
    /// </summary>
    public CypherBuilder OrderBy(string orderClause)
    {
        _query = _query.OrderBy(orderClause);
        return this;
    }

    /// <summary>
    /// 添加 SKIP 子句
    /// </summary>
    public CypherBuilder Skip(int skip)
    {
        _query = _query.Skip(skip);
        return this;
    }

    /// <summary>
    /// 添加 LIMIT 子句
    /// </summary>
    public CypherBuilder Limit(int limit)
    {
        _query = _query.Limit(limit);
        return this;
    }

    /// <summary>
    /// 执行 RETURN 子句,并将结果映射为 T。调用后自动 Reset()。
    /// 示例:ReturnAsync<UserNode>("f")
    /// </summary>
    public async Task<IEnumerable<T>> ReturnAsync<T>(string alias)
    {
        var result = await _query.Return(alias).ResultsAsync;
        Reset(); // 每次调用后重置,避免状态残留
        return result.Cast<T>();
    }
}
  • ✅ 此版本支持:
    1. StartMatch + AppendMatch 分别用于“从头开始”或“在现有查询上追加”。
    2. 完整的链式方法:WhereOrderBySkipLimit,方便构建复杂查询。
    3. ReturnAsync<T> 调用后自动 Reset(),确保下一次查询从头开始。

七、社交系统示例

下面演示一个“社交系统”示例,包含 Neo4j 初始化脚本与代码调用。

1. Neo4j CLI/Cypher 初始化脚本 📝

// 1. 创建 User 节点及示例数据(使用 MERGE 保证幂等性)
MERGE (a:User {name: 'Alice'})
  ON CREATE SET a.email = 'alice@test.com';
MERGE (b:User {name: 'Bob'})
  ON CREATE SET b.email = 'bob@test.com';
MERGE (c:User {name: 'Carol'})
  ON CREATE SET c.email = 'carol@test.com';

// 2. 建立好友关系(使用 MERGE 保证幂等性)
MERGE (a:User {name: 'Alice'}) 
MERGE (b:User {name: 'Bob'})
MERGE (a)-[:FRIENDS_WITH {since: date('2021-01-01')}]->(b);

MERGE (a:User {name: 'Alice'})
MERGE (c:User {name: 'Carol'})
MERGE (a)-[:FRIENDS_WITH {since: date('2022-05-10')}]->(c);

// 3. 创建索引与约束(使用 IF NOT EXISTS 保证幂等性)
CREATE INDEX IF NOT EXISTS user_name_index FOR (u:User) ON (u.name);
CREATE CONSTRAINT IF NOT EXISTS user_email_unique FOR (u:User) REQUIRE u.email IS UNIQUE;
  • 📌 将以上脚本保存至独立文件(如 init.cypher),并在 Neo4j Browser 或 CI/CD 环境执行,确保环境一致性。

2. 在代码中使用 Repository 查询示例 🔍

在某个服务或控制器中注入 IUserGraphRepository,调用分页查询方法获取好友列表:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class FriendsController : ControllerBase
{
    private readonly IUserGraphRepository _repo;
    private readonly ILogger<FriendsController> _logger;

    public FriendsController(IUserGraphRepository repo, ILogger<FriendsController> logger)
    {
        _repo = repo;
        _logger = logger;
    }

    [HttpGet("{userName}")]
    public async Task<IActionResult> GetFriends(string userName, int skip = 0, int limit = 10)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            return BadRequest("用户名不能为空");
        }

        var (friends, totalCount) = await _repo.GetFriendsPagedAsync(userName, skip, limit);
        if (!friends.Any())
        {
            // 返回空数组而非 404,保持接口一致性
            return Ok(new { Total = totalCount, Data = Array.Empty<UserNode>() });
        }

        return Ok(new { Total = totalCount, Data = friends });
    }

    [HttpPost("create")]
    public async Task<IActionResult> CreateUserWithFriend(
        [FromQuery] string userName, 
        [FromQuery] string userEmail, 
        [FromQuery] string friendName,
        [FromQuery] string friendEmail)
    {
        if (string.IsNullOrWhiteSpace(userName) 
            || string.IsNullOrWhiteSpace(userEmail) 
            || string.IsNullOrWhiteSpace(friendName) 
            || string.IsNullOrWhiteSpace(friendEmail))
        {
            return BadRequest("参数缺失");
        }

        await _repo.CreateUserWithFriendAsync(userName, userEmail, friendName, friendEmail);
        return Ok("用户与好友关系创建成功");
    }
}
  • 🔄 在 /api/friends/{userName}?skip=0&limit=10 调用分页查询,返回示例:

    {
      "total": 2,
      "data": [
        { "name": "Bob", "email": "bob@test.com" },
        { "name": "Carol", "email": "carol@test.com" }
      ]
    }
    
  • 💡 创建用户与好友关系示例:

    POST http://localhost:5000/api/friends/create?userName=Dave&userEmail=dave@test.com&friendName=Alice&friendEmail=alice@test.com
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2403432.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5.Nginx+Tomcat负载均衡群集

Tomcat服务器应用场景&#xff1a;tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;属于轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;是开发和调试JSP程序的首选。一般来说&#xff0c;Tomcat虽然和Apache或…

React项目的状态管理:Redux Toolkit

目录 1、搭建环境 2、Redux Toolkit 包含了什么 3、使用示例 &#xff08;1&#xff09;创建user切片 &#xff08;2&#xff09;合并切片得到store &#xff08;3&#xff09;配置store和使用store 使用js来编写代码&#xff0c;方便理解一些 1、搭建环境 首先&#xf…

跨界破局者鲁力:用思辨与创新重塑汽车流通行业标杆

来源&#xff1a;投资家 在汽车流通行业深度变革的浪潮中&#xff0c;东莞东风南方汽车销售服务有限公司塘厦分公司总经理鲁力历经近二十年行业深耕&#xff0c;构建了一条从汽车销售顾问到区域运营掌舵者的进阶范本。作为东风日产体系内兼具理论建构与实战穿透力的标杆管理者…

OS11.【Linux】vim文本编辑器

目录 1.四种模式 命令模式 几个命令 插入模式 底行模式 一图展示三种模式之间的关系 2.分屏(多文件操作) 3.配置vim的原理 4.脚本一键配置vim CentOS 7 x86_64 其他发行版 5.NeoVim(推荐) vim文本编辑器是一个多模式的编辑器,因此先介绍它的四种模式 附vim的官网:…

基于SFC的windows系统损坏修复程序

前言 在平时使用Windows操作系统时会遇到很多因为系统文件损坏而出现的错误 例如:系统应用无法打开 系统窗口(例如开始菜单)无法使用 电脑蓝屏或者卡死 是如果想要修复很多人只能想到重装系统。但其实Windows有一个内置的系统文件检查器可以修复此类错误。 原理 SFC命令…

WAF绕过,网络层面后门分析,Windows/linux/数据库提权实验

一、WAF绕过文件上传漏洞 win7&#xff1a;10.0.0.168 思路&#xff1a;要想要绕过WAF&#xff0c;第一步是要根据上传的内容找出来被拦截的原因。对于文件上传有三个可以考虑的点&#xff1a;文件后缀名&#xff0c;文件内容&#xff0c;文件类型。 第二步是根据找出来的拦截原…

Vue 3 弹出式计算器组件(源码 + 教程)

&#x1f9ee; Vue 3 弹出式计算器组件&#xff08;源码 教程&#xff09; &#x1f4cc; 建议收藏 点赞 关注&#xff0c;本组件支持加减乘除、双向绑定、计算过程展示&#xff0c;适用于表单辅助输入场景。 &#x1f527; 一、完整源码&#xff08;复制即用&#xff09; …

监测预警系统重塑隧道安全新范式

在崇山峻岭的脉络间延伸的隧道&#xff0c;曾是交通安全的薄弱环节。智慧隧道监测预警系统的诞生&#xff0c;正在彻底改变这种被动防御格局&#xff0c;通过数字神经网络的构建&#xff0c;为地下交通动脉注入智能守护基因。 一、安全防控体系的质变升级 1.风险感知维度革命…

技巧小结:外部总线访问FPGA寄存器

概述 需求&#xff1a;stm32的fsmc总线挂载fpga&#xff0c;stm32需要访问fpga内部寄存器 1、分散加载文件将变量存放到指定地址即FPGA寄存器地址 sct文件指定变量存储地址&#xff0c;从而可以直接访问外设&#xff0c;&#xff08;28335也可以&#xff0c;不过用的是cmd文件…

jenkins集成gitlab发布到远程服务器

jenkins集成gitlab发布到远程服务器 前面我们讲了通过创建maven项目部署在jenkins本地服务器&#xff0c;这次实验我们将部署在远程服务器&#xff0c;再以nginx作为前端项目做一个小小的举例 1、部署nginx服务 [rootweb ~]# docker pull nginx [rootweb ~]# docker images …

当主观认知遇上机器逻辑:减少大模型工程化中的“主观性”模糊

一、人类与机器的认知差异 当自动驾驶汽车遇到紧急情况需要做出选择时&#xff0c;人类的决策往往充满矛盾&#xff1a;有人会优先保护儿童和老人&#xff0c;有人坚持"不主动变道"的操作原则。这种差异背后&#xff0c;体现着人类特有的情感判断与价值选择。而机器的…

会计 - 金融负债和权益工具

一、金融负债和权益工具区分的基本原则 (1)是否存在无条件地避免交付现金或其他金融资产的合同义务 如果企业不能无条件地避免以交付现金或其他金融资产来履行一项合同义务,则该合同义务符合金融负债的义务。 常见的该类合同义务情形包括:- 不能无条件避免的赎回; -强制…

Dify工具插件开发和智能体开发全流程

想象一下&#xff0c;你正在开发一个 AI 聊天机器人&#xff0c;想让它能实时搜索 Google、生成图像&#xff0c;甚至自动规划任务&#xff0c;但手动集成这些功能耗时又复杂。Dify 来了&#xff01;这个开源的 AI 应用平台让你轻松开发工具插件和智能体策略插件&#xff0c;快…

AI书签管理工具开发全记录(十三):TUI基本框架搭建

文章目录 AI书签管理工具开发全记录&#xff08;十三&#xff09;&#xff1a;TUI基本框架搭建前言 &#x1f4dd;1.TUI介绍 &#x1f50d;2. 框架选择 ⚙️3. 功能梳理 &#x1f3af;4. 基础框架搭建⚙️4.1 安装4.2 参数设计4.3 绘制ui4.3.1 设计结构体4.3.2 创建头部4.3.3 创…

初识结构体,整型提升及操作符的属性

目录 一、结构体成员访问操作符1.1 结构体二、操作符的属性&#xff1a;优先级、结合性2.1 优先级2.2 结合性C 运算符优先级 三、表达式求值3.1 整型提升3.2 算数转化 总结 一、结构体成员访问操作符 1.1 结构体 C语言已经提供了内置类型&#xff0c;如&#xff1a;char,shor…

检测到 #include 错误。请更新 includePath。已为此翻译单元(D:\软件\vscode\test.c)禁用波形曲线

原文链接&#xff1a;【VScodeMinGw】安装配置教程 下载mingw64 打开可以看到bin文件夹下是多个.exe文件&#xff0c;gcc.exe地址在环境配置中要用到 原文链接&#xff1a;VSCode中出现“#include错误&#xff0c;请更新includePath“问题&#xff0c;解决方法 重新VScode后…

2025年,百度智能云打响AI落地升维战

如果说从AI到Agent是对于产品落地形态的共识&#xff0c;那么如今百度智能云打响的恰是一个基于Agent进行TO B行业表达的AI生产力升维战。 在这个新的工程体系能力里&#xff0c;除了之前百度Create大会上提出的面向Agent的RAG能力等通用能力模块&#xff0c;对更为专业、个性…

Seed1.5-VL登顶,国产闭源模型弯道超车丨多模态模型5月最新榜单揭晓

随着图像、文本、语音、视频等多模态信息融合能力的持续增强&#xff0c;多模态大模型在感知理解、逻辑推理和内容生成等任务中的综合表现不断提升&#xff0c;正在展现出愈发接近人类的智能水平。多模态能力也正在从底层的感知理解&#xff0c;迈向具备认知、推理、决策能力的…

第3章——SSM整合

一、整合持久层框架MyBatis 1.准备数据库表及数据 创建数据库&#xff1a;springboot 使用IDEA工具自带的mysql插件来完成表的创建和数据的准备&#xff1a; 创建表 表创建成功后&#xff0c;为表准备数据&#xff0c;如下&#xff1a; 2.创建SpringBoot项目 使用脚手架创建…

VTK 显示文字、图片及2D/3D图

1. 基本环境设置 首先确保你已经安装了VTK库&#xff0c;并配置好了C开发环境。 #include <vtkSmartPointer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkRenderer.h> 2. 显示文字 2D文字 #include &l…