
前段时间跟朋友见面,提起到他公司项目和想做的系统,我们半开玩笑的闲聊着,结果我便顺手接了下来。两个多月的工期使得本就不富裕的周末时间再次忙碌起来。不过对于项目来说整体时间勉强算够用,拉了几个朋友一起来弄,两个多月的业余时间来开发,顺利上线。闲来无事就这个小的项目做个简单的总结。
项目内容
项目所涉及的业务并不算复杂,内容是家庭医生相关的健康服务应用,需要建设的是一个PC端的业务系统,其中核心功能是支持C端用户的注册及管理,虚拟卡产品的管理,订单和派单及卡服务的核销。
移动端分为H5和小程序两块,H5支持用户注册,认证,订单提交,查看订单及服务评价,以及体检报告上传解析等。
小程序端则为医生护工等使用,可在后端查看订单及线上问诊等。其中整体上比较耗费精力的有两块,一个是和采购的服务提供商做系统的对接,由于对健康报告的解读我们使用了第三方的供应商,因此在服务的集成上确实花费了不少时间。另一个是和我们所服务的客户方系统做系统上的对接,在接口的联调上也确实花费了一些精力,其他的问题通过我们多年的研发经验都轻松解决。
团队配置
项目团队的配置研发在6人左右,2前端,2后端,1全栈,1测试,产品和设计各1人。
在两个多月的时间里,从产品设计到研发上线,兄弟们也都比较辛苦。基本上都是周六日开发,我们每周末开会过一次进度,有问题随时沟通解决,最终保证项目按时交付。
项目架构
项目整体架构及部署也不算复杂,刚上线的业务,所以暂时也无需太多的资源,保证系统的资源能够应对现有的流量即可。
项目中所使用的服务器及中间件,我们采用某云服务器提供商的产品。其中包括ECS,云数据库,OSS,短信,域名证书等
缓存因为量小我们使用1台服务器自建,当然后期可迁移到云产品上。
对于我们所采购的供应商的服务,在服务的集成上,则通过HTTP接口进行对接。与客户系统的对接也通过HTTP加加密参数的方式进行调用。当然各个接口在调用过程中要做好的就是加密和鉴权。
分享两个细节
这里简单分享一些我们在开发的过程中的一些设计
1. 虚拟卡号及秘钥的生成
每当创建一张虚拟卡产品时,可能会生成上千上万个卡号和卡密,每一张卡都需要创建一个卡号和8位的卡密,卡号就不说了,日期加流水号等等就可以搞定,这里着重说说卡密。因为用户激活是通过输入卡密来绑定这张卡的,这就要求我们所生成的每一张卡的卡密都是不能重复的,当然你也可以简单粗暴的使用随机字符串来生成,但这并不是一个好的方式,下面看看我们的8位卡密生成方式:
/**
* 生成产品卡密,规则:
* 2位随机数+当月流水号6位+2位年+2位月,转34进制后再反转
* @return
*/
public String genCardSecretKey() {
//从缓存中取当月流水号
String currentMonth = DateUtil.formatDate(DateUtil.currentDate(), DateUtil.TimeFormat.SHORT_DATE_PATTERN_YEAR_MONTH);
//卡密的缓存KEY
String flowNumberKey = String.format("card_number_%s", currentMonth);
Long flowNumber= stringRedisTemplate.opsForValue().increment(flowNumberKey,1);
stringRedisTemplate.expire(flowNumberKey,24*60*60, TimeUnit.SECONDS);
//生成新流水号:两位字符前缀+ YYMM + 6位流水号(左补齐0),转34进制后再反转
String prefix= HxStringUtils.generateUpperRandomString(2,1);
String simpleYearMonth = DateUtil.formatDate(DateUtil.currentDate(), DateUtil.TimeFormat.SHORT_DATE_PATTERN_2Y_YEAR_MONTH);
String strBaseNumber= org.apache.commons.lang3.StringUtils.leftPad(String.valueOf(flowNumber), 6, '0');
Long tmpBaseNumber = Long.parseLong(String.format("%s%s%s",prefix,strBaseNumber,simpleYearMonth ));
//10进制转34进制
return HxStringUtils.convertNumberTo34Base(tmpBaseNumber,8);
}
// 34进制字符集
public static final char[] NUMBER_AND_UPPER_CHAR_SET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ".toCharArray();
/**
* 将纯数字转换为数字加大写字母的编码,34进制转换
* @param number 纯数字流水号
* @return 数字加字母的流水号
*/
public static String convertNumberTo34Base(long number, int formatLength) {
return convertNumberTo34Base(number,NUMBER_AND_UPPER_CHAR_SET,formatLength);
}
/**
* 进制转换
* @param number 纯数字
* @return 数字加大写字母的编码
*/
private static String convertNumberTo34Base(long number, char[] charsSett, int formatLength) {
int chartLength= charsSett.length;
StringBuilder sb = new StringBuilder();
while (number != 0) {
int remainder = (int) (number % chartLength);
sb.append(charsSett[remainder]);
number /= chartLength;
}
String code= sb.reverse().toString();
code = StringUtils.leftPad(code, formatLength, '0');
return code;
}
生成出来的结果如下:
FZRSLFRD
5Q6SM2QB
1WQXNQR9
5Q6SML0K
7ZVWE71D
8RS90KTR
DWJS0NAF
8RS9133Z
H4LBJP4P
A9K07UNP
2. HTTP接口的定义
对于每一个接口,我们通过分配给不同系统的accessId和accessKey来进行验签和身份识别,简单来看下其中一个接口的定义:
/**
* 数据同步接口
* @param appId
* @param secretKey
* @param paramsDto
* @return
* @throws ServiceException
*/
@PostMapping(value = "/sync/policy")
public ResultDto syncPolicy(@RequestParam("accessId") String appId,
@RequestParam("accessKey") String secretKey,
@RequestBody ParamsDto paramsDto) throws ServiceException {
Result<ApiAuthInfo> authResult = this.validAuthParams(appId, secretKey);
if (!authResult.isSuccess()) {
return ResultDto.failed(authResult.getMsg());
}
String decryptData = SmUtil.sm4(sm4Key.getBytes()).decryptStr(paramsDto.getParam());
List<PolicyDto> dtoList = JSONUtil.toList(decryptData, PolicyDto.class);
return insurancePolicyApiService.syncPolicy(authResult.getData(), dtoList);
}
/**
* 校验应用ID和访问密钥是否合法
* @param appId
* @param secretKey
* @return
*/
protected Result<ApiAuthInfo> validAuthParams(String appId, String secretKey) {
if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(secretKey)) {
return Result.failed(ResCodeEnum.SECRET_CHECK_INVALID.getDesc());
}
Result<ApiAuthInfo> authResult = AuthUtils.authCheck(appId, secretKey,companyAppSecretKeySalt);
if(authResult.isSuccess()) {
Company company =companyService.findByAppId(appId);
if(company!=null) {
authResult.getData().setCompany(company);
}else{
return Result.failed(ResCodeEnum.SECRET_CHECK_INVALID.getDesc());
}
}
return authResult;
}
/**
* 验签
* @param appId
* @param secretKey
* @param companyAppSecretKeySalt 公司秘钥盐
* @return
*/
public static Result<ApiAuthInfo> authCheck(String appId, String secretKey,String companyAppSecretKeySalt) {
if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(secretKey)) {
return Result.failed(ResCodeEnum.SECRET_CHECK_INVALID.getDesc(),null);
}
String existSecretKey = SECRET_KEY_CACHE.get(appId);
if (StringUtils.isEmpty(existSecretKey)) {
existSecretKey = encrypt(appId, companyAppSecretKeySalt);
}
if (StringUtils.equals(existSecretKey, secretKey)) {
SECRET_KEY_CACHE.put(appId,secretKey);
return Result.success(new ApiAuthInfo(appId,secretKey));
} else {
return Result.failed(ResCodeEnum.SECRET_CHECK_INVALID.getDesc(),null);
}
}
遇到的问题
- 沟通的有效性:对于我们这样一个松散的团队来说,其实沟通是我们遇到的最大障碍,因为没有一个固定集中的场地,多是居家开发,因此沟通上的及时和有效性就很难保证,当然了这是异地办公的通病。
- 代码质量:质量这个比较考验个人经验和能力,对于经验丰富的人来说,考虑问题会相对严谨,经验较少的人则处理问题的能力会相对有所欠缺。所以在质量问题上,因人而异。
- 项目进度管理:不能面对面办公,只能通过会议沟通和查看项目代码确认项目的进度,每个人在业余时间都会有这样或者那样的事情,因此在进度的把控上需要考虑增加冗余,并多花费精力来管理。
总的来说,不论我们跟客户的对接还是在和供应商做系统集成的工作上,我们都尽可能的努力完成,以至于最终项目能够成功交付,也算是对这段时间以来辛苦付出的认可了。