Commit ec872c702c6a5da948371a16b9aafa11a13a912d
1 parent
541ee813
mp:增加上传临时素材的接口
Showing
14 changed files
with
426 additions
and
66 deletions
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java
| @@ -4,9 +4,6 @@ import lombok.*; | @@ -4,9 +4,6 @@ import lombok.*; | ||
| 4 | import io.swagger.annotations.*; | 4 | import io.swagger.annotations.*; |
| 5 | import javax.validation.constraints.*; | 5 | import javax.validation.constraints.*; |
| 6 | 6 | ||
| 7 | -/** | ||
| 8 | - * @author fengdan | ||
| 9 | - */ | ||
| 10 | @ApiModel("管理后台 - 公众号账号更新 Request VO") | 7 | @ApiModel("管理后台 - 公众号账号更新 Request VO") |
| 11 | @Data | 8 | @Data |
| 12 | @EqualsAndHashCode(callSuper = true) | 9 | @EqualsAndHashCode(callSuper = true) |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http
0 → 100644
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.controller.admin.material; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO; | ||
| 5 | +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; | ||
| 6 | +import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert; | ||
| 7 | +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; | ||
| 8 | +import cn.iocoder.yudao.module.mp.service.material.MpMaterialService; | ||
| 9 | +import io.swagger.annotations.Api; | ||
| 10 | +import io.swagger.annotations.ApiOperation; | ||
| 11 | +import org.springframework.validation.annotation.Validated; | ||
| 12 | +import org.springframework.web.bind.annotation.PostMapping; | ||
| 13 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 14 | +import org.springframework.web.bind.annotation.RestController; | ||
| 15 | + | ||
| 16 | +import javax.annotation.Resource; | ||
| 17 | +import javax.validation.Valid; | ||
| 18 | +import java.io.IOException; | ||
| 19 | + | ||
| 20 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 21 | + | ||
| 22 | +@Api(tags = "管理后台 - 公众号素材") | ||
| 23 | +@RestController | ||
| 24 | +@RequestMapping("/mp/material") | ||
| 25 | +@Validated | ||
| 26 | +public class MpMaterialController { | ||
| 27 | + | ||
| 28 | + @Resource | ||
| 29 | + private MpMaterialService mpMaterialService; | ||
| 30 | + | ||
| 31 | + @ApiOperation("上传临时素材") | ||
| 32 | + @PostMapping("/upload-temporary") | ||
| 33 | + public CommonResult<MpMaterialUploadRespVO> uploadTemporaryMaterial( | ||
| 34 | + @Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException { | ||
| 35 | + MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO); | ||
| 36 | + return success(MpMaterialConvert.INSTANCE.convert(material)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.controller.admin.material.vo; | ||
| 2 | + | ||
| 3 | +import io.swagger.annotations.ApiModel; | ||
| 4 | +import io.swagger.annotations.ApiModelProperty; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +@ApiModel("管理后台 - 公众号素材上传结果 Response VO") | ||
| 8 | +@Data | ||
| 9 | +public class MpMaterialUploadRespVO { | ||
| 10 | + | ||
| 11 | + @ApiModelProperty(value = "素材的 media_id", required = true, example = "123") | ||
| 12 | + private String mediaId; | ||
| 13 | + | ||
| 14 | + @ApiModelProperty(value = "素材的 URL", required = true, example = "https://www.iocoder.cn/1.png") | ||
| 15 | + private String url; | ||
| 16 | + | ||
| 17 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.controller.admin.material.vo; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.annotation.JsonIgnore; | ||
| 4 | +import io.swagger.annotations.ApiModel; | ||
| 5 | +import io.swagger.annotations.ApiModelProperty; | ||
| 6 | +import lombok.Data; | ||
| 7 | +import org.springframework.web.multipart.MultipartFile; | ||
| 8 | + | ||
| 9 | +import javax.validation.constraints.NotEmpty; | ||
| 10 | +import javax.validation.constraints.NotNull; | ||
| 11 | + | ||
| 12 | +@ApiModel("管理后台 - 公众号素材上传临时 Request VO") | ||
| 13 | +@Data | ||
| 14 | +public class MpMaterialUploadTemporaryReqVO { | ||
| 15 | + | ||
| 16 | + @ApiModelProperty(value = "公众号账号的编号", required = true, example = "2048") | ||
| 17 | + @NotNull(message = "公众号账号的编号不能为空") | ||
| 18 | + private Long accountId; | ||
| 19 | + | ||
| 20 | + @ApiModelProperty(value = "文件类型", required = true, example = "image", notes = "参见 WxConsts.MediaFileType 枚举") | ||
| 21 | + @NotEmpty(message = "文件类型不能为空") | ||
| 22 | + private String type; | ||
| 23 | + | ||
| 24 | + @ApiModelProperty(value = "文件附件", required = true) | ||
| 25 | + @NotNull(message = "文件不能为空") | ||
| 26 | + @JsonIgnore // 避免被操作日志,进行序列化,导致报错 | ||
| 27 | + private MultipartFile file; | ||
| 28 | + | ||
| 29 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.convert.material; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO; | ||
| 4 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | ||
| 5 | +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; | ||
| 6 | +import org.mapstruct.Mapper; | ||
| 7 | +import org.mapstruct.Mapping; | ||
| 8 | +import org.mapstruct.Mappings; | ||
| 9 | +import org.mapstruct.factory.Mappers; | ||
| 10 | + | ||
| 11 | +@Mapper | ||
| 12 | +public interface MpMaterialConvert { | ||
| 13 | + | ||
| 14 | + MpMaterialConvert INSTANCE = Mappers.getMapper(MpMaterialConvert.class); | ||
| 15 | + | ||
| 16 | + @Mappings({ | ||
| 17 | + @Mapping(target = "id", ignore = true), | ||
| 18 | + @Mapping(source = "account.id", target = "accountId"), | ||
| 19 | + @Mapping(source = "account.appId", target = "appId"), | ||
| 20 | + }) | ||
| 21 | + MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account); | ||
| 22 | + | ||
| 23 | + MpMaterialUploadRespVO convert(MpMaterialDO bean); | ||
| 24 | + | ||
| 25 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.dal.dataobject.material; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | ||
| 4 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | ||
| 5 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 8 | +import lombok.*; | ||
| 9 | +import me.chanjar.weixin.common.api.WxConsts; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 公众号素材 DO | ||
| 13 | + * | ||
| 14 | + * 1. <a href="https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html">临时素材</a> | ||
| 15 | + * 2. <a href="https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Adding_Permanent_Assets.html">永久素材</a> | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +@TableName("mp_material") | ||
| 20 | +@KeySequence("mp_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 | ||
| 21 | +@Data | ||
| 22 | +@EqualsAndHashCode(callSuper = true) | ||
| 23 | +@ToString(callSuper = true) | ||
| 24 | +@Builder | ||
| 25 | +@NoArgsConstructor | ||
| 26 | +@AllArgsConstructor | ||
| 27 | +public class MpMaterialDO extends BaseDO { | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 主键 | ||
| 31 | + */ | ||
| 32 | + @TableId | ||
| 33 | + private Long id; | ||
| 34 | + /** | ||
| 35 | + * 微信公众号 ID | ||
| 36 | + * | ||
| 37 | + * 关联 {@link MpAccountDO#getId()} | ||
| 38 | + */ | ||
| 39 | + private Long accountId; | ||
| 40 | + /** | ||
| 41 | + * 微信公众号 appid | ||
| 42 | + * | ||
| 43 | + * 冗余 {@link MpAccountDO#getAppId()} | ||
| 44 | + */ | ||
| 45 | + private String appId; | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 公众号素材 id | ||
| 49 | + */ | ||
| 50 | + private String mediaId; | ||
| 51 | + /** | ||
| 52 | + * 文件类型 | ||
| 53 | + * | ||
| 54 | + * 枚举 {@link WxConsts.MediaFileType} | ||
| 55 | + */ | ||
| 56 | + private String type; | ||
| 57 | + /** | ||
| 58 | + * 是否永久 | ||
| 59 | + * | ||
| 60 | + * true - 永久素材 | ||
| 61 | + * false - 临时素材 | ||
| 62 | + */ | ||
| 63 | + private Boolean permanent; | ||
| 64 | + /** | ||
| 65 | + * 文件服务器的 URL | ||
| 66 | + */ | ||
| 67 | + private String url; | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * 名字 | ||
| 71 | + * | ||
| 72 | + * 只有【永久素材】使用 | ||
| 73 | + */ | ||
| 74 | + private String name; | ||
| 75 | + /** | ||
| 76 | + * 公众号文件 URL | ||
| 77 | + * | ||
| 78 | + * 只有【永久素材】使用 | ||
| 79 | + */ | ||
| 80 | + private String mpUrl; | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 视频素材的标题 | ||
| 84 | + * | ||
| 85 | + * 只有【永久素材】使用 | ||
| 86 | + */ | ||
| 87 | + private String title; | ||
| 88 | + /** | ||
| 89 | + * 视频素材的描述 | ||
| 90 | + * | ||
| 91 | + * 只有【永久素材】使用 | ||
| 92 | + */ | ||
| 93 | + private String introduction; | ||
| 94 | + | ||
| 95 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.dal.mysql.material; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 4 | +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; | ||
| 5 | +import org.apache.ibatis.annotations.Mapper; | ||
| 6 | + | ||
| 7 | +@Mapper | ||
| 8 | +public interface MpMaterialMapper extends BaseMapperX<MpMaterialDO> { | ||
| 9 | + | ||
| 10 | + default MpMaterialDO selectByAccountIdAndMediaId(Long accountId, String mediaId) { | ||
| 11 | + return selectOne(MpMaterialDO::getAccountId, accountId, | ||
| 12 | + MpMaterialDO::getMediaId, mediaId); | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java
| @@ -80,4 +80,25 @@ public class MpUtils { | @@ -80,4 +80,25 @@ public class MpUtils { | ||
| 80 | ValidationUtils.validate(validator, message, group); | 80 | ValidationUtils.validate(validator, message, group); |
| 81 | } | 81 | } |
| 82 | 82 | ||
| 83 | + /** | ||
| 84 | + * 根据消息类型,获得对应的媒体文件类型 | ||
| 85 | + * | ||
| 86 | + * 注意,不会返回 WxConsts.MediaFileType.THUMB,因为该类型会有明确标注 | ||
| 87 | + * | ||
| 88 | + * @param messageType 消息类型 {@link WxConsts.XmlMsgType} | ||
| 89 | + * @return 媒体文件类型 {@link WxConsts.MediaFileType} | ||
| 90 | + */ | ||
| 91 | + public static String getMediaFileType(String messageType) { | ||
| 92 | + switch (messageType) { | ||
| 93 | + case WxConsts.XmlMsgType.IMAGE: | ||
| 94 | + return WxConsts.MediaFileType.IMAGE; | ||
| 95 | + case WxConsts.XmlMsgType.VOICE: | ||
| 96 | + return WxConsts.MediaFileType.VOICE; | ||
| 97 | + case WxConsts.XmlMsgType.VIDEO: | ||
| 98 | + return WxConsts.MediaFileType.VIDEO; | ||
| 99 | + default: | ||
| 100 | + return WxConsts.MediaFileType.FILE; | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | + | ||
| 83 | } | 104 | } |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.service.material; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; | ||
| 4 | +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; | ||
| 5 | +import me.chanjar.weixin.common.api.WxConsts; | ||
| 6 | + | ||
| 7 | +import javax.validation.Valid; | ||
| 8 | +import java.io.IOException; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 公众号素材 Service 接口 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +public interface MpMaterialService { | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * 获得素材的 URL | ||
| 19 | + * | ||
| 20 | + * 该 URL 来自我们自己的文件服务器存储的 URL,不是公众号存储的 URL | ||
| 21 | + * | ||
| 22 | + * @param accountId 公众号账号编号 | ||
| 23 | + * @param mediaId 公众号素材 id | ||
| 24 | + * @param type 文件类型 {@link WxConsts.MediaFileType} | ||
| 25 | + * @return 素材的 URL | ||
| 26 | + */ | ||
| 27 | + String downloadMaterialUrl(Long accountId, String mediaId, String type); | ||
| 28 | + | ||
| 29 | + MpMaterialDO uploadTemporaryMaterial(@Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException; | ||
| 30 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.service.material; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.io.FileTypeUtil; | ||
| 4 | +import cn.hutool.core.io.FileUtil; | ||
| 5 | +import cn.hutool.core.util.ObjUtil; | ||
| 6 | +import cn.iocoder.yudao.module.infra.api.file.FileApi; | ||
| 7 | +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert; | ||
| 9 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | ||
| 10 | +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; | ||
| 11 | +import cn.iocoder.yudao.module.mp.dal.mysql.material.MpMaterialMapper; | ||
| 12 | +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; | ||
| 13 | +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; | ||
| 14 | +import lombok.extern.slf4j.Slf4j; | ||
| 15 | +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||
| 16 | +import me.chanjar.weixin.common.error.WxErrorException; | ||
| 17 | +import me.chanjar.weixin.mp.api.WxMpService; | ||
| 18 | +import org.springframework.context.annotation.Lazy; | ||
| 19 | +import org.springframework.stereotype.Service; | ||
| 20 | +import org.springframework.validation.annotation.Validated; | ||
| 21 | + | ||
| 22 | +import javax.annotation.Resource; | ||
| 23 | +import java.io.File; | ||
| 24 | +import java.io.IOException; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * 公众号素材 Service 接口 | ||
| 28 | + * | ||
| 29 | + * @author 芋道源码 | ||
| 30 | + */ | ||
| 31 | +@Service | ||
| 32 | +@Validated | ||
| 33 | +@Slf4j | ||
| 34 | +public class MpMaterialServiceImpl implements MpMaterialService { | ||
| 35 | + | ||
| 36 | + @Resource | ||
| 37 | + private MpMaterialMapper mpMaterialMapper; | ||
| 38 | + | ||
| 39 | + @Resource | ||
| 40 | + private FileApi fileApi; | ||
| 41 | + | ||
| 42 | + @Resource | ||
| 43 | + @Lazy // 延迟加载,解决循环依赖的问题 | ||
| 44 | + private MpAccountService mpAccountService; | ||
| 45 | + | ||
| 46 | + @Resource | ||
| 47 | + @Lazy // 延迟加载,解决循环依赖的问题 | ||
| 48 | + private MpServiceFactory mpServiceFactory; | ||
| 49 | + | ||
| 50 | + @Override | ||
| 51 | + public String downloadMaterialUrl(Long accountId, String mediaId, String type) { | ||
| 52 | + // 第一步,直接从数据库查询。如果已经下载,直接返回 | ||
| 53 | + MpMaterialDO material = mpMaterialMapper.selectByAccountIdAndMediaId(accountId, mediaId); | ||
| 54 | + if (material != null) { | ||
| 55 | + return material.getUrl(); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // 第二步,尝试从临时素材中下载 | ||
| 59 | + String url = downloadMedia(accountId, mediaId); | ||
| 60 | + if (url == null) { | ||
| 61 | + return null; | ||
| 62 | + } | ||
| 63 | + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); | ||
| 64 | + material = MpMaterialConvert.INSTANCE.convert(mediaId, type, url, account) | ||
| 65 | + .setPermanent(false); | ||
| 66 | + mpMaterialMapper.insert(material); | ||
| 67 | + | ||
| 68 | + // 不考虑下载永久素材,因为上传的时候已经保存 | ||
| 69 | + return url; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + @Override | ||
| 73 | + public MpMaterialDO uploadTemporaryMaterial(MpMaterialUploadTemporaryReqVO reqVO) throws IOException { | ||
| 74 | + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); | ||
| 75 | + // 第一步,上传到公众号 | ||
| 76 | + File file = null; | ||
| 77 | + WxMediaUploadResult result; | ||
| 78 | + String mediaId; | ||
| 79 | + String url; | ||
| 80 | + try { | ||
| 81 | + // 写入到临时文件 | ||
| 82 | + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); | ||
| 83 | + reqVO.getFile().transferTo(file); | ||
| 84 | + // 上传到公众号 | ||
| 85 | + result = mpService.getMaterialService().mediaUpload(reqVO.getType(), file); | ||
| 86 | + // 上传到文件服务 | ||
| 87 | + mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getThumbMediaId()); | ||
| 88 | + url = uploadFile(mediaId, file); | ||
| 89 | + } catch (WxErrorException e) { | ||
| 90 | + // TODO yunai:待完善 | ||
| 91 | + throw new RuntimeException(e); | ||
| 92 | + } finally { | ||
| 93 | + FileUtil.del(file); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + // 第二步,存储到数据库 | ||
| 97 | + MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); | ||
| 98 | + MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account) | ||
| 99 | + .setPermanent(false); | ||
| 100 | + mpMaterialMapper.insert(material); | ||
| 101 | + return material; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 下载微信媒体文件的内容,并上传到文件服务 | ||
| 106 | + * | ||
| 107 | + * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 | ||
| 108 | + * | ||
| 109 | + * @param accountId 公众号账号的编号 | ||
| 110 | + * @param mediaId 媒体文件编号 | ||
| 111 | + * @return 上传后的 URL | ||
| 112 | + */ | ||
| 113 | + public String downloadMedia(Long accountId, String mediaId) { | ||
| 114 | + WxMpService mpService = mpServiceFactory.getMpService(accountId); | ||
| 115 | + for (int i = 0; i < 3; i++) { | ||
| 116 | + try { | ||
| 117 | + // 第一步,从公众号下载媒体文件 | ||
| 118 | + File file = mpService.getMaterialService().mediaDownload(mediaId); | ||
| 119 | + // 第二步,上传到文件服务 | ||
| 120 | + return uploadFile(mediaId, file); | ||
| 121 | + } catch (WxErrorException e) { | ||
| 122 | + log.error("[mediaDownload][media({}) 第 ({}) 次下载失败]", mediaId, i); | ||
| 123 | + } | ||
| 124 | + } | ||
| 125 | + return null; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + private String uploadFile(String mediaId, File file) { | ||
| 129 | + String path = mediaId + "." + FileTypeUtil.getType(file); | ||
| 130 | + return fileApi.createFile(path, FileUtil.readBytes(file)); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | +} |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java
| 1 | package cn.iocoder.yudao.module.mp.service.message; | 1 | package cn.iocoder.yudao.module.mp.service.message; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.io.FileTypeUtil; | ||
| 4 | -import cn.hutool.core.io.FileUtil; | ||
| 5 | import cn.hutool.core.lang.Assert; | 3 | import cn.hutool.core.lang.Assert; |
| 6 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 7 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 8 | -import cn.iocoder.yudao.module.infra.api.file.FileApi; | ||
| 9 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; | 6 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; |
| 10 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO; | 7 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO; |
| 11 | import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; | 8 | import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; |
| @@ -17,9 +14,11 @@ import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; | @@ -17,9 +14,11 @@ import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; | ||
| 17 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; | 14 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; |
| 18 | import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; | 15 | import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; |
| 19 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; | 16 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 17 | +import cn.iocoder.yudao.module.mp.service.material.MpMaterialService; | ||
| 20 | import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | 18 | import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; |
| 21 | import cn.iocoder.yudao.module.mp.service.user.MpUserService; | 19 | import cn.iocoder.yudao.module.mp.service.user.MpUserService; |
| 22 | import lombok.extern.slf4j.Slf4j; | 20 | import lombok.extern.slf4j.Slf4j; |
| 21 | +import me.chanjar.weixin.common.api.WxConsts; | ||
| 23 | import me.chanjar.weixin.common.error.WxErrorException; | 22 | import me.chanjar.weixin.common.error.WxErrorException; |
| 24 | import me.chanjar.weixin.mp.api.WxMpService; | 23 | import me.chanjar.weixin.mp.api.WxMpService; |
| 25 | import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; | 24 | import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; |
| @@ -31,7 +30,6 @@ import org.springframework.validation.annotation.Validated; | @@ -31,7 +30,6 @@ import org.springframework.validation.annotation.Validated; | ||
| 31 | 30 | ||
| 32 | import javax.annotation.Resource; | 31 | import javax.annotation.Resource; |
| 33 | import javax.validation.Validator; | 32 | import javax.validation.Validator; |
| 34 | -import java.io.File; | ||
| 35 | 33 | ||
| 36 | /** | 34 | /** |
| 37 | * 粉丝消息表 Service 实现类 | 35 | * 粉丝消息表 Service 实现类 |
| @@ -48,6 +46,8 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -48,6 +46,8 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 48 | private MpAccountService mpAccountService; | 46 | private MpAccountService mpAccountService; |
| 49 | @Resource | 47 | @Resource |
| 50 | private MpUserService mpUserService; | 48 | private MpUserService mpUserService; |
| 49 | + @Resource | ||
| 50 | + private MpMaterialService mpMaterialService; | ||
| 51 | 51 | ||
| 52 | @Resource | 52 | @Resource |
| 53 | private MpMessageMapper mpMessageMapper; | 53 | private MpMessageMapper mpMessageMapper; |
| @@ -57,9 +57,6 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -57,9 +57,6 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 57 | private MpServiceFactory mpServiceFactory; | 57 | private MpServiceFactory mpServiceFactory; |
| 58 | 58 | ||
| 59 | @Resource | 59 | @Resource |
| 60 | - private FileApi fileApi; | ||
| 61 | - | ||
| 62 | - @Resource | ||
| 63 | private Validator validator; | 60 | private Validator validator; |
| 64 | 61 | ||
| 65 | @Override | 62 | @Override |
| @@ -69,7 +66,6 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -69,7 +66,6 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 69 | 66 | ||
| 70 | @Override | 67 | @Override |
| 71 | public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { | 68 | public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { |
| 72 | - WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); | ||
| 73 | // 获得关联信息 | 69 | // 获得关联信息 |
| 74 | MpAccountDO account = mpAccountService.getAccountFromCache(appId); | 70 | MpAccountDO account = mpAccountService.getAccountFromCache(appId); |
| 75 | Assert.notNull(account, "公众号账号({}) 不存在", appId); | 71 | Assert.notNull(account, "公众号账号({}) 不存在", appId); |
| @@ -79,7 +75,7 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -79,7 +75,7 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 79 | // 记录消息 | 75 | // 记录消息 |
| 80 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); | 76 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); |
| 81 | message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); | 77 | message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); |
| 82 | - downloadMessageMedia(mpService, message); | 78 | + downloadMessageMedia(message); |
| 83 | mpMessageMapper.insert(message); | 79 | mpMessageMapper.insert(message); |
| 84 | } | 80 | } |
| 85 | 81 | ||
| @@ -97,6 +93,7 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -97,6 +93,7 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 97 | // 记录消息 | 93 | // 记录消息 |
| 98 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user); | 94 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user); |
| 99 | message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); | 95 | message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); |
| 96 | + // TODO 芋艿:downloadMessageMedia | ||
| 100 | mpMessageMapper.insert(message); | 97 | mpMessageMapper.insert(message); |
| 101 | 98 | ||
| 102 | // 转换返回 WxMpXmlOutMessage 对象 | 99 | // 转换返回 WxMpXmlOutMessage 对象 |
| @@ -124,7 +121,7 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -124,7 +121,7 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 124 | // 记录消息 | 121 | // 记录消息 |
| 125 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); | 122 | MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); |
| 126 | message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); | 123 | message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); |
| 127 | - downloadMessageMedia(mpService, message); | 124 | + downloadMessageMedia(message); |
| 128 | mpMessageMapper.insert(message); | 125 | mpMessageMapper.insert(message); |
| 129 | return message; | 126 | return message; |
| 130 | } | 127 | } |
| @@ -132,38 +129,17 @@ public class MpMessageServiceImpl implements MpMessageService { | @@ -132,38 +129,17 @@ public class MpMessageServiceImpl implements MpMessageService { | ||
| 132 | /** | 129 | /** |
| 133 | * 下载消息使用到的媒体文件,并上传到文件服务 | 130 | * 下载消息使用到的媒体文件,并上传到文件服务 |
| 134 | * | 131 | * |
| 135 | - * @param mpService 公众号 Service | ||
| 136 | * @param message 消息 | 132 | * @param message 消息 |
| 137 | */ | 133 | */ |
| 138 | - private void downloadMessageMedia(WxMpService mpService, MpMessageDO message) { | 134 | + private void downloadMessageMedia(MpMessageDO message) { |
| 139 | if (StrUtil.isNotEmpty(message.getMediaId())) { | 135 | if (StrUtil.isNotEmpty(message.getMediaId())) { |
| 140 | - message.setMediaUrl(downloadMedia(mpService, message.getMediaId())); | 136 | + message.setMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), |
| 137 | + message.getMediaId(), MpUtils.getMediaFileType(message.getType()))); | ||
| 141 | } | 138 | } |
| 142 | if (StrUtil.isNotEmpty(message.getThumbMediaId())) { | 139 | if (StrUtil.isNotEmpty(message.getThumbMediaId())) { |
| 143 | - message.setThumbMediaUrl(downloadMedia(mpService, message.getThumbMediaId())); | ||
| 144 | - } | ||
| 145 | - } | ||
| 146 | - | ||
| 147 | - /** | ||
| 148 | - * 下载微信媒体文件的内容,并上传到文件服务 | ||
| 149 | - * | ||
| 150 | - * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 | ||
| 151 | - * | ||
| 152 | - * @param mpService 微信公众号 Service | ||
| 153 | - * @param mediaId 媒体文件编号 | ||
| 154 | - * @return 上传后的 URL | ||
| 155 | - */ | ||
| 156 | - private String downloadMedia(WxMpService mpService, String mediaId) { | ||
| 157 | - try { | ||
| 158 | - // 第一步,从公众号下载媒体文件 | ||
| 159 | - File file = mpService.getMaterialService().mediaDownload(mediaId); | ||
| 160 | - // 第二步,上传到文件服务 | ||
| 161 | - String path = mediaId + "." + FileTypeUtil.getType(file); | ||
| 162 | - return fileApi.createFile(path, FileUtil.readBytes(file)); | ||
| 163 | - } catch (WxErrorException e) { | ||
| 164 | - log.error("[mediaDownload][media({}) 下载失败]", mediaId); | 140 | + message.setThumbMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), |
| 141 | + message.getThumbMediaId(), WxConsts.MediaFileType.THUMB)); | ||
| 165 | } | 142 | } |
| 166 | - return null; | ||
| 167 | } | 143 | } |
| 168 | 144 | ||
| 169 | } | 145 | } |
yudao-ui-admin/src/views/mp/components/wx-msg/main.vue
| @@ -158,7 +158,7 @@ import {getMessagePage, sendMessage} from '@/api/mp/message' | @@ -158,7 +158,7 @@ import {getMessagePage, sendMessage} from '@/api/mp/message' | ||
| 158 | }, { | 158 | }, { |
| 159 | ...this.objData, | 159 | ...this.objData, |
| 160 | type: this.objData.repType, | 160 | type: this.objData.repType, |
| 161 | - content: this.objData.repContent, | 161 | + // content: this.objData.repContent, |
| 162 | // TODO 芋艿:临时适配,保证可用 | 162 | // TODO 芋艿:临时适配,保证可用 |
| 163 | })).then(response => { | 163 | })).then(response => { |
| 164 | this.sendLoading = false | 164 | this.sendLoading = false |
yudao-ui-admin/src/views/mp/components/wx-reply/main.vue
| @@ -4,15 +4,12 @@ | @@ -4,15 +4,12 @@ | ||
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | <el-tabs type="border-card" v-model="objData.repType" @tab-click="handleClick"> | 6 | <el-tabs type="border-card" v-model="objData.repType" @tab-click="handleClick"> |
| 7 | + <!-- 类型 1:文本 --> | ||
| 7 | <el-tab-pane name="text"> | 8 | <el-tab-pane name="text"> |
| 8 | <span slot="label"><i class="el-icon-document"></i> 文本</span> | 9 | <span slot="label"><i class="el-icon-document"></i> 文本</span> |
| 9 | - <el-input | ||
| 10 | - type="textarea" | ||
| 11 | - :rows="5" | ||
| 12 | - placeholder="请输入内容" | ||
| 13 | - v-model="objData.repContent"> | ||
| 14 | - </el-input> | 10 | + <el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="objData.content" /> |
| 15 | </el-tab-pane> | 11 | </el-tab-pane> |
| 12 | + <!-- 类型 2:图片 --> | ||
| 16 | <el-tab-pane name="image"> | 13 | <el-tab-pane name="image"> |
| 17 | <span slot="label"><i class="el-icon-picture"></i> 图片</span> | 14 | <span slot="label"><i class="el-icon-picture"></i> 图片</span> |
| 18 | <el-row> | 15 | <el-row> |
| @@ -29,19 +26,10 @@ | @@ -29,19 +26,10 @@ | ||
| 29 | <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button> | 26 | <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button> |
| 30 | </el-col> | 27 | </el-col> |
| 31 | <el-col :span="12" class="col-add"> | 28 | <el-col :span="12" class="col-add"> |
| 32 | - <el-upload | ||
| 33 | - :action="actionUrl" | ||
| 34 | - :headers="headers" | ||
| 35 | - multiple | ||
| 36 | - :limit="1" | ||
| 37 | - :on-success="handleUploadSuccess" | ||
| 38 | - :file-list="fileList" | ||
| 39 | - :before-upload="beforeImageUpload" | ||
| 40 | - :data="uploadData"> | 29 | + <el-upload :action="actionUrl" :headers="headers" multiple :limit="1" :file-list="fileList" :data="uploadData" |
| 30 | + :before-upload="beforeImageUpload" :on-success="handleUploadSuccess"> | ||
| 41 | <el-button type="primary">上传图片</el-button> | 31 | <el-button type="primary">上传图片</el-button> |
| 42 | - <div slot="tip" class="el-upload__tip"> | ||
| 43 | - 支持bmp/png/jpeg/jpg/gif格式,大小不超过2M | ||
| 44 | - </div> | 32 | + <div slot="tip" class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div> |
| 45 | </el-upload> | 33 | </el-upload> |
| 46 | </el-col> | 34 | </el-col> |
| 47 | </el-row> | 35 | </el-row> |
| @@ -69,12 +57,7 @@ | @@ -69,12 +57,7 @@ | ||
| 69 | <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button> | 57 | <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button> |
| 70 | </el-col> | 58 | </el-col> |
| 71 | <el-col :span="12" class="col-add"> | 59 | <el-col :span="12" class="col-add"> |
| 72 | - <el-upload | ||
| 73 | - :action="actionUrl" | ||
| 74 | - :headers="headers" | ||
| 75 | - multiple | ||
| 76 | - :limit="1" | ||
| 77 | - :on-success="handleUploadSuccess" | 60 | + <el-upload :action="actionUrl" :headers="headers" multiple :limit="1" :on-success="handleUploadSuccess" |
| 78 | :file-list="fileList" | 61 | :file-list="fileList" |
| 79 | :before-upload="beforeVoiceUpload" | 62 | :before-upload="beforeVoiceUpload" |
| 80 | :data="uploadData"> | 63 | :data="uploadData"> |
| @@ -188,9 +171,9 @@ | @@ -188,9 +171,9 @@ | ||
| 188 | }, | 171 | }, |
| 189 | props: { | 172 | props: { |
| 190 | objData:{ | 173 | objData:{ |
| 191 | - type:Object | 174 | + type: Object |
| 192 | }, | 175 | }, |
| 193 | - //图文类型:1、已发布图文;2、草稿箱图文 | 176 | + // 图文类型:1、已发布图文;2、草稿箱图文 |
| 194 | newsType:{ | 177 | newsType:{ |
| 195 | type: String, | 178 | type: String, |
| 196 | default: "1" | 179 | default: "1" |