OO第三单元作业总结

艾绍泽23373317 2025-05-18 16:11:16

测试过程

对各种测试的理解

就我个人而言,本单元的测试做的还是比较草率的。前两次作业完全是通过自己构造一些数据进行的。但显然这种方式的效率是很低的,很容易给人测了几组数据就觉得没什么问题的幻觉。所以后来借鉴了本单元的Junit的测试,自己对一些较为复杂的方法都构建了相应的单元测试,并且借助AI的力量构造了不少边缘的测试样例,这在自我检测和互测中都起到了不小的作用。

  1. 单元测试主要是针对一个具体的方法进行的测试,其实我们完成的Junit任务就是单元测试的体现,我们通过编写对这一个方法的测试样例,来测试这个单元是否按照我们预期的方式运行。
  2. 功能测试我认为主要是对整体进行的测试,就是说我们并不关心这个功能的内部实现原理,不管你是写了多少行还是调用了多少个其他方法,我们只是对你最后的实现效果进行检测。和单元测试相比的话,我认为单元测试可以集中到更小的层面上,比如说某一个功能利用的其中一个方法上。
  3. 集成测试主要是针对各个单元之间的交互是否正确。以本单元的代码为例,集成测试其实就是我们的中测,强测,这些测试最终的效果。也就是看ap,ar,coa,ca,am,sm等等这些指令,相互之间会不会出现矛盾,前者的操作能否对后者产生正确的影响。
  4. 压力测试是对我们代码在大数据量等极端情况下的考验。当我们的network中添加了1000个人,甚至更多的时候,我们的各项功能能否在指定的CPU时间内执行完毕,成为了一个重要的检测指标。这一点很重要,因为我们经常遇到网站因为流量过大而导致崩溃的情况。为了避免这种情况,我们非常需要对我们的程序进行压力测试(很明显这几次的代码我都没有进行很好的压力测试)。
  5. 回归测试其实就是在我们找到问题后代码重新进行测试的过程。显然我们不能保证代码修改后一定就没有bug了(甚至会出现新bug),所以需要进行回归测试,确保自己的代码万无一失。

数据构造策略

我的数据构造策略其实就是特殊加许多一般。

  • 特殊指的是针对一些边界情况和特殊情况进行测试,并判断是否能正常运行。比如我们的network没有人的时候,进行各种操作能不能返回正确的值。当然我们不只是对正确的操作进行测试,也该对返回异常的操作进行测试,就比如说对一个没有出现过的人进行各种操作,看异常是否能返回正常值。
  • 许多一般其实就是类似上面说的压力测试了,我的测试主要是在Junit中进行的,所以我选择通过添加大量的person和relation然后再随机的对消息和公众号进行操作。这只是一次测试,然后我还会利用RepeatedTest反复测试100次或者更多,保证随机性的覆盖面可以更加广泛。
public class SendMessageTest {
    private Network network;
    private Person p1, p2, p3;
    private Tag tag;
    private OfficialAccount account;
    private Random rand = new Random();
    private ArrayList<Person> people = new ArrayList<>();
    private ArrayList<Tag> tags = new ArrayList<>();

    @BeforeEach
    void setUp() throws Exception {
    }

    // ------------------------ 类型0消息测试 ------------------------
    @Test
    void sendType0_RedEnvelopeMessage() throws Exception {
    }

    @Test
    void sendType0_EmojiMessage() throws Exception {
    }

    // ------------------------ 类型1消息测试 ------------------------
    @Test
    void sendType1_ForwardMessage() throws Exception {
    }

    @Test
    void sendType1_RedEnvelopeToTag() throws Exception {
    }

    // ------------------------ 异常测试 ------------------------
    @Test
    void sendInvalidMessageId() {
    }

    @Test
    void sendType0_NoRelation() throws Exception {
    }

    @Test
    void sendType1_InvalidTag() throws Exception {
    }

    @RepeatedTest(100)
    void randomMessageTest() throws Exception {
        randomSetUp();
        // 随机选择消息类型(0或1)
        int messageType = rand.nextInt(2);

        // 生成消息内容
        Message msg = generateRandomMessage(messageType);
        int msgId = msg.getId();

        // 记录初始状态
        StateSnapshot snapshot = captureSystemState(msg);
        network.addMessage(msg);

        try {
            network.sendMessage(msgId);

            // 验证消息已移除
            assertFalse(network.containsMessage(msgId));

            // 类型相关验证
            if (messageType == 0) {
                verifyType0Effects(msg, snapshot);
            } else {
                verifyType1Effects(msg, snapshot);
            }

        } catch (Exception e) {
            // 异常后验证状态不变
            throw e;
        }
    }

    private Message generateRandomMessage(int type) throws Exception {
    }

    private Person randomPerson() {
    }

    private Person getLinkedPerson(Person sender) {
    }

    private int storeRandomEmoji() throws Exception {
    }

    private int contributeRandomArticle(Person sender) throws Exception {
    }

    private void verifyType0Effects(Message msg, StateSnapshot snapshot) throws Exception {
    }

    private void verifyType1Effects(Message msg, StateSnapshot snapshot) throws Exception {
    }

    private void randomSetUp() throws Exception {
    }

    private static class StateSnapshot {
        HashMap<Integer, Integer> socialValues = new HashMap<>();
        HashMap<Integer, Integer> money = new HashMap<>();
        int emojiHeat;
        HashMap<Integer, List<Integer>> articles = new HashMap<>();
    }
}

以上代码展示了我对sendMessage方法进行的各类测试,包含了各种类型消息的测试(特殊测试),同时也进行了随机性测试(许多一般)。具体内容鉴于篇幅问题就删去了。

综上所述,其实就是针对上面的单元测试,功能测试,集成测试,压力测试和回归测试来进行全面的测试(形式上的全面),我们在构造的时候主要就是针对功能和压力两个方面来进行,完成好这两项测试,我们的程序就不会出现很大的问题。

大模型使用经验

本单元的规格化设计非常容易让人看的头昏眼花,这时合理的使用大模型就成为了我们提高效率的一大利器。对此我总结了以下经验:

  1. 提供代码的时候要尽量提供完整,我们应该避免只把一段毫无上下文的JML喂给大模型,这样大模型的回答有时候会不尽人意,和我们的实际实现过程产生偏差。我们可以将整个文件或项目提交上去,或者在提问的时候给足预设,使用什么,不用什么,让大模型尽量按照自己的构想来实现。
  2. 大模型一次的回答可能并不能满足我们的需求,我们应该反复询问,并且根据回答,添加更多的限制性条件,一般比较强劲的大模型都会有上下文记忆,大模型会结合我们前面的代码来给出更加贴近我们需求的答案的。
  3. 使用大模型绝对不能无脑生成后直接使用,我们尽量要根据他的回答进行判断后再去使用,同时在性能上大模型的回答并不一定是最优的,我们可以结合第二点所说的,进一步提要求,让大模型生成更优的结果。

PS:我个人使用大模型的时候,针对deepseek和chatGPT会有不同的使用风格。前者我会更倾向于当一个工具来使用,后者我会倾向于当一个朋友那样唠嗑使用。因为GPT现在回答问题非常的俏皮,不太忍心当工具使唤(乐)。

构建与维护策略

其实本单元的构建和维护策略的决定并不在个人,而是在提供的各种接口上。分析整个network的构建,其实主要操作都是集中在Network类里面的,NetworkInterface为我们提供了各式各样的操作,添加person(添加结点),添加relation(添加边),添加tag,officialAccount,Message等等操作。

如果只谈对整个network图的构建的话,我们只需要关注addPerson,addRelation,modifyRelation即可。在社交网络中,每一个独立的person就是一个节点,每当我们添加一个person,整张图就多一个节点,每当我们添加一对relation,这个图就多了一条边(无向,有权),modifyRelation会稍微复杂一些,如果边的权重大于0就只是改权重即可,如果权重小于0,那么我们就需要删掉这条关系(注意,虽然我们前面说这条边是无向的,其实我们在具体维护的时候是按照类似双向边的操作进行的,所以我们应该对二者的acquaintance同时进行删除操作),同时还要对tag进行修改(断绝一切关系)。以上操作其实都是按照JML的内容进行的,并没有什么个人的发挥空间,只需要按照JML一步一步完成,把ensures的内容都完成就OK。下面是展示的具体实现过程:

public void addPerson(PersonInterface person) throws EqualPersonIdException {
        if (containsPerson(person.getId())) {
            throw new EqualPersonIdException(person.getId());
        }
        if (person instanceof Person) {
            Person person1 = (Person) person;
            persons.add(person1);
        }
    }

public void addRelation(int id1, int id2, int value)
            throws PersonIdNotFoundException, EqualRelationException
    {
        handleNoPersonException(id1);
        handleNoPersonException(id2);
        Person person1 = getPerson(id1);
        Person person2 = getPerson(id2);
        if (person1.isLinked(person2)) {
            throw new EqualRelationException(id1, id2);
        }
        person1.addAcquaintance(person2);
        person2.addAcquaintance(person1);
        person1.addValue(person2, value);
        person2.addValue(person1, value);

        //维护tripleSum变量
        tripleSum += calcTripleSum(person1, person2);
    }

    public void modifyRelation(int id1, int id2, int value)
            throws PersonIdNotFoundException, EqualPersonIdException, RelationNotFoundException
    {
        handleNoPersonException(id1);
        handleNoPersonException(id2);
        if (id1 == id2) {
            throw new EqualPersonIdException(id1);
        }
        Person person1 = getPerson(id1);
        Person person2 = getPerson(id2);
        if (!person1.isLinked(person2)) {
            throw new RelationNotFoundException(id1, id2);
        }
        int oldValue = person1.queryValue(person2);
        if (oldValue + value > 0) {
            person2.modifyValue(person1, oldValue + value);
            person1.modifyValue(person2, oldValue + value);
        } else {
            person1.delValueAndAcquaintance(person2);
            person2.delValueAndAcquaintance(person1);
            person1.delRelationInTag(person2);
            person2.delRelationInTag(person1);

            //维护tripleSum变量
            tripleSum -= calcTripleSum(person1, person2);
        }
    }

我们发现对于构建图而言是相当简单的,但是除了构建操作外,还有对图的访问操作,这个是相对困难的。我们需要考虑最短路径,有没有环路,三元环数量等等。这些操作单独看并不复杂,但是对于大数据量的图来讲,如果选用多重循环这种操作的话,很容易就超时,所以我们需要采用更加聪明的办法来解决,比如说动态维护一个变量这样的操作,虽然并没有改变图的形状,但是我们的确是在维护一个和这个图紧密相关的变量,欲知具体实现如何,请看下一个部分。

规格与实现分离

上一个部分讲到对图的访问操作很容易就发生超时的问题,如果我们直接按照JML去写就大概率会出问题。比如:

/*@ ensures \result ==
      @         (\sum int i; 0 <= i && i < persons.length;
      @             (\sum int j; i < j && j < persons.length;
      @                 (\sum int k; j < k && k < persons.length
      @                     && getPerson(persons[i].getId()).isLinked(getPerson(persons[j].getId()))
      @                     && getPerson(persons[j].getId()).isLinked(getPerson(persons[k].getId()))
      @                     && getPerson(persons[k].getId()).isLinked(getPerson(persons[i].getId()));
      @                     1)));
      @*/
    public /*@ pure @*/ int queryTripleSum();

就这个获取三元环的操作,这三重循环绝对用不得。那我们就要想办法用更加高效的方式,比如动态维护一个tripleSum变量,我们需要考虑什么时候会产生或减少三元环,显然就是addRelation和modifyRelation(具体就是添加和删除关系的时候),addRelation时,我们通过判断添加的两个节点的共同朋友的个数来判断最后添加的三元环的个数,这很显然。modifyRelation时,如果是删除关系的话,我们也是通过判断共同的朋友来判断删除三元环的个数。(在上一个部分的modifyRelation方法中展示了这两个操作)后面还有类似的操作也是通过动态维护变量的操作来改变复杂度的。

通过上面举的例子,我们就很好理解什么是规格与实现分离了,规格限制的是我们的过程吗?不是。规格限制的是最后的结果!我们只需要让我们的方法的返回的结果符合最后规格所描述的限制即可。还是以上面为例,规格在说什么?在说“我希望结果是三元环的数量”,所以我们只需要最后返回正确的三元环数量就好了,我不管你具体怎么实现的。所以我们这时就需要聪明一些,使用效率更高的方法来取代这个复杂的方法。

一句话总结,规格只限制最终的结果而不在意过程的实现

Junit测试

本单元的Junit测试的编写因为JML规格而变得更好写了。

前面我们提到JML规格之限制了最终的结果,而没有限制过程,所以我们可以选择走捷径,但是如果捷径没走对就很容易出现bug,所以我们可以结合Junit进行检查。因为Junit并没有约束时间限制,我们完全就可以将规格的限制照搬到Junit中,然后利用规格的一定正确的限制去检查我们的实现是否有问题。这个是JML规格在测试我们用非规格限制实现的方法时的好处。

这时候有的同学可能会问,“我的实现方式和规格描述的一模一样,我该怎么检查呢?”,这确实值得考虑,我们总不能自己检查自己,这样毫无意义。其实这时候我认为我们只需要保证和JML规格一模一样就OK了,上几个特值样例进行校验即可,不必过多纠结。

对于较为复杂的规格限制,我们可以通过观察assignable来判断需要检查的对象,比如(下面是一段超长JML):

/*@ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
      @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
      @ assignable messages, emojiHeatList[*];
      @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getPerson2().messages,
      @            getMessage(id).getPerson2().socialValue,
      @            getMessage(id).getPerson2().money,
      @            getMessage(id).getPerson2().receivedArticles;
      @ ensures !containsMessage(id);
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @         \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
      @         \old(getMessage(id)).getPerson2().getSocialValue() ==
      @         \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessageInterface) ==>
      @         (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney() &&
      @         \old(getMessage(id)).getPerson2().getMoney() ==
      @         \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney());
      @ ensures (\old(getMessage(id)) instanceof ForwardMessageInterface) ==>
      @         ((\old(getMessage(id)).getPerson2().getReceivedArticles().get(0).equals(((ForwardMessageInterface)\old(getMessage(id))).getArticleId())) &&
      @         (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getReceivedArticles().size());
      @                                       \old(getMessage(id)).getPerson2().getReceivedArticles().get(i+1) == \old(getMessage(id).getPerson2().getReceivedArticles().get(i))) &&
      @         \old(getMessage(id)).getPerson2().getReceivedArticles().size() == \old(getMessage(id).getPerson2().getReceivedArticles().size()) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessageInterface)) ==> (\not_assigned(persons[*].money));
      @ ensures (\old(getMessage(id)) instanceof EmojiMessageInterface) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessageInterface)\old(getMessage(id))).getEmojiId();
      @         emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessageInterface)) ==> \not_assigned(emojiHeatList);
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
      @          \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
      @ also
      @ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 1 &&
      @           getMessage(id).getPerson1().containsTag(getMessage(id).getTag().getId());
      @ assignable messages, emojiHeatList[*];
      @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getTag().persons[*].messages,
      @            getMessage(id).getTag().persons[*].socialValue,
      @            getMessage(id).getTag().persons[*].money,
      @            getMessage(id).getTag().persons[*].receivedArticles;
      @ ensures !containsMessage(id)
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @         \old(getMessage(id).getPerson1().getSocialValue())+ \old(getMessage(id)).getSocialValue();
      @ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getSocialValue() ==
      @         \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessageInterface) && (\old(getMessage(id)).getTag().getSize() > 0) ==>
      @          (\exists int i; i == ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney()/\old(getMessage(id)).getTag().getSize();
      @           \old(getMessage(id)).getPerson1().getMoney() ==
      @           \old(getMessage(id).getPerson1().getMoney()) - i*\old(getMessage(id)).getTag().getSize() &&
      @           (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p);
      @           p.getMoney() == \old(p.getMoney()) + i));
      @ ensures (\old(getMessage(id)) instanceof ForwardMessageInterface) && (\old(getMessage(id)).getTag().getSize() > 0) ==>
      @         (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getReceivedArticles().get(0).equals(((ForwardMessageInterface)\old(getMessage(id))).getArticleId())) &&
      @         (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.getReceivedArticles().size());
      @                                                                         p.getReceivedArticles().get(i+1) == \old(p.getReceivedArticles().get(i)))) &&
      @         (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getReceivedArticles().size() == \old(p.getReceivedArticles().size()) + 1);
      @ ensures (\old(getMessage(id)) instanceof EmojiMessageInterface) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessageInterface)\old(getMessage(id))).getEmojiId();
      @          emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getMessages().get(0).equals(\old(getMessage(id))));
      @ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.getMessages().size());
      @                                                                         p.getMessages().get(i+1) == \old(p.getMessages().get(i))));
      @ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getMessages().size() == \old(p.getMessages().size()) + 1);
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
      @          !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
      @ signals (TagIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
      @          !getMessage(id).getPerson1().containsTag(getMessage(id).getTag().getId());
      @*/
    public /*@ safe @*/ void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, TagIdNotFoundException;

对于上面这段超长JML,我们在测试的时候关注assignable部分就可以帮助我们快速的确定我们需要检查的部分,而不需要一步一步去看这个方法改变了什么变量,添加了哪些新东西,删除了哪些部分,这是合理利用JML进行Junit编写给我们带来的一大好处。下面是我对这个理论的应用:

  private void verifyType0Effects(Message msg, StateSnapshot snapshot) throws Exception {
        for (Person p : people) {
            if (p.equals(msg.getPerson2())) {
                assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()) + msg.getSocialValue());
                if (msg instanceof RedEnvelopeMessage) {
                    assertEquals(p.getMoney(), ((RedEnvelopeMessage) msg).getMoney() + snapshot.money.get(p.getId()));
                }
                if (msg instanceof ForwardMessage) {
                    assertEquals(((ForwardMessage) msg).getArticleId(), p.getReceivedArticles().get(0));
                    for (int i = 0; i < snapshot.articles.get(p.getId()).size(); i++) {
                        assertEquals(snapshot.articles.get(p.getId()).get(i), p.getReceivedArticles().get(i + 1));
                    }
                }
                continue;
            }
            if (p.equals(msg.getPerson1())) {
                assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()) + msg.getSocialValue());
                if (msg instanceof RedEnvelopeMessage) {
                    assertEquals(p.getMoney(), snapshot.money.get(p.getId()) - ((RedEnvelopeMessage) msg).getMoney());
                }
                continue;
            }
            assertEquals(p.getMoney(), snapshot.money.get(p.getId()));
            assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()));
            assertEquals(p.getReceivedArticles(), snapshot.articles.get(p.getId()));
            assertEquals(p.getMoney(), snapshot.money.get(p.getId()));
            if (rand.nextBoolean()) {
                System.out.println("qsv" + " " + p.getId());
                System.out.println("qra" + " " + p.getId());
                System.out.println("qm" + " " + p.getId());
            }
        }
        if (msg instanceof EmojiMessage) {
            assertEquals(network.queryPopularity(((EmojiMessage) msg).getEmojiId()), snapshot.emojiHeat + 1);
            System.out.println("qp" + " " + ((EmojiMessage) msg).getEmojiId());
        }
    }

仔细观察我的assert,你会发现,我判断的内容完全就是assignable所强调的内容,这一点JML规格的确帮了很大的忙,减轻了我个人的工作量。

学习体会

JML规格带给我的收获不只是读懂语法,学会编写代码,还有谨慎和细心。前两次作业都是因为漏掉了JML的部分要求而导致最终强测分数很低,最后一次作业可谓是谨慎至极,先是自己检查了数遍,随后又让AI帮我检查了一遍,然后又编写了许多Junit测试来检测问题,虽然都没查出问题,看起来是没起到一点效果,但是这样的测试还是让我十分安心,最后强测结果也的确比前几次强了不少,只能说功夫不负有心人,耕耘不一定有收获,但是不耕耘绝对没有收获。

在写Junit测试的时候,前两次完全是无头苍蝇,完全不知道自己的数据为何不全面,而最后一次测试的时候,根据前两次的测试经验和AI的辅助,在耐心的思考和编写下一次便通过了Junit测试。

虽然这个单元的前两次强测分数都不高,但是最后一次有了极大的进步,这点令我很是兴奋(尴尬的是互测截至时间记错了,有几个自己编的好数据还没来得及测),我认为只要有所收获,收获的比别人多就是好事。并且在最近也看到一段话觉得很有道理“自然界起的最大的雾是骄傲”,当我们收获成果的时候绝对不要张扬,也没什么好炫耀的,继续专注自我,保持谦逊,怀揣着一颗学徒的心继续努力。

...全文
10 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

271

社区成员

发帖
与我相关
我的任务
社区描述
2025年北航面向对象设计与构造
学习 高校
社区管理员
  • Alkaid_Zhong
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧