闲暇时间的事-健康管理项目总结
By: Date: 2025年7月19日 Categories: 程序,案例 标签:,

前段时间跟朋友见面,提起到他公司项目和想做的系统,我们半开玩笑的闲聊着,结果我便顺手接了下来。两个多月的工期使得本就不富裕的周末时间再次忙碌起来。不过对于项目来说整体时间勉强算够用,拉了几个朋友一起来弄,两个多月的业余时间来开发,顺利上线。闲来无事就这个小的项目做个简单的总结。

项目内容

项目所涉及的业务并不算复杂,内容是家庭医生相关的健康服务应用,需要建设的是一个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);
    }
}

遇到的问题

  1. 沟通的有效性:对于我们这样一个松散的团队来说,其实沟通是我们遇到的最大障碍,因为没有一个固定集中的场地,多是居家开发,因此沟通上的及时和有效性就很难保证,当然了这是异地办公的通病。
  2. 代码质量:质量这个比较考验个人经验和能力,对于经验丰富的人来说,考虑问题会相对严谨,经验较少的人则处理问题的能力会相对有所欠缺。所以在质量问题上,因人而异。
  3. 项目进度管理:不能面对面办公,只能通过会议沟通和查看项目代码确认项目的进度,每个人在业余时间都会有这样或者那样的事情,因此在进度的把控上需要考虑增加冗余,并多花费精力来管理。

总的来说,不论我们跟客户的对接还是在和供应商做系统集成的工作上,我们都尽可能的努力完成,以至于最终项目能够成功交付,也算是对这段时间以来辛苦付出的认可了。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注