From 3b73dc21350160f98071f9e6d303a808c7c392bf Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Mon, 18 May 2026 15:36:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E6=B6=88=E6=81=AF=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../freemarker/MyBatisPlusGenerator.java | 1 + .../boot/common/enums/LogModuleEnum.java | 3 +- .../controller/AiGenerationController.java | 63 ++++-- .../mini/mapper/MiniUserSubscribeMapper.java | 17 ++ .../mini/model/entity/MiniUserSubscribe.java | 74 +++++++ .../mini/model/enums/SubscribeStatusEnum.java | 46 ++++ .../mini/model/form/WxSubscribeAuthForm.java | 26 +++ .../mini/model/form/WxSubscribeSendForm.java | 33 +++ .../boot/mini/model/vo/UserUploadMediaVO.java | 43 ++++ .../boot/mini/model/vo/WxApiResponse.java | 16 ++ .../mini/service/AiGenerationService.java | 6 + .../boot/mini/service/WxSubscribeService.java | 14 ++ .../service/impl/AiGenerationServiceImpl.java | 209 +++++++++++++----- .../service/impl/WxSubscribeServiceImpl.java | 147 ++++++++++++ src/main/resources/application-dev.yml | 9 +- .../mapper/mini/MiniUserSubscribeMapper.xml | 16 ++ 16 files changed, 652 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java create mode 100644 src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java create mode 100644 src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java create mode 100644 src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java create mode 100644 src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java create mode 100644 src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml diff --git a/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java b/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java index 50625c8..5aef6e4 100644 --- a/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java +++ b/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java @@ -101,6 +101,7 @@ public class MyBatisPlusGenerator { ,new TableConfig("mini_sign_record", IdType.AUTO, "mini") ,new TableConfig("mini_ai_generation_task", IdType.AUTO, "mini") ,new TableConfig("mini_ai_task_media", IdType.AUTO, "mini") + ,new TableConfig("mini_user_subscribe", IdType.AUTO, "mini") // ,new TableConfig("mini_stray_animal", IdType.AUTO, "mini") // ,new TableConfig("mini_stray_animal", IdType.INPUT, "minitest") diff --git a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java index 124b1c2..7c9bbea 100644 --- a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java @@ -34,7 +34,8 @@ public enum LogModuleEnum implements IBaseEnum { POINT_RULE(102, "积分规则"), SIGN_RECORD(103, "签到记录"), AI_TASK_MEDIA(104, "AI任务媒体"), - AI_GENERATION_TASK(105, "AI生成任务"); + AI_GENERATION_TASK(105, "AI生成任务"), + USER_SUBSCRIBE(106, "用户订阅模板消息"); @EnumValue private final Integer value; diff --git a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java index fe0bb3e..abdf9cc 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,14 +6,15 @@ import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.framework.security.util.SecurityUtils; -import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; -import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; -import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; -import com.youlai.boot.mini.model.form.AiVideoGenerateForm; -import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; +import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; +import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; +import com.youlai.boot.mini.service.WxSubscribeService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -22,8 +23,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; -@Tag(name = "AI生成图片视频相关接口") +@Tag(name = "AI生成相关接口") @RestController @RequestMapping("/api/v1/mini/ai/generation") @RequiredArgsConstructor @@ -31,24 +33,21 @@ import java.util.List; public class AiGenerationController { private final AiGenerationService aiGenerationService; + private final WxSubscribeService wxSubscribeService; - @Operation(summary = "上传AI生成参考文件", operationId = "AiReferenceSaveFile") - @PostMapping(value = "/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "上传AI生成参考文件") + @PostMapping(value = "/upload/reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @RepeatSubmit @Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.INSERT) public Result> uploadReferenceFile( - @RequestPart(name = "images", required = false) List images, - @RequestPart(name = "videos", required = false) List videos + @RequestParam(name = "images", required = false) List images, + @RequestParam(name = "videos", required = false) List videos ) { Long userId = SecurityUtils.getUserId(); List urlList = aiGenerationService.uploadReferenceFile(images, videos, userId); return Result.success(urlList); } - //TODO: 查询用户最近上传的图片视频接口 - - //TODO:用户删除上次的图片视频接口 - @Operation(summary = "提交单图生成任务") @PostMapping("/generate-single-image") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) @@ -100,5 +99,39 @@ public class AiGenerationController { return Result.success(success); } - //TODO: 后续在回调中 增加用户小程序订阅通知 + @Operation(summary = "上报微信订阅消息授权状态") + @PostMapping("/subscribe/auth/report") + @Log(module = LogModuleEnum.USER_SUBSCRIBE, value = ActionTypeEnum.UPDATE) + public Result reportSubscribeAuth(@Valid @RequestBody WxSubscribeAuthForm form) { + form.setUserId(SecurityUtils.getUserId()); + wxSubscribeService.reportAuth(form); + return Result.success(); + } + + @Operation(summary = "发送微信订阅消息") + @PostMapping("/subscribe/send") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result sendSubscribeMessage(@Valid @RequestBody WxSubscribeSendForm form) { + form.setUserId(SecurityUtils.getUserId()); + Boolean result = wxSubscribeService.sendMessage(form); + return Result.success(result); + } + + @Operation(summary = "查询当前用户最近上传的图片视频") + @GetMapping("/my/recent-upload") + public Result> getMyRecentUpload() { + Long userId = SecurityUtils.getUserId(); + List voList = aiGenerationService.getRecentUploadVO(userId); + return Result.success(voList); + } + + @Operation(summary = "删除用户上传的图片/视频") + @DeleteMapping("/my/upload/delete") + @Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.DELETE) + public Result deleteMyUpload(@RequestParam String uuid) { + Long userId = SecurityUtils.getUserId(); + boolean success = aiGenerationService.deleteUploadMedia(userId, uuid); + return Result.success(success); + } + } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java new file mode 100644 index 0000000..bb7b331 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java @@ -0,0 +1,17 @@ +package com.youlai.boot.mini.mapper; + +import com.youlai.boot.mini.model.entity.MiniUserSubscribe; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +/** +* 用户订阅状态表 Mapper 接口 +* +* @author jwy +* @since +*/ +public interface MiniUserSubscribeMapper extends BaseMapper { + + String getOpenidByUserId(Long aLong); +} diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java new file mode 100644 index 0000000..eaceff0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java @@ -0,0 +1,74 @@ +package com.youlai.boot.mini.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("mini_user_subscribe") +@Schema(description = "用户订阅状态表") +public class MiniUserSubscribe implements Serializable { + + @TableId(value = "id", type = IdType.AUTO) + @Schema(description = "") + private Long id; + + + @TableField("uuid") + @Schema(description = "uuid唯一标识,前后端用这个进行数据交互") + private String uuid; + + @TableField("user_id") + @Schema(description = "用户id") + private Long userId; + + @TableField("template_id") + @Schema(description = "订阅消息模板ID") + private String templateId; + + @TableField("status") + @Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意") + private Integer status; + + @TableField("create_time") + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + @TableField("create_timestamp") + @Schema(description = "创建时间毫秒级时间戳") + private Long createTimestamp; + + @TableField("create_by") + @Schema(description = "创建人ID") + private Long createBy; + + @TableField("update_time") + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + + @TableField("update_timestamp") + @Schema(description = "更新时间毫秒级时间戳") + private Long updateTimestamp; + + @TableField("update_by") + @Schema(description = "修改人ID") + private Long updateBy; + + @TableField("is_deleted") + @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") + private Boolean deleted; + + +} diff --git a/src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java b/src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java new file mode 100644 index 0000000..28bbd7c --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java @@ -0,0 +1,46 @@ +package com.youlai.boot.mini.model.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; + + +@Schema(description = "微信订阅消息授权状态") +public enum SubscribeStatusEnum { + + REJECT(0, "拒绝授权"), + ONCE(1, "同意一次"), + ALWAYS(2, "总是同意"); + + private final Integer value; + private final String desc; + + SubscribeStatusEnum(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + @JsonValue + public Integer getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + @JsonCreator + public static SubscribeStatusEnum from(Integer value) { + if (value == null) return null; + for (SubscribeStatusEnum e : values()) { + if (e.value.equals(value)) { + return e; + } + } + return null; + } + + public static boolean contains(Integer value) { + return from(value) != null; + } +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java new file mode 100644 index 0000000..e6c15d4 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java @@ -0,0 +1,26 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 上报用户订阅授权请求 + */ +@Data +@Schema(description = "上报用户订阅授权请求") +public class WxSubscribeAuthForm { + + @NotBlank(message = "用户ID不能为空") + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long userId; + + @NotBlank(message = "模板ID不能为空") + @Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String templateId; + + @NotNull(message = "授权状态不能为空") + @Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java new file mode 100644 index 0000000..c51ad18 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java @@ -0,0 +1,33 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import java.util.Map; + +/** + * 发送订阅消息请求 + */ +@Data +@Schema(description = "发送订阅消息请求") +public class WxSubscribeSendForm { + + @NotBlank(message = "用户ID不能为空") + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long userId; + + @NotBlank(message = "模板ID不能为空") + @Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String templateId; + + @Schema(description = "跳转页面,例:/pages/result/index?id=123") + private String page; + + @Schema(description = "跳转版本:developer=开发版 trial=体验版 formal=正式版,默认formal") + private String miniProgramState = "developer"; + + @NotNull(message = "模板参数不能为空") + @Schema(description = "模板参数,key是模板字段名,value是对应的值", requiredMode = Schema.RequiredMode.REQUIRED) + private Map templateParams; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java new file mode 100644 index 0000000..f0ef7f1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java @@ -0,0 +1,43 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 用户上传媒体返回VO + * + * @author youlai + */ +@Data +@Schema(description = "用户上传媒体返回VO") +public class UserUploadMediaVO { + + @Schema(description = "媒体ID") + private Long id; + + @Schema(description = "媒体类型:image-图片,video-视频") + private String mediaType; + + @Schema(description = "媒体访问地址") + private String sourceUrl; + + @Schema(description = "缩略图地址(视频才有)") + private String thumbnailUrl; + + @Schema(description = "视频时长(秒,视频才有)") + private Integer duration; + + @Schema(description = "图片宽度") + private Integer width; + + @Schema(description = "图片高度") + private Integer height; + + @Schema(description = "上传时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java b/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java new file mode 100644 index 0000000..9b903b4 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java @@ -0,0 +1,16 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 微信接口通用返回 + */ +@Data +public class WxApiResponse { + @JsonProperty("errcode") + private Integer errcode; + + @JsonProperty("errmsg") + private String errmsg; +} diff --git a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java index 316f3ef..9634936 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,5 +1,6 @@ package com.youlai.boot.mini.service; +import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; @@ -7,6 +8,7 @@ import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -29,4 +31,8 @@ public interface AiGenerationService { MiniAiGenerationTask getTaskByUuid(String uuid); + List getRecentUploadVO(Long userId); + + boolean deleteUploadMedia(Long userId, String uuid); + } diff --git a/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java b/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java new file mode 100644 index 0000000..99945d2 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java @@ -0,0 +1,14 @@ +package com.youlai.boot.mini.service; + +import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; +import com.youlai.boot.mini.model.form.WxSubscribeSendForm; + +/** + * 微信订阅消息服务 + */ +public interface WxSubscribeService { + + void reportAuth(WxSubscribeAuthForm form); + + Boolean sendMessage(WxSubscribeSendForm form); +} diff --git a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java index be37209..9678429 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java @@ -1,11 +1,10 @@ package com.youlai.boot.mini.service.impl; -import cn.hutool.core.date.DateUtil; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -16,7 +15,6 @@ import com.youlai.boot.common.exception.MsgException; import com.youlai.boot.common.util.FileUtils; import com.youlai.boot.common.util.JavaVCUtils; import com.youlai.boot.common.util.RandomNumberUtils; -import com.youlai.boot.file.model.FileInfo; import com.youlai.boot.file.service.FileService; import com.youlai.boot.file.service.impl.AliyunFileService; import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper; @@ -24,18 +22,15 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; -import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; -import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; -import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; -import com.youlai.boot.mini.model.form.AiVideoGenerateForm; -import com.youlai.boot.mini.model.form.MiniDeductPointForm; +import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; -import com.youlai.boot.mini.model.form.AiCallbackImage; -import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; +import com.youlai.boot.mini.service.WxSubscribeService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,7 +41,11 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; @Slf4j @Service @@ -58,6 +57,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final FileService fileService; private final AliyunFileService aliyunFileService; private final MiniPointRecordService pointRecordService; + private final WxSubscribeService wxSubscribeService; //OSS存储目录配置 private static final String OSS_IMAGE_DIR = "ai/image/"; @@ -91,6 +91,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${ai.default.video-model:doubao-seedance-2-0-260128}") private String aiDefaultVideoModel; + @Value("${subscribe.template}") + private String subscribeTemplate; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 @@ -478,8 +481,8 @@ public class AiGenerationServiceImpl implements AiGenerationService { String videoTaskUuid = vo.getId(); // 根据第三方返回的视频任务uuid查询对应的任务 MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() - .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) - .eq(MiniAiGenerationTask::getDeleted, false)); + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); if (task == null) { log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); return false; @@ -515,28 +518,46 @@ public class AiGenerationServiceImpl implements AiGenerationService { MiniAiTaskMedia media = new MiniAiTaskMedia(); String mediaUuid = UUID.randomUUID().toString().replace("-", ""); media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") - .setMediaType("video") - .setSourceUrl(ossUrl) - .setDuration(vo.getDuration()) - .setCreateBy(task.getCreateBy()) - .setCreateTimestamp(System.currentTimeMillis()) - .setCreateTime(new Date()); + .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") + .setMediaType("video") + .setSourceUrl(ossUrl) + .setDuration(vo.getDuration()) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI视频作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("视频任务{}发送订阅消息失败", task.getId(), e); + } + } } success = true; @@ -603,30 +624,49 @@ public class AiGenerationServiceImpl implements AiGenerationService { } media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") - .setMediaType("image") - .setSourceUrl(ossUrl) - .setWidth(width) - .setHeight(height) - .setCreateBy(task.getCreateBy()) - .setCreateTimestamp(System.currentTimeMillis()) - .setCreateTime(new Date()); + .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") + .setMediaType("image") + .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + // 根据实际模板字段调整参数 + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI绘画作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("单图任务{}发送订阅消息失败", task.getId(), e); + } + } } log.info("四宫格任务{}回调处理完成,状态:{}", taskUuid, status); @@ -703,14 +743,33 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + // 根据实际模板字段调整参数 + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI绘画作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("单图任务{}发送订阅消息失败", task.getId(), e); + } + } } // 更新任务状态 @@ -793,4 +852,48 @@ public class AiGenerationServiceImpl implements AiGenerationService { } + @Override + public List getRecentUploadVO(Long userId) { + List mediaList = aiTaskMediaMapper.selectList(new LambdaQueryWrapper() + .eq(MiniAiTaskMedia::getMiniUserId, userId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .orderByDesc(MiniAiTaskMedia::getCreateTime) + .last("limit 20")); + + if (mediaList == null || mediaList.isEmpty()) { + return Collections.emptyList(); + } + + return mediaList.stream() + .map(this::convertToVO) + .toList(); + } + + private UserUploadMediaVO convertToVO(MiniAiTaskMedia media) { + UserUploadMediaVO vo = new UserUploadMediaVO(); + BeanUtil.copyProperties(media, vo); + return vo; + } + + @Override + public boolean deleteUploadMedia(Long userId, String uuid) { + // 校验是否是当前用户的上传媒体 + MiniAiTaskMedia media = aiTaskMediaMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniAiTaskMedia::getUuid, uuid) + .eq(MiniAiTaskMedia::getMiniUserId, userId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .last("limit 1")); + if (media == null) { + log.warn("用户{}删除媒体失败,媒体{}不存在或无权限", userId, uuid); + return false; + } + // 软删除 + media.setDeleted(true); + media.setUpdateTime(new Date()); + media.setUpdateTimestamp(System.currentTimeMillis()); + return aiTaskMediaMapper.updateById(media) > 0; + } + } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java new file mode 100644 index 0000000..6d0f2a0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java @@ -0,0 +1,147 @@ +package com.youlai.boot.mini.service.impl; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.common.exception.MsgException; +import com.youlai.boot.mini.model.enums.SubscribeStatusEnum; +import com.youlai.boot.mini.mapper.MiniUserSubscribeMapper; +import com.youlai.boot.mini.model.entity.MiniUserSubscribe; +import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; +import com.youlai.boot.mini.model.form.WxSubscribeSendForm; +import com.youlai.boot.mini.model.vo.WxApiResponse; +import com.youlai.boot.mini.service.WxSubscribeService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * 微信订阅消息服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WxSubscribeServiceImpl implements WxSubscribeService { + + private final WxMaService wxMaService; + private final MiniUserSubscribeMapper userSubscribeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void reportAuth(WxSubscribeAuthForm form) { + try { + // 查询用户openid + String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); + if (StrUtil.isBlank(openid)) { + throw new MsgException("用户未绑定微信"); + } + long currentTime = System.currentTimeMillis(); + + // 查询是否已有授权记录 + MiniUserSubscribe exist = userSubscribeMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniUserSubscribe::getUserId, form.getUserId()) + .eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) + .eq(MiniUserSubscribe::getDeleted, 0) + .last("limit 1")); + + if (exist != null) { + exist.setStatus(form.getStatus()); + exist.setUpdateTime(new Date()); + exist.setUpdateTimestamp(currentTime); + userSubscribeMapper.updateById(exist); + } else { + MiniUserSubscribe subscribe = new MiniUserSubscribe(); + subscribe.setUserId(form.getUserId()); + subscribe.setTemplateId(form.getTemplateId()); + subscribe.setStatus(form.getStatus()); + subscribe.setCreateTime(new Date()); + subscribe.setCreateTimestamp(currentTime); + subscribe.setUpdateTime(new Date()); + subscribe.setUpdateTimestamp(currentTime); + subscribe.setDeleted(false); + userSubscribeMapper.insert(subscribe); + } + log.info("用户{}订阅授权上报成功,模板ID:{},状态:{}", form.getUserId(), form.getTemplateId(), form.getStatus()); + } catch (Exception e) { + log.error("上报用户订阅授权失败,请求参数:{}", JSONUtil.toJsonStr(form), e); + throw new MsgException("上报授权状态失败"); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean sendMessage(WxSubscribeSendForm form) { + log.info("开始发送订阅消息,请求参数:{}", JSONUtil.toJsonStr(form)); + try { + // 1. 查询用户openid + String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); + if (StrUtil.isBlank(openid)) { + log.error("用户{}未绑定微信openid,无法发送订阅消息", form.getUserId()); + return false; + } + + // 2. 构建微信订阅消息参数 + List> dataList = new ArrayList<>(); + for (Map.Entry entry : form.getTemplateParams().entrySet()) { + Map dataMap = new HashMap<>(); + dataMap.put("name", entry.getKey()); + dataMap.put("value", entry.getValue()); + dataList.add(dataMap); + } + + Map messageMap = new HashMap<>(); + messageMap.put("touser", openid); + messageMap.put("template_id", form.getTemplateId()); + messageMap.put("page", form.getPage()); + messageMap.put("miniprogram_state", form.getMiniProgramState()); + messageMap.put("lang", "zh_CN"); + messageMap.put("data", dataList); + + // 3. 调用微信接口发送 + String responseStr = wxMaService.post("https://api.weixin.qq.com/cgi-bin/message/subscribe/send", JSONUtil.toJsonStr(messageMap)); + WxApiResponse response = JSONUtil.toBean(responseStr, WxApiResponse.class); + log.info("微信订阅消息发送结果,用户:{},返回:{}", form.getUserId(), JSONUtil.toJsonStr(response)); + + // 4. 根据返回结果更新用户订阅状态 + MiniUserSubscribe subscribe = userSubscribeMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniUserSubscribe::getUserId, form.getUserId()) + .eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) + .eq(MiniUserSubscribe::getDeleted, 0) + .last("limit 1") + ); + + if (response.getErrcode() == 0) { + log.info("订阅消息发送成功,用户:{},模板ID:{}", form.getUserId(), form.getTemplateId()); + // 如果是同意一次的状态,发送后更新为未授权 + if (subscribe != null && SubscribeStatusEnum.ONCE.getValue().equals(subscribe.getStatus())) { + subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); + subscribe.setUpdateTime(new Date()); + userSubscribeMapper.updateById(subscribe); + } + return true; + } else if (response.getErrcode() == 43101) { + // 用户未订阅,更新本地状态为拒绝 + log.warn("用户{}未订阅该模板消息,更新本地状态为拒绝", form.getUserId()); + if (subscribe != null) { + subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); + subscribe.setUpdateTime(new Date()); + userSubscribeMapper.updateById(subscribe); + } + return false; + } else { + log.error("订阅消息发送失败,错误码:{},错误信息:{}", response.getErrcode(), response.getErrmsg()); + return false; + } + } catch (Exception e) { + log.error("发送订阅消息异常,请求参数:{}", JSONUtil.toJsonStr(form), e); + return false; + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6c512aa..35c102f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -226,8 +226,8 @@ captcha: # 微信小程序配置 wx: miniapp: - appid: Your_AppId - secret: Your_AppSecret + appid: wx56425c3301f5c6df + secret: 7c4060199f49b0ab872b06b97cef2ac4 # AIGeneration 配置 ai: @@ -244,3 +244,8 @@ ai: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 +# 订阅模板配置 +subscribe: + template: "7m5Vu4gaCo2zY6hzID4yqEv94y1guuOGxdPOJYV_xHE" + + diff --git a/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml new file mode 100644 index 0000000..c418984 --- /dev/null +++ b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml @@ -0,0 +1,16 @@ + + + + + + + +