同城O2O海外版二次开发实战:从支付网关到配送算法

一、独立部署只是起点,二次开发才是灵魂

让系统适应海外不同市场的,一定是二次开发------比如接入柬埔寨的 Wing 支付、泰国的 K PLUS,或者修改配送费公式为"距离 × 基础费率 + 高峰倍数"。

本文假设你已经完成了源码独立部署(Spring Boot + MyBatis-Plus + Uni-app),我们将深入到代码层面,讲解三个最典型的二次开发场景:

  1. 新增一个自定义支付插件(Stripe 之外的聚合支付,如 PayMongo)

  2. 修改配送调度算法,从简单的就近派单改为多因素加权

  3. 扩展数据库与 API,增加"预订时间段"功能

所有代码示例均为真实可运行的简化版本,完整源码可以在授权后获取。


二、准备工作:理解系统模块结构

克隆代码后,你会看到类似这样的目录结构:

复制代码
guanghe-o2o/
├── backend/                 # 后端 Java Spring Boot
│   ├── guanghe-core/        # 核心实体、工具类
│   ├── guanghe-module-xxx/  # 用户、订单、商家等模块
│   ├── guanghe-plugin/      # 插件目录
│   │   ├── payment/         # 支付插件
│   │   ├── sms/             # 短信插件
│   │   └── map/             # 地图插件
│   └── guanghe-admin/       # 运营后台 API
├── frontend/                # Uni-app 前端
├── database/                # SQL 初始化脚本
└── docs/                    # 开发手册

二次开发的核心原则:不要直接修改 core 或 module 下的核心类,而是通过插件接口或继承重写的方式扩展。如果实在要改核心,务必记录在自定义分支的 Commit Message 中。


三、实战一:开发一个自定义支付插件(以 PayMongo 为例)

3.1 实现支付接口

光合同城定义了一个通用支付接口:

// 文件路径:guanghe-core/src/main/java/com/guanghe/payment/PaymentGateway.java

public interface PaymentGateway {

// 生成支付凭证(返回支付链接或二维码)

PaymentResponse createPayment(PaymentRequest request);

// 查询订单状态

PaymentStatus queryPayment(String transactionId);

// 处理异步回调

void handleWebhook(HttpServletRequest request);

}

现在我们要为菲律宾常用的支付 PayMongo 实现它。在 guanghe-plugin/payment/ 下新建模块 paymongo

@Component("paymongoGateway")

public class PayMongoPaymentGateway implements PaymentGateway {

@Value("${payment.paymongo.secret-key}")

private String secretKey;

@Autowired

private RestTemplate restTemplate;

@Override

public PaymentResponse createPayment(PaymentRequest request) {

// 1. 组装 PayMongo 需要的 JSON

JSONObject payload = new JSONObject();

JSONObject data = new JSONObject();

JSONObject attributes = new JSONObject();

attributes.put("amount", request.getAmount().multiply(new BigDecimal(100)).intValue()); // 单位分

attributes.put("currency", request.getCurrencyCode());

attributes.put("description", request.getOrderNo());

data.put("attributes", attributes);

payload.put("data", data);

// 2. 发送 HTTP 请求

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

headers.setBasicAuth(secretKey, "");

HttpEntity<String> entity = new HttpEntity<>(payload.toJSONString(), headers);

ResponseEntity<String> response = restTemplate.exchange(

"https://api.paymongo.com/v1/checkout_sessions",

HttpMethod.POST, entity, String.class);

// 3. 解析返回的 checkout_url

JSONObject respJson = JSONObject.parseObject(response.getBody());

String checkoutUrl = respJson.getJSONObject("data").getJSONObject("attributes").getString("checkout_url");

String transactionId = respJson.getJSONObject("data").getString("id");

return new PaymentResponse(transactionId, checkoutUrl);

}

@Override

public PaymentStatus queryPayment(String transactionId) { /* 省略实现 */ }

@Override

public void handleWebhook(HttpServletRequest request) { /* 解析回调,更新订单状态 */ }

}

3.2 前端调用

前端支付组件会遍历已启用的支付网关列表。用户选中 PayMongo 后,后端返回 checkout_url,前端直接 WebView 打开即可。

// frontend/pages/order/pay.vue

async selectPayMethod(gatewayCode) {

if (gatewayCode === 'paymongo') {

const res = await api.createPayment({orderId: this.orderId, gateway: 'paymongo'});

plus.runtime.openURL(res.checkoutUrl); // 打开外部浏览器

}

}

四、实战二:修改配送调度算法(就近抢单 → 加权派单)

原始系统采用"广播给附近3公里骑手,先抢多得"。但海外某些地区劳动力充足但路况复杂,需要改为派单制:系统根据骑手评分、繁忙度、距离、历史完单率计算综合分,派给最高分者。

4.1 重写订单分配服务

找到 guanghe-module-order/src/main/.../service/OrderDispatchService.java,官方预留了扩展点:

public interface DispatchStrategy {

List<Long> selectCandidates(Long orderId, List<Rider> availableRiders);

}

默认实现是 NearbyFirstStrategy。现在我们创建 WeightedScoreStrategy

@Component("weightedScore")

public class WeightedScoreStrategy implements DispatchStrategy {

@Override

public List<Long> selectCandidates(Long orderId, List<Rider> availableRiders) {

// 1. 获取订单的经纬度

Order order = orderService.getById(orderId);

GeoPoint orderPoint = new GeoPoint(order.getLat(), order.getLng());

// 2. 为每个骑手打分

Map<Rider, Double> scoreMap = new HashMap<>();

for (Rider rider : availableRiders) {

double distance = GeoUtils.distance(orderPoint, rider.getCurrentLocation());

double distanceScore = Math.max(0, 1 - distance / 5000.0); // 5km内得分高

double ratingScore = rider.getAvgRating() / 5.0; // 评分 0~1

double busyScore = 1.0 - (rider.getActiveOrdersCount() / 10.0);

double completionScore = rider.getCompletionRate() / 100.0;

// 权重可配置:距离30% 评分25% 空闲30% 完单率15%

double total = distanceScore * 0.3 + ratingScore * 0.25

  • busyScore * 0.3 + completionScore * 0.15;

scoreMap.put(rider, total);

}

// 3. 排序取第一名(派单)

Rider best = scoreMap.entrySet().stream()

.max(Map.Entry.comparingByValue())

.map(Map.Entry::getKey)

.orElse(null);

return best == null ? Collections.emptyList() : Collections.singletonList(best.getId());

}

}

4.2 在后台管理界面增加配置项

修改 admin 模块的 DispatchConfigController,增加 strategy 字段。运营人员可以在后台选择"加权派单"或"就近抢单",系统动态加载对应 Bean。

// 在 OrderDispatchService 中加一个策略缓存

private DispatchStrategy getStrategy() {

String strategyName = configService.getConfig("dispatch.strategy", "nearbyFirst");

if ("weightedScore".equals(strategyName)) {

return (DispatchStrategy) ApplicationContextHelper.getBean("weightedScore");

}

return new NearbyFirstStrategy();

}

五、实战三:扩展数据库与API(增加预订时间段)

很多海外用户喜欢预订明天的早餐或后天的聚餐,而原系统只有"立即配送"。我们需要扩展订单表,增加 book_time 字段,并修改下单流程。

5.1 数据库变更(使用 Flyway)

database/migration/ 下创建 V1.2__add_book_time_to_order.sql

sql

复制代码
ALTER TABLE `orders` 
ADD COLUMN `book_time` DATETIME NULL COMMENT '用户预订的送达时间',
ADD COLUMN `is_booking` TINYINT(1) DEFAULT 0 COMMENT '是否为预订订单';

运行 mvn flyway:migrate 自动执行。

5.2 修改实体类

java

复制代码
// Order.java
@TableName("orders")
public class Order {
    // ... 原有字段
    private Date bookTime;
    private Integer isBooking;
}

5.3 后端 API 调整

修改 OrderControllercreateOrder 方法,接收 bookTime 参数:

java

复制代码
@PostMapping("/create")
public R createOrder(@RequestBody OrderCreateVo vo) {
    Order order = new Order();
    BeanUtils.copyProperties(vo, order);
    if (vo.getBookTime() != null) {
        order.setIsBooking(1);
        order.setBookTime(vo.getBookTime());
    } else {
        order.setIsBooking(0);
    }
    // ... 后续保存逻辑
}

5.4 前端 UI 添加日期选择器

使用 uni-app 的 <picker mode="datetime">

vue

复制代码
<template>
  <view>
    <picker mode="datetime" @change="onDateChange" :value="bookTime">
      <view class="picker">{{ bookTimeText || '选择预约时间' }}</view>
    </picker>
    <button @click="submitOrder">预订下单</button>
  </view>
</template>

<script>
export default {
  data() {
    return { bookTime: '' }
  },
  methods: {
    onDateChange(e) {
      this.bookTime = e.detail.value;
    },
    async submitOrder() {
      const res = await api.createOrder({
        items: this.cartItems,
        bookTime: this.bookTime, // 没有则是即时单
      });
    }
  }
}
</script>

5.5 调度逻辑适配

在配送服务中,如果是预订订单(is_booking=1),则不会立即分配骑手,而是延迟到 book_time - 30分钟 时触发分配。可以使用消息队列的延迟消息实现:

java

复制代码
if (order.getIsBooking() == 1) {
    long delay = order.getBookTime().getTime() - System.currentTimeMillis() - 30 * 60 * 1000;
    if (delay > 0) {
        rabbitTemplate.convertAndSend("order.exchange", "dispatch.delayed", order.getId(), 
            message -> { message.getMessageProperties().setDelay((int)delay); return message; });
    }
}

这样预订功能就完整扩展好了。

结语

二次开发不是对源码的粗暴 hacking,而是利用系统预留的扩展点优雅地添加业务特性。光合同城国际版将支付、调度、通知等核心模块全部插件化,并提供了详细的开发文档和测试用例,让开发者可以像搭积木一样构建自己的同城 O2O 平台。

相关推荐
冰暮流星2 小时前
javascript事件案例-全选框案例
服务器·前端·javascript
niucloud-admin2 小时前
PHP V6 单商户常见问题——在线升级版本失败后如何回退版本数据
php
南子北游2 小时前
Python学习(基础语法1)
开发语言·python·学习
AI木马人2 小时前
13.【多租户架构实战】如何让一个AI系统同时服务多个用户且数据完全隔离?(完整设计方案)
人工智能·架构
张健11564096483 小时前
使用信号量限制并发数量
开发语言·c++
sjsjsbbsbsn3 小时前
大模型核心知识总结
java·人工智能·后端
0xR3lativ1ty3 小时前
关闭公网IP的两种方式
网络协议·tcp/ip·php
Csvn3 小时前
前端性能优化实战指南
前端
Moment3 小时前
2026 年,AI 全栈时代到了,前端简历别再只写前端技术了 🫠🫠🫠
前端·后端·面试