在上篇文章中,我们搞定了渠道管理功能,这个相对来说比较简单。今天我们来看看促销活动的管理,在这个模块中,会有许多涉及到脚手架本身的修改,在这个过程中可以加深我们对这个脚手架的理解。
先来看看最终效果图吧:
这个页面上,你看到的所有功能按钮,均已实现。所以,就不废话了,开搞。
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 依赖,如下图:
<description>
促销活动模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.javaboy</groupId>
<artifactId>tienchin-common</artifactId>
</dependency>
</dependencies>
当然这个新建的 activity 模块也拿去给 admin 模块依赖一下,将来在 admin 模块中调用 activity 模块的 service。
2.2 自动生成代码
MP 相关的依赖我们在上篇文章中已经配过了,这里咱就直接开始用就行了。
我们在 admin 模块的单元测试中新加一个方法,来用生成基础操作代码,如下:
@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")); // 设置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();
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
接口,是我自己写的,因为涉及到几个查询条件,该方法的定义如下:
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
,那么我的配置类就也写在这个位置吧,如下:
@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";
/**
* string转localdate
*/
@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));
}
}
};
}
/**
* string转localdatetime
*/
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@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 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);
}
}
这里涉及到了四个工具方法如下:
/**
* 时间戳转 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) {
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
文件,内容如下:
// 查询所有的活动信息
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 越多更的越快哈哈~
- https://github.com/lenve/tienchin