Administrator
2025-09-05 6fb9052c35348987cb4876cfab65f59d0558ab1b
feat(ai): 添加 AI 生成题目的功能

- 新增 AI生成题目页面和相关功能
- 实现 AI 生成题目的后端接口和逻辑
- 更新列表页面,增加 AI 新增按钮和相关权限控制
- 添加必要的依赖和配置
8 files modified
1 files added
390 ■■■■ changed files
pom.xml 23 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/productQuestion/AiProductQuestionController.java 15 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/productQuestion/ViewController.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/req/AiProductQuestionAiDto.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/AiProductQuestionService.java 2 ●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiProductQuestionServiceImpl.java 119 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java 59 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/productQuestion/aiAdd.html 148 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/productQuestion/list.html 15 ●●●●● patch | view | raw | blame | history
pom.xml
@@ -30,6 +30,29 @@
    <dependencies>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.9</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.21.5</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.volcengine</groupId>
            <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
            <version>LATEST</version>
src/main/java/cc/mrbird/febs/ai/controller/productQuestion/AiProductQuestionController.java
@@ -1,6 +1,5 @@
package cc.mrbird.febs.ai.controller.productQuestion;
import cc.mrbird.febs.ai.entity.AiProductPoint;
import cc.mrbird.febs.ai.entity.AiProductQuestion;
import cc.mrbird.febs.ai.req.AiProductQuestionAiDto;
import cc.mrbird.febs.ai.service.AiProductQuestionService;
@@ -53,6 +52,13 @@
        return aiProductQuestionService.add(dto);
    }
    @PostMapping("aiAdd")
    @ControllerEndpoint(operation = "AI生成题目", exceptionMessage = "操作失败")
    public FebsResponse aiAdd(@RequestBody @Valid AiProductQuestionAiDto dto) {
        return aiProductQuestionService.aiAdd(dto);
    }
    @PostMapping("update")
    @ControllerEndpoint(operation = "更新", exceptionMessage = "操作失败")
    public FebsResponse update(@RequestBody @Valid AiProductQuestion dto) {
@@ -67,13 +73,6 @@
    ) {
        return aiProductQuestionService.delete(id);
    }
    @PostMapping("aiCreate/{id}")
    @ControllerEndpoint(operation = "AI生成题目", exceptionMessage = "操作失败")
    public FebsResponse aiCreate(@RequestBody @Valid AiProductQuestionAiDto dto) {
        return aiProductQuestionService.aiCreate(dto);
    }
    @GetMapping(value = "/questionTree")
src/main/java/cc/mrbird/febs/ai/controller/productQuestion/ViewController.java
@@ -45,6 +45,13 @@
        return FebsUtil.view("modules/ai/productQuestion/add");
    }
    @GetMapping(value = "/aiAdd")
    @RequiresPermissions("productQuestionList:aiAdd")
    public String aiAdd() {
        return FebsUtil.view("modules/ai/productQuestion/aiAdd");
    }
    @GetMapping("info/{id}")
    @RequiresPermissions("productQuestionList:info")
    public String artInfo(@PathVariable String id, Model model) {
src/main/java/cc/mrbird/febs/ai/req/AiProductQuestionAiDto.java
@@ -7,6 +7,8 @@
    private String productCategoryId;
    private Integer difficulty;
    private String query;
    private String promptAiSystem;
src/main/java/cc/mrbird/febs/ai/service/AiProductQuestionService.java
@@ -39,5 +39,5 @@
    List<AiProductQuestion> productQuestionTree(LambdaQueryWrapper<AiProductQuestion> aiProductQuestionLambdaQueryWrapper);
    FebsResponse aiCreate(AiProductQuestionAiDto dto);
    FebsResponse aiAdd(AiProductQuestionAiDto dto);
}
src/main/java/cc/mrbird/febs/ai/service/impl/AiProductQuestionServiceImpl.java
@@ -11,6 +11,9 @@
import cc.mrbird.febs.common.entity.QueryRequest;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -166,9 +169,121 @@
    }
    @Override
    public FebsResponse aiCreate(AiProductQuestionAiDto dto) {
    public FebsResponse aiAdd(AiProductQuestionAiDto dto) {
        String jsonFormat = "{\"question_list\":[{\"title\": \"消费者对透明质酸的主要担忧是什么?\",\"answer_list\":[{\"answer\": \"消费者担心透明质酸维持时间短,效果不明显。\",\"type\": 1,\"analysis\": \"\"},{\"answer\": \"消费者担心透明质酸注射后可能出现移位、凹陷、馒化、僵硬以及炎症反应。\",\"type\": 1,\"analysis\": \"\"},{\"answer\": \"消费者的主要担忧包括效果不明显、维持时间短、注射后出现移位、凹陷、馒化、僵硬以及炎症反应。他们希望既达到理想效果,又避免这些副作用。\",\"type\": 2,\"analysis\": \"标准答案涵盖了消费者对透明质酸的所有主要担忧,既包括效果问题,也涵盖副作用风险,全面反映消费者心理需求。\"}]}]} ";
        dto.setJsonFormat(jsonFormat);
        String questionAndAnswerStr = aiService.llmInvokeNonStreaming(dto);
        return null;
        // 解析AI返回的结果
        List<JSONObject> questionList = parseAiQuestionResponse(questionAndAnswerStr);
        String productCategoryId = dto.getProductCategoryId();
        Integer difficulty = dto.getDifficulty();
        Date createdTime = new Date();
        // 处理解析后的问题列表
        for (JSONObject questionObj : questionList) {
            String title = questionObj.getStr("title");
            AiProductQuestion aiProductQuestion = new AiProductQuestion();
            aiProductQuestion.setId(UUID.getSimpleUUIDString());
            aiProductQuestion.setProductCategoryId(productCategoryId);
            aiProductQuestion.setTitle(title);
            aiProductQuestion.setDifficulty(difficulty);
            aiProductQuestion.setCreatedTime(createdTime);
            this.save(aiProductQuestion);
            JSONArray answerList = questionObj.getJSONArray("answer_list");
            System.out.println("问题: " + title);
            for (int i = 0; i < answerList.size(); i++) {
                JSONObject answer = answerList.getJSONObject(i);
                System.out.println("答案" + (i+1) + ": " + answer.getStr("answer"));
                System.out.println("类型: " + answer.getStr("type"));
                System.out.println("分析: " + answer.getStr("analysis"));
                AiProductQuestionItem aiProductQuestionItem = new AiProductQuestionItem();
                aiProductQuestionItem.setId(UUID.getSimpleUUIDString());
                aiProductQuestionItem.setProductQuestionId(aiProductQuestion.getId());
                aiProductQuestionItem.setTitle(aiProductQuestion.getTitle());
                aiProductQuestionItem.setAnswer(answer.getStr("answer"));
                aiProductQuestionItem.setCorrectAnswer(answer.getInt("type") == 2 ? 1 : 0);
                aiProductQuestionItem.setAnswerAnalysis(answer.getStr("analysis"));
                aiProductQuestionItem.setCreatedTime(createdTime);
                aiProductQuestionItemService.getBaseMapper().insert(aiProductQuestionItem);
            }
        }
        return new FebsResponse().success().message("操作成功");
    }
    /**
     * 解析AI返回的包含问题和答案的JSON字符串
     * @param aiResponse AI返回的原始响应字符串
     * @return 解析后的问题列表
     */
    public List<JSONObject> parseAiQuestionResponse(String aiResponse) {
        try {
            // 解析外层JSON
            JSONObject outerJson = JSONUtil.parseObj(aiResponse);
            // 提取output字段
            String output = outerJson.getStr("output");
            // 去除<start>和</start>标签,并清理多余字符
            String jsonContent = output.replace("<start>", "")
                    .replace("</start>", "")
                    .replace("\\n", "")
                    .replace("\\\"", "\"")
                    .trim();
            // 解析内部JSON
            JSONObject innerJson = JSONUtil.parseObj(jsonContent);
            // 提取question_list
            JSONArray questionList = innerJson.getJSONArray("question_list");
            // 转换为List<JSONObject>返回
            return questionList.toList(JSONObject.class);
        } catch (Exception e) {
            log.error("解析AI问题响应失败: ", e);
            throw new RuntimeException("解析AI响应失败", e);
        }
    }
    public static void main(String[] args) {
        String questionStr = "{\"output\":\"<start>\\n{\\n  \\\"question_list\\\": [\\n    {\\n      \\\"title\\\": \\\"1. 如何通过菲欧曼品牌背景和价值塑造透明质酸产品的高端形象?\\\",\\n      \\\"answer_list\\\": [\\n        {\\n          \\\"answer\\\": \\\"强调菲欧曼拥有45年历史,是法国第一家医学美容实验室,产品行销全球60多个国家,树立其国际权威形象。\\\",\\n          \\\"type\\\": \\\"2\\\",\\n          \\\"analysis\\\": \\\"该答案全面结合品牌历史、科研投入及市场影响力,系统塑造高端定位,符合标准答案要求。\\\"\\n        },\\n        {\\n          \\\"answer\\\": \\\"通过讲述菲欧曼产品获得欧盟CE、中国NMPA等国际认证,强调其安全性和合规性。\\\",\\n          \\\"type\\\": \\\"1\\\",\\n          \\\"analysis\\\": \\\"\\\"\\n        },\\n        {\\n          \\\"answer\\\": \\\"通过明星使用、医生推荐等口碑效应,提升菲欧曼在消费者心中的高端认知。\\\",\\n          \\\"type\\\": \\\"1\\\",\\n          \\\"analysis\\\": \\\"\\\"\\n        }\\n      ]\\n    }\\n  ]\\n}\\n</start>\"}";
        // 第一步:解析外层JSON
        JSONObject outerJson = JSONUtil.parseObj(questionStr);
        // 第二步:提取output字段
        String output = outerJson.getStr("output");
        // 第三步:去除<start>和</start>标签
        String jsonContent = output.replace("<start>", "").replace("</start>", "").trim();
        // 第四步:解析内部的JSON
        JSONObject innerJson = JSONUtil.parseObj(jsonContent);
        // 第五步:提取question_list
        JSONArray questionList = innerJson.getJSONArray("question_list");
        // 遍历问题列表
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject question = questionList.getJSONObject(i);
            System.out.println("问题标题: " + question.getStr("title"));
            JSONArray answerList = question.getJSONArray("answer_list");
            System.out.println("答案列表:");
            // 遍历答案列表
            for (int j = 0; j < answerList.size(); j++) {
                JSONObject answer = answerList.getJSONObject(j);
                System.out.println("  答案 " + (j+1) + ": " + answer.getStr("answer"));
                System.out.println("  类型: " + answer.getStr("type"));
                System.out.println("  分析: " + answer.getStr("analysis"));
                System.out.println();
            }
        }
    }
}
src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java
@@ -5,6 +5,13 @@
import cc.mrbird.febs.common.exception.FebsException;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.app.FlowStreamMode;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -35,38 +42,24 @@
        bizParams.put(bizParam_3,dto.getJsonFormat());
        String query = dto.getQuery();
        long startTime = System.currentTimeMillis();
//        ApplicationParam param = ApplicationParam.builder()
//                // 若没有配置环境变量,可用百炼API Key将下行替换为:.apiKey("sk-xxx")。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
//                .apiKey(apiKey)
//                .appId(appId) //替换为实际的应用 ID
//                .flowStreamMode(FlowStreamMode.MESSAGE_FORMAT)
//                .prompt(query)
//                .bizParams(JsonUtils.toJsonObject( bizParams))
//                .build();
//
//        Application application = new Application();
//        Flowable<ApplicationResult> result;
//        try {
//            result = application.streamCall(param);
//        } catch (NoApiKeyException | InputRequiredException e) {
//            throw new FebsException(StrUtil.format("百炼工作流输出失败:{}",e.getMessage()));
//        }
//
//        return Flux.from(result)
//                .map(message -> {
//                    HashMap<String, String> stringStringHashMap = new HashMap<>();
//                    if (!message.getOutput().getFinishReason().equals("stop")){
//                        stringStringHashMap.put(LlmStrategyContextEnum.CONTENT.name(),message.getOutput().getWorkflowMessage().getMessage().getContent());
//                    }
//                    return new FebsResponse().success().data(stringStringHashMap);
//                })
//                .doOnComplete(() -> {
//                    long endTime = System.currentTimeMillis();
//                    System.out.println("百炼工作流输出:" + (endTime - startTime) + "毫秒");
//                })
//                .doOnError(error -> {
//                    throw new FebsException(StrUtil.format("百炼工作流输出失败:{}",error));
//                });
        return null;
        ApplicationParam param = ApplicationParam.builder()
                // 若没有配置环境变量,可用百炼API Key将下行替换为:.apiKey("sk-xxx")。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
                .apiKey(apiKey)
                .appId(appId) //替换为实际的应用 ID
                .flowStreamMode(FlowStreamMode.MESSAGE_FORMAT)
                .prompt(query)
                .bizParams(JsonUtils.toJsonObject( bizParams))
                .build();
        Application application = new Application();
        ApplicationResult result = null;
        try {
            result = application.call(param);
        } catch (NoApiKeyException e) {
            throw new FebsException("百炼工作流调用异常");
        } catch (InputRequiredException e) {
            throw new FebsException("百炼工作流调用异常");
        }
        return  result.getOutput().getText();
    }
}
src/main/resources/templates/febs/views/modules/ai/productQuestion/aiAdd.html
New file
@@ -0,0 +1,148 @@
<div class="layui-fluid layui-anim febs-anim" id="febs-productQuestion-ai-add" lay-title="新增">
    <div class="layui-row febs-container">
        <div class="layui-col-md12">
            <div class="layui-fluid" id="productQuestion-add">
                <form class="layui-form" action="" lay-filter="productQuestion-add-form">
                    <div class="layui-tab layui-tab-brief" lay-filter="docDemoTabBrief">
                        <ul class="layui-tab-title">
                            <li class="layui-this">基础信息</li>
                        </ul>
                        <div class="layui-tab-content">
                            <div class="layui-tab-item layui-show">
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">分类:</label>
                                        <div class="layui-input-block">
                                            <div id="product-category"></div>
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">AI角色设定:</label>
                                        <div class="layui-input-block">
                                            <input type="text" name="promptAiSystem" lay-verify="required"
                                                   placeholder="" autocomplete="off" class="layui-input">
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">题目数量:</label>
                                        <div class="layui-input-block">
                                            <input type="number" name="questionCnt" lay-verify="required"
                                                   placeholder="" autocomplete="off" class="layui-input">
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">核心内容:</label>
                                        <div class="layui-input-block">
                                            <input type="text" name="query" lay-verify="required"
                                                   placeholder="" autocomplete="off" class="layui-input">
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">难度:</label>
                                        <div class="layui-input-block">
                                            <input type="radio" name="difficulty" value="1" title="简单"  checked="">
                                            <input type="radio" name="difficulty" value="2" title="中等" >
                                            <input type="radio" name="difficulty" value="3" title="困难" >
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="layui-form-item febs-hide">
                        <button class="layui-btn" lay-submit="" lay-filter="productQuestion-add-form-submit" id="submit">保存</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
<!-- 表格操作栏 end -->
<script data-th-inline="javascript">
    layui.use(['febs', 'form', 'formSelects', 'validate', 'treeSelect', 'eleTree','dropdown', 'laydate', 'layedit', 'upload', 'element', 'table', 'xmSelect','jquery'], function () {
        var $ = layui.jquery,
            febs = layui.febs,
            layer = layui.layer,
            table = layui.table,
            formSelects = layui.formSelects,
            treeSelect = layui.treeSelect,
            form = layui.form,
            laydate = layui.laydate,
            eleTree = layui.eleTree,
            $view = $('#productQuestion-add'),
            layedit = layui.layedit,
            upload = layui.upload,
            validate = layui.validate,
            element = layui.element;
        form.render();
        var category = xmSelect.render({
            el: '#product-category',
            language: 'zn',
            prop : {
                value : 'id',
                children : 'child'
            },
            iconfont: {
                parent: 'hidden',
            },
            tips: '请选择',
            filterable: true,
            radio: true,
            clickClose: true,
            tree: {
                show: true,
                //非严格模式
                strict: false,
            },
            data: []
        })
        febs.get(ctx + 'admin/productCategory/categoryTree', null, function(res) {
            category.update({
                data : res.data,
                autoRow: true,
            });
        })
        form.on('submit(productQuestion-add-form-submit)', function (data) {
            data.field.productCategoryId = category.getValue('valueStr');
            $.ajax({
                'url':ctx + 'admin/productQuestion/aiAdd',
                'type':'post',
                'dataType':'json',
                'headers' : {'Content-Type' : 'application/json;charset=utf-8'}, //接口json格式
                'traditional': true,//ajax传递数组必须添加属性
                'data':JSON.stringify(data.field),
                'success':function (data) {
                    if(data.code==200){
                        layer.closeAll();
                        febs.alert.success(data.message);
                        $('#febs-productQuestion').find('#query').click();
                    }else{
                        febs.alert.warn(data.message);
                    }
                },
                'error':function () {
                    febs.alert.warn('服务器繁忙');
                }
            })
            return false;
        });
    });
</script>
src/main/resources/templates/febs/views/modules/ai/productQuestion/list.html
@@ -45,7 +45,8 @@
<script type="text/html" id="productQuestionToolbar">
    <div class="layui-btn-container">
        <button class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain" shiro:hasPermission="productQuestionList:add" lay-event="productQuestionAdd">新增</button>
        <button class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain" shiro:hasPermission="productQuestionList:add" lay-event="productQuestionAdd">手动新增</button>
        <button class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain" shiro:hasPermission="productQuestionList:aiAdd" lay-event="productQuestionAiAdd">AI新增</button>
    </div>
</script>
@@ -157,6 +158,18 @@
                    }
                });
            }
            if(layEvent === 'productQuestionAiAdd'){
                febs.modal.open('新增', 'modules/ai/productQuestion/aiAdd/', {
                    btn: ['提交', '取消'],
                    area:['100%','100%'],
                    yes: function (index, layero) {
                        $('#febs-productQuestion-ai-add').find('#submit').trigger('click');
                    },
                    btn2: function () {
                        layer.closeAll();
                    }
                });
            }
        });
        function initProductQuestionTable() {