物理机安装虚拟机做网站,大学校园门户网站建设方案,建被采集的网站,深圳网站建设叶林目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务服务项远程接口jzo2o-ap… 目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务服务项远程接口jzo2o-api工程定义接口foundations服务实现接口Mapper开发Service层开发Controller开发 1.4 熔断降级1.4.1 复习1.4.2 使用sentinel实现熔断降级1.4.3 客户端集成sentinel1.4.4 sentinel实现熔断降级代码1.4.5 测试 1.5 下单接口设计1.5.0 订单号生成规则常见的订单号生成规则本项目订单号生成规则代码实现 1.5.1 接口分析1.5.1 接口开发Controller层开发Service层开发下单预约功能测试 2 支付功能2.1 支付方式2.2 支付服务的设计2.2.1 表设计2.2.2 数据流2.2.3 支付/退款代码ControllerService 2.3 订单支付接口2.3.0 需求2.3.1 接口分析2.3.2 接口开发Controller层开发Mapper层Service层开发 2.3.3 接口测试 2.4 对接查询支付结果2.4.1 需求分析2.4.2 接口分析2.4.3 接口开发Controller层开发Mapper层开发Service层开发 2.4.4 接口测试 2.5 接收支付通知2.5.1 需求分析2.5.2 接受通知实现2.5.3 定时查询支付状态并通知的实现2.5.3 测试不点支付成功成功更新的业务 1 预约下单
1.1 需求分析
1.1.1 业务流程
首先明确本模块在核心业务流程的位置下图是项目的核心业务流程 大概界面原型如下
1.1.2 订单状态
本项目订单状态共有7种如下图 待支付订单的初始状态。 派单中用户支付成功后订单的状态由待支付变为派单中。 待服务服务人员或机构抢单成功订单的状态由派单中变为待服务。 服务中服务人员开始服务订单状态变为服务中。 订单完成服务人员完成服务订单状态变为订单完成。 已取消订单是待支付状态时用户取消订单订单状态变为已取消。 已关闭订单已支付状态下取消订单后订单状态变为已关闭。
1.2 系统设计
1.2.1 订单表设计
在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构比如在电商系统中一个订单购买的多件不同的商品设计订单表和订单明细表 订单表记录订单号、订单金额、下单人、订单状态等信息。 订单明细表记录该订单购买商品的信息包括商品名称、商品价格、交易价格、购买商品数量等。
如下图
1.2.2 表结构的设置
除了订单号、订单金额、订单状态、下单人ID等字段外订单表还存储哪些信息 根据需求梳理预约下单提交的数据如下 通过分析订单表包括以下几部分 订单基础信息订单号、订单状态、排序字段、是否显示标记等。 价格信息单价、购买数量、优惠金额、订单总金额等。 下单人信息下单人ID、联系方式、位置信息相当于收货地址等。 服务(商品)相关信息服务类型名称、服务项名称、服务单价、价格单位、购买数量等。 服务信息相当于商品如果有订单明细表要在订单明细表中存储本项目将服务相关信息存储在订单表。
Mysql表结构如下
create table jzo2o-orders.orders
(id bigint not null comment 订单idconstraint PRIMARYprimary key,user_id bigint not null comment 订单所属人,serve_type_id bigint null comment 服务类型id,serve_type_name varchar(50) null comment 服务类型名称,serve_item_id bigint not null comment 服务项id,serve_item_name varchar(50) null comment 服务项名称,serve_item_img varchar(255) null comment 服务项图片,unit int null comment 服务单位,serve_id bigint not null comment 服务id,orders_status int not null comment 订单状态0待支付100派单中200待服务300服务中400待评价500订单完成600已取消700已关闭,pay_status int null comment 支付状态2待支付4支付成功,refund_status int null comment 退款状态 1退款中 2退款成功 3退款失败,price decimal(10, 2) not null comment 单价,pur_num int default 1 not null comment 购买数量,total_amount decimal(10, 2) not null comment 订单总金额,real_pay_amount decimal(10, 2) not null comment 实际支付金额,discount_amount decimal(10, 2) not null comment 优惠金额,city_code varchar(20) not null comment 城市编码,serve_address varchar(255) not null comment 服务详细地址,contacts_phone varchar(20) not null comment 联系人手机号,contacts_name varchar(255) not null comment 联系人姓名,serve_start_time datetime not null comment 服务开始时间,lon double(10, 5) null comment 经度,lat double(10, 5) null comment 纬度,pay_time datetime null comment 支付时间,evaluation_time datetime null comment 评价时间,trading_order_no bigint null comment 支付服务交易单号,transaction_id varchar(50) null comment 第三方支付的交易号,refund_no bigint null comment 支付服务退款单号,refund_id varchar(50) null comment 第三方支付的退款单号,trading_channel varchar(50) null comment 支付渠道,display int default 1 null comment 用户端是否展示1展示0隐藏,sort_by bigint null comment 排序字段serve_start_time毫秒级时间戳订单id后六位,create_time datetime default CURRENT_TIMESTAMP not null,update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
)数据来源分析 其中serve_idpur_num serve_start_time 是前端传过来的
1.3 开发远程调用接口
下单接口保存的数据较多有一些数据需要远程调用来获取
根据地址簿Id远程调用客户中心查询我的地址簿信息。根据服务Id远程调用运营基础服务查询服务相关的信息。
1.3.0 复习下远程调用的开发
由于远程调用接口会被大量微服务所用因此可以把接口抽取复用到一个API工程 之后例如查询用户的远程调用我们需要在API工程写一个接口而在Customer微服务远程调用中的服务端进行实现接口即可因此在开发中一般先开发服务端再开发远程调用的客户端最后把API工程打包放入依赖仓库即可使用。
1.3.1 查询地址簿远程接口
微服务之间远程调用的接口统一定义在jzo2o-api工程。 查询地址簿远程接口是根据地址簿ID查询地址簿信息接口定义如下 接口路径GET/customer/inner/address-book/{id} 请求数据类型 application/x-www-form-urlencoded jzo2o-api工程定义接口
/*** 地址薄相关的远程调用接口*/
//contextId 指定FeignClient实例的上下文id如果不设置默认为类名value指定微服务的名称path:指定接口地址
FeignClient(contextId jzo2o-customer, value jzo2o-customer, path /customer/inner/address-book)
public interface AddressBookApi {GetMapping(/{id})AddressBookResDTO detail(PathVariable(id) Long id);
}Customer服务实现接口
/*** 地址薄远程调用*/
RestController
RequestMapping(inner/address-book)
Api(tags 内部接口 - 地址薄相关接口)
public class InnerAddressBookController implements AddressBookApi {private IAddressBookService addressBookService;OverrideGetMapping(/{id})ApiOperation(地址薄详情)ApiImplicitParams({ApiImplicitParam(name id, value 地址薄id, required true, dataTypeClass Long.class)})public AddressBookResDTO detail(PathVariable(id) Long id) {AddressBook addressBook addressBookService.getById(id);return BeanUtil.toBean(addressBook, AddressBookResDTO.class);}1.3.2 查询服务服务项远程接口
jzo2o-api工程定义接口
FeignClient(contextId jzo2o-foundations, value jzo2o-foundations, path /foundations/inner/serve)
public interface ServeApi {GetMapping(/{id})ServeAggregationResDTO findById(PathVariable(id) Long id);}foundations服务实现接口
很明显这是个多表关联查询不能用MP因此先开发Mapper
Mapper开发 /*** 根据id查询详情** param id 服务id* return 服务详情*/ServeAggregationResDTO findServeDetailById(Param(id) Long id);select idfindServeDetailById resultTypecom.jzo2o.api.foundations.dto.response.ServeAggregationResDTOSELECTserve.id,serve.city_code,serve.price,serve.is_hot,serve.hot_time_stamp,serve.sale_status,item.id AS serve_item_id,item.name AS serve_item_name,item.img AS serve_item_img,item.detail_img,item.serve_item_icon,item.unit,item.sort_num AS serve_item_sort_num,item.serve_type_id AS serve_type_id,type.name AS serve_type_name,type.img AS serve_type_img,type.serve_type_icon,type.sort_num AS serve_type_sort_numFROMserveinner JOIN serve_item AS item ON item.id serve.serve_item_idinner JOIN serve_type AS type ON type.id item.serve_type_idWHEREserve.id #{id}/selectService层开发 /*** 根据id查询详情** param id 服务id* return 服务详情*/Overridepublic ServeAggregationResDTO findServeDetailById(Long id) {return baseMapper.findServeDetailById(id);}Controller开发
RestController
RequestMapping(/inner/serve)
Api(tags 内部接口 - 服务相关接口)
public class InnerServeController implements ServeApi {Resourceprivate IServeService serveService;OverrideGetMapping(/{id})ApiOperation(根据id查询服务)ApiImplicitParams({ApiImplicitParam(name id, value 服务项id, required true, dataTypeClass Long.class)})public ServeAggregationResDTO findById(NotNull(message id不能为空) PathVariable(id) Long id) {return serveService.findServeDetailById(id);}
}1.4 熔断降级
1.4.1 复习
什么是熔断降级 在微服务架构一定要去预防微服务雪崩问题微服务雪崩问题是指在微服务架构中当一个服务出现故障时由于服务之间的依赖关系故障可能会传播到其他服务导致大规模的服务失败系统无法正常运行。这种情况就像雪崩一样最初一个小问题最终引发了整个系统的崩溃。简单理解微服务雪崩就是微服务之间相互调用因为调用链中的一个服务故障引起整个链路都无法访问的情况。
常用的预防微服务雪崩的的方法 超时处理设定超时时间请求超过一定时间没有响应就返回错误信息不会无休止等待。 熔断降级当服务的异常数或异常比例超过了预设的阈值时熔断器会进入开启状态暂时中断对该服务的请求此时走降级方法能够快速响应确保系统的基本功能能够继续运行。 限流限制对服务的请求速率避免短时间内大量的请求导致系统崩溃。 线程池隔离给要请求的资源分配一个线程池线程池去控制请求数量
1.4.2 使用sentinel实现熔断降级
本项目使用Sentinel实现限流、熔断等机制预防微服务雪崩。 熔断降级是微服务保护的一种方法当使用Feign进行远程调用在客户端通过熔断降级措施进行微服务保护。 如下图 orders-manager订单服务请求customer查询地址簿在进行feign远程调用过程出现异常将走降级方法当异常比例或异常数达到一定的阈值将触发熔断熔断期间将直接走降级逻辑快速响应。 当customer服务恢复后熔断时间结束此时会再次尝试请求customer如果成功请求将关闭熔断恢复原来的链路。
根据上图可知熔断、降级发生在客户端下边在订单管理服务调用customer的客户端定义CustomerClient类用于请求customer服务。
1.4.3 客户端集成sentinel
这里是以服务提供者为单独定义远程调用Client类如果要远程调用jzo2o-foundations服务则定义CustomerClient 类。
添加nacos配置文件shared-sentinel.yaml如下 在order项目中引入shared-sentinel.yaml配置文件 cloud:nacos:config:file-extension: yamlshared-configs: # 共享配置- data-id: shared-redis-cluster.yaml # 共享redis集群配置refresh: false- data-id: shared-xxl-job.yaml # xxl-job配置refresh: false- data-id: shared-rabbitmq.yaml # rabbitmq配置refresh: false- data-id: shared-es.yaml # esrefresh: false- data-id: shared-mysql.yaml # mysql配置refresh: false- data-id: shared-sentinel.yaml # msentinel配置refresh: false 项目代码中添加依赖
dependencygroupIdcom.jzo2o/groupIdartifactIdjzo2o-sentinel/artifactId
/dependency问为什么这样呢 答我在fremwork中定义了全部依赖只需要在需要这个sentinel的地方导入这一个依赖就可以全部导入所有相关依赖
1.4.4 sentinel实现熔断降级代码
问为什么不在api定义这个熔断处理器 答因为每个客户端的业务需求不一样
SentinelResource注解的属性说明 value: 用于定义资源的名称即 Sentinel 会对该资源进行流量控制和熔断降级。 fallback 非限流、熔断等导致的异常执行的降级方法 blockHandler :触发限流、熔断时执行的降级方法
测试
/*** 调用customer的客户端类*/
Component
Slf4j
public class CustomerClient {Resourceprivate AddressBookApi addressBookApi;/*** 客户端定义自己的降级逻辑* param id* return*///value 资源名称 将来在sentinel可以查到//fallback 定义降级逻辑//blockHandler 定义降级逻辑SentinelResource(value getAddressBookDetail, fallback detailFallback, blockHandler detailBlockHandler)public AddressBookResDTO getDetail(Long id){AddressBookResDTO detail addressBookApi.detail(id);return detail;}//getDetail执行异常走这个方法public AddressBookResDTO detailFallback(Long id, Throwable throwable) {log.error(非限流、熔断等导致的异常执行的降级方法id:{},throwable:, id, throwable);return null;}//熔断后的降级逻辑public AddressBookResDTO detailBlockHandler(Long id, BlockException blockException) {log.error(触发限流、熔断时执行的降级方法id:{},blockException:, id, blockException);return null;}
}
下边在下单方法中通过CustomerClient 调用customer
Slf4j
Service
public class OrdersCreateServiceImpl extends ServiceImplOrdersMapper, Orders implements IOrdersCreateService {Resourceprivate CustomerClient customerClient;/*** 下单服务* param placeOrderReqDTO* return*/Overridepublic PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//地址簿idLong addressBookId placeOrderReqDTO.getAddressBookId();//下单人信息获取地址簿调用jzo2o-customer服务获取AddressBookResDTO detail customerClient.getDetail(addressBookId);//服务相关信息,调用jzo2o-foundations获取//生成订单号//计算价格//组装订单信息插入数据库订单表return null;}}使用项目使用EnableFeignClients扫描Feign接口生成代理对象。 具体代码在jzo2o-api工程
Slf4j
Configuration
EnableFeignClients(basePackages com.jzo2o.api)
Import({com.jzo2o.utils.MyQueryMapEncoder.class})
ConditionalOnProperty(prefix feign, name enable, havingValue true)
public class ClientScanConfiguration {........在CustomerClient 中注入了Feign接口的代理对象通过Feign进行远程调用。
1.4.5 测试
1、通过接口文档测试下单接口触发customerClient.getDetail(addressBookId); 五秒内俩异常就测完了关闭customer服务发两次请求就行了 2、在sentinel中配置熔断规则 5秒以内最少请求2次有1次异常则进行熔断。熔断时长为30秒。
3、测试 一次异常后熔断 后面再发请求就是熔断降级方法
1.5 下单接口设计
整个订单模块包括订单管理、抢单、派单、历史订单四个小模块对应的工程如下
1.5.0 订单号生成规则
常见的订单号生成规则
自增数字序列 使用数据库的自增主键或者其他递增的数字序列比如redis的INCR命令作为订单号的一部分。例如订单号可以是202310280001其中20231028表示日期0001是自增的订单序号。时间戳随机数 将年月日时分秒和一定范围内的随机数组合起来。例如订单号可以是20181028124523 “1234”其中20181028124523表示日期和时间1234是随机生成的数字。 使用时间戳随机数作为主键有重复的风险。订单类型日期序号 将订单类型例如01表示普通订单“02表示VIP订单等、日期和序号组合起来。例如订单号可以是0101028100001”其中01表示订单类型20181028表示日期00001是序号。 加上订单类型的好处是方便客户服务根据订单号就可以知道订单的类型。分布式唯一ID生成器 使用分布式唯一ID生成器例如Snowflake雪花算法生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。 Snowflake 算法根据机器ID、时间戳、序号等因素生成保证全局唯一性它的优势在于生成的 ID 具有趋势递增、唯一性、高效性等特点. Snowflake 算法对系统时钟的依赖性较强如果系统时钟发生回拨可能会导致 ID 生成出现问题。因此在使用 Snowflake 算法时需要定时进行时钟同步确保系统时钟的稳定性。
本项目订单号生成规则
19位2位年2位月2位日13位序号 例如2406011000000000001 实现方案 1、前6位通过当前时间获取。 2、后13位通过Redis的INCR 命令实现。
代码实现 Autowiredprivate RedisTemplate redisTemplate;/*** 生成订单号id 格式:yymmdd13位id* return*/private Long generateOrderId(){//调用redis生成自增序号Long id redisTemplate.opsForValue().increment(RedisConstants.Lock.ORDERS_SHARD_KEY_ID_GENERATOR, 1);long orderId DateUtils.getFormatDate(LocalDateTime.now(), yyMMdd) * 10000000000000L id;return orderId;}
1.5.1 接口分析
除了serve_id、pur_num、serve_start_time 由前端传入以外还需要传入以下参数 优惠券ID用户选择优惠券系统根据优惠券的信息计算优惠金额需要前端传入优惠券的Id。 我的地址簿ID用户从我的地址簿中选择地址前端传入我的地址簿Id系统从我的地址簿中查询服务地址及具体的经纬度坐标。 其中服务和服务项的信息需要远程调用Foundations服务而关于客户的信息需要远程调用Costumer服务 接口定义如下 接口名称下单接口 接口功能普通用户创建订单 接口路径POST/orders-manager/consumer/orders/place 请求数据类型 application/json
1.5.1 接口开发
Controller层开发 ApiOperation(下单接口)PostMapping(/place)public PlaceOrderResDTO place(RequestBody PlaceOrderReqDTO placeOrderReqDTO) {return ordersCreateService.placeOrder(placeOrderReqDTO);}Service层开发 /*** 下单服务* param placeOrderReqDTO* return*/OverrideTransactional(rollbackFor Exception.class)public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//下单人信息获取地址簿调用jzo2o-customer服务获取Long addressBookId placeOrderReqDTO.getAddressBookId();AddressBookResDTO detail customerClient.getDetail(addressBookId);//服务相关信息,调用jzo2o-foundations获取ServeAggregationResDTO serveAggregationResDTO serveApi.findById(placeOrderReqDTO.getServeId());//准备组装数据Orders orders new Orders();//生成订单号Long orderId generateOrderId();orders.setId(orderId);//服务类型组装orders.setServeTypeId(serveAggregationResDTO.getServeTypeId());orders.setServeTypeName(serveAggregationResDTO.getServeTypeName());orders.setServeItemId(serveAggregationResDTO.getServeItemId());orders.setServeItemName(serveAggregationResDTO.getServeItemName());orders.setServeItemImg(serveAggregationResDTO.getServeItemImg());orders.setUnit(serveAggregationResDTO.getUnit());orders.setServeId(placeOrderReqDTO.getServeId());//下单人idorders.setUserId(UserContext.currentUserId());//订单状态默认待支付orders.setOrdersStatus(OrderStatusEnum.NO_PAY.getStatus());//支付状态orders.setPayStatus(OrderPayStatusEnum.NO_PAY.getStatus());//价格orders.setPrice(serveAggregationResDTO.getPrice());orders.setPurNum(placeOrderReqDTO.getPurNum());orders.setTotalAmount(serveAggregationResDTO.getPrice().multiply(new BigDecimal(placeOrderReqDTO.getPurNum())));//优惠价格orders.setDiscountAmount(BigDecimal.ZERO);//实际价格orders.setRealPayAmount(NumberUtils.sub(orders.getTotalAmount(),orders.getDiscountAmount()));//地址薄方面属性组装orders.setCityCode(serveAggregationResDTO.getCityCode());String ServeAddress new StringBuffer(detail.getProvince()).append(detail.getCity()).append(detail.getCounty()).append(detail.getAddress()).toString();orders.setServeAddress(ServeAddress);orders.setContactsPhone(detail.getPhone());orders.setContactsName(detail.getName());orders.setServeStartTime(placeOrderReqDTO.getServeStartTime());orders.setLon(detail.getLon());orders.setLat(detail.getLat());//排序字段,根据服务开始时间转为毫秒时间戳订单后5位long sortBy DateUtils.toEpochMilli(orders.getServeStartTime()) orders.getId() % 100000;orders.setSortBy(sortBy);//插入数据库订单表boolean save this.save(orders);if (!save) {throw new DbRuntimeException(下单失败);}return new PlaceOrderResDTO(orders.getId());}下单预约功能测试 输入信息点击立即预约
查一下数据库 预约功能实现完成
2 支付功能
2.1 支付方式
小程序调起支付这里微信会校验小程序的APPID与微信支付商户的ID是否绑定微信支付商户的ID怎么获取呢是需要注册商户上传企业资料及法人资料微信审核通过后方可 注册成功所以注册成为一个普通商户对项目测试有限制。 为了简便使用支付接口使用扫码支付支付此接口不存在小程序端调起支付的限制也就是使用Native支付接口 具体使用https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html
2.2 支付服务的设计
2.2.1 表设计
支付渠道表 支付渠道表存储了第三方支付微信、支付宝的支付参数如商户号、证书序列号、api私钥等信息
create table jzo2o-trade.pay_channel
(id bigint not null comment 主键constraint PRIMARYprimary key,channel_name varchar(32) null comment 通道名称,channel_label varchar(32) null comment 通道唯一标记,domain varchar(255) null comment 域名,app_id varchar(32) collate utf8_bin not null comment 商户appid,public_key varchar(2000) collate utf8_bin not null comment 支付公钥,merchant_private_key varchar(2000) collate utf8_bin not null comment 商户私钥,other_config varchar(1000) null comment 其他配置,encrypt_key varchar(255) charset utf8mb4 null comment AES混淆密钥,remark varchar(400) null comment 说明,notify_url varchar(255) null comment 回调地址,enable_flag varchar(10) null comment 是否有效,enterprise_id bigint null comment 商户ID【系统内部识别使用】,create_time datetime default CURRENT_TIMESTAMP null comment 创建时间,update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment 更新时间
)交易单表 支付服务请求第三方支付下单成功向交易表写入一条记录 家政服务的一个订单可能对应支付服务的多条交易单比如用户用微信支付在交易单表生成一条交易单如果微信支付失败再用支付宝支付时也会在交易单表中生成一条记录。 用户支付成功后支付服务更新交易单表的支付状态。
create table jzo2o-trade.trading
(id bigint not null comment 主键constraint PRIMARYprimary key,product_order_no bigint not null comment 业务系统订单号,trading_order_no bigint not null comment 交易系统订单号【对于三方来说商户订单】,transaction_id varchar(50) null comment 第三方支付交易号,trading_channel varchar(32) charset utf8mb4 not null comment 支付渠道【支付宝、微信、现金、免单挂账】,trading_type varchar(22) not null comment 交易类型【付款、退款、免单、挂账】,trading_state int not null comment 交易单状态【2-付款中,3-付款失败,4-已结算,5-取消订单,6-免单,7-挂账】,payee_name varchar(50) null comment 收款人姓名,payee_id bigint null comment 收款人账户ID,payer_name varchar(50) null comment 付款人姓名,payer_id bigint null comment 付款人Id,trading_amount decimal(22, 2) not null comment 交易金额单位元,refund decimal(12, 2) null comment 退款金额【付款后单位元,is_refund varchar(32) charset utf8mb4 null comment 是否有退款YESNO,result_code varchar(80) null comment 第三方交易返回编码【最终确认交易结果】,result_msg varchar(255) null comment 第三方交易返回提示消息【最终确认交易信息】,result_json varchar(2000) null comment 第三方交易返回信息json【分析交易最终信息】,place_order_code varchar(80) null comment 统一下单返回编码,place_order_msg varchar(255) null comment 统一下单返回信息,place_order_json text null comment 统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】,enterprise_id bigint not null comment 商户号,memo varchar(150) null comment 备注【订单门店桌台信息】,qr_code text null comment 二维码base64数据,open_id varchar(36) collate utf8mb4_unicode_ci null comment open_id标识,enable_flag varchar(10) null comment 是否有效,create_time datetime default CURRENT_TIMESTAMP null comment 创建时间,update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment 更新时间,constraint trading_order_nounique (trading_order_no) comment 支付订单号
)退款记录表 用户申请退款在退款记录表写一条记录。 退款成功后支付服务更新退款状态。
create table jzo2o-trade.refund_record
(id bigint not null comment 主键constraint PRIMARYprimary key,trading_order_no bigint not null comment 交易系统订单号【对于三方来说商户订单】,product_order_no bigint not null comment 业务系统订单号,refund_no bigint not null comment 本次退款订单号,refund_id varchar(50) null comment 第三方支付的退款单号,enterprise_id bigint not null comment 商户号,trading_channel varchar(32) charset utf8mb4 not null comment 退款渠道【支付宝、微信、现金】,refund_status int not null comment 退款状态0-发起退款,1-退款中2-成功, 3-失败,refund_code varchar(80) charset utf8 null comment 返回编码,refund_msg text charset utf8 null comment 返回信息,memo varchar(150) charset utf8 null comment 备注【订单门店桌台信息】,refund_amount decimal(12, 2) not null comment 本次退款金额,total decimal(12, 2) not null comment 原订单金额,create_time datetime default CURRENT_TIMESTAMP null comment 创建时间,update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment 更新时间,constraint refund_nounique (refund_no)
)2.2.2 数据流
支付接口收到支付请求后请求第三方支付的下单接口并向交易单表新增记录。 查询交易结果接口请求第三方支付的查询支付结果并更新交易单表的支付状态。 接收第三方通过支付结果更新交易单表的支付状态。 退款接口新增退款记录 更新退款状态请求第三方退款结果查询接口查询退款状态并更新退款状态。
2.2.3 支付/退款代码
Controller
Validated
RestController(innerNativePayController)
Api(tags 内部接口 - Native支付)
RequestMapping(/inner/native)
public class NativePayController implements NativePayApi {Resourceprivate NativePayService nativePayService;/**** 扫码支付收银员通过收银台或商户后台调用此接口生成二维码后展示给用户由用户扫描二维码完成订单支付。** param nativePayDTO 扫码支付提交参数* return 扫码支付响应数据其中包含二维码路径*/OverridePostMappingApiOperation(value 统一收单线下交易, notes 统一收单线下交易)ApiImplicitParam(name nativePayDTO, value 扫码支付提交参数, required true)public NativePayResDTO createDownLineTrading(RequestBody NativePayReqDTO nativePayDTO) {Trading tradingEntity BeanUtil.toBean(nativePayDTO, Trading.class);Trading trading this.nativePayService.createDownLineTrading(nativePayDTO.isChangeChannel(),tradingEntity);return BeanUtil.toBean(trading, NativePayResDTO.class);}}Service public Trading createDownLineTrading(boolean changeChannel,Trading tradingEntity) {//获取付款中的记录Trading trading tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel());//如果切换二维码需要查询其它支付渠道付款中的交易单进行退款操作if(changeChannel){changeChannelAndCloseTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(),tradingEntity.getTradingChannel());}//付款中的记录直接返回无需生成二维码if (ObjectUtil.isNotNull(trading)){return trading;}//交易前置处理检测交易单参数beforePayHandler.checkCreateTrading(tradingEntity);tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK);tradingEntity.setEnableFlag(Constants.YES);//对交易订单加锁Long productOrderNo tradingEntity.getProductOrderNo();String key TradingCacheConstant.CREATE_PAY productOrderNo;RLock lock redissonClient.getFairLock(key);try {//获取锁if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {//交易前置处理幂等性处理
// this.beforePayHandler.idempotentCreateTrading(tradingEntity);//#x8C03;#x7528;#x4E0D;#x540C;#x7684;#x652F;#x4ED8;#x6E20;#x9053;#x8FDB;#x884C;#x5904;#x7406;PayChannelEnum payChannel PayChannelEnum.valueOf(tradingEntity.getTradingChannel());NativePayHandler nativePayHandler HandlerFactory.get(payChannel, NativePayHandler.class);nativePayHandler.createDownLineTrading(tradingEntity);//生成统一收款二维码String placeOrderMsg tradingEntity.getPlaceOrderMsg();String qrCode this.qrCodeService.generate(placeOrderMsg, payChannel);tradingEntity.setQrCode(qrCode);//指定交易状态为付款中tradingEntity.setTradingState(TradingStateEnum.FKZ);//新增交易数据boolean flag this.tradingService.save(tradingEntity);if (!flag) {throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.SAVE_OR_UPDATE_FAIL.getValue());}return tradingEntity;}throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());} catch (CommonException e) {throw e;} catch (Exception e) {log.error(统一收单线下交易预创建异常:{}, ExceptionUtil.stacktraceToString(e));throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());} finally {lock.unlock();}}其中微信接口 Overridepublic void createDownLineTrading(Trading tradingEntity) throws CommonException {// 查询配置WechatPayHttpClient client WechatPayHttpClient.get(tradingEntity.getEnterpriseId());//请求地址String apiPath /v3/pay/transactions/native;//请求参数MapString, Object params MapUtil.String, Objectbuilder().put(mchid, client.getMchId()).put(appid, client.getAppId()).put(description, tradingEntity.getMemo()).put(notify_url, client.getNotifyUrl()).put(out_trade_no, Convert.toStr(tradingEntity.getTradingOrderNo())).put(amount, MapUtil.String, Objectbuilder().put(total, Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额单位分.put(currency, CNY) //人民币.build()).build();try {WeChatResponse response client.doPost(apiPath, params);if (!response.isOk()) {//下单失败throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());}//指定统一下单codetradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));//二维码需要展现的信息tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr(code_url));//指定统一下单json字符串tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));} catch (Exception e) {throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());}}2.3 订单支付接口
2.3.0 需求
用户点击支付按钮前端请求订单服务订单服务请求支付服务最后支付服务请求微信的下单接口。 流程如下 通过交互流程可知 订单管理服务请求支付服务生成支付二维码拿到交易单号将其和支付渠道更新到订单表。 最后订单管理服务将交易单信息及二维码返回给前端。
2.3.1 接口分析
要生成二维码需要由订单管理服务请求支付服务的支付接口支付服务的支付接口如下 接口路径POST/trade/inner/native 请求数据类型 application/json 因此支付接口需要
productOrderNo是业务系统的订单号本项目就是家政服务的订单号。
tradingAmount支付金额
enterpriseId商户号进入微信或支付宝商户平台获取。
memo: 备注信息
tradingChannel微信支付传入WECHAT_PAY支付宝支付传入ALI_PAY
changeChannel当用户先微信支付然后又进行支付宝支付表示切换了支付渠道此时传入true我们需要根据支付服务的支付接口的参数分析这些参数的数据来源 productOrderNo即订单号由前端传入 tradingAmount根据订单号查询订单信息即可拿到金额 enterpriseId在nacos配置好 memo程序拼装 tradingChannel支付渠道前端传入 changeChannel根据订单号查询订单表的trading_channel字段来判断。第一次支付后将第一次支付的支付渠道更新至订单表第二次如果切换了支付渠道通过trading_channel字段可以知道是否切换支付渠道。
所以综上分析前端请求订单管理服务提供的支付接口需要传入 productOrderNo: 订单id tradingChannel:支付渠道。
接口定义如下
2.3.2 接口开发
Controller层开发
RestController(consumerOrdersController)
Api(tags 用户端-订单相关接口)
RequestMapping(/consumer/orders)
public class ConsumerOrdersController {Resourceprivate IOrdersCreateService ordersCreateService;Resourceprivate IOrdersManagerService ordersManagerService;PutMapping(/pay/{id})ApiOperation(订单支付)ApiImplicitParams({ApiImplicitParam(name id, value 订单id, required true, dataTypeClass Long.class)})public OrdersPayResDTO pay(PathVariable(id) Long id, RequestBody OrdersPayReqDTO ordersPayReqDTO) {return null;}Mapper层
根据接口分析请求支付服务生成支付二维码成功将交易单号和支付渠道更新到订单表中。 单表操作不需要写了
Service层开发
其中我们需要将nacos配置文件的商户号拿出来因本次写一个配置文件类去进行注入
Data
Component
ConfigurationProperties(prefix jzo2o.trade)
public class TradeProperties {/*** 支付宝商户id*/private Long aliEnterpriseId;/*** 微信支付商户id*/private Long wechatEnterpriseId;
}之后正式进行Service层开发其中远程调用还是使用API工程进行调用支付微服务暂时不写熔断降级业务流程了 /*** 订单支付实现* param id 订单id* param ordersPayReqDTO 支付类型请求体* return*/Overridepublic OrdersPayResDTO pay(Long id, OrdersPayReqDTO ordersPayReqDTO) {//看看订单支付了吗或者存在吗Orders orders this.getById(id);if(ObjectUtils.isNull(orders)){throw new CommonException(订单不存在);}else if(orders.getPayStatus().equals(OrderPayStatusEnum.PAY_SUCCESS)){OrdersPayResDTO ordersPayResDTO BeanUtils.copyBean(orders,OrdersPayResDTO.class);ordersPayResDTO.setProductOrderNo(orders.getId());return ordersPayResDTO;}else{//生成支付二维码NativePayResDTO nativePayResDTO generateQrCode(orders,ordersPayReqDTO.getTradingChannel());OrdersPayResDTO ordersPayResDTO BeanUtil.toBean(nativePayResDTO, OrdersPayResDTO.class);return ordersPayResDTO;}}/*** 请求支付服务生成二维码* param orders 订单对象* param tradingChannel 请求渠道*/private NativePayResDTO generateQrCode(Orders orders, PayChannelEnum tradingChannel) {//封装请求支付服务的参数NativePayReqDTO nativePayReqDTO new NativePayReqDTO();//判断请求渠道微信还是支付宝从而取出相应商户号Long enterpriseId ObjectUtils.equal(tradingChannel, PayChannelEnum.WECHAT_PAY) ? tradeProperties.getWechatEnterpriseId() : tradeProperties.getAliEnterpriseId();nativePayReqDTO.setEnterpriseId(enterpriseId);//家政订单号nativePayReqDTO.setProductOrderNo(orders.getId());//金额nativePayReqDTO.setTradingAmount(orders.getRealPayAmount());//业务系统标识 统一为jzo2o.ordersnativePayReqDTO.setProductAppId(jzo2o.orders);//请求的支付渠道nativePayReqDTO.setTradingChannel(tradingChannel);//是否切换支付渠道//首先拿到当前orders的支付渠道和现在的比一下if(ObjectUtils.isNotEmpty(orders.getTradingChannel()) ObjectUtils.notEqual(orders.getTradingChannel(),tradingChannel.getValue())){//表示切换渠道了nativePayReqDTO.setChangeChannel(true);}else{nativePayReqDTO.setChangeChannel(false);}//设置备注nativePayReqDTO.setMemo(orders.getServeItemName());//远程调用请求支付服务生成支付二维码NativePayResDTO downLineTrading nativePayApi.createDownLineTrading(nativePayReqDTO);//二维码生成成功拿到支付服务返回的交易单号更新数据库中if(ObjectUtils.isNotNull(downLineTrading)){log.info(订单:{}请求支付,生成二维码:{},orders.getId(),downLineTrading.toString());//更新订单表的支付订单号和交易渠道boolean update lambdaUpdate().eq(Orders::getId, downLineTrading.getProductOrderNo()).set(Orders::getTradingOrderNo, downLineTrading.getTradingOrderNo()).set(Orders::getTradingChannel, downLineTrading.getTradingChannel()).update();if(!update){throw new CommonException(订单:orders.getId()请求支付更新交易单号失败);}}else{throw new CommonException(请求支付服务生成二维码失败);}return downLineTrading;}2.3.3 接口测试
点击预约之后 之后付钱查看数据库
2.4 对接查询支付结果
2.4.1 需求分析
在用户支付后用户点击“完成支付”此时前端请求订单服务的查询支付结果接口如果支付成功则跳转到支付成功界面。 交互流程如下 此接口对于支付中的订单最终由支付服务调用微信查询支付结果。 订单管理服务查询到支付结果后更新订单的支付状态。
2.4.2 接口分析 传入参数 本接口要调用支付服务的支付结果查询接口根据支付服务的支付结果查询接口的传入参数分析本接口的传入参数。 支付服务的支付结果查询接口需要传入交易单号。交易单号在订单表已经保存所以前端传入订单号即可拿到交易单号。 所以传入参数订单号 响应结果 本接口的目的是查询支付结果所以响应结果中要有支付结果其它的参数就是订单号、交易单号等相关参数。
2.4.3 接口开发
Controller层开发 GetMapping(/pay/{id}/result)ApiOperation(查询订单支付结果)ApiImplicitParams({ApiImplicitParam(name id, value 订单id, required true, dataTypeClass Long.class)})public OrdersPayResDTO payResult(PathVariable(id) Long id) {return ordersCreateService.getPayResultFromTradServer(id);}Mapper层开发
拿到支付结果更新订单表的支付结果。不需要写直接mp即可
Service层开发
根据接口分析查询支付结果需要做两部分的工作 调用支付服务的查询支付结果接口获取支付结果。 将支付结果更新至订单表的支付状态字段。 /*** 请求支付服务获取支付结果* param id* return*/Overridepublic OrdersPayResDTO getPayResultFromTradServer(Long id) {//根据订单号查询订单信息拿到交易单号Orders orders this.getById(id);//校验存在性if(ObjectUtils.isNull(orders)){throw new CommonException(订单不存在);}//未支付且已存在支付服务的交易单号此时远程调用支付服务查询支付结果if (OrderPayStatusEnum.NO_PAY.getStatus() orders.getPayStatus() ObjectUtil.isNotEmpty(orders.getTradingOrderNo())) {//根据交易单号请求支付服务查询支付结果TradingResDTO tradResultByTradingOrderNo tradingApi.findTradResultByTradingOrderNo(orders.getTradingOrderNo());//如果支付成功更新数据库if(ObjectUtils.equal((tradResultByTradingOrderNo.getTradingState()), TradingStateEnum.YJS)){TradeStatusMsg tradeStatusMsg new TradeStatusMsg();tradeStatusMsg.setProductOrderNo(id);tradeStatusMsg.setTradingChannel(tradResultByTradingOrderNo.getTradingChannel());tradeStatusMsg.setTradingOrderNo(tradResultByTradingOrderNo.getTradingOrderNo());tradeStatusMsg.setTransactionId(tradResultByTradingOrderNo.getTransactionId());tradeStatusMsg.setStatusCode(OrderPayStatusEnum.PAY_SUCCESS.getStatus());tradeStatusMsg.setStatusName(OrderPayStatusEnum.PAY_SUCCESS.name());owner.paySuccess(tradeStatusMsg);//构造返回数据OrdersPayResDTO ordersPayResDTO BeanUtils.toBean(tradeStatusMsg,OrdersPayResDTO.class);ordersPayResDTO.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());return ordersPayResDTO;}}//支付未成功 或者 早就支付了OrdersPayResDTO ordersPayResDTO new OrdersPayResDTO();ordersPayResDTO.setPayStatus(orders.getPayStatus());ordersPayResDTO.setProductOrderNo(orders.getId());ordersPayResDTO.setTradingOrderNo(orders.getTradingOrderNo());ordersPayResDTO.setTradingChannel(orders.getTradingChannel());return ordersPayResDTO;}/*** 支付成功 更新数据库的订单表及其他信息** param tradeStatusMsg 交易状态消息*/OverrideTransactional(rollbackFor Exception.class)public void paySuccess(TradeStatusMsg tradeStatusMsg){boolean update lambdaUpdate().eq(Orders::getId, tradeStatusMsg.getProductOrderNo()).eq(Orders::getOrdersStatus,0) //订单状态只能由待支付 才可以变为派单中.set(Orders::getPayStatus, OrderPayStatusEnum.PAY_SUCCESS.getStatus()).set(Orders::getTransactionId, tradeStatusMsg.getTransactionId()).set(Orders::getOrdersStatus, OrderStatusEnum.DISPATCHING.getStatus()) //订单状态变为派单中.set(Orders::getPayTime, LocalDateTime.now()).update();if(!update){throw new CommonException(支付成功更新tradeStatusMsg.getProductOrderNo()订单状态为派单中失败);}}2.4.4 接口测试
支付完成后点击支付完成 之后可以查看订单状态
2.5 接收支付通知
2.5.1 需求分析
(尴尬的一点就是扫码支付需要用户付完钱点击支付完成那如果不点击就不会更新支付状态因此需要这个模块) 用户支付成功后支付服务 获取支付结果后会通知业务系统业务系统收到支付结果只处理属于自己的支付通知根据支付结果更新订单表的支付状态现在来实现订单管理服务接收支付通知更新支付状态功能。
订单管理服务接收支付通知的交互流程如下 支付服务将支付结果发送到MQ订单管理服务监听MQ收到支付结果更新订单表的支付状态。 这里有个问题是支付服务作为项目的公共支付服务对接支付服务的可能不止家政服务订单还可能有其它收费订单比如年会员订单购买优惠券等等支付服务如何将属于每个收费订单的支付结果通知给它们呢
首先在请求支付服务支付接口中需要传入product_app_id它表示请求支付业务系统的应用标识 此应用标识会存储到交易单信息 支付服务通知支付结果时将交易单中的product_app_id一起发给各个监听MQ的微服务。 具体的方法是如下 支付服务向jzo2o.exchange.topic.trade交换机发送消息Routing KeyUPDATE_STATUS 绑定此交换机的有多个队列每个队列是不同的收费订单支付通知队列如下图 当支付服务向jzo2o.exchange.topic.trade交换机发送一条支付通知消息所有绑定此交换机的队列且Routing KeyUPDATE_STATUS都会收到支付通知。 业务系统收到支付结果后解析出product_app_id判断是否属于自己的支付结果通知如果是再进行处理。 整体交互流程如下
2.5.2 接受通知实现 Resourceprivate IOrdersCreateService ordersCreateService;/*** 更新支付结果* 支付成功** param msg 消息*/RabbitListener(bindings QueueBinding(value Queue(name MqConstants.Queues.ORDERS_TRADE_UPDATE_STATUS),exchange Exchange(name MqConstants.Exchanges.TRADE, type ExchangeTypes.TOPIC),key MqConstants.RoutingKeys.TRADE_UPDATE_STATUS))public void listenTradeUpdatePayStatusMsg(String msg) {//解析消息 消息内容是ListTradeStatusMsg的json格式ListTradeStatusMsg tradeStatusMsgs JSON.parseArray(msg, TradeStatusMsg.class);//判断是不是自己的 根据productAppId判断//还需要只取出支付成功的结果ListTradeStatusMsg collect tradeStatusMsgs.stream().filter(item - item.getProductAppId().equals(jzo2o.orders) item.getStatusCode() 4).collect(Collectors.toList());//调用方法更新数据库订单表的支付状态和订单状态for (TradeStatusMsg tradeStatusMsg : collect) {ordersCreateService.paySuccess(tradeStatusMsg);}}2.5.3 定时查询支付状态并通知的实现
首先分为两类付款中和其他 对于付款中的订单计算是否超时 对于其他类型的订单例如付款成功付款失败等等直接发到交换机让相关业务去处理相关业务会将付款成功的写入数据库 * 分片广播方式查询支付状态* 逻辑每次最多查询{tradingCount}个未完成的交易单交易单id与shardTotal取模值等于shardIndex进行处理*/XxlJob(tradingJob)public void tradingJob() {// 分片参数int shardIndex NumberUtil.max(XxlJobHelper.getShardIndex(), 0);int shardTotal NumberUtil.max(XxlJobHelper.getShardTotal(), 1);ListTrading list this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);if (CollUtil.isEmpty(list)) {XxlJobHelper.log(查询到交易单列表为空shardIndex {}, shardTotal {}, shardIndex, shardTotal);return;}//定义消息通知列表只要是状态不为【付款中】就需要通知其他系统ListTradeStatusMsg tradeMsgList new ArrayList();for (Trading trading : list) {if (trading.getTradingOrderNo() % shardTotal ! shardIndex) {continue;}try {//查询交易单TradingDTO tradingDTO this.basicPayService.queryTradingResult(trading.getTradingOrderNo());if (TradingStateEnum.FKZ ! tradingDTO.getTradingState()) {TradeStatusMsg tradeStatusMsg TradeStatusMsg.builder().tradingOrderNo(trading.getTradingOrderNo()).productOrderNo(trading.getProductOrderNo()).productAppId(trading.getProductAppId()).transactionId(tradingDTO.getTransactionId()).tradingChannel(tradingDTO.getTradingChannel()).statusCode(tradingDTO.getTradingState().getCode()).statusName(tradingDTO.getTradingState().name()).info(tradingDTO.getMemo())//备注信息.build();tradeMsgList.add(tradeStatusMsg);}else{//如果是未支付需要判断下时间超过20分钟未支付的订单需要关闭订单以及设置状态为QXDDlong between LocalDateTimeUtil.between(trading.getCreateTime(), LocalDateTimeUtil.now(), ChronoUnit.MINUTES);if (between 20) {try {basicPayService.closeTrading(trading.getTradingOrderNo());} catch (Exception e) {log.error(超过20分钟未支付自动关单出现异常,交易单号:{},trading.getTradingOrderNo());}}}} catch (Exception e) {XxlJobHelper.log(查询交易单出错shardIndex {}, shardTotal {}, trading {}, shardIndex, shardTotal, trading, e);}}if (CollUtil.isEmpty(tradeMsgList)) {return;}//发送消息通知其他系统String msg JSONUtil.toJsonStr(tradeMsgList);rabbitClient.sendMsg(MqConstants.Exchanges.TRADE, MqConstants.RoutingKeys.TRADE_UPDATE_STATUS, msg);}之后在xxl-job中配置定时任务
2.5.3 测试不点支付成功成功更新的业务
下个订单支付了但是不点击支付完成 之后看一看订单会不会自动更新状态 测试成功 文章转载自: http://www.morning.ndngj.cn.gov.cn.ndngj.cn http://www.morning.wgcng.cn.gov.cn.wgcng.cn http://www.morning.yqhdy.cn.gov.cn.yqhdy.cn http://www.morning.nkjjp.cn.gov.cn.nkjjp.cn http://www.morning.rjjys.cn.gov.cn.rjjys.cn http://www.morning.ycmpk.cn.gov.cn.ycmpk.cn http://www.morning.ydzly.cn.gov.cn.ydzly.cn http://www.morning.clkyw.cn.gov.cn.clkyw.cn http://www.morning.rqrxh.cn.gov.cn.rqrxh.cn http://www.morning.cbczs.cn.gov.cn.cbczs.cn http://www.morning.rswtz.cn.gov.cn.rswtz.cn http://www.morning.ntnml.cn.gov.cn.ntnml.cn http://www.morning.ckfqt.cn.gov.cn.ckfqt.cn http://www.morning.jtfsd.cn.gov.cn.jtfsd.cn http://www.morning.kbqbx.cn.gov.cn.kbqbx.cn http://www.morning.kqgqy.cn.gov.cn.kqgqy.cn http://www.morning.mzydm.cn.gov.cn.mzydm.cn http://www.morning.skrh.cn.gov.cn.skrh.cn http://www.morning.jllnh.cn.gov.cn.jllnh.cn http://www.morning.rpfpx.cn.gov.cn.rpfpx.cn http://www.morning.grnhb.cn.gov.cn.grnhb.cn http://www.morning.zsthg.cn.gov.cn.zsthg.cn http://www.morning.rftk.cn.gov.cn.rftk.cn http://www.morning.gsdbg.cn.gov.cn.gsdbg.cn http://www.morning.qctsd.cn.gov.cn.qctsd.cn http://www.morning.nnpwg.cn.gov.cn.nnpwg.cn http://www.morning.ccsdx.cn.gov.cn.ccsdx.cn http://www.morning.qysnd.cn.gov.cn.qysnd.cn http://www.morning.jgcxh.cn.gov.cn.jgcxh.cn http://www.morning.lhztj.cn.gov.cn.lhztj.cn http://www.morning.cdrzw.cn.gov.cn.cdrzw.cn http://www.morning.mwmxs.cn.gov.cn.mwmxs.cn http://www.morning.zpnfc.cn.gov.cn.zpnfc.cn http://www.morning.datadragon-auh.cn.gov.cn.datadragon-auh.cn http://www.morning.mbnhr.cn.gov.cn.mbnhr.cn http://www.morning.kgjyy.cn.gov.cn.kgjyy.cn http://www.morning.xnkb.cn.gov.cn.xnkb.cn http://www.morning.nxbkw.cn.gov.cn.nxbkw.cn http://www.morning.fllx.cn.gov.cn.fllx.cn http://www.morning.qlsbz.cn.gov.cn.qlsbz.cn http://www.morning.wdshp.cn.gov.cn.wdshp.cn http://www.morning.jxscp.cn.gov.cn.jxscp.cn http://www.morning.rhzzf.cn.gov.cn.rhzzf.cn http://www.morning.mlfmj.cn.gov.cn.mlfmj.cn http://www.morning.pnntx.cn.gov.cn.pnntx.cn http://www.morning.fslxc.cn.gov.cn.fslxc.cn http://www.morning.rcmcw.cn.gov.cn.rcmcw.cn http://www.morning.rhsr.cn.gov.cn.rhsr.cn http://www.morning.fswml.cn.gov.cn.fswml.cn http://www.morning.xywfz.cn.gov.cn.xywfz.cn http://www.morning.ptysj.cn.gov.cn.ptysj.cn http://www.morning.mztyh.cn.gov.cn.mztyh.cn http://www.morning.qpqwd.cn.gov.cn.qpqwd.cn http://www.morning.kybyf.cn.gov.cn.kybyf.cn http://www.morning.qkqzm.cn.gov.cn.qkqzm.cn http://www.morning.qgmwt.cn.gov.cn.qgmwt.cn http://www.morning.mjqms.cn.gov.cn.mjqms.cn http://www.morning.ckwxs.cn.gov.cn.ckwxs.cn http://www.morning.tnhqr.cn.gov.cn.tnhqr.cn http://www.morning.btqqh.cn.gov.cn.btqqh.cn http://www.morning.rmfw.cn.gov.cn.rmfw.cn http://www.morning.qbxdt.cn.gov.cn.qbxdt.cn http://www.morning.qqfcf.cn.gov.cn.qqfcf.cn http://www.morning.ftntr.cn.gov.cn.ftntr.cn http://www.morning.zcwwb.cn.gov.cn.zcwwb.cn http://www.morning.lsgjf.cn.gov.cn.lsgjf.cn http://www.morning.ruyuaixuexi.com.gov.cn.ruyuaixuexi.com http://www.morning.mbrbg.cn.gov.cn.mbrbg.cn http://www.morning.glwyn.cn.gov.cn.glwyn.cn http://www.morning.wgkz.cn.gov.cn.wgkz.cn http://www.morning.nwqyq.cn.gov.cn.nwqyq.cn http://www.morning.ffgbq.cn.gov.cn.ffgbq.cn http://www.morning.cthrb.cn.gov.cn.cthrb.cn http://www.morning.wbrf.cn.gov.cn.wbrf.cn http://www.morning.rlfr.cn.gov.cn.rlfr.cn http://www.morning.jgncd.cn.gov.cn.jgncd.cn http://www.morning.wyfpc.cn.gov.cn.wyfpc.cn http://www.morning.nlcw.cn.gov.cn.nlcw.cn http://www.morning.mztyh.cn.gov.cn.mztyh.cn http://www.morning.kxrld.cn.gov.cn.kxrld.cn