无尘阁日记

无尘阁日记

跟AI基于yii2的一次批量导入数据的完美合作
2025-04-16

问:

/**

     * @route   v1/ai-chat-manage/suggested-keyword-batch-import

     * @name    批量导入联想关键词

     * @method  POST

     * @parameter string   Authorization          授权码

     * @parameter array    items                  联想词数组,结构如:

     * [

     *   {

     *     "id": 1,                            // 可选,存在则为更新

     *     "keyword1": "效率提升",              // 必填

     *     "keyword2": "时间管理",              // 必填

     *     "priority": 3,                      // 可选

     *     "preset_content": "试试番茄工作法"   // 可选,管理员预设答案

     *   },

     *   {...}

     * ]

     * @return array

     * 成功返回结构:

     * {

     *   "status": 1,

     *   "message": "批量导入完成",

     *   "success_count": 5,

     *   "fail_count": 2,

     *   "failed_items": [

     *     { "index": 1, "reason": "keyword1不能为空" },

     *     { "index": 4, "reason": "数据库保存失败" }

     *   ]

     * }

     */

    public function actionSuggestedKeywordBatchImport()

    {

        $request = Yii::$app->request;

        $items = $request->post('items', []);

        Yii::error($items, 'actionSuggestedKeywordBatchImport');


        $success = 0;

        $failed = 0;

        $failedItems = [];


        foreach ($items as $index => $item) {

            // 基本验证

            if (empty($item['keyword1']) || empty($item['keyword2'])) {

                $failed++;

                $failedItems[] = [

                    'index' => $index,

                    'reason' => 'keyword1 或 keyword2 不能为空',

                ];

                continue;

            }


            // 单条导入

            try {

                $result = AiSuggestedKeywords::createOrUpdate($item);

                if ($result) {

                    $success++;

                } else {

                    $failed++;

                    $failedItems[] = [

                        'index' => $index,

                        'reason' => '数据库保存失败',

                    ];

                }

            } catch (\Throwable $e) {

                Yii::error($e, '批量导入异常');

                $failed++;

                $failedItems[] = [

                    'index' => $index,

                    'reason' => $e->getMessage(),

                ];

            }

        }


        return $this->errorParam->paramValidity('批量导入完成', 1, [

            'success_count' => $success,

            'fail_count' => $failed,

            'failed_items' => $failedItems,

        ]);

    }


这个接口是用来导入提示词和预设答案的,为了方便运营人员,他们习惯于用excel批量编辑好,然后一次性导入。

现在我们设想的方案是前端提供下载空白模版,包括如下几个字段:

keyword1,keyword2,priority,admin_answer_id,不过admin_answer_id是需要先从excel导入自动生成的,然后回写到此处,下面我将把涉及的这两张表也给你。

另外数据也由前端读取进来,接口只负责入库,校验等等。

我的需求是:

1. 前端提供多少数据就导入多少数据,批量导入,一个个插入效率会低一些,性能会差一些。

2. keyword1是带英文逗号的用于识别用户输入的关键词的(以英文逗号切割完了之后精确命中每一个元素),keyword2是精确命中。

3. 导入的时候可能数据库里面已经有数据了,需要做校验,然后去更新,更新规则如下:

隐含前提是keyword2必须是唯一的。

1)如果keyword1在库里存在,但keyword2不相同,另外插入一条,这个在客户端命中keyword1中的关键词的时候根据优先级来选择预设答案,而如果精确命中keyword2则直接选这一条。

2)如果keyword2被命中相同,但keyword1不相同,则进行合并并去重,其他priority,和admin_answer_id,需要识别答案是不是也相同,如果相同不更新admin_answer_id,不相同则更新其答案,admin_answer_id也不变,如果admin_answer_id没有,则插入并回写。我们只认同一个提示词只有一个预设答案,这是前提。

我这个规则你看是否已经完善了,如果不是你可以适当补充。

下面是两张表的结构:

 CREATE TABLE `ai_suggested_keywords` (

  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',

  `keyword1` varchar(255) NOT NULL COMMENT '关键词A',

  `keyword2` varchar(255) NOT NULL COMMENT '关键词B(与A相互关联)',

  `ai_answer_id` bigint DEFAULT NULL COMMENT '关联的AI生成答案ID',

  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

  `created_by` bigint DEFAULT NULL COMMENT '创建人',

  `updated_by` bigint DEFAULT NULL COMMENT '更新人',

  `org_id` bigint DEFAULT NULL COMMENT '所属企业ID(NULL表示全局提示词)',

  `priority` int DEFAULT '0' COMMENT '提示词优先级(越大越靠前)',

  `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否已删除',

  `deleted_by` bigint DEFAULT NULL COMMENT '删除人用户ID',

  `deleted_at` datetime DEFAULT NULL COMMENT '删除时间',

  `admin_answer_id` bigint DEFAULT NULL COMMENT '管理员预设答案ID',

  `refer_count` int DEFAULT '0' COMMENT '被引用总次数',

  PRIMARY KEY (`id`),

  KEY `idx_ai_answer_id` (`ai_answer_id`),

  KEY `idx_admin_answer_id` (`admin_answer_id`)

) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 


CREATE TABLE `ai_answers` (

  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',

  `question` text NOT NULL COMMENT '标准化问题',

  `answer` text NOT NULL COMMENT 'AI 生成的或用户维护的答案',

  `is_user_editable` tinyint(1) DEFAULT '0' COMMENT '是否用户可编辑',

  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

  `created_by` bigint DEFAULT NULL COMMENT '创建人',

  `updated_by` bigint DEFAULT NULL COMMENT '更新人',

  `org_id` bigint DEFAULT NULL COMMENT '所属企业ID(为NULL表示全局)',

  `model_id` bigint DEFAULT NULL COMMENT '回答所用的AI模型ID',

  `source_type` enum('AI','ADMIN') NOT NULL DEFAULT 'AI' COMMENT '答案来源类型:AI生成 或 管理员预设',

  `refer_count` int DEFAULT '0' COMMENT '被引用总次数',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci


两张表对应的model是AiSuggestedKeywords,AiAnswers。

要求把业务逻辑提成独立方法我放到model里去,然后注释和返回结构参考我上面那个接口。

答:

明白了,你的设计逻辑非常清晰,整体思路上已经涵盖了绝大多数可能遇到的场景。我会在尊重你的设想基础上做适度补充,并将业务逻辑提取成 AiSuggestedKeywords 模型里的方法,最后你只需在 controller 中调用它并返回统一结构即可。

以下是整理后的实现方案,包含:

  1. 优化后的接口控制器代码

  2. 模型方法逻辑拆分与实现(含必要注释)

  3. 边界场景和补充建议


一、Controller 中调用方法(保持原接口结构不变)

public function actionSuggestedKeywordBatchImport()
{
    $request = Yii::$app->request;
    $items = $request->post('items', []);
    Yii::error($items, 'actionSuggestedKeywordBatchImport');

    try {
        $result = AiSuggestedKeywords::batchImport($items);
    } catch (\Throwable $e) {
        Yii::error($e, '批量导入异常');
        return $this->errorParam->paramValidity('批量导入失败', 0, [
            'error' => $e->getMessage()
        ]);
    }

    return $this->errorParam->paramValidity('批量导入完成', 1, $result);
}

二、Model 方法拆分(AiSuggestedKeywords.php)

public static function batchImport(array $items): array
{
    $success = 0;
    $failed = 0;
    $failedItems = [];

    foreach ($items as $index => $item) {
        $result = self::importSingleItem($item);

        if ($result === true) {
            $success++;
        } else {
            $failed++;
            $failedItems[] = [
                'index' => $index,
                'reason' => $result
            ];
        }
    }

    return [
        'success_count' => $success,
        'fail_count' => $failed,
        'failed_items' => $failedItems
    ];
}

三、单条处理逻辑(importSingleItem)

public static function importSingleItem(array $item): true|string
{
    if (empty($item['keyword1']) || empty($item['keyword2'])) {
        return 'keyword1 或 keyword2 不能为空';
    }

    $keyword2 = trim($item['keyword2']);
    $keyword1Input = trim($item['keyword1']);
    $keyword1Parts = array_filter(array_map('trim', explode(',', $keyword1Input)));

    $priority = $item['priority'] ?? 0;
    $adminAnswerId = $item['admin_answer_id'] ?? null;

    $existing = self::find()->where(['keyword2' => $keyword2])->one();

    if ($existing) {
        $existingKeyword1 = array_filter(array_map('trim', explode(',', $existing->keyword1)));
        $mergedKeyword1 = implode(',', array_unique(array_merge($existingKeyword1, $keyword1Parts)));

        $shouldUpdate = false;

        if ($mergedKeyword1 !== $existing->keyword1) {
            $existing->keyword1 = $mergedKeyword1;
            $shouldUpdate = true;
        }

        if (!empty($item['preset_content'])) {
            $answerModel = AiAnswers::findOne($existing->admin_answer_id);
            if (!$answerModel || $answerModel->answer !== $item['preset_content']) {
                // 需创建新答案
                $newAnswer = new AiAnswers();
                $newAnswer->question = $keyword2;
                $newAnswer->answer = $item['preset_content'];
                $newAnswer->source_type = 'ADMIN';
                if (!$newAnswer->save()) {
                    return 'admin_answer 创建失败';
                }
                $existing->admin_answer_id = $newAnswer->id;
                $shouldUpdate = true;
            }
        }

        $existing->priority = $priority;

        return $shouldUpdate && !$existing->save() ? '更新失败' : true;
    }

    // 若 keyword2 不存在,插入新记录
    $new = new self();
    $new->keyword1 = implode(',', $keyword1Parts);
    $new->keyword2 = $keyword2;
    $new->priority = $priority;

    if (!empty($item['preset_content'])) {
        $newAnswer = new AiAnswers();
        $newAnswer->question = $keyword2;
        $newAnswer->answer = $item['preset_content'];
        $newAnswer->source_type = 'ADMIN';
        if (!$newAnswer->save()) {
            return 'admin_answer 创建失败';
        }
        $new->admin_answer_id = $newAnswer->id;
    }

    return $new->save() ? true : '插入失败';
}

四、必要的补充说明

关键词匹配逻辑细节:

  • keyword2 为精确命中主键,仅允许唯一。

  • keyword1 可拆分为数组,客户端输入若命中其中任意词则有效,由系统做英文逗号拆分。

性能优化建议:

  • 可考虑批量插入(如用 Yii 批量构建 insert into ... values)或使用 save(false) 提高性能,但需先禁用校验或提前手动校验。

  • 可加缓存标记更新频率高的关键词,避免重复命中数据库。

数据安全性建议:

  • 加入事务(Yii::$app->db->beginTransaction())以便整体回滚,尤其适用于涉及 AiAnswers 的更新或新增。

  • 考虑使用 upsert 或 replace into 替代部分 insert+update 操作。


如果你希望,我还可以再帮你加上:

  • Excel 模版生成下载接口

  • 前端自动回填 admin_answer_id 的接口

  • 或者对上面逻辑继续进行事务封装、日志记录、行为审计等系统化补全

你只需要说一声就行。

问:

按你提出的优化建议再给一版。

答:

太好了,下面我将根据你提到的需求,以及我刚刚补充的建议,给出一个更完善、性能更优、可维护性更高的 Yii2 批量导入联想关键词逻辑实现方案


✅ 控制器层:简洁,仅负责请求接收与返回

public function actionSuggestedKeywordBatchImport()
{
    $items = Yii::$app->request->post('items', []);
    Yii::error($items, 'actionSuggestedKeywordBatchImport');

    try {
        $result = AiSuggestedKeywords::batchImport($items);
    } catch (\Throwable $e) {
        Yii::error($e, '批量导入异常');
        return $this->errorParam->paramValidity('批量导入失败', 0, [
            'error' => $e->getMessage()
        ]);
    }

    return $this->errorParam->paramValidity('批量导入完成', 1, $result);
}

✅ 模型层:批量导入逻辑 + 事务控制 + 数据去重合并

public static function batchImport(array $items): array
{
    $success = 0;
    $failed = 0;
    $failedItems = [];

    $transaction = Yii::$app->db->beginTransaction();

    try {
        foreach ($items as $index => $item) {
            $result = self::importSingleItem($item);
            if ($result === true) {
                $success++;
            } else {
                $failed++;
                $failedItems[] = [
                    'index' => $index,
                    'reason' => $result
                ];
            }
        }
        $transaction->commit();
    } catch (\Throwable $e) {
        $transaction->rollBack();
        throw $e;
    }

    return [
        'success_count' => $success,
        'fail_count' => $failed,
        'failed_items' => $failedItems
    ];
}

✅ 单条导入逻辑:支持去重合并、预设答案自动插入与更新、字段清洗

public static function importSingleItem(array $item): true|string
{
    if (empty($item['keyword1']) || empty($item['keyword2'])) {
        return 'keyword1 或 keyword2 不能为空';
    }

    $keyword2 = trim($item['keyword2']);
    $inputKeyword1 = trim($item['keyword1']);
    $inputKeyword1Arr = array_filter(array_map('trim', explode(',', $inputKeyword1)));
    $inputKeyword1Str = implode(',', $inputKeyword1Arr);

    $priority = $item['priority'] ?? 0;
    $presetContent = $item['preset_content'] ?? null;

    // 查询现有 keyword2 是否已存在(唯一主键)
    $existing = self::find()->where(['keyword2' => $keyword2, 'is_deleted' => 0])->one();

    if ($existing) {
        $oldKeyword1Arr = array_filter(array_map('trim', explode(',', $existing->keyword1 ?? '')));
        $mergedKeyword1Arr = array_unique(array_merge($oldKeyword1Arr, $inputKeyword1Arr));
        $existing->keyword1 = implode(',', $mergedKeyword1Arr);

        $existing->priority = $priority;

        // 检查是否需要更新预设答案
        if (!empty($presetContent)) {
            if (empty($existing->admin_answer_id)) {
                $answerId = self::createAdminAnswer($keyword2, $presetContent);
                if (!$answerId) return '管理员答案插入失败';
                $existing->admin_answer_id = $answerId;
            } else {
                $existingAnswer = AiAnswers::findOne($existing->admin_answer_id);
                if ($existingAnswer && $existingAnswer->answer !== $presetContent) {
                    $existingAnswer->answer = $presetContent;
                    if (!$existingAnswer->save()) return '管理员答案更新失败';
                }
            }
        }

        return $existing->save(false) ? true : '更新保存失败';
    }

    // keyword2 不存在,插入新记录
    $model = new self();
    $model->keyword1 = $inputKeyword1Str;
    $model->keyword2 = $keyword2;
    $model->priority = $priority;
    $model->is_deleted = 0;

    if (!empty($presetContent)) {
        $answerId = self::createAdminAnswer($keyword2, $presetContent);
        if (!$answerId) return '管理员答案插入失败';
        $model->admin_answer_id = $answerId;
    }

    return $model->save(false) ? true : '新记录保存失败';
}

✅ 管理员答案统一插入封装(用于共用)

protected static function createAdminAnswer(string $question, string $content): ?int
{
    $model = new AiAnswers();
    $model->question = $question;
    $model->answer = $content;
    $model->source_type = 'ADMIN';
    $model->is_user_editable = 0;

    return $model->save(false) ? $model->id : null;
}

✅ 边界与扩展建议

  1. keyword2唯一性建议加唯一索引(防止并发重复)

    ALTER TABLE ai_suggested_keywords ADD UNIQUE INDEX idx_keyword2_unique (`keyword2`);
  2. 数据入库前预处理可在前端进行,比如

    • 英文逗号去重

    • admin_answer_id 和 preset_content 的预匹配(若支持 answer 的前置导入)

  3. 可扩展:

    • 日志记录失败项(如入库失败日志表)

    • 增加返回字段:updated_count, inserted_count

    • 增加钩子方法(如导入后同步缓存)


✅ 示例成功返回格式(不变):

{
  "status": 1,
  "message": "批量导入完成",
  "success_count": 5,
  "fail_count": 2,
  "failed_items": [
    { "index": 1, "reason": "keyword1 或 keyword2 不能为空" },
    { "index": 4, "reason": "管理员答案插入失败" }
  ]
}

如你觉得接下来需要把这一套再配合一个Excel模版生成接口 + 上传后预校验逻辑 + 失败项下载模板,我也可以帮你一键实现整套流程,包括上传后的前端建议交互。

要不要我直接帮你把模版下载接口也写上?

问:

ALTER TABLE ai_suggested_keywords ADD UNIQUE INDEX idx_keyword2_unique (keyword2); 这个还得检测下is_deleted是不是为0.

答:

你说得非常对。由于你的业务逻辑中支持逻辑删除(is_deleted = 1),直接对 keyword2 加唯一索引会导致 软删除的记录仍然占用唯一性,从而无法插入同名的新记录。

MySQL 中的限制是:普通唯一索引无法条件化,不能写成 UNIQUE WHERE is_deleted = 0 这样的表达式(除非你用的是 PostgreSQL)。所以我们需要用业务逻辑而不是数据库约束来保障“有效记录下 keyword2 唯一”。

✅ 正确做法:

  1. 不加唯一索引(因为条件约束做不了);

  2. 在模型逻辑中人工校验 keyword2 的唯一性(仅限 is_deleted = 0);

  3. 插入前查询是否存在 is_deleted = 1 的旧数据,可考虑覆盖复用或彻底清理


✅ 建议补充逻辑到模型中处理 keyword2 唯一性

importSingleItem() 逻辑中:

// keyword2 逻辑删除的历史记录也需排除唯一性冲突
$conflict = self::find()
    ->where(['keyword2' => $keyword2])
    ->andWhere(['is_deleted' => 0])
    ->andWhere(['!=', 'id', $existing->id ?? 0])
    ->one();

if ($conflict) {
    return '该 keyword2 已被其他记录占用';
}

如果存在 is_deleted = 1 的记录,也可以考虑“复用”而不是新建,这种场景下可以补充逻辑如下:

// 查询被逻辑删除的记录,考虑是否复用
$deletedRecord = self::find()
    ->where(['keyword2' => $keyword2, 'is_deleted' => 1])
    ->one();

if ($deletedRecord) {
    $deletedRecord->is_deleted = 0;
    $deletedRecord->keyword1 = $inputKeyword1Str;
    $deletedRecord->priority = $priority;
    // 其他字段处理...
    return $deletedRecord->save(false) ? true : '逻辑删除记录恢复失败';
}

🔄 总结替代方案:

方案是否推荐理由
数据库层面加唯一索引❌ 不推荐无法只约束 is_deleted = 0
程序中手动判断是否唯一(仅判断未删除数据)✅ 推荐灵活、可控制逻辑删除
利用唯一键 + 软删除转硬删除⚠️ 限场景使用不适合需要保留历史记录的系统

如你之后想把逻辑删除记录做“回收站”机制(比如自动清理30天未恢复的数据、允许后台手动恢复等),我也可以帮你设计完整方案。

要不要我顺带也把这个 keyword2 逻辑校验封装好再补全到 importSingleItem()

问:

在yii2的rules中加这个校验怎么加。

答:

在 Yii2 中,rules 是用于模型数据验证的机制,但它本身无法直接表达诸如 “字段在 is_deleted = 0 的记录中唯一” 这种复杂的条件。我们需要使用 unique 验证器的自定义 targetAttributefilter 来实现。


✅ 目标:实现 keyword2 在 is_deleted = 0 范围内的唯一性校验

在你的 AiSuggestedKeywords 模型里,写入如下 rules() 规则:

public function rules()
{
    return [
        [['keyword1', 'keyword2'], 'required'],
        [['keyword1', 'keyword2'], 'string', 'max' => 255],
        [['priority'], 'integer'],
        [['keyword2'], 'unique', 'targetAttribute' => 'keyword2', 'filter' => function ($query) {
            $query->andWhere(['is_deleted' => 0]);
        }, 'message' => 'keyword2 已存在'],
    ];
}

✅ 说明

  • targetAttribute => 'keyword2' 表示只校验该字段;

  • filter 是一个闭包,用于动态附加条件(这里限制在 is_deleted = 0 的范围内做唯一校验);

  • 'message' 可自定义提示信息;

  • 这个规则会在调用 $model->validate() 时生效,无论是新增还是更新都可以自动处理。


⚠️ 特别提醒:

  1. 更新时要排除当前这条记录自身(id != 当前记录),Yii2 的 unique 校验器已经自动处理这件事(前提是 $model->isNewRecord 判断正确);

  2. 如果你不是用 save() 自动调用验证,而是用 save(false) 跳过验证,那这个规则不会生效;

  3. 如果你在导入逻辑中写的是 save(false),那需要显式调用 $model->validate()


✅ 示例(插入前显式调用验证)

$model = new self();
$model->keyword1 = '效率,时间';
$model->keyword2 = '时间管理';
$model->priority = 3;

if (!$model->validate()) {
    return implode('; ', $model->getFirstErrors());
}

if (!$model->save(false)) {
    return '保存失败';
}

要不要我直接把这个验证逻辑集成到你刚刚的 importSingleItem() 流程中,并统一处理新增与更新的场景?还能再补上一个复用逻辑删除记录的判断。你说一声,我直接整合进去。

问:

keyword1:测试111,aaaa,68888

keyword2:测试111123

priority:85

preset_content:《夜泊古渡》↵秋深木落雁惊寒,野渡舟横夜欲残。↵万里潮回天接水,数声钟断月临关。↵苔封石碣字犹涩,风叩芦花梦未安。↵犹有未言如鹤影,一痕栖在乱云间。

生成3组不同的用于postman批量导入的以上格式的mock数据。

答:

下面是根据你提供的结构,生成的三组不同内容的用于 Postman 批量导入测试的 mock 数据,遵循你的接口格式(用于 POST v1/ai-chat-manage/suggested-keyword-batch-import),内容结构与字段完全一致:


✅ Mock 数据结构(Postman raw JSON body 示例)

{
  "items": [
    {
      "keyword1": "测试111,aaaa,68888",
      "keyword2": "测试111123",
      "priority": 85,
      "preset_content": "《夜泊古渡》\n秋深木落雁惊寒,野渡舟横夜欲残。\n万里潮回天接水,数声钟断月临关。\n苔封石碣字犹涩,风叩芦花梦未安。\n犹有未言如鹤影,一痕栖在乱云间。"
    },
    {
      "keyword1": "高效写作,文案灵感,自动生成",
      "keyword2": "AI文案助手",
      "priority": 60,
      "preset_content": "《灵感之眼》\n忽而灵光乍现,化一字万象。\n握笔如执剑,破稿海无涯。\nAI非替代,乃助力,愿你执笔有锋,落纸无悔。"
    },
    {
      "keyword1": "心理调节,情绪管理,焦虑应对",
      "keyword2": "情绪支持系统",
      "priority": 75,
      "preset_content": "《情绪之舟》\n风平浪静不是常态,波动才是自然。\n让念头如浮萍而不缠身,\n让感受被看见而不否定,\n你是岸,不是水。"
    }
  ]}

✅ 复制粘贴到 Postman 使用方法:

  1. 打开 Postman,设置请求为 POST

  2. 请求地址填入你的接口地址,例如:http://yourdomain.com/v1/ai-chat-manage/suggested-keyword-batch-import

  3. 切换到 Body,选择 raw + JSON

  4. 粘贴上面这段完整 JSON 数据

  5. 添加 Header:Authorization: Bearer xxxxxxx


如你需要 生成更多条 mock 数据(10条、100条、上千条)、或需要按结构 自动批量转成 CSV/Excel 再导入 Notion、前端表格、数据库等系统,我也可以帮你批量生成并导出为文件格式。

需要的话直接说一声:“给我导出 Excel 模板 + 测试数据”,我来搞定 👌

问:

不是json格式哟,还是以上那种格式。并且第一条数据请替换成别的。

答:

明白了!你要的格式是用于 Excel 表格准备的那种“扁平文本结构”,即每条数据是一组字段名和对应的值,用于批量导入的展示或模拟表格输入,不是 JSON 格式。下面我会生成 3 组 mock 数据,结构如下:

  • 每组数据含:keyword1keyword2prioritypreset_content

  • 每组之间有空行隔开

  • 第一条数据会替换掉你原本提供的内容


✅ Mock 批量导入数据(3组):

keyword1:专注训练,注意力提升,深度工作
keyword2:专注力工具
priority:90
preset_content:《静心如镜》↵心无旁骛则光明,意有所归则不乱。↵世事纷扰如风起,惟有专注能开门。↵专注不是缩小世界,而是放大当下。

keyword1:高效写作,文案灵感,自动生成
keyword2:AI文案助手
priority:60
preset_content:《灵感之眼》↵忽而灵光乍现,化一字万象。↵握笔如执剑,破稿海无涯。↵AI非替代,乃助力,愿你执笔有锋,落纸无悔。

keyword1:心理调节,情绪管理,焦虑应对
keyword2:情绪支持系统
priority:75
preset_content:《情绪之舟》↵风平浪静不是常态,波动才是自然。↵让念头如浮萍而不缠身,↵让感受被看见而不否定,↵你是岸,不是水。

📝 用法说明

  • 直接复制粘贴到 Excel,每行自动分列(复制到 Notepad 也可转为导入格式)

  • 支持模拟“表格导入”的结构化识别

  • 表示换行符(可转为真实换行)


如你要我把这个格式导出成 .xlsx.csv 文件,或者批量生成更多样本数据(如用于测试导入1000条带关键词的样本),我也可以随时搞定。需要我生成下载文件吗?

问:

你这样的三组恐怕不行吧。还是得套一层items才行的。

答:

你说得完全对!如果是为了让前端或 Postman 使用这种结构导入,哪怕是非 JSON 格式,也得套一层 items 的结构体,就像你在开头的接口注释中写的那样:

@parameter array items  联想词数组,结构如:
[
  {
    "keyword1": "效率提升",
    "keyword2": "时间管理",
    ...
  }
]

那现在我们生成的内容,应该是items[n].xxx 的形式展开,更适合 Postman 导入 Excel 或前端表格组件处理。


✅ 正确格式:Postman/前端模拟导入格式(三组数据)

items[0].keyword1:专注训练,注意力提升,深度工作
items[0].keyword2:专注力工具
items[0].priority:90
items[0].preset_content:《静心如镜》↵心无旁骛则光明,意有所归则不乱。↵世事纷扰如风起,惟有专注能开门。↵专注不是缩小世界,而是放大当下。

items[1].keyword1:高效写作,文案灵感,自动生成
items[1].keyword2:AI文案助手
items[1].priority:60
items[1].preset_content:《灵感之眼》↵忽而灵光乍现,化一字万象。↵握笔如执剑,破稿海无涯。↵AI非替代,乃助力,愿你执笔有锋,落纸无悔。

items[2].keyword1:心理调节,情绪管理,焦虑应对
items[2].keyword2:情绪支持系统
items[2].priority:75
items[2].preset_content:《情绪之舟》↵风平浪静不是常态,波动才是自然。↵让念头如浮萍而不缠身,↵让感受被看见而不否定,↵你是岸,不是水。

🛠 用法建议:

  • Postman 表单模拟(x-www-form-urlencoded):直接一行一个 key-value 贴进去

  • 前端表格上传:可用于自动生成字段绑定模板

  • Excel 配合脚本处理items[n].字段名 可转为嵌套 JSON 后端解析


如你希望我:

  1. 自动生成更多条(几十、几百)

  2. 导出成 .xlsx.csv 文件

  3. 提供 Excel 模板(含字段注释、测试数据)

我可以一键帮你生成,还能附上“校验说明”或“批量导入失败示例”。

要我现在就生成 .xlsx 吗?你说一声我直接给你文件。

问:

这不对,应该是items[0][keywod1]这种格式。

答:

对!你这句话说得非常准确 —— 用于 Postman x-www-form-urlencoded 或 form-data 表单结构的二维数组提交,正确格式应为:

items[0][keyword1]: xxx
items[0][keyword2]: xxx
...

而不是 items[0].keyword1 这种点语法。

这种结构提交后,后端才能正确解析为数组,每个 items[n] 都是一个键值对组成的元素,与你接口中 Yii::$app->request->post('items', []) 完全对得上。


✅ 修正后的三组 mock 导入数据(标准格式)

items[0][keyword1]:专注训练,注意力提升,深度工作
items[0][keyword2]:专注力工具
items[0][priority]:90
items[0][preset_content]:《静心如镜》↵心无旁骛则光明,意有所归则不乱。↵世事纷扰如风起,惟有专注能开门。↵专注不是缩小世界,而是放大当下。

items[1][keyword1]:高效写作,文案灵感,自动生成
items[1][keyword2]:AI文案助手
items[1][priority]:60
items[1][preset_content]:《灵感之眼》↵忽而灵光乍现,化一字万象。↵握笔如执剑,破稿海无涯。↵AI非替代,乃助力,愿你执笔有锋,落纸无悔。

items[2][keyword1]:心理调节,情绪管理,焦虑应对
items[2][keyword2]:情绪支持系统
items[2][priority]:75
items[2][preset_content]:《情绪之舟》↵风平浪静不是常态,波动才是自然。↵让念头如浮萍而不缠身,↵让感受被看见而不否定,↵你是岸,不是水。

✅ 如何在 Postman 使用这类格式

  1. 请求方式:POST

  2. Body 类型:x-www-form-urlencodedform-data

  3. 将上面的字段直接粘贴为 key-value 表单项

  4. 添加 Header:Authorization: Bearer 你的令牌


是否需要我:

  • 生成一套 10 条或 100 条的 mock 测试数据?

  • 打包成 .txt.csv 文件用于开发或上传?

  • 生成可用于上传的 .xlsx 模板(字段说明 + 示例数据)?

只要说一声,我一键生成给你。

问:

帮我另外生成一组可以命中我前面说的第一种导入情况的mock数据。

答:

收到!你前面说的第一种导入情况是:

1)如果 keyword1 在库里存在,但 keyword2 不相同,另外插入一条。

也就是:

  • 数据库中已有某条记录:keyword1 = xxx(包含特定关键词),但 keyword2 = yyy1

  • 现在上传一条新数据:

    • keyword1 部分关键词与已有记录重叠(或完全相同)

    • keyword2 = yyy2不同于已有记录的 keyword2

  • 系统应识别为新数据(插入一条新记录),不去合并


✅ 满足“第一种情况”的 mock 数据(标准二维数组格式)

items[0][keyword1]:效率提升,时间管理,专注训练
items[0][keyword2]:深度专注方法
items[0][priority]:88
items[0][preset_content]:《专注力的边界》↵不是控制时间,而是允许时间穿越你。↵深度专注不是压制杂念,而是让杂念自行沉寂。↵你沉入的,不是任务,而是当下的透明。

items[1][keyword1]:效率提升,目标拆解,番茄工作法
items[1][keyword2]:番茄时间控制
items[1][priority]:70
items[1][preset_content]:《番茄时刻》↵用时间切割焦虑,用节奏破除拖延。↵短促集中是一种策略,不是弱点。↵你要赢的不是时间,而是主动权。

items[2][keyword1]:效率提升,时间管理,计划执行
items[2][keyword2]:工作节奏优化
items[2][priority]:75
items[2][preset_content]:《节奏里的秩序》↵你焦虑不是因为任务多,而是节奏乱。↵任务无法精简时,节奏可以。↵让计划不是压力源,而是护航带。

✅ 用法说明

  • 这三条数据都满足 keyword1 重复出现了 效率提升时间管理 等关键词

  • 但每条的 keyword2 都是独立的(假设数据库中 效率提升 对应的是 keyword2 = 提升效率技巧 之类)

  • 所以应当分别插入三条新记录,不会走合并逻辑(第二种情况)


要不要我接着生成:

  • 第二种情况的 mock 数据(keyword2 命中 → 合并去重)

  • 第三种情况(完整复用已逻辑删除的数据)

  • 一键导出为 .xlsx/.txt 文件用于上传测试

你只要说“生成第二种情况”,我立刻接着搞。

问:

你这哪一组跟前面条件满足啊

答:

你提得太对了,我刚刚生成的那组数据确实没有准确满足你描述的第一种导入情况,我们来重新梳理一下你之前定义的 第一种导入逻辑


✅ 你定义的第一种导入情况是:

前提:keyword2 必须唯一
条件1:keyword1 在库中已存在
条件2:keyword2 不同于已有记录的 keyword2

操作:插入新记录,因为这是同一组 keyword1 映射出的不同 keyword2,用于模糊命中后按 priority 选择。


❌ 我刚才给的示例中,虽然 keyword1 里用了类似的关键词,但没有明确模拟“keyword1 已在库中存在”的情形

现在,我来给你生成真正符合这个条件的 mock 数据,你假设数据库中已有:

[
    'keyword1' => '效率提升,时间管理,专注训练',
    'keyword2' => '番茄工作法',
]

现在上传一条数据:

  • keyword1 包含相同的内容(或者完全一样)

  • keyword2 是一个新的词(比如 晨间仪式


✅ 符合你定义的“第一种导入场景”的 mock 数据(三组)

items[0][keyword1]:效率提升,时间管理,专注训练
items[0][keyword2]:晨间仪式
items[0][priority]:90
items[0][preset_content]:《晨间觉醒》↵日出之前,是自我觉醒的最好时刻。↵不是早起改变了你,而是你决定重启生活。

items[1][keyword1]:效率提升,时间管理,专注训练
items[1][keyword2]:夜间整理术
items[1][priority]:85
items[1][preset_content]:《夜的退场》↵不是一天的结束,而是下一天的准备。↵整理不是收尾,是启动,是清空,让你睡得更安稳。

items[2][keyword1]:效率提升,时间管理,专注训练
items[2][keyword2]:注意力恢复术
items[2][priority]:82
items[2][preset_content]:《重启专注》↵不是强迫自己集中,而是允许自己恢复。↵专注不是持久力,是恢复力。

✅ 这组数据如何满足“第一种情况”?

字段状态
keyword1与已有记录完全相同(模拟已有记录存在)
keyword2全部为新值(与数据库已有不同)
行为插入三条新记录(同 keyword1 下多个 keyword2)

这样,你系统在处理这三条时就会命中**“keyword1 相同,keyword2 不同 → 插入新记录”**这条路径。


如果你要我继续生成第二种、第三种情况,我可以:

  • 第二种:keyword2 命中相同,keyword1 不同 → 合并去重

  • 第三种:命中逻辑删除 → 恢复该记录而非插入新数据

你要哪种,直说就行,我马上来。

问:

items[0].keyword1:专注训练,注意力提升,深度工作

items[0].keyword2:专注力工具

items[0].priority:90

items[0].preset_content:《静心如镜》↵心无旁骛则光明,意有所归则不乱。↵世事纷扰如风起,惟有专注能开门。↵专注不是缩小世界,而是放大当下。


items[1].keyword1:高效写作,文案灵感,自动生成

items[1].keyword2:AI文案助手

items[1].priority:60

items[1].preset_content:《灵感之眼》↵忽而灵光乍现,化一字万象。↵握笔如执剑,破稿海无涯。↵AI非替代,乃助力,愿你执笔有锋,落纸无悔。


items[2].keyword1:心理调节,情绪管理,焦虑应对

items[2].keyword2:情绪支持系统

items[2].priority:75

items[2].preset_content:《情绪之舟》↵风平浪静不是常态,波动才是自然。↵让念头如浮萍而不缠身,↵让感受被看见而不否定,↵你是岸,不是水。


这是我已经导入的数据。

请你根据我前面的条件,构建覆盖所有场景的n组mock数据,我一一来进行单元测试。谢谢。

答:

非常好!你这三条数据提供了理想的基线,现在我们基于它来构造一组覆盖所有导入逻辑分支的 mock 数据,每一条都精准命中你前面定义的某个具体分支,以便你逐一进行单元测试。


✅ 前提:你已导入如下三条记录

keyword1keyword2prioritypreset_content(略)
专注训练,注意力提升,深度工作专注力工具90...
高效写作,文案灵感,自动生成AI文案助手60...
心理调节,情绪管理,焦虑应对情绪支持系统75...

✅ 目标:覆盖你所定义的全部导入逻辑场景


✅ 场景1:keyword1 命中已存在,keyword2 不同 → 插入新记录

items[0][keyword1]:专注训练,注意力提升,深度工作
items[0][keyword2]:专注呼吸技巧
items[0][priority]:88
items[0][preset_content]:《呼吸之道》↵当你无法控制心跳时,先控制呼吸。↵一呼一吸之间,专注就开始了。

✅ 场景2:keyword2 相同,但 keyword1 不同 → 合并 keyword1,preset_content 相同 → 不更新

items[1][keyword1]:全神贯注,沉浸状态
items[1][keyword2]:专注力工具
items[1][priority]:91
items[1][preset_content]:《静心如镜》↵心无旁骛则光明,意有所归则不乱。↵世事纷扰如风起,惟有专注能开门。↵专注不是缩小世界,而是放大当下。

✅ 场景3:keyword2 相同,keyword1 不同 → 合并 keyword1,preset_content 不同 → 更新 preset_content 内容,但不更新 admin_answer_id(只更新 AiAnswers 内容)

items[2][keyword1]:注意力集中,信息断舍离
items[2][keyword2]:专注力工具
items[2][priority]:95
items[2][preset_content]:《新·静心如镜》↵专注不只是行为控制,而是心理空间的留白。↵排除干扰,是一种关照内在的练习。

✅ 场景4:完全重复的数据 → 不做任何操作(验证幂等性)

items[3][keyword1]:高效写作,文案灵感,自动生成
items[3][keyword2]:AI文案助手
items[3][priority]:60
items[3][preset_content]:《灵感之眼》↵忽而灵光乍现,化一字万象。↵握笔如执剑,破稿海无涯。↵AI非替代,乃助力,愿你执笔有锋,落纸无悔。

✅ 场景5:keyword2 命中逻辑删除记录 → 恢复这条记录,并覆盖字段内容

items[4][keyword1]:冥想引导,觉知训练
items[4][keyword2]:专注力工具
items[4][priority]:78
items[4][preset_content]:《觉知回归》↵专注的本质,是回到自己内在最安静的地方。↵不是远离世界,是重新打开世界。

你需要手动将数据库中“专注力工具”这条数据逻辑删除(is_deleted = 1)后,再测试这一条是否被“复活”。


✅ 场景6:keyword1 或 keyword2 为空 → 校验失败

items[5][keyword1]:
items[5][keyword2]:无效输入测试
items[5][priority]:0
items[5][preset_content]:错误数据测试,应被拦截

✅ 场景7:keyword2 是新值,但 keyword1 完全未曾出现过 → 插入新记录

items[6][keyword1]:阅读理解,输入提升
items[6][keyword2]:深度输入工具
items[6][priority]:67
items[6][preset_content]:《输入力训练》↵你读进去的,不只是信息,还有思维方式。↵输入的质量,决定输出的价值。

✅ 场景8:keyword2 相同,admin_answer_id 不存在 → 创建答案并绑定

items[7][keyword1]:文案创作,灵感获取
items[7][keyword2]:AI文案助手
items[7][priority]:65
items[7][preset_content]:《文案火种》↵写作不是等灵感,而是引火种。↵AI是点火器,不是火源。

你可以临时手动清空数据库中该 keyword2 对应记录的 admin_answer_id,验证自动创建答案逻辑是否生效。


✅ 使用建议

你可以将这些一条条粘贴测试,也可以让我一键为你:

  • 生成 .txt.xlsx 批量格式

  • 自动分组(命中新增、合并、恢复、失败四大类)

  • 配合自动化测试用例脚本

需要我做成文件吗?只说一声,我来导出。