概述
拼车小程序。支持多个账号,部署后可以分配到多个小区使用。 发布行程
详细
项目介绍
顺风车拼车小程序,前后端代码(Java+微信小程序),可以用于学习,或者用于拼车类小程序二次开发。
功能介绍
发布行程
- 支持出发地、目的地、时间、车牌号等基本字段
- 支持需要乘客回答问题再加入
- 也支持“提醒”,可以第二天同样的信息继续发布,方便快捷。
查看行程
- 首页列表查看所有未关闭的顺风拼车行程。
- 如果行程太多,可以一键回到顶部。
在行程详情页面,可以看到:
- 查看拼车行程的基本信息
- 可以关注司机(关注之后司机发布的拼车行程后可以收到订阅消息推送)
- 拼车沟通(详见以下“即时聊天”)
即时聊天
可以进行即时聊天,沟通顺风拼车,你可以发送
- 文字(包括字符表情)
- 位置
路线订阅
可以通过订阅“关键词”,系统检车到相关的信息,会发送订阅通知
广告系统
除了【系统公告】菜单,其他菜单都是可以配置的,可以配置
- 菜单名称(一般四个字)
- 菜单icon
- 菜单跳转动作(支持H5链接,或者小程序)
横幅广告也是可以配置的。
系统公告
支持富文本
目录结构
开发细节
本项目的开发体验一般是:
- 先设计表结构,并写好字段备注
- 运行代码生成器,生成代码(普通表可以生成POJO等,常量表可以生成常量)
- 开始开发逻辑
代码生成器
详细见 util 下的 BeanGenerator.java
# Bean生成器配置
gen-config:
# 普通bean
normal_package_name: com.ubatis.circleserver.bean
normal_extend_class:
normal_prefix:
normal_suffix: Bean
normal_out_dir: src/main/java/com/ubatis/circleserver/bean
# 参数的
param_package_name: com.ubatis.circleserver.bean.param
param_extend_class: com.ubatis.circleserver.util.daoutils.MyParams
param_prefix: Param
param_suffix:
param_out_dir: src/main/java/com/ubatis/circleserver/bean/param
# 常量文件的路径
constant_package: com.ubatis.circleserver.util.constant
constant_path: src/main/java/com/ubatis/circleserver/util/constant
Spring JDBC 封装
通过代码生成器生成的Param开头的Bean,是可以操作数据库的,用法如下
// 增
ParamAcUser paramAcUser = new ParamAcUser();
paramAcUser.save();
// paramAcUser.saveAndGetId(); 保存并返回id
// paramAcUser.saveAndGetId(String idname); 指定id字段
// paramAcUser.saveDesignateField(String fileds); 保存指定的字段,逗号隔开
// paramAcUser.saveExceptField(String fileds); 保存除外的字段,逗号隔开
// 删
ParamSysConfig paramSysConfig = new ParamSysConfig();
paramSysConfig.setId(123);
paramSysConfig.delete(); // 默认键是id,可以指定
// 查
.queryForPage() // 分页查询
.queryForList() // 查询列表
.queryForMap() // 查询对象
.queryForInt()
可以结合Kotlin查询,构造动态SQL,方便灵活,便于管理
// 改
update()
update(String idname) // 指定id更新
update(String[] idNames) // 指定多个字段
updateExceptField(String exceptFields) // 更新除了指定字段
updateExceptField(String idname, String exceptFields) // 更新指定字段
updateExceptField(String[] idNames, String exceptFields)
updateDesignateField(String designateFields)
updateDesignateField(String idname, String designateFields)
updateDesignateField(String[] idNames, String designateFields)
即时聊天
// 单例模式存储Session
public enum Sessions {
SESSIONS;
private ConcurrentHashMap<String, Session> sessionMap = null;
public int maxOnline = 0; // 最大在线人数
Sessions(){
sessionMap = new ConcurrentHashMap<>();
}
public ConcurrentHashMap<String, Session> getSessionMap(){
if (sessionMap.size() > maxOnline) {
maxOnline = sessionMap.size();
}
return sessionMap;
}
}
// 发送消息
public void sendMessage(String openid, String action, String content) {
if (Sessions.SESSIONS.getSessionMap().containsKey(openid)) {
Sessions.SESSIONS.getSessionMap().get(openid).sendText(JsonUtil.toJson(CM.getReturnHeaderInfo(content, action)));
}
}
客服消息自动回复
public String wcPushMsgPost(Map<String, Object> map, String appId) {
// 回复者openid
String userOpenid = map.get("FromUserName").toString().trim();
//
if ("miniprogrampage".equals(map.get("MsgType")) || "text".equals(map.get("MsgType"))) {
String replyContent = null;
if (map.get("Content") != null) {
replyContent = map.get("Content").toString().trim();
} else {
replyContent = "快捷回复";
}
// 添加自动回复的记录
ParamAcUserTrace paramNewAcUserTrace = new ParamAcUserTrace();
paramNewAcUserTrace.setTrace_type(CS.USER_TRACE_REPLY_TEXT);
paramNewAcUserTrace.setOperator(userOpenid);
paramNewAcUserTrace.setObject(replyContent.length() > 250 ? replyContent.substring(0, 240) + "......" : replyContent); // 最多长度255
userService.addUserTrace(paramNewAcUserTrace);
// 回复对应的付款码
// 获取最近的点击支付的记录
AcUserTraceBean clickPayTrace = userService.getLatestUserRequestPayCodeTrace(userOpenid);
// 回复消息用到的材料
String str2beSend = null;
AcUserPaymentCodeBean targetPaymentCodeInfoBean = null;
FnCarpoolOrderBean fnCarpoolOrderBean = null;
AcUserBean targetBePaidUser = null;
long circleId = 0;
// 找到对应的订单
fnCarpoolOrderBean = new ParamFnCarpoolOrder("id", clickPayTrace.getObject()).queryForObject();
circleId = fnCarpoolOrderBean.getCircle_id();
// logger.info("fnCarpoolOrderBean=======: {}", JsonUtil.toJson(fnCarpoolOrderBean));
targetPaymentCodeInfoBean = new ParamAcUserPaymentCode("openid", fnCarpoolOrderBean.getDriver_openid()).queryForObject();
targetBePaidUser = new ParamAcUser("openid", fnCarpoolOrderBean.getDriver_openid()).queryForObject();
// logger.info("acUserBeanDriver=======: {}", JsonUtil.toJson(acUserBeanDriver));
// logger.info("acUserPaymentCodeBean=======: {}", "" + acUserPaymentCodeBean.getPayment_code().length());
str2beSend = TextTmplKt.getPayOrderTemperate(
fnCarpoolOrderBean.getFrom_where()
, fnCarpoolOrderBean.getTo_where()
, DateUtil.timestamp2DateTime(fnCarpoolOrderBean.getStart_time())
, targetBePaidUser.getNickname()
, StringUtils.isNullOrEmpty(fnCarpoolOrderBean.getPrice()) ? "请和车主商议" : fnCarpoolOrderBean.getPrice()
, mSysConfig.getPay_code_prefix() + Md5Util.Md5(targetBePaidUser.getOpenid()).trim() +"?t=" + DateUtil.getCurrentTimestamp()
);
// 乘客记录设置为已回复-已支付
ParamFnCarpoolPassenger paramFnCarpoolPassenger = new ParamFnCarpoolPassenger();
paramFnCarpoolPassenger.setOrder_id(fnCarpoolOrderBean.getId());
paramFnCarpoolPassenger.setPassenger_openid(userOpenid);
paramFnCarpoolPassenger.setJoin_state(CS.CARPOOL_JOIN_STATE_IN);
paramFnCarpoolPassenger.setHas_paid(CS.BOOLEAN_STRING_Y);
int update = paramFnCarpoolPassenger.update("order_id,passenger_openid,join_state");
logger.info("乘客【{}】回复消息:{},默认设置为已支付 - {}", userOpenid, replyContent, update);
//
if (targetPaymentCodeInfoBean == null) {
replyMessage(circleId, userOpenid, MSGTYPE_TEXT, "车主尚未上传收款码");
} else {
// 先发文字提示
replyMessage(circleId, userOpenid, MSGTYPE_TEXT, str2beSend);
// 再发一个图片
replyMessage(circleId, userOpenid, MSGTYPE_IMAGE, targetPaymentCodeInfoBean.getPayment_code());
}
} else if ("event".equals(map.get("MsgType")) && "user_enter_tempsession".equals(map.get("Event"))) {
AcUserBean acUserBean = new ParamAcUser("openid", userOpenid).queryForObject();
replyMessage(acUserBean.getCircle_id(), userOpenid, MSGTYPE_TEXT, "请回复任意内容,获取车主收款码");
// 添加记录
ParamAcUserTrace paramNewAcUserTrace = new ParamAcUserTrace();
paramNewAcUserTrace.setTrace_type(CS.USER_TRACE_OPEN_CONTACT);
paramNewAcUserTrace.setOperator(userOpenid);
paramNewAcUserTrace.setObject("进入客服消息");
userService.addUserTrace(paramNewAcUserTrace);
}
return "SUCCESS";
}
Redis监听实现行程生命周期管理
编写一个类,实现 KeyExpirationEventMessageListener
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
logger.info("expiredKey:{}", expiredKey);
// 即可做监听的动作
}
}
用第三方服务实现告警
// 利用线程池,发送告警
// 告警用的是server酱:http://sc.ftqq.com/?c=default
public void alertFF(String title, String errorStr) {
String currentDate = DateUtil.getCurrentDate();
if (currentDate.equals(mToday)) {
count++;
} else {
mToday = currentDate;
count = 0;
}
if (!sysConfig.getUser_name().equals(username) && count < MAX_ALERT_TIMES) {
HttpClientUtil.doGetAsync(getALERT_URL(title, MyStringUtil.removeSpecialChar(errorStr)), result -> {
logger.info("result:{}", result);
});
}
}