一、背景
在装饰装修公司的管理系统中,实现对客户的基本营销管理,包括客户有意向装修、预约量房、现场量房、设计出图等业务流程变换。
二、概念说明
- 客户,指装修公司家装顾问通过不同渠道获得的需要进行装修的客户信息。
- 意向装修,指客户有装修意向,家装顾问可以进行多次的电话拜访进行了解客户装修需要。
- 预约量房,家装顾问多次拜访后,与客户约定具体时间进行现场房屋测量。
- 现场量房,家装顾问与家装设计师进行现场拜访,了解房屋的户型及测量勘测房屋各空间面积周长。
- 设计出图,家装设计师根据现场量房及客户需求,进行装修效果设计等。
三、代码实现
1、后端代码
(1)代码结构
spring-boot项目,按照包进行业务分组,设定营销业务分包market
- biz 业务流程转换
- building 房屋项目信息
- market 营销管理
(2)业务流程
- 设计思路,通过流程运转的方式进行对象的状态切换,即在流程运转过程中目标对象通过步骤执行实现目标对象的状态切换,并保留记录在该阶段关注的回单信息。
- 目标对象,即包含业务阶段的对象,在本项目中指客户
java
public interface Target<Stage extends com.th4.edge.framework.data.flow.Stage> {
/***
* 设置当前状态阶段
* @param stage 目标阶段
*/
void setStage(Stage stage);
/***
* 获取当前状态阶段
* @return 当前阶段
*/
Stage getStage();
/***
* 设置目标对象更新时间
* @param updateTime 更新时间
*/
void setUpdateTime(DateTime updateTime);
}
- 状态阶段,目标对象所处的业务流程中阶段,在项目中指意向装修、预约量房、现场量房、设计出图等。
java
public interface Stage extends Named {
/***
* 未启用
* 是否支持该阶段
* @param stage 阶段
* @return 是否支持
*/
default boolean support(Stage stage){
return true;
};
/***
* 初始化阶段
* @return 是否初始化
*/
default boolean isInitial(){
return false;
};
/***
* 获取阶段步骤
* @return 步骤列表
*/
java.util.List<Step> getSteps();
/***
* 获取阶段收据类信息接口
* @return 信息类
*/
Class<? extends Receipt<? extends Target<?>>> getReceiptClass();
}
- 业务步骤,指对业务对象进行业务操作后完成目标对象的状态阶段切换,如在本项目中对客户进行预约量房操作后,客户的阶段从意向装修到预约量房阶段。
java
public interface Step extends Named {
/***
* 获取当前步骤对应的阶段状态
* @return 阶段状态
*/
Stage getCurrentStage();
/***
* 获取下一个步骤对应的阶段状态
* @return 阶段状态
*/
Stage getNextStage();
}
- 回单信息,指业务目标对象在进行某个业务步骤操作后产生记录的信息,如预约量房的信息,包括预约时间、备注等。
java
public interface Receipt<Target extends com.th4.edge.framework.data.flow.Target<?>> {
Target getTarget();
void setTarget(Target target);
Stage getStage();
void setStage(Stage stage);
Step getStep();
void setStep(Step step);
DateTime getDateTime();
void setDateTime(DateTime dateTime);
}
(3)代码实现
- 设计实体类
java
/**
* 定义业务步骤
*/
public enum BuildingStep implements Attribute<String>, com.th4.edge.framework.data.flow.Step {
Meet("预约量房"){
@Override
public Stage getCurrentStage() {
return BuildingStage.Intention;
}
@Override
public Stage getNextStage() {
return BuildingStage.Meet;
}
},
Measure("现场量房"){
@Override
public Stage getCurrentStage() {
return BuildingStage.Meet;
}
@Override
public Stage getNextStage() {
return BuildingStage.Measure;
}
},
Design("预约到店"){
@Override
public Stage getCurrentStage() {
return BuildingStage.Measure;
}
@Override
public Stage getNextStage() {
return BuildingStage.Store;
}
},
Payment("现场签单"){
@Override
public Stage getCurrentStage() {
return BuildingStage.Store;
}
@Override
public Stage getNextStage() {
return BuildingStage.Order;
}
};
private final String name;
BuildingStep(String name) {
this.name = name;
}
@Override
public String getValue() {
return toString();
}
@Override
public String getName() {
return name;
}
@Override
public Stage getCurrentStage() {
return null;
}
@Override
public Stage getNextStage() {
return null;
}
}
定义业务阶段
java
public enum BuildingStage implements Attribute<String>, Stage {
/**
* 意向装修客户
*/
Intention("意向",null,BuildingStep.Meet),
/**
* 量房
*/
Meet("待量房", MeetReceipt.class,BuildingStep.Measure),
Measure("已量房", MeasureReceipt.class,BuildingStep.Design),
/**
* 到店
*/
Store("待到店",null,BuildingStep.Payment),
/**
* 签单
*/
Order("签单",null),
/**
* 静默客户
*/
Silent("静默",null);
private final String name;
private final Step[] steps;
private final Class<? extends Receipt<? extends Target<?>>> receiptClass;
BuildingStage(String name,Class<? extends Receipt<? extends Target<?>>> receiptClass,Step...steps) {
this.name = name;
this.receiptClass=receiptClass;
this.steps=steps;
}
@Override
public String getValue() {
return toString();
}
@Override
public String getName() {
return name;
}
@Override
public List<Step> getSteps() {
return java.util.Arrays.asList(steps);
}
@Override
public Class<? extends Receipt<? extends Target<?>>> getReceiptClass() {
return receiptClass;
}
}
定义客户信息
less
@View(name = "意向客户", parent = "客户管理", path = "dr/market/building/building/intention",
route = @Route(name = "意向客户", component = "views/dr/market/market/building/building/intention/BuildingIntentionView.vue"),
roles = {@com.th4.edge.admin.system.annotation.Role(idKey = "market", name = "家装顾问", title = "家装客户营销管理系统",component = "views/dr/market/Home.vue")}
)
@View(name = "静默客户", parent = "客户管理", path = "dr/market/building/building/silent",
route = @Route(name = "静默客户", component = "views/dr/market/market/building/building/silent/BuildingSilentView.vue"),
roles = {@com.th4.edge.admin.system.annotation.Role(idKey = "market", name = "家装顾问", title = "家装客户营销管理系统",component = "views/dr/market/Home.vue")}
)
@ServiceProperties(uris = {"auth/building/marketer/info","auth/building/marketer/page"},properties = @Properties(
ignore = {
@IgnoreProperties(model =Building.class,properties = {"layout","families","tips","persons","spaces","remark","source","stage","marketer","createTime","updateTime"})
}
))
@Note("客户")
@Model
@Entity
@Table(name = "market_build_building")
public class Building extends AbstractModel implements Target<BuildingStage>, Record<Employee> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long buildId;
@Keyword
@Name
@Note("项目")
@Description("用于标识客户装修房屋的相关信息")
@Example("坝上街4-2208")
@Column(length = 24,nullable = false)
private String name;
@Keyword
@Note("姓名")
@Description("客户的昵称,如王哥")
@Example("张三")
@Column(length = 24,nullable = false)
private String nickName;
@Keyword
@Note("电话")
@Description("客户的手机号码")
@Column(length = 12,nullable = false)
private String telNo;
@Keyword
@Note("小区")
@Description("项目所在的小区名称")
@ManyToOne(fetch = FetchType.LAZY)
private District district;
@Note("楼栋")
@Embedded
private Site site;
@Note("房屋面积")
@Description("房屋的预估面积")
@Example("125")
@Unit("m²")
@Column(precision = 10,scale = 2)
private java.math.BigDecimal area;
@Note("房屋户型")
@Enumerated(EnumType.STRING)
@Column(length = 32,nullable = false)
private BuildingLayout layout=BuildingLayout.L3B1L1K1B;
@Note("性别")
@Enumerated(EnumType.STRING)
@Column(length = 32,nullable = false)
private Sex sex=Sex.Male;
@Note("年龄")
@Unit("岁")
@Enumerated(EnumType.STRING)
@Column(length = 32,nullable = false)
private Age age=Age.Adult;
@Note("职业")
@Description("客户从事的行业")
@Example("上班族")
@Column(length = 64)
private String job;
@Note("家庭情况")
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "market_build_building_families")
@Column(length = 32)
private java.util.Set<Family> families;
@Note("装修风格")
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "market_build_building_styles")
@Column(length = 32)
private java.util.Set<Style> styles;
@Note("标签")
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "market_build_building_tags")
@Column(length = 32)
private java.util.Set<Tag> tags;
@Note("印象")
@Description("简单标注下项目或者客户的特点")
@Example("城市白领,毛坯新房")
@ElementCollection
@CollectionTable(name = "market_build_building_tips")
@Column(length = 128)
private java.util.Set<String> tips;
@Note("家庭人数")
@Description("房屋大概入住的人数")
@Column
private Integer persons=1;
@Note("房间信息")
@ElementCollection
@CollectionTable(name = "market_build_building_spaces")
private java.util.List<Space> spaces;
@Lob
@Note("备注")
@Description("补充描述项目或者客户的其他信息")
private String remark;
@Note("客户来源")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Source source;
@Note("客户阶段")
@LogicField
@Enumerated(EnumType.STRING)
@Column(length = 32,nullable = false)
private BuildingStage stage = BuildingStage.Intention;
@Note("用户信息")
@LogicField
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Employee marketer;
@Note("维护时间")
@Embedded
@UpdateTime
private DateTime updateTime;
@Note("创建时间")
@CreateTime
@Embedded
@Column(updatable = false)
private DateTime createTime;
public Long getBuildId() {
return buildId;
}
public void setBuildId(Long buildId) {
this.buildId = buildId;
}
public District getDistrict() {
return district;
}
public void setDistrict(District district) {
this.district = district;
}
public BigDecimal getArea() {
return area;
}
public void setArea(BigDecimal area) {
this.area = area;
}
public Site getSite() {
return site;
}
public void setSite(Site site) {
this.site = site;
}
public DateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(DateTime updateTime) {
this.updateTime = updateTime;
}
public Source getSource() {
return source;
}
public void setSource(Source source) {
this.source = source;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTelNo() {
return telNo;
}
public void setTelNo(String telNo) {
this.telNo = telNo;
}
public Sex getSex() {
return sex;
}
public void setSex(Sex sex) {
this.sex = sex;
}
public Age getAge() {
return age;
}
public void setAge(Age age) {
this.age = age;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public Set<Family> getFamilies() {
return families;
}
public void setFamilies(Set<Family> families) {
this.families = families;
}
public Integer getPersons() {
return persons;
}
public void setPersons(Integer persons) {
this.persons = persons;
}
public DateTime getCreateTime() {
return createTime;
}
public void setCreateTime(DateTime createTime) {
this.createTime = createTime;
}
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
public Set<String> getTips() {
return tips;
}
public void setTips(Set<String> tips) {
this.tips = tips;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public List<Space> getSpaces() {
return spaces;
}
public void setSpaces(List<Space> spaces) {
this.spaces = spaces;
}
public BuildingLayout getLayout() {
return layout;
}
public void setLayout(BuildingLayout layout) {
this.layout = layout;
}
public Employee getMarketer() {
return marketer;
}
public void setMarketer(Employee marketer) {
this.marketer = marketer;
}
public BuildingStage getStage() {
return stage;
}
public void setStage(BuildingStage stage) {
this.stage = stage;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public Set<Style> getStyles() {
return styles;
}
public void setStyles(Set<Style> styles) {
this.styles = styles;
}
@Override
public Employee getAuditor() {
return marketer;
}
@Override
public void setAuditor(Employee auditor) {
setMarketer(auditor);
}
}
2、前端代码
(1)代码结构
vite-vue3 项目,按照包进行角色-业务-分组 market为家装顾问角色分包
- market 营销业务
- building 房屋项目信息
- market 营销管理
(2)实现流程
- 设计思路,通过后端实体类进行解析,通过实体类标识(如客户在后端的设计Building的实体类标识是building)进行通用业务的自动生成。
(3)代码实现
意向客户阶段代码,通过通用的组件进行组建页面
vue
<template>
<route-container :active="active">
<basic-manage-view key="home" :queryable="queryable" :sortable="{'updateTime.dateTimeDesc':''}"
auth="marketer"
disable-delete
ignored-save-field-names="spaces"
ignored-update-field-names="spaces"
model-name="building"
>
<template #tree="{queryable,loadData}">
<building-query-tree :loadData="loadData" :queryable="queryable" auth="marketer"/>
</template>
<template #table-actions="{row}">
<flow-stage-step-group property="stage" model-name="building" stage="Intention" :target="row" @step="doStep"/>
<el-tooltip content="电话拜访">
<el-button icon="Phone" link @click="contactBuilding(row)"></el-button>
</el-tooltip>
</template>
</basic-manage-view>
<flow-step-receipt
auth="marketer" key="biz" property="stage" model-name="building"
stage="Intention" :step="step" :target="building"
@launch="active='home'"
>
<el-button icon="back" round @click="active='home'"></el-button>
</flow-step-receipt>
<basic-save-view key="contact" v-if="building"
model-name="contact" ignored-field-names="building"
:data="{building:building}"
@form-submit="active='home'"
>
<template #action-bar>
<el-button icon="back" round @click="active='home'"></el-button>
</template>
<embed-info-view model-name="building" :model="building" auth="marketer"/>
<div class="empty"></div>
</basic-save-view>
</route-container>
</template>
<script lang="ts" setup>
import BasicManageView from "@/views/admin/components/view/manage/basic/BasicManageView.vue";
import BuildingQueryTree from "@/views/dr/market/market/building/building/compoents/query/BuildingQueryTree.vue";
import {ref} from "vue";
import RouteContainer from "@/views/admin/components/view/manage/components/container/RouteContainer.vue";
import FlowStageStepGroup from "@/views/admin/components/flow/step/FlowStageStepGroup.vue";
import FlowStepReceipt from "@/views/admin/components/flow/receipt/FlowStepReceipt.vue";
import {ElButton} from "element-plus";
import BasicSaveView from "@/views/admin/components/view/save/basic/BasicSaveView.vue";
import EmbedInfoView from "@/views/admin/components/view/info/embed/EmbedInfoView.vue";
const queryable = ref({stageEq: 'Intention'});
const active = ref('home')
const building=ref<dr.market.building.Building>()
const step=ref<th4.flow.Step>()
const doStep=(target:dr.market.building.Building,stepObject:th4.flow.Step)=>{
building.value=target
step.value=stepObject
active.value='biz'
}
const contactBuilding=(row:dr.market.building.Building)=>{
building.value=row
active.value='contact'
}
</script>
<style lang="scss" scoped>
.empty{
width: 100%;
height: 15px;
background-color: rgb(242, 242, 242);
margin-bottom: 10px;
}
</style>
效果图:
预约量房客户阶段代码,通过通用的组件进行组建页面
vue
<template>
<route-container :active="active">
<basic-manage-view key="home" model-name="meet-receipt" auth="marketer"
disable-delete
disable-save
:queryable="{'target.stageEq':'Meet'}"
ignored-update-field-names="designer"
>
<template #query="{queryable,loadData}">
<field-date-query model-name="meet-receipt" property-name="meetTime" :load-data="loadData" :queryable="queryable"/>
</template>
<template #action-bar="{queryable,loadData}">
<field-date-radio-query model-name="meet-receipt" property-name="meetTime" :load-data="loadData" :queryable="queryable"/>
</template>
<template #table-actions="{row}">
<flow-stage-step-group property="stage" model-name="building" stage="Meet" :target="row" @step="doStep"/>
</template>
</basic-manage-view>
<flow-step-receipt
auth="marketer" key="biz" property="stage" model-name="building"
stage="Meet" :step="step" :target="building"
@launch="active='home'"
>
<el-button icon="back" round @click="active='home'"></el-button>
</flow-step-receipt>
</route-container>
</template>
<script setup lang="ts">
import BasicManageView from "@/views/admin/components/view/manage/basic/BasicManageView.vue";
import {ref} from "vue";
import FlowStageStepGroup from "@/views/admin/components/flow/step/FlowStageStepGroup.vue";
import {ElButton} from "element-plus";
import FlowStepReceipt from "@/views/admin/components/flow/receipt/FlowStepReceipt.vue";
import RouteContainer from "@/views/admin/components/view/manage/components/container/RouteContainer.vue";
import FieldDateQuery from "@/views/admin/components/query/field/date/FieldDateQuery.vue";
import FieldDateRadioQuery from "@/views/admin/components/query/field/date/FieldDateRadioQuery.vue";
const active = ref('home')
const building=ref<th4.flow.Target>()
const step=ref<th4.flow.Step>()
const doStep=(target:th4.flow.Receipt,stepObject:th4.flow.Step)=>{
building.value=target.target
step.value=stepObject
active.value='biz'
}
</script>
<style scoped lang="scss">
效果图:
三、简要说明
后端通过设计实体类和相关枚举,实现业务场景的构建,这里讲的是后端只写实体类和枚举。然后针对实体类通过相关注解进行丰富实体类要体现的相关概念,如
java
@View(name = "意向客户", parent = "客户管理", path = "dr/market/building/building/intention",
route = @Route(name = "意向客户", component = "views/dr/market/market/building/building/intention/BuildingIntentionView.vue"),
roles = {@com.th4.edge.admin.system.annotation.Role(idKey = "market", name = "家装顾问", title = "家装客户营销管理系统",component = "views/dr/market/Home.vue")}
)
实现对菜单、角色、路由的自动生成和数据初始化,前端通过后端的实体类设计,通过基本管理页面进行业务交互的实现。如basicManageView实现对某一实体类的通用增删改查的操作,
js
<flow-stage-step-group property="stage" model-name="building" stage="Meet" :target="row" @step="doStep"/>
<!-- -->
<flow-step-receipt
auth="marketer" key="biz" property="stage" model-name="building"
stage="Meet" :step="step" :target="building"
@launch="active='home'"
>
<el-button icon="back" round @click="active='home'"></el-button>
</flow-step-receipt>
实现对业务流程的自动化生成。
四、总结
通过面向对象思想和设计模式实现对业务通用型的管理和操作,包括基本的通用增删改查及业务状态字段的变换,前端通过vue组件化的方式进行不同业务组件的自动化绘制,实现通用型的业务操作页面。用足够少的代码来实现产品业务的需要。
- 看完了是不是很懵,这傻X在讲什么?
- 为什么会这么设计业务状态字段的处理,status=1,2,3,4 这样不好吗?
- 你所谓的通用是不是在一定范围内,有限制的,比如上述building实体类怎么体现的房屋测量的信息?
- 你实现了吗,代码呢?
- 有病快点治吧。 欢迎讨论,代码正在github上上传...