一、独立部署只是起点,二次开发才是灵魂
让系统适应海外不同市场的,一定是二次开发------比如接入柬埔寨的 Wing 支付、泰国的 K PLUS,或者修改配送费公式为"距离 × 基础费率 + 高峰倍数"。
本文假设你已经完成了源码独立部署(Spring Boot + MyBatis-Plus + Uni-app),我们将深入到代码层面,讲解三个最典型的二次开发场景:
-
新增一个自定义支付插件(Stripe 之外的聚合支付,如 PayMongo)
-
修改配送调度算法,从简单的就近派单改为多因素加权
-
扩展数据库与 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 调整
修改 OrderController 的 createOrder 方法,接收 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 平台。