【问题标题】:Mongoose Populate after AggregateMongoose 在聚合后填充
【发布时间】:2019-04-23 07:07:40
【问题描述】:

在运行聚合管道和填充后,我试图获取特定的数据模型,但我还差一点。

最终想要的结果如下:

[
  {
    _accountId: "5beee0966d17bc42501f1234",
    name: "Company Name 1",
    contactEmail: "email1@email.com",
    contactName: "contact Name 1"
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  },
  {
    _accountId: "5beee0966d17bc42501f1235",
    name: "Company Name 2",
    contactEmail: "email2@email.com",
    contactName: "contact Name 2"
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  }
]

我正在从以下两个模型中收集这些数据:

保修

{
  _id: "5beee0966d17bc42501f5086",
  jobsiteAddressStreet: String,
  jobsiteAddressCity: String,
  jobsiteAddressState" String,
  jobsiteAddressZip: Number,
  warrantyFee: Number,
  _accountId: {
    type: Schema.Types.ObjectId,
    ref: "accounts"
  },
  payStatus: String
}

帐户

{
  _id: "5beee0966d17bc42501f1235",
  name: String,
  contactName: String,
  contactEmail: String
}

我当前的查询如下:

Warranty.aggregate([
    {
      $match: {
        payStatus: "Invoiced Next Billing Cycle"
      }
    },
    {
      $group: {
        _id: "$_accountId",
        total: {
          $sum: "$warrantyFee"
        },
        lineItems: {
          $push: {
            _id: "$_id",
            jobsiteAddress: {
              $concat: [
                "$jobsiteAddressStreet",
                " ",
                "$jobsiteAddressCity",
                ", ",
                "$jobsiteAddressState",
                " ",
                "$jobsiteAddressZip"
              ]
            },
            warrantyFee: "$warrantyFee"
          }
        }
      }
    },
    {
      $project: {
        reason: "Warranties",
        total: "$total",
        lineItems: "$lineItems"
      }
    }
  ])
    .then(warranties => {
      console.log(warranties);
      Account.populate(warranties, {
        path: "_id",
        select: "contactName contactEmail name"
      })
        .then(warranties => {
          res.send(warranties);
        })
        .catch(err => {
          res.status(422).send(err);
          throw err;
        });
    })
    .catch(err => {
      res.status(422).send(err);
      throw err;
    });

结果如下:

[
  {
    _id: {
      _id: "5bc39dfa331c0e2cb897b61e",
      name: "Company Name 1",
      contactEmail: "email1@email.com",
      contactName: "Contact Name 1"
    },
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  },
  {
    _id: {
      _id: "5bc39dfa331c0e2cb897b61e",
      name: "Company Name 2",
      contactEmail: "email2@email.com",
      contactName: "Contact Name 2"
    },
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  }
]

如您所见,这与一些小问题非常接近。

  1. 它显示 _id 而不是 _accountId。我默认这样做是因为每当我尝试在 $group 中返回 _accountId 时,它会将其标记为非累加器字段,而当我在 $project 中执行此操作时,它就不会出现。数据集必须按保修模型中的 _accountId 分组。
  2. 如果可能的话,我宁愿将附加的(contactName、contactEmail、name)字段添加到顶级对象,而不是创建子文档。这可能很简单或不可能,因为我对填充不是非常熟悉,但找不到任何东西可以直接回答我的问题。

最后的目标是获取返回的对象并使用对象数组将文档批量创建到另一个集合。

-- 回答我的具体用例--

Warranty.aggregate([
    {
      $match: {
        payStatus: "Invoiced Next Billing Cycle"
      }
    },
    {
      $group: {
        _id: "$_accountId",
        total: {
          $sum: "$warrantyFee"
        },
        lineItems: {
          $push: {
            _id: "$_id",
            jobsiteAddress: {
              $concat: [
                "$jobsiteAddressStreet",
                " ",
                "$jobsiteAddressCity",
                ", ",
                "$jobsiteAddressState",
                " ",
                "$jobsiteAddressZip"
              ]
            },
            warrantyFee: "$warrantyFee"
          }
        }
      }
    },
    {
      $lookup: {
        from: Account.collection.name,
        localField: "_id",
        foreignField: "_id",
        as: "accounts"
      }
    },
    {
      $unwind: "$accounts"
    },
    {
      $project: {
        lineItems: "$lineItems",
        reason: "Warranties",
        total: "$total",
        type: "Invoice",
        date: new Date(),
        company: "$accounts.name",
        contactName: "$accounts.contactName",
        contactEmail: "$accounts.contactEmail"
      }
    },
    {
      $addFields: {
        _accountId: "$_id"
      }
    },
    {
      $project: {
        _id: 0
      }
    }
  ])

这给了我结果:

[
  {
    lineItems: [
      {
        _id: "5be203eb3afd8098d4988152",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ],
    reason: "Warranties",
    total: 384,
    type: "Invoice",
    date: "2018-11-21T14:08:15.052Z",
    company: "Company Name 1",
    contactName: "Contact Name 1",
    contactEmail: "email1@email.com",
    _accountId: "5be203eb3afd8098d4988152",
    referenceNumber: 1542809296615
  },
  {
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ],
    reason: "Warranties",
    total: 1152,
    type: "Invoice",
    date: "2018-11-21T14:08:15.052Z",
    company: "Company Name 2",
    contactName: "Contact Name 2",
    contactEmail: "email2@email.com",
    _accountId: "5bc39dfa331c0e2cb897b61e",
    referenceNumber: 1542809295680
  }
]

【问题讨论】:

    标签: mongodb mongoose mongodb-query aggregation-framework mongoose-populate


    【解决方案1】:

    因此,当您要求“填充”聚合结果时,您实际上在这里遗漏了一些概念。通常这不是你实际做的,而是解释要点:

    1. aggregate() 的输出与Model.find() 或类似操作不同,因为这里的目的是“重塑结果”。这基本上意味着您用作聚合源的模型不再被视为输出模型。如果您在输出时仍然保持完全相同的文档结构,情况也是如此,但在您的情况下,输出显然与源文档不同。

      无论如何,它不再是您从中采购的Warranty 模型的实例,而只是一个普通对象。我们可以在稍后讨论时解决这个问题。

    2. 这里的主要观点可能是populate() 有点“老帽子”。这实际上只是在实施的早期添加到 Mongoose 的一个便利功能。它真正所做的只是对单独集合中的相关数据执行“另一个查询”,然后将内存中的结果合并到原始集合输出。

      出于很多原因,在大多数情况下,这并不是真正有效的,甚至是不可取的。与流行的误解相反,这不是实际上是“加入”。

      对于真正的“加入”,您实际上使用了$lookup 聚合管道阶段,MongoDB 使用该阶段从另一个集合返回匹配项。与populate() 不同,这实际上是在对服务器的单个请求中通过单个响应完成的。这避免了网络开销,通常速度更快,并且作为“真正的加入”,您可以做populate() 无法做的事情。

    改用 $lookup

    这里缺少的非常快速版本是,在返回结果后,不要尝试在.then() 中添加populate(),而是将$lookup 添加到管道:

      { "$lookup": {
        "from": Account.collection.name,
        "localField": "_id",
        "foreignField": "_id",
        "as": "accounts"
      }},
      { "$unwind": "$accounts" },
      { "$project": {
        "_id": "$accounts",
        "total": 1,
        "lineItems": 1
      }}
    

    注意这里有一个限制,$lookup 的输出是总是一个数组。是否只有一个相关项目或许多要作为输出提取并不重要。管道阶段将从当前文档中查找"localField" 的值,并使用它来匹配指定的"foreignField" 中的值。在这种情况下,它是从聚合 $group 目标到外部集合的 _id_id

    如前所述,由于输出始终是一个数组,对于此实例,最有效的处理方法是直接在$lookup 之后添加一个$unwind 阶段。所有这一切都会为目标数组中返回的每个项目返回一个新文档,在这种情况下,您希望它是一个。在外部集合中没有匹配到_id的情况下,没有匹配的结果将被删除。

    作为一个小提示,这实际上是核心文档中$lookup + $unwind Coalescence 中描述的优化模式。这里发生了一件特殊的事情,$unwind 指令实际上以一种有效的方式合并到$lookup 操作中。您可以在那里阅读更多相关信息。

    使用填充

    从上面的内容你应该可以基本明白为什么populate()这里做错了。除了输出不再包含 Warranty 模型对象这一基本事实之外,该模型实际上只知道在输出中不存在的 _accountId 属性中描述的外来项目。

    现在您可以真正定义一个模型,该模型可用于将输出对象显式转换为定义的输出类型。一个简短的演示将涉及为您的应用程序添加代码,例如:

    // Special models
    
    const outputSchema = new Schema({
      _id: { type: Schema.Types.ObjectId, ref: "Account" },
      total: Number,
      lineItems: [{ address: String }]
    });
    
    const Output = mongoose.model('Output', outputSchema, 'dontuseme');
    

    然后可以使用这个新的 Output 模型将生成的纯 JavaScript 对象“转换”到 Mongoose 文档中,以便实际调用像 Model.populate() 这样的方法:

    // excerpt
    result2 = result2.map(r => new Output(r));   // Cast to Output Mongoose Documents
    
    // Call populate on the list of documents
    result2 = await Output.populate(result2, { path: '_id' })
    log(result2);
    

    由于Output 定义了一个架构,该架构知道其文档的_id 字段上的“引用”,因此Model.populate() 知道它需要做什么并返回项目。

    请注意,因为这实际上会生成另一个查询。即:

    Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
    Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
    

    第一行是聚合输出,然后您再次联系服务器以返回相关的Account 模型条目。

    总结

    所以这些是你的选择,但很明显,现代的方法是使用 $lookup 并获得一个 真正的“加入”,这不是 populate() 是确实在做。

    包括一个列表,作为对这些方法中的每一个在实践中如何实际工作的完整演示。此处获取了一些艺术许可,因此所展示的模型可能与您所拥有的完全相同相同,但那里足以以可重复的方式展示基本概念:

    const { Schema } = mongoose = require('mongoose');
    
    const uri = 'mongodb://localhost:27017/joindemo';
    const opts = { useNewUrlParser: true };
    
    // Sensible defaults
    mongoose.Promise = global.Promise;
    mongoose.set('debug', true);
    mongoose.set('useFindAndModify', false);
    mongoose.set('useCreateIndex', true);
    
    // Schema defs
    
    const warrantySchema = new Schema({
      address: {
        street: String,
        city: String,
        state: String,
        zip: Number
      },
      warrantyFee: Number,
      _accountId: { type: Schema.Types.ObjectId, ref: "Account" },
      payStatus: String
    });
    
    const accountSchema = new Schema({
      name: String,
      contactName: String,
      contactEmail: String
    });
    
    // Special models
    
    
    const outputSchema = new Schema({
      _id: { type: Schema.Types.ObjectId, ref: "Account" },
      total: Number,
      lineItems: [{ address: String }]
    });
    
    const Output = mongoose.model('Output', outputSchema, 'dontuseme');
    
    const Warranty = mongoose.model('Warranty', warrantySchema);
    const Account = mongoose.model('Account', accountSchema);
    
    
    // log helper
    const log = data => console.log(JSON.stringify(data, undefined, 2));
    
    // main
    (async function() {
    
      try {
    
        const conn = await mongoose.connect(uri, opts);
    
        // clean models
        await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
        )
    
        // set up data
        let [first, second, third] = await Account.insertMany(
          [
            ['First Account', 'First Person', 'first@example.com'],
            ['Second Account', 'Second Person', 'second@example.com'],
            ['Third Account', 'Third Person', 'third@example.com']
          ].map(([name, contactName, contactEmail]) =>
            ({ name, contactName, contactEmail })
          )
        );
    
        await Warranty.insertMany(
          [
            {
              address: {
                street: '1 Some street',
                city: 'Somewhere',
                state: 'TX',
                zip: 1234
              },
              warrantyFee: 100,
              _accountId: first,
              payStatus: 'Invoiced Next Billing Cycle'
            },
            {
              address: {
                street: '2 Other street',
                city: 'Elsewhere',
                state: 'CA',
                zip: 5678
              },
              warrantyFee: 100,
              _accountId: first,
              payStatus: 'Invoiced Next Billing Cycle'
            },
            {
              address: {
                street: '3 Other street',
                city: 'Elsewhere',
                state: 'NY',
                zip: 1928
              },
              warrantyFee: 100,
              _accountId: first,
              payStatus: 'Invoiced Already'
            },
            {
              address: {
                street: '21 Jump street',
                city: 'Anywhere',
                state: 'NY',
                zip: 5432
              },
              warrantyFee: 100,
              _accountId: second,
              payStatus: 'Invoiced Next Billing Cycle'
            }
          ]
        );
    
        // Aggregate $lookup
        let result1 = await Warranty.aggregate([
          { "$match": {
            "payStatus": "Invoiced Next Billing Cycle"
          }},
          { "$group": {
            "_id": "$_accountId",
            "total": { "$sum": "$warrantyFee" },
            "lineItems": {
              "$push": {
                "_id": "$_id",
                "address": {
                  "$trim": {
                    "input": {
                      "$reduce": {
                        "input": { "$objectToArray": "$address" },
                        "initialValue": "",
                        "in": {
                          "$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
                      }
                    },
                    "chars": " "
                  }
                }
              }
            }
          }},
          { "$lookup": {
            "from": Account.collection.name,
            "localField": "_id",
            "foreignField": "_id",
            "as": "accounts"
          }},
          { "$unwind": "$accounts" },
          { "$project": {
            "_id": "$accounts",
            "total": 1,
            "lineItems": 1
          }}
        ])
    
        log(result1);
    
        // Convert and populate
        let result2 = await Warranty.aggregate([
          { "$match": {
            "payStatus": "Invoiced Next Billing Cycle"
          }},
          { "$group": {
            "_id": "$_accountId",
            "total": { "$sum": "$warrantyFee" },
            "lineItems": {
              "$push": {
                "_id": "$_id",
                "address": {
                  "$trim": {
                    "input": {
                      "$reduce": {
                        "input": { "$objectToArray": "$address" },
                        "initialValue": "",
                        "in": {
                          "$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
                      }
                    },
                    "chars": " "
                  }
                }
              }
            }
          }}
        ]);
    
        result2 = result2.map(r => new Output(r));
    
        result2 = await Output.populate(result2, { path: '_id' })
        log(result2);
    
      } catch(e) {
        console.error(e)
      } finally {
        process.exit()
      }
    
    })()
    

    以及完整的输出:

    Mongoose: dontuseme.deleteMany({}, {})
    Mongoose: warranties.deleteMany({}, {})
    Mongoose: accounts.deleteMany({}, {})
    Mongoose: accounts.insertMany([ { _id: 5bf4b591a06509544b8cf75b, name: 'First Account', contactName: 'First Person', contactEmail: 'first@example.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75c, name: 'Second Account', contactName: 'Second Person', contactEmail: 'second@example.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75d, name: 'Third Account', contactName: 'Third Person', contactEmail: 'third@example.com', __v: 0 } ], {})
    Mongoose: warranties.insertMany([ { _id: 5bf4b591a06509544b8cf75e, address: { street: '1 Some street', city: 'Somewhere', state: 'TX', zip: 1234 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf75f, address: { street: '2 Other street', city: 'Elsewhere', state: 'CA', zip: 5678 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf760, address: { street: '3 Other street', city: 'Elsewhere', state: 'NY', zip: 1928 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Already', __v: 0 }, { _id: 5bf4b591a06509544b8cf761, address: { street: '21 Jump street', city: 'Anywhere', state: 'NY', zip: 5432 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75c, payStatus: 'Invoiced Next Billing Cycle', __v: 0 } ], {})
    Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } }, { '$lookup': { from: 'accounts', localField: '_id', foreignField: '_id', as: 'accounts' } }, { '$unwind': '$accounts' }, { '$project': { _id: '$accounts', total: 1, lineItems: 1 } } ], {})
    [
      {
        "total": 100,
        "lineItems": [
          {
            "_id": "5bf4b591a06509544b8cf761",
            "address": "21 Jump street Anywhere NY 5432"
          }
        ],
        "_id": {
          "_id": "5bf4b591a06509544b8cf75c",
          "name": "Second Account",
          "contactName": "Second Person",
          "contactEmail": "second@example.com",
          "__v": 0
        }
      },
      {
        "total": 200,
        "lineItems": [
          {
            "_id": "5bf4b591a06509544b8cf75e",
            "address": "1 Some street Somewhere TX 1234"
          },
          {
            "_id": "5bf4b591a06509544b8cf75f",
            "address": "2 Other street Elsewhere CA 5678"
          }
        ],
        "_id": {
          "_id": "5bf4b591a06509544b8cf75b",
          "name": "First Account",
          "contactName": "First Person",
          "contactEmail": "first@example.com",
          "__v": 0
        }
      }
    ]
    Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
    Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
    [
      {
        "_id": {
          "_id": "5bf4b591a06509544b8cf75c",
          "name": "Second Account",
          "contactName": "Second Person",
          "contactEmail": "second@example.com",
          "__v": 0
        },
        "total": 100,
        "lineItems": [
          {
            "_id": "5bf4b591a06509544b8cf761",
            "address": "21 Jump street Anywhere NY 5432"
          }
        ]
      },
      {
        "_id": {
          "_id": "5bf4b591a06509544b8cf75b",
          "name": "First Account",
          "contactName": "First Person",
          "contactEmail": "first@example.com",
          "__v": 0
        },
        "total": 200,
        "lineItems": [
          {
            "_id": "5bf4b591a06509544b8cf75e",
            "address": "1 Some street Somewhere TX 1234"
          },
          {
            "_id": "5bf4b591a06509544b8cf75f",
            "address": "2 Other street Elsewhere CA 5678"
          }
        ]
      }
    ]
    

    【讨论】:

    • 哇,这是一个非常详细的回复。谢谢你,我明天会花一些时间来消化这个并回复你。真的很翔实。喜欢学习这个!
    • 这正是我想要的。我做了一些调整以满足我的确切需求,但这让我越过了终点线!谢谢!对于任何需要我的确切用例的人,我已经用最终结果更新了我的问题。
    • 有些答案需要和那些有奖牌奖励机制,就是这样的答案!
    • 很好的答案!有什么建议可以用于分片集合吗?因为在这种情况下你不能使用查找
    • 不是所有的英雄都穿斗篷,但有些人肯定会写代码:)
    【解决方案2】:
    1. 它显示 _id 而不是 _accountId,因为当您使用 $group 时, 结果按指定的_accountId分组,所以它变成了 文档的新 _id。
    2. 将contactName、contactEmail 和name 移到顶层有两种可能的解决方案:
      • 一个是在填充后使用 javascript 处理它。为此,您可以使用函数“map()”。
      • 另一种解决方案是在聚合管道中使用 $lookup 来填充同一 mongoDB 查询中的文档,并且在 $lookup 之后,您必须再次使用 $project 来构建所需的输出文档。

    【讨论】:

      猜你喜欢
      • 2017-10-29
      • 2018-05-20
      • 2021-03-19
      • 2021-12-15
      • 2018-03-10
      • 2016-12-19
      • 1970-01-01
      • 2021-10-22
      • 2023-03-14
      相关资源
      最近更新 更多