促销活动管理,文件导入导出都有了!
2022-04-28 16:51:39来源:江南一点雨
在上篇文章中,我们搞定了渠道管理功能,这个相对来说比较简单。今天我们来看看促销活动的管理,在这个模块中,会有许多涉及到脚手架本身的修改,在这个过程中可以加深我们对这个脚手架的理解。
先来看看最终效果图吧:
这个页面上,你看到的所有功能按钮,均已实现。所以,就不废话了,开搞。
1. 数据库设计数据库这里主要修改的地方有两处。
1.1 修改字典表首先是修改字典表。在前端展示活动类型的时候,有两种不同的取值:
年卡折扣券年卡代金券像下面这样:
这里的活动类型下拉框我们当然可以直接在前端硬编码,但是既然用了这个脚手架,且这个脚手架又刚好提供了数据字典的功能,那么我们不妨将这两个选项加入到数据字典中,方便我们后面使用。
可以直接利用脚手架中的数据字典网页来添加,也可以直接在数据库表中来添加,我就省事一点,直接改表吧,修改两张表,分别是 sys_dict_type 和 sys_dict_data 两张表,其中 sys_dict_type 中加的是字典类型,而 sys_dict_data 中加的则是字典的具体值,我添加的数据分别如下:
sys_dict_type:
sys_dict_data:
1.2 添加促销活动表接下来就是活动促销表了,这个没啥好说的,直接开整就行了:
2. 创建新模块2.1 新建模块接下来创建一个专门写活动管理的新模块,有了前面写 channel 的经验,现在写 activity 不过是手到擒来的事。
新建一个名为 tienchin-activity 的模块,然后加入 common 依赖,如下图:
促销活动模块 org.javaboy tienchin-common
当然这个新建的 activity 模块也拿去给 admin 模块依赖一下,将来在 admin 模块中调用 activity 模块的 service。
2.2 自动生成代码MP 相关的依赖我们在上篇文章中已经配过了,这里咱就直接开始用就行了。
我们在 admin 模块的单元测试中新加一个方法,来用生成基础操作代码,如下:
@Testpublic void activityGenerator() { FastAutoGenerator.create("jdbc:mysql:///tienchin?serverTimezone=Asia/Shanghai&useSSL=false", "root", "123") .globalConfig(builder -> { builder.author("javaboy") // 设置作者 .disableOpenDir() .fileOverride() // 覆盖已生成文件 .outputDir("/Users/sang/workspace/workspace02/tienchin/tienchin-activity/src/main/java"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("org.javaboy") // 设置父包名 .moduleName("activity") // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/sang/workspace/workspace02/tienchin/tienchin-activity/src/main/resources/mapper/channel")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude("tienchin_activity") // 设置需要生成的表名 .addTablePrefix("tienchin_"); }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute();}
将自动生成的 controller 删除掉,我们将来重新写,最终生成的代码如下:
3. 服务端接口接下来我们来看看服务端接口的开发。
我们在 admin 模块中,新建 ActivityController,来准备开发活动相关的接口。
3.1 常规 CRUD首先是常规的 CRUD。
@RestController@RequestMapping("/tienchin/activity")public class ActivityController extends BaseController { @Autowired IActivityService activityService; @PreAuthorize("@ss.hasPermi("tienchin:activity:add")") @Log(title = "促销活动", businessType = BusinessType.INSERT) @PostMapping("/") public AjaxResult add(@Validated @RequestBody Activity activity) { activity.setCreateBy(getUsername()); return toAjax(activityService.saveActivity(activity)); } /** * 状态修改 */ @PreAuthorize("@ss.hasPermi("tienchin:activity:edit")") @Log(title = "促销活动" , businessType = BusinessType.UPDATE) @PutMapping("/changeStatus") public AjaxResult changeStatus(@RequestBody Activity activity) { activity.setUpdateTime(LocalDateTime.now()); activity.setUpdateBy(getUsername()); return toAjax(activityService.updateById(activity)); } @PreAuthorize("@ss.hasPermi("tienchin:activity:edit")") @Log(title = "促销活动" , businessType = BusinessType.UPDATE) @PutMapping("/") public AjaxResult edit(@Validated @RequestBody Activity activity) { activity.setUpdateBy(getUsername()); activity.setUpdateTime(LocalDateTime.now()); return toAjax(activityService.saveOrUpdate(activity)); } @PreAuthorize("@ss.hasPermi("tienchin:activity:query")") @GetMapping("/list") public TableDataInfo getActivityList(Activity activity) { startPage(); Listlist = activityService.getActivityList(activity); return getDataTable(list); } @PreAuthorize("@ss.hasPermi("tienchin:activity:remove")") @Log(title = "促销活动" , businessType = BusinessType.DELETE) @DeleteMapping("/{activityIds}") public AjaxResult remove(@PathVariable Long[] activityIds) { //待完善,将来加了其他功能后再继续完善 return toAjax(activityService.removeBatchByIds(Arrays.asList(activityIds))); } @PreAuthorize("@ss.hasPermi("tienchin:activity:query")") @GetMapping(value = "/{id}") public AjaxResult getActivityById(@PathVariable Long id) { return AjaxResult.success(activityService.getById(id)); }}
这些都是基础操作,其实也没啥好说的,大部分都用了 MP 自动生成的代码,自己几乎不需要写啥。
其中分页加条件查询的 /list 接口,是我自己写的,因为涉及到几个查询条件,该方法的定义如下:
public ListgetActivityList(Activity activity) { QueryWrapper qw = new QueryWrapper<>(); if (activity.getChannel() != null) { qw.lambda().eq(Activity::getChannel, activity.getChannel()); } if (activity.getStatus() != null) { qw.lambda().eq(Activity::getStatus, activity.getStatus()); } if (activity.getEndTime() != null && activity.getBeginTime() != null) { qw.lambda().ge(Activity::getBeginTime, activity.getBeginTime()).le(Activity::getEndTime, activity.getEndTime()); } return list(qw);}
用了 MP 的查询方法。涉及到一点点 Lambda,不过都很好懂。
另外这里还有一个小小细节,就是小伙伴们知道,从 JDK1.8 开始,推荐用 LocalDate 和 LocalDateTime,所以我这个项目涉及到时间的基本上都是用这两种类型,但是在原本的脚手架中,当涉及到对象和 JSON 的互转是,只支持对 Date 的转换,所以这块需要我自己手动处理下。
看了下,脚手架中相关的配置都放在 framework 中,具体位置在 org.javaboy.tienchin.framework.config,那么我的配置类就也写在这个位置吧,如下:
@Configurationpublic class LocalDateTimeSerializerConfig { private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; private static final String DATE_PATTERN = "yyyy-MM-dd"; /** * string转localdate */ @Bean public ConverterlocalDateConverter() { return new Converter () { @Override public LocalDate convert(String source) { if (source.trim().length() == 0) { return null; } try { return LocalDate.parse(source); } catch (Exception e) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_PATTERN)); } } }; } /** * string转localdatetime */ @Bean public Converter localDateTimeConverter() { return new Converter () { @Override public LocalDateTime convert(String source) { if (source.trim().length() == 0) { return null; } // 先尝试ISO格式: 2019-07-15T16:00:00 try { return LocalDateTime.parse(source); } catch (Exception e) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)); } } }; } /** * 统一配置 */ @Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { JavaTimeModule module = new JavaTimeModule(); LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer); return builder -> { builder.simpleDateFormat(DATE_TIME_PATTERN); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.modules(module); }; }}
配置类本身到也没啥说的,配了之后,将来项目中 LocalDate 转 JSON,就都是 yyyy-MM-dd 格式,LocalDateTime 转 JSON 就都是 yyyy-MM-dd HH:mm:ss 格式,反过来也一样。
3.2 导入导出再来看看跟数据导入导出相关的几个接口。
首先 Excel 导入导出相关工具在脚手架中已经有了,我们直接用即可,需要做的准备工作,首先是在 Activity 实体类上加上相关注解,配置将来生成 Excel 时表格的 title,具体如下:
@TableName("tienchin_activity")public class Activity implements Serializable { /** * id */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 活动编号 */ @Excel(name = "活动编号") private String code; /** * 活动名称 */ @Excel(name = "活动名称") private String name; /** * 渠道来源 */ @Excel(name = "渠道来源") private String channel; /** * 活动简介 */ @Excel(name = "活动简介") private String info; /** * 活动类型 */ @Excel(name = "活动类型",readConverterExp = "1=年费折扣卡,2=年费代金券") private String type; /** * 年费折扣 */ @Excel(name = "年费折扣") private Float discount; /** * 年费代金券 */ @Excel(name = "年费代金券") private Double voucher; /** * 状态 */ @Excel(name = "活动状态",readConverterExp = "0=正常,1=停用") private String status; /** * 开始时间 */ @Excel(name = "活动开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private LocalDateTime beginTime; /** * 结束时间 */ @Excel(name = "活动结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private LocalDateTime endTime; private LocalDateTime createTime; private LocalDateTime updateTime; @Excel(name = "活动创建人") private String updateBy; @Excel(name = "活动修改人") private String createBy;}
没加 @Excel 注解的字段,也是将来导出 Excel 表格时不需要导出的字段。
这里有一个小问题,就是我的时间格式使用了 LocalDateTime,原本的脚手架在这块只支持 Date,LocalDateTime 的转换会有问题,为了支持 LocalDateTime,我这里修改了 org.javaboy.tienchin.common.utils.reflect.ReflectUtils#invokeMethodByName 方法,增加了对 LocalDateTime 的枚举,如下:
public staticE invokeMethodByName(final Object obj, final String methodName, final Object[] args) { Method method = getAccessibleMethodByName(obj, methodName, args.length); if (method == null) { // 如果为空不报错,直接返回空。 logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); return null; } try { // 类型转换(将参数数据类型转换为目标方法参数类型) Class>[] cs = method.getParameterTypes(); for (int i = 0; i < cs.length; i++) { if (args[i] != null && !args[i].getClass().equals(cs[i])) { if (cs[i] == String.class) { args[i] = Convert.toStr(args[i]); if (StringUtils.endsWith((String) args[i], ".0")) { args[i] = StringUtils.substringBefore((String) args[i], ".0"); } } else if (cs[i] == Integer.class) { args[i] = Convert.toInt(args[i]); } else if (cs[i] == Long.class) { args[i] = Convert.toLong(args[i]); } else if (cs[i] == Double.class) { args[i] = Convert.toDouble(args[i]); } else if (cs[i] == Float.class) { args[i] = Convert.toFloat(args[i]); } else if (cs[i] == Date.class) { if (args[i] instanceof String) { args[i] = DateUtils.parseDate(args[i]); } else { args[i] = DateUtil.getJavaDate((Double) args[i]); } } else if (cs[i] == boolean.class || cs[i] == Boolean.class) { args[i] = Convert.toBool(args[i]); } else if (cs[i] == LocalDateTime.class) { if (args[i] instanceof String) { args[i] = DateUtils.getLocalDateTime((String) args[i]); } else { args[i] = DateUtils.getLocalDateTimeOfTimestamp((Long) args[i]); } }else if (cs[i] == LocalDate.class) { if (args[i] instanceof String) { args[i] = DateUtils.getLocalDate((String) args[i]); } else { args[i] = DateUtils.getLocalDateOfTimestamp((Long) args[i]); } } } } return (E) method.invoke(obj, args); } catch (Exception e) { String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; throw convertReflectionExceptionToUnchecked(msg, e); }}
这里涉及到了四个工具方法如下:
/** * 时间戳转 LocalDateTime * * @param timestamp * @return */public static LocalDateTime getLocalDateTimeOfTimestamp(long timestamp) { Instant instant = Instant.ofEpochMilli(timestamp); ZoneId zone = ZoneId.systemDefault(); return LocalDateTime.ofInstant(instant, zone);}/** * 时间戳转 LocalDate * * @param timestamp * @return */public static LocalDate getLocalDateOfTimestamp(long timestamp) { Instant instant = Instant.ofEpochMilli(timestamp); ZoneId zone = ZoneId.systemDefault(); return instant.atZone(zone).toLocalDate();}/** * 字符串转 LocalDateTime * * @param datetime * @return */public static LocalDateTime getLocalDateTime(String datetime) { return LocalDateTime.parse(datetime, DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS));}/** * 字符串转 LocalDate * * @param date * @return */public static LocalDate getLocalDate(String date) { return LocalDate.parse(date, DateTimeFormatter.ofPattern(YYYY_MM_DD));}
好了,最后我们再提供三个导入导出相关的接口:
@PostMapping("/importTemplate")public void importTemplate(HttpServletResponse response) { ExcelUtilutil = new ExcelUtil (Activity.class); util.importTemplateExcel(response, "活动数据");}@Log(title = "促销活动" , businessType = BusinessType.EXPORT)@PreAuthorize("@ss.hasPermi("tienchin:activity:export")")@PostMapping("/export")public void export(HttpServletResponse response, Activity activity) { List list = activityService.getActivityList(activity); ExcelUtil util = new ExcelUtil (Activity.class); util.exportExcel(response, list, "促销活动数据");}@Log(title = "促销活动" , businessType = BusinessType.IMPORT)@PreAuthorize("@ss.hasPermi("tienchin:activity:import")")@PostMapping("/importData")public AjaxResult importData(MultipartFile file) throws Exception { ExcelUtil util = new ExcelUtil (Activity.class); List activityList = util.importExcel(file.getInputStream()); return toAjax(activityService.saveBatch(activityList));}
这三个基本上也是照着用户接口写的,照猫画虎。
4. 前端页面开发接下来开发前端页面。
4.1 请求接口首先我们来开发请求接口,还是老规矩,新建一个 src/api/activity/index.js 文件,内容如下:
// 查询所有的活动信息export function listActivity(query) { return request({ url: "/tienchin/activity/list", method: "get", params: query })}// 根据 id 查询某一个活动的信息export function getActivity(activityId) { return request({ url: "/tienchin/activity/" + activityId, method: "get" })}// 添加活动export function addActivity(data) { return request({ url: "/tienchin/activity/", method: "post", data: data })}// 更新活动信息export function updateActivity(data) { return request({ url: "/tienchin/activity/", method: "put", data: data })}// 更新活动状态export function changeActivityStatus(id, status) { const data = { id, status } return request({ url: "/tienchin/activity/changeStatus", method: "put", data: data })}// 根据 id 删除活动export function delActivity(activityIds) { return request({ url: "/tienchin/activity/" + activityIds, method: "delete" })}
这个基本上就是我们活动增删改查的所有信息了。对于文件导入导出是请求是单独封装的,一会直接在 .vue 文件中调用即可。
4.2 页面开发具体的页面开发倒是不难,我们来看下最终的效果:
还有其他的我就不一一截图了。前端 vue 也不难,能做出 vhr 的小伙伴都能做出来这里的页面。没有特别直接说的地方,我也就不贴代码了。小伙伴们可以直接 GitHub 上下载源码查看。有不懂的地方欢迎留言讨论。
5. 小结好啦,这次提交的功能是促销活动管理~小伙伴们赶紧去给个 star 呀,star 越多更的越快哈哈~