[TOC]
在上篇文章中,我们搞定了渠道管理功能,这个相对来说比较简单。今天我们来看看促销活动的管理,在这个模块中,会有许多涉及到脚手架本身的修改,在这个过程中可以加深我们对这个脚手架的理解。
先来看看最终效果图吧:
这个页面上,你看到的所有功能按钮,均已实现。所以,就不废话了,开搞。
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 依赖,如下图:
1 2 3 4 5 6 7 8 9 10
| <description> 促销活动模块 </description> <dependencies> <dependency> <groupId>org.javaboy</groupId> <artifactId>tienchin-common</artifactId> </dependency> </dependencies>
|
当然这个新建的 activity 模块也拿去给 admin 模块依赖一下,将来在 admin 模块中调用 activity 模块的 service。
2.2 自动生成代码
MP 相关的依赖我们在上篇文章中已经配过了,这里咱就直接开始用就行了。
我们在 admin 模块的单元测试中新加一个方法,来用生成基础操作代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test public 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")); }) .strategyConfig(builder -> { builder.addInclude("tienchin_activity") .addTablePrefix("tienchin_"); }) .templateEngine(new FreemarkerTemplateEngine()) .execute(); }
|
将自动生成的 controller 删除掉,我们将来重新写,最终生成的代码如下:
3. 服务端接口
接下来我们来看看服务端接口的开发。
我们在 admin 模块中,新建 ActivityController,来准备开发活动相关的接口。
3.1 常规 CRUD
首先是常规的 CRUD。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @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(); List<Activity> list = 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
接口,是我自己写的,因为涉及到几个查询条件,该方法的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public List<Activity> getActivityList(Activity activity) { QueryWrapper<Activity> 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,不过都很好懂,不熟悉 Lambda 的小伙伴可以在公众号后台回复 webflux,有相关教程。
另外这里还有一个小小细节,就是小伙伴们知道,从 JDK1.8 开始,推荐用 LocalDate 和 LocalDateTime,所以我这个项目涉及到时间的基本上都是用这两种类型,但是在原本的脚手架中,当涉及到对象和 JSON 的互转是,只支持对 Date 的转换,所以这块需要我自己手动处理下。
看了下,脚手架中相关的配置都放在 framework 中,具体位置在 org.javaboy.tienchin.framework.config
,那么我的配置类就也写在这个位置吧,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| @Configuration public class LocalDateTimeSerializerConfig { private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; private static final String DATE_PATTERN = "yyyy-MM-dd";
@Bean public Converter<String, LocalDate> localDateConverter() { return new Converter<String, LocalDate>() { @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)); } } }; }
@Bean public Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<String, LocalDateTime>() { @Override public LocalDateTime convert(String source) { if (source.trim().length() == 0) { return null; } 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,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| @TableName("tienchin_activity") public class Activity implements Serializable {
@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 的枚举,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public static <E> E 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); } }
|
这里涉及到了四个工具方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
public static LocalDateTime getLocalDateTimeOfTimestamp(long timestamp) { Instant instant = Instant.ofEpochMilli(timestamp); ZoneId zone = ZoneId.systemDefault(); return LocalDateTime.ofInstant(instant, zone); }
public static LocalDate getLocalDateOfTimestamp(long timestamp) { Instant instant = Instant.ofEpochMilli(timestamp); ZoneId zone = ZoneId.systemDefault(); return instant.atZone(zone).toLocalDate(); }
public static LocalDateTime getLocalDateTime(String datetime) { return LocalDateTime.parse(datetime, DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)); }
public static LocalDate getLocalDate(String date) { return LocalDate.parse(date, DateTimeFormatter.ofPattern(YYYY_MM_DD)); }
|
好了,最后我们再提供三个导入导出相关的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) { ExcelUtil<Activity> util = new ExcelUtil<Activity>(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<Activity> list = activityService.getActivityList(activity); ExcelUtil<Activity> util = new ExcelUtil<Activity>(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<Activity> util = new ExcelUtil<Activity>(Activity.class); List<Activity> activityList = util.importExcel(file.getInputStream()); return toAjax(activityService.saveBatch(activityList)); }
|
这三个基本上也是照着用户接口写的,照猫画虎。
4. 前端页面开发
接下来开发前端页面。
4.1 请求接口
首先我们来开发请求接口,还是老规矩,新建一个 src/api/activity/index.js
文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| export function listActivity(query) { return request({ url: '/tienchin/activity/list', method: 'get', params: query }) }
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 }) }
export function delActivity(activityIds) { return request({ url: '/tienchin/activity/' + activityIds, method: 'delete' }) }
|
这个基本上就是我们活动增删改查的所有信息了。对于文件导入导出是请求是单独封装的,一会直接在 .vue 文件中调用即可。
4.2 页面开发
具体的页面开发倒是不难,我们来看下最终的效果:
还有其他的我就不一一截图了。前端 vue 也不难,能做出 vhr 的小伙伴都能做出来这里的页面。没有特别直接说的地方,我也就不贴代码了。小伙伴们可以直接 GitHub 上下载源码查看。有不懂的地方欢迎留言讨论。
5. 小结
好啦,这次提交的功能是促销活动管理~小伙伴们赶紧去给个 star 呀,star 越多更的越快哈哈~