Commit e781129dbea5933198d073b6e519e536d1629524
1 parent
90f920f5
【同步】AI:最新 MJ 的 code review
Showing
9 changed files
with
48 additions
and
22 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/client/MidjourneyProxyClient.java
| ... | ... | @@ -17,7 +17,7 @@ import org.springframework.web.client.RestTemplate; |
| 17 | 17 | import java.util.Collection; |
| 18 | 18 | import java.util.List; |
| 19 | 19 | |
| 20 | -// TODO @fan:这个写到 starter-ai 里哈。搞个 MidjourneyApi,参考 https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java 的风格写哈 | |
| 20 | +// TODO @fan:【高优】这个写到 starter-ai 里哈。搞个 MidjourneyApi,参考 https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java 的风格写哈 | |
| 21 | 21 | /** |
| 22 | 22 | * Midjourney Proxy 客户端 |
| 23 | 23 | * | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/client/enums/MidjourneySubmitCodeEnum.java
| ... | ... | @@ -11,18 +11,14 @@ import java.util.List; |
| 11 | 11 | * Midjourney 提交任务 code 枚举 |
| 12 | 12 | * |
| 13 | 13 | * @author fansili |
| 14 | - * @time 2024/5/30 14:33 | |
| 15 | - * @since 1.0 | |
| 16 | 14 | */ |
| 17 | 15 | @Getter |
| 18 | 16 | @AllArgsConstructor |
| 19 | 17 | public enum MidjourneySubmitCodeEnum { |
| 20 | 18 | |
| 21 | - // 状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误) | |
| 22 | 19 | SUBMIT_SUCCESS("1", "提交成功"), |
| 23 | 20 | ALREADY_EXISTS("21", "已存在"), |
| 24 | 21 | QUEUING("22", "排队中"), |
| 25 | - | |
| 26 | 22 | ; |
| 27 | 23 | |
| 28 | 24 | public static final List<String> SUCCESS_CODES = Lists.newArrayList( |
| ... | ... | @@ -31,7 +27,7 @@ public enum MidjourneySubmitCodeEnum { |
| 31 | 27 | QUEUING.code |
| 32 | 28 | ); |
| 33 | 29 | |
| 34 | - private String code; | |
| 35 | - private String name; | |
| 30 | + private final String code; | |
| 31 | + private final String name; | |
| 36 | 32 | |
| 37 | 33 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
| ... | ... | @@ -63,25 +63,27 @@ public class AiImageController { |
| 63 | 63 | return success(true); |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | - // ================ midjourney 接口 | |
| 66 | + // ================ midjourney 接口 ================ | |
| 67 | 67 | |
| 68 | - @Operation(summary = "midjourney-imagine 绘画", description = "...") | |
| 68 | + @Operation(summary = "Midjourney imagine(绘画)") | |
| 69 | 69 | @PostMapping("/midjourney/imagine") |
| 70 | 70 | public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiImageMidjourneyImagineReqVO req) { |
| 71 | 71 | return success(imageService.midjourneyImagine(getLoginUserId(), req)); |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | - @Operation(summary = "midjourney proxy - 回调通知") | |
| 74 | + @Operation(summary = "Midjourney 回调通知", description = "由 Midjourney Proxy 回调") | |
| 75 | 75 | @PostMapping("/midjourney-notify") |
| 76 | 76 | @PermitAll |
| 77 | 77 | public CommonResult<Boolean> midjourneyNotify(@RequestBody MidjourneyNotifyReqVO notifyReqVO) { |
| 78 | 78 | return success(imageService.midjourneyNotify(notifyReqVO)); |
| 79 | 79 | } |
| 80 | 80 | |
| 81 | - @Operation(summary = "midjourney - action(放大、缩小、U1、U2...)") | |
| 81 | + @Operation(summary = "Midjourney Action", description = "例如说:放大、缩小、U1、U2 等") | |
| 82 | 82 | @GetMapping("/midjourney/action") |
| 83 | + // TODO @fan:id、customerId 的 swagger 注解 | |
| 83 | 84 | public CommonResult<Boolean> midjourneyAction(@RequestParam("id") Long imageId, |
| 84 | 85 | @RequestParam("customId") String customId) { |
| 85 | 86 | return success(imageService.midjourneyAction(getLoginUserId(), imageId, customId)); |
| 86 | 87 | } |
| 88 | + | |
| 87 | 89 | } |
| 88 | 90 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
| ... | ... | @@ -50,9 +50,11 @@ public class AiImageRespVO { |
| 50 | 50 | @Schema(description = "绘画 response") |
| 51 | 51 | private MidjourneyNotifyReqVO response; |
| 52 | 52 | |
| 53 | + // TODO @fan:进度是百分比,还是一个数字哈?感觉这个可以统一成通用字段; | |
| 53 | 54 | @Schema(description = "mj 进度") |
| 54 | 55 | private String progress; |
| 55 | 56 | |
| 56 | 57 | @Schema(description = "mj buttons 按钮") |
| 57 | 58 | private List<MidjourneyNotifyReqVO.Button> buttons; |
| 59 | + | |
| 58 | 60 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
| ... | ... | @@ -123,6 +123,7 @@ public class AiImageDO extends BaseDO { |
| 123 | 123 | */ |
| 124 | 124 | private String errorMessage; |
| 125 | 125 | |
| 126 | + // TODO @芋艿:看看是不是 MidjourneyNotifyReqVO.Button 搞到 MJ API 那 | |
| 126 | 127 | public static class ButtonTypeHandler extends AbstractJsonTypeHandler<Object> { |
| 127 | 128 | |
| 128 | 129 | @Override |
| ... | ... | @@ -134,6 +135,7 @@ public class AiImageDO extends BaseDO { |
| 134 | 135 | protected String toJson(Object obj) { |
| 135 | 136 | return JsonUtils.toJsonString(obj); |
| 136 | 137 | } |
| 138 | + | |
| 137 | 139 | } |
| 138 | 140 | |
| 139 | 141 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/MidjourneyJob.java
| ... | ... | @@ -30,6 +30,7 @@ import java.util.stream.Collectors; |
| 30 | 30 | @Slf4j |
| 31 | 31 | public class MidjourneyJob implements JobHandler { |
| 32 | 32 | |
| 33 | + // TODO @fan:@Resource | |
| 33 | 34 | @Autowired |
| 34 | 35 | private MidjourneyProxyClient midjourneyProxyClient; |
| 35 | 36 | @Autowired |
| ... | ... | @@ -37,10 +38,13 @@ public class MidjourneyJob implements JobHandler { |
| 37 | 38 | @Autowired |
| 38 | 39 | private AiImageService imageService; |
| 39 | 40 | |
| 41 | + // TODO @fan:这个方法,建议实现到 AiImageService,例如说 midjourneySync,返回 int 同步数量。 | |
| 40 | 42 | @Override |
| 41 | 43 | public String execute(String param) throws Exception { |
| 42 | 44 | // 1、获取 midjourney 平台,状态在 “进行中” 的 image |
| 45 | + // TODO @fan:43 和 51 其实有点重叠,日志,建议只打 51 | |
| 43 | 46 | log.info("Midjourney 同步 - 开始..."); |
| 47 | + // TODO @fan:Job、Service 等业务层,不要直接使用 LambdaUpdateWrapper,这样会导致 mapper 穿透到逻辑层。要收敛到 mapper 里。 | |
| 44 | 48 | List<AiImageDO> imageList = imageMapper.selectList( |
| 45 | 49 | new LambdaUpdateWrapper<AiImageDO>() |
| 46 | 50 | .eq(AiImageDO::getStatus, AiImageStatusEnum.IN_PROGRESS.getStatus()) |
| ... | ... | @@ -48,11 +52,14 @@ public class MidjourneyJob implements JobHandler { |
| 48 | 52 | ); |
| 49 | 53 | log.info("Midjourney 同步 - 任务数量 {}!", imageList.size()); |
| 50 | 54 | if (CollUtil.isEmpty(imageList)) { |
| 55 | + // TODO @fan:51 和 54,其实有点重叠。建议 51 挪到 55 之后打。 | |
| 51 | 56 | return "Midjourney 同步 - 数量为空!"; |
| 52 | 57 | } |
| 53 | 58 | // 2、批量拉去 task 信息 |
| 59 | + // TODO @fan:imageList.stream().map(AiImageDO::getTaskId).collect(Collectors.toSet())),可以使用 CollectionUtils.convertSet 简化 | |
| 54 | 60 | List<MidjourneyNotifyReqVO> taskList = midjourneyProxyClient |
| 55 | 61 | .listByCondition(imageList.stream().map(AiImageDO::getTaskId).collect(Collectors.toSet())); |
| 62 | + // TODO @fan:taskList.stream().collect(Collectors.toMap(MidjourneyNotifyReqVO::getId, o -> o)),也可以使用 CollectionUtils.convertMap;本质上,重用 set、map 转换,要 convert 简化 | |
| 56 | 63 | Map<String, MidjourneyNotifyReqVO> taskIdMap = taskList.stream().collect(Collectors.toMap(MidjourneyNotifyReqVO::getId, o -> o)); |
| 57 | 64 | // 3、更新 image 状态 |
| 58 | 65 | List<AiImageDO> updateImageList = new ArrayList<>(); |
| ... | ... | @@ -62,13 +69,16 @@ public class MidjourneyJob implements JobHandler { |
| 62 | 69 | log.warn("Midjourney 同步 - {} 任务为空!", aiImageDO.getTaskId()); |
| 63 | 70 | continue; |
| 64 | 71 | } |
| 72 | + // TODO @ 3.1 和 3.2 是不是融合下;get,然后判空,continue; | |
| 65 | 73 | // 3.2 获取通知对象 |
| 66 | 74 | MidjourneyNotifyReqVO notifyReqVO = taskIdMap.get(aiImageDO.getTaskId()); |
| 67 | 75 | // 3.2 构建更新对象 |
| 76 | + // TODO @fan:建议 List<MidjourneyNotifyReqVO> 作为 imageService 去更新; | |
| 68 | 77 | updateImageList.add(imageService.buildUpdateImage(aiImageDO.getId(), notifyReqVO)); |
| 69 | 78 | } |
| 70 | 79 | // 4、批了更新 updateImageList |
| 71 | 80 | imageMapper.updateBatch(updateImageList); |
| 72 | 81 | return "Midjourney 同步 - ".concat(String.valueOf(updateImageList.size())).concat(" 任务!"); |
| 73 | 82 | } |
| 83 | + | |
| 74 | 84 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
| ... | ... | @@ -36,17 +36,18 @@ public interface AiImageService { |
| 36 | 36 | * |
| 37 | 37 | * @param userId 用户编号 |
| 38 | 38 | * @param drawReqVO 绘制请求 |
| 39 | + * @return 绘画编号 | |
| 39 | 40 | */ |
| 40 | 41 | Long drawImage(Long userId, AiImageDrawReqVO drawReqVO); |
| 41 | 42 | |
| 42 | 43 | /** |
| 43 | - * midjourney 图片生成 | |
| 44 | + * Midjourney imagine(绘画) | |
| 44 | 45 | * |
| 45 | - * @param loginUserId | |
| 46 | - * @param req | |
| 47 | - * @return | |
| 46 | + * @param userId 用户编号 | |
| 47 | + * @param imagineReqVO 绘制请求 | |
| 48 | + * @return 绘画编号 | |
| 48 | 49 | */ |
| 49 | - Long midjourneyImagine(Long loginUserId, AiImageMidjourneyImagineReqVO req); | |
| 50 | + Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO imagineReqVO); | |
| 50 | 51 | |
| 51 | 52 | /** |
| 52 | 53 | * 删除【我的】绘画记录 | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
| ... | ... | @@ -133,10 +133,12 @@ public class AiImageServiceImpl implements AiImageService { |
| 133 | 133 | |
| 134 | 134 | @Override |
| 135 | 135 | @Transactional(rollbackFor = Exception.class) |
| 136 | - public Long midjourneyImagine(Long loginUserId, AiImageMidjourneyImagineReqVO req) { | |
| 136 | + public Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO req) { | |
| 137 | + // TODO @fan:1 和 2 应该放在一个 1 里面。不然 = = 一个逻辑就显得有很多 1、/2、/3、/4;这么分的原因,是方便阅读的时候,容易理解。 | |
| 137 | 138 | // 1、构建 AiImageDO |
| 139 | + // TODO @fan:1)aiImageDO 可以缩写成 image 更简洁;2)可以链式调用,把相同的放在一行里,这样更容易分组 | |
| 138 | 140 | AiImageDO aiImageDO = new AiImageDO(); |
| 139 | - aiImageDO.setUserId(loginUserId); | |
| 141 | + aiImageDO.setUserId(userId); | |
| 140 | 142 | aiImageDO.setPrompt(req.getPrompt()); |
| 141 | 143 | aiImageDO.setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()); |
| 142 | 144 | aiImageDO.setModel(req.getModel()); |
| ... | ... | @@ -145,6 +147,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 145 | 147 | aiImageDO.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); |
| 146 | 148 | // 2、保存 image |
| 147 | 149 | imageMapper.insert(aiImageDO); |
| 150 | + // TODO @fan:3 和 2 之间,应该空一行;因为这里是开始发起请求第三方,是个单独的小块逻辑 | |
| 148 | 151 | // 3、调用 MidjourneyProxy 提交任务 |
| 149 | 152 | MidjourneyImagineReqVO imagineReqVO = BeanUtils.toBean(req, MidjourneyImagineReqVO.class); |
| 150 | 153 | imagineReqVO.setNotifyHook(midjourneyNotifyUrl); |
| ... | ... | @@ -152,12 +155,15 @@ public class AiImageServiceImpl implements AiImageService { |
| 152 | 155 | imagineReqVO.setState(buildParams(req.getWidth(), |
| 153 | 156 | req.getHeight(), req.getVersion(), MidjourneyModelEnum.valueOfModel(req.getModel()))); |
| 154 | 157 | // 5、提交绘画请求 |
| 158 | + // TODO @fan:5 这里,失败的情况,到底抛出异常,还是 RespVO,可以参考 OpenAI 的 API 封装 | |
| 155 | 159 | MidjourneySubmitRespVO submitRespVO = midjourneyProxyClient.imagine(imagineReqVO); |
| 156 | 160 | // 6、保存任务 id (状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)) |
| 157 | 161 | if (!MidjourneySubmitCodeEnum.SUCCESS_CODES.contains(submitRespVO.getCode())) { |
| 158 | 162 | throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, submitRespVO.getDescription()); |
| 159 | 163 | } |
| 164 | + // TODO @fan:7 和 6 之间,应该空一行;这样,最终这个逻辑,就会有 2 个空行,3 小块逻辑:1)插入;2)调用;3)更新 | |
| 160 | 165 | // 7、构建 imageOptions 参数 |
| 166 | + // TODO @fan:1)链式调用;2)其实可以直接使用 AiImageMidjourneyImagineReqVO。不用单独一个 options 类哈。 | |
| 161 | 167 | MidjourneyImageOptions imageOptions = new MidjourneyImageOptions() |
| 162 | 168 | .setWidth(req.getWidth()) |
| 163 | 169 | .setHeight(req.getHeight()) |
| ... | ... | @@ -181,10 +187,11 @@ public class AiImageServiceImpl implements AiImageService { |
| 181 | 187 | if (ObjUtil.notEqual(image.getUserId(), userId)) { |
| 182 | 188 | throw exception(AI_IMAGE_NOT_EXISTS); |
| 183 | 189 | } |
| 184 | - // 2、删除记录 | |
| 190 | + // 2. 删除记录 | |
| 185 | 191 | imageMapper.deleteById(id); |
| 186 | 192 | } |
| 187 | 193 | |
| 194 | + // TODO @fan:建议返回 void;然后如果不存在,就抛出异常哈; | |
| 188 | 195 | @Override |
| 189 | 196 | public Boolean midjourneyNotify(MidjourneyNotifyReqVO notifyReqVO) { |
| 190 | 197 | // 1、根据 job id 查询关联的 image |
| ... | ... | @@ -228,15 +235,19 @@ public class AiImageServiceImpl implements AiImageService { |
| 228 | 235 | .setErrorMessage(notifyReqVO.getFailReason()); |
| 229 | 236 | } |
| 230 | 237 | |
| 238 | + // TODO @fan:1)不用返回 Boolean | |
| 231 | 239 | @Override |
| 232 | - @Transactional(rollbackFor = Exception.class) | |
| 240 | + @Transactional(rollbackFor = Exception.class) // TODO @fan:只操作一个 db,不用事务哈; | |
| 233 | 241 | public Boolean midjourneyAction(Long loginUserId, Long imageId, String customId) { |
| 242 | + // TODO @fan:1)1 和 2,可以写成 1.1、1.2,都是在做校验;2)validateCustomId 可以直接抛出 AI_IMAGE_CUSTOM_ID_NOT_EXISTS 异常;一般情况下,validateXXX 都是失败抛出异常,isXXXValid 返回 true、false | |
| 234 | 243 | // 1、检查 image |
| 244 | + // TODO @fan:1)aiImageDO 缩写成 image; | |
| 235 | 245 | AiImageDO aiImageDO = validateImageExists(imageId); |
| 236 | 246 | // 2、检查 customId |
| 237 | 247 | if (!validateCustomId(customId, aiImageDO.getButtons())) { |
| 238 | 248 | throw exception(AI_IMAGE_CUSTOM_ID_NOT_EXISTS); |
| 239 | 249 | } |
| 250 | + // TODO @fan:2 和 3 之间,空一行 | |
| 240 | 251 | // 3、调用 midjourney proxy |
| 241 | 252 | MidjourneySubmitRespVO submitRespVO = midjourneyProxyClient.action( |
| 242 | 253 | new MidjourneyActionReqVO() |
| ... | ... | @@ -248,8 +259,10 @@ public class AiImageServiceImpl implements AiImageService { |
| 248 | 259 | if (!MidjourneySubmitCodeEnum.SUCCESS_CODES.contains(submitRespVO.getCode())) { |
| 249 | 260 | throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, submitRespVO.getDescription()); |
| 250 | 261 | } |
| 262 | + // TODO 6 和 4 之间空一行; | |
| 251 | 263 | // 4、新增 image 记录 |
| 252 | 264 | AiImageDO newImage = BeanUtils.toBean(aiImageDO, AiImageDO.class); |
| 265 | + // TODO @fan:最好不要 copy 属性。因为未来如果加属性,可能会导致额外 copy 了;最好是 new 赋值下,显示声明。 | |
| 253 | 266 | // 4.1、重置参数 |
| 254 | 267 | newImage.setId(null); |
| 255 | 268 | newImage.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); |
| ... | ... | @@ -290,6 +303,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 290 | 303 | return SpringUtil.getBean(getClass()); |
| 291 | 304 | } |
| 292 | 305 | |
| 306 | + // TODO @fan:这个是不是应该放在 MJ API 的封装里面搞哈? | |
| 293 | 307 | /** |
| 294 | 308 | * 构建 Midjourney 自定义参数 |
| 295 | 309 | * | ... | ... |
yudao-server/src/main/resources/application-local.yaml
| ... | ... | @@ -76,14 +76,13 @@ server: |
| 76 | 76 | enabled: true |
| 77 | 77 | charset: UTF-8 |
| 78 | 78 | force: true |
| 79 | -# ai | |
| 79 | +# ai TODO @fan:这个融合到 yudao.ai 那好点哈 | |
| 80 | 80 | ai: |
| 81 | 81 | midjourney-proxy: |
| 82 | 82 | url: https://api.holdai.top/mj |
| 83 | 83 | notifyUrl: http://61d61685.r21.cpolar.top/admin-api/ai/image/midjourney-notify |
| 84 | 84 | key: sk-c3qxUCVKsPfdQiYU8440E3Fc8dE5424d9cB124A4Ee2489E3 |
| 85 | 85 | |
| 86 | - | |
| 87 | 86 | --- #################### 定时任务相关配置 #################### |
| 88 | 87 | |
| 89 | 88 | # Quartz 配置项,对应 QuartzProperties 配置类 | ... | ... |