Files
Smart-Farm/doc/backend-technical-deep-dive.md
2026-01-04 15:01:16 +08:00

8.7 KiB
Raw Blame History

后端架构深度解析与开发指南

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 方法中硬编码了大量白名单路由,这种实现方式维护成本较高,建议后续改为中间件或注解配置。

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 自带的 SoftDelete Trait。
  • 代码现状:核心业务 ModelLandOrder)往往重写了 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
  • 字符集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 的复杂流程:

  1. 预下单 (计价)

    • 入口IndexLand::landPayOrderInfo
    • 逻辑:调用 LandOrder::payOrderInfo
    • 计算公式总价 = 土地规格单价 + (地块单价 * 认养周期) + ∑(种子单价 * 数量) - 优惠券金额
  2. 创建订单

    • 入口IndexLand::landPayOrder
    • 逻辑:再次调用 payOrderInfo 校验价格,然后写入 land_order 表,同时写入 land_order_seed 表。
  3. 支付回调 (核心)

    • 逻辑位置LandOrder::orderResult
    • 事务处理
      1. 开启事务 (Db::startTrans())。
      2. 更新订单状态 (pay_type=2, pay_time)。
      3. 余额扣除:如果使用了余额抵扣,调用 BalanceWater::addWater
      4. 资金流水:记录 FinanceWater
      5. 分销结算:调用 DistributionCash::addUserCash 计算分销佣金。
      6. 提交事务 (Db::commit())。
    • 后续动作:发送微信订阅消息 (paySendService),这是异步或非事务性的操作,放在 commit 之后执行是正确的。

5. 开发实战指南

5.1 如何新增一个 API 接口?

假设你需要新增一个“获取最近的农场列表”接口:

  1. 创建控制器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);
        }
    }
    
  2. 实现 Model 逻辑app/farm/model/Farmer.php 中实现 getNearby 方法。建议参考 LandList::indexDataList 中的距离计算 SQL。

  3. 配置路由app/farm/route/route.php 中注册路由(如果项目未开启自动路由)。目前看项目启用了自动路由,直接访问 farm/index_farm/nearby 即可。

  4. 配置鉴权 如果该接口不需要登录,需要在 ApiRest::shareChangeData 或控制器的构造函数中将其加入 $noNeedLogin 数组。

5.2 常见坑与规避

  1. 数据库事务 由于大量表使用 MyISAM,在涉及多表更新时(如订单状态+库存扣减),事务可能不会生效。

    • 建议:在开发新功能涉及核心交易表时,务必检查表引擎,必要时手动 ALTER TABLE ... ENGINE=InnoDB
  2. 参数获取 尽量使用 $this->_param 获取参数,它合并了 GET 和 POST 数据。避免直接使用 $_GET$_POST

  3. 浮点数计算 项目中存在直接使用浮点数计算金额的情况(如 round($price, 2))。在涉及金额比对时,建议使用 bccomp 等高精度函数,避免精度丢失导致的 0.01 元误差。

  4. SQL 注入风险 虽然 TP 的 ORM 有防注入机制,但在拼接原生 SQL如距离计算务必使用参数绑定严禁直接拼接变量。


6. 附录:推荐的重构方向

  1. 引入 .env 配置:将 config/database.php 中的硬编码配置移至环境变量。
  2. 统一 Model 规范:废弃 dataAdd 等自定义方法,统一使用 TP 标准的 save/create,或完善 BaseModel 并强制继承。
  3. 中间件鉴权:将 ApiRest 中的硬编码鉴权逻辑重构为 TP 中间件。
  4. 数据库迁移:编写脚本将所有 MyISAM 表迁移至 InnoDB