8.7 KiB
后端架构深度解析与开发指南
1. 引言
本文档旨在为开发人员提供一份关于本项目(智慧农场系统)后端架构的深度技术解析。不同于基础的架构介绍,本文档聚焦于代码实现细节、数据库设计范式以及核心业务流程的底层逻辑。它将帮助你在接手项目后,快速理解现有代码模式(Patterns),并安全、高效地扩展新功能。
2. 核心架构与技术实现
本项目基于 ThinkPHP 6 框架开发,采用了典型的 MVC 架构,并结合了多应用(Multi-app)模式。
2.1 C层(Controller)技术实现
控制器层主要负责请求接收、参数校验和业务调度。项目在控制器层有以下显著的实现模式:
2.1.1 基类封装 (app/ApiRest.php)
所有 API 控制器均继承自 app\ApiRest,该基类提供了以下核心能力:
- 统一响应格式:通过
success($data)和error($msg)方法,强制统一 API 返回结构:{ "code": 200, "data": { ... }, "sign": "..." // 签名(可选) } - 多租户隔离:在构造函数中自动解析
uniacid(商户/租户ID),并赋值给$this->_uniacid。这是系统支持多商户SaaS模式的核心。 - 鉴权机制:
- 检查 HTTP Header 中的
autograph(小程序用户标识)。 - 维护一个
$noNeedLogin白名单。 - 注意:
shareChangeData方法中硬编码了大量白名单路由,这种实现方式维护成本较高,建议后续改为中间件或注解配置。
- 检查 HTTP Header 中的
2.1.2 胖模型,瘦控制器
控制器(如 app/farm/controller/IndexLand.php)通常比较轻量,不包含复杂的计算逻辑。
- 示例:在“土地下单”流程中,价格计算、优惠券抵扣等逻辑全部下沉到了 Model 层 (
LandOrder::payOrderInfo)。 - 优点:业务逻辑可复用,控制器代码整洁。
2.2 M层(Model)技术实现
模型层不仅负责数据库交互,还承担了大部分业务逻辑。
2.2.1 基类扩展 (app/BaseModel.php)
- 实现了基础的 CRUD 方法(
getRow,listRow,createRow等)。 - 软删除机制:手动实现了基于
deleted字段和delete_time的软删除,未使用 TP 自带的SoftDeleteTrait。 - 代码现状:核心业务 Model(如
LandOrder)往往重写了dataAdd/dataUpdate方法,直接调用 TP 的insert/update,并未完全依赖BaseModel的封装。这导致项目中存在两种 CRUD 风格。
2.2.2 高级特性应用
- 获取器 (Getters):大量使用 TP 的获取器处理字段格式化。例如
LandList::getImgsAttr将数据库存储的逗号分隔字符串自动转换为数组。 - 动态追加字段:使用
$append属性在 JSON 序列化时追加计算字段(如min_price)。 - 观察者模式:在
LandList::updateSome中,手动实现了观察者模式 (app\farm\server\Land) 来同步更新关联表数据。这是项目中少见的设计模式应用,值得保留和参考。
3. 数据库深度解析
3.1 数据库概览
- 引擎:大部分表使用
MyISAM。- 风险:MyISAM 不支持事务,且在并发写入时是表级锁。对于涉及资金交易的表(如
balance_water,land_order),强烈建议迁移到InnoDB。
- 风险:MyISAM 不支持事务,且在并发写入时是表级锁。对于涉及资金交易的表(如
- 字符集:
utf8/utf8mb4。 - 多租户设计:几乎所有表都包含
uniacid字段,用于物理隔离不同商户的数据。
3.2 核心业务实体关系 (ER图)
以下是“土地认养”核心业务的实体关系推导:
erDiagram
User ||--o{ LandOrder : "下单"
User ||--o{ Address : "拥有"
Farmer ||--o{ LandList : "管理"
LandList ||--|{ LandSpe : "包含规格"
LandList ||--o{ LandMassif : "包含地块"
LandOrder }|--|| LandList : "关联土地"
LandOrder }|--|| LandSpe : "关联规格"
LandOrder }|--|| LandMassif : "关联地块"
LandOrder ||--|{ LandOrderSeed : "包含种子"
User {
int id PK
int uniacid
string openid
string nickName
}
LandList {
int id PK
int farmer_id FK "农场主"
string title "土地名称"
int status
}
LandOrder {
int id PK
string order_code "订单号"
decimal pay_price "支付金额"
int pay_type "1:未支付 2:已支付"
int status
}
3.3 关键表结构解析
1. ims_lbfarm_land_list (土地表)
- 核心商品表,存储土地的基本信息。
- 关联表:
ims_lbfarm_land_spe(规格),ims_lbfarm_land_massif(地块/位置)。
2. ims_lbfarm_land_order (土地订单表)
- 核心字段:
order_code: 业务订单号。transaction_id: 微信支付流水号。pay_type: 支付状态 (1=未支付, 2=微信支付, 3=余额支付)。seed_data: 种子信息的 JSON 快照(反范式设计,避免关联查询)。
4. 核心业务流程剖析
4.1 土地认养下单与支付流程
这是一个跨越 Controller、Model 和 Service 的复杂流程:
-
预下单 (计价)
- 入口:
IndexLand::landPayOrderInfo - 逻辑:调用
LandOrder::payOrderInfo。 - 计算公式:
总价 = 土地规格单价 + (地块单价 * 认养周期) + ∑(种子单价 * 数量) - 优惠券金额。
- 入口:
-
创建订单
- 入口:
IndexLand::landPayOrder - 逻辑:再次调用
payOrderInfo校验价格,然后写入land_order表,同时写入land_order_seed表。
- 入口:
-
支付回调 (核心)
- 逻辑位置:
LandOrder::orderResult - 事务处理:
- 开启事务 (
Db::startTrans())。 - 更新订单状态 (
pay_type=2,pay_time)。 - 余额扣除:如果使用了余额抵扣,调用
BalanceWater::addWater。 - 资金流水:记录
FinanceWater。 - 分销结算:调用
DistributionCash::addUserCash计算分销佣金。 - 提交事务 (
Db::commit())。
- 开启事务 (
- 后续动作:发送微信订阅消息 (
paySendService),这是异步或非事务性的操作,放在 commit 之后执行是正确的。
- 逻辑位置:
5. 开发实战指南
5.1 如何新增一个 API 接口?
假设你需要新增一个“获取最近的农场列表”接口:
-
创建控制器: 在
app/farm/controller下创建IndexFarm.php,继承ApiRest。class IndexFarm extends ApiRest { public function nearby() { $lat = $this->_param['lat']; $lng = $this->_param['lng']; // 调用 Model 获取数据 $data = (new Farmer())->getNearby($lat, $lng); return $this->success($data); } } -
实现 Model 逻辑: 在
app/farm/model/Farmer.php中实现getNearby方法。建议参考LandList::indexDataList中的距离计算 SQL。 -
配置路由: 在
app/farm/route/route.php中注册路由(如果项目未开启自动路由)。目前看项目启用了自动路由,直接访问farm/index_farm/nearby即可。 -
配置鉴权: 如果该接口不需要登录,需要在
ApiRest::shareChangeData或控制器的构造函数中将其加入$noNeedLogin数组。
5.2 常见坑与规避
-
数据库事务: 由于大量表使用
MyISAM,在涉及多表更新时(如订单状态+库存扣减),事务可能不会生效。- 建议:在开发新功能涉及核心交易表时,务必检查表引擎,必要时手动
ALTER TABLE ... ENGINE=InnoDB。
- 建议:在开发新功能涉及核心交易表时,务必检查表引擎,必要时手动
-
参数获取: 尽量使用
$this->_param获取参数,它合并了 GET 和 POST 数据。避免直接使用$_GET或$_POST。 -
浮点数计算: 项目中存在直接使用浮点数计算金额的情况(如
round($price, 2))。在涉及金额比对时,建议使用bccomp等高精度函数,避免精度丢失导致的 0.01 元误差。 -
SQL 注入风险: 虽然 TP 的 ORM 有防注入机制,但在拼接原生 SQL(如距离计算)时,务必使用参数绑定,严禁直接拼接变量。
6. 附录:推荐的重构方向
- 引入 .env 配置:将
config/database.php中的硬编码配置移至环境变量。 - 统一 Model 规范:废弃
dataAdd等自定义方法,统一使用 TP 标准的save/create,或完善BaseModel并强制继承。 - 中间件鉴权:将
ApiRest中的硬编码鉴权逻辑重构为 TP 中间件。 - 数据库迁移:编写脚本将所有
MyISAM表迁移至InnoDB。