110 lines
4.9 KiB
Plaintext
110 lines
4.9 KiB
Plaintext
---
|
||
description: 后端 PHP 规范:API 控制器、Service 层、模型、事务、资金与安全(ThinkPHP 5 / FastAdmin)
|
||
globs: backend/application/**/*.php
|
||
alwaysApply: false
|
||
---
|
||
|
||
# 后端 PHP 规范
|
||
|
||
## 语言版本与风格
|
||
|
||
- 兼容 PHP 7.4:可用箭头函数 `fn`、类型属性;**禁止** `match`、命名参数、构造器属性提升、`?->`、联合类型、`enum`、`readonly`
|
||
- PSR-12 风格,4 空格缩进;类名 PascalCase,方法/变量 camelCase,数据库字段与请求字段 snake_case
|
||
- 命名空间与目录一致:`app\api\controller`、`app\common\service`、`app\common\model\{子目录}`
|
||
|
||
## 分层职责
|
||
|
||
| 层 | 位置 | 职责 |
|
||
| --- | --- | --- |
|
||
| API 控制器 | `api/controller/` | 取参、校验、登录判断、组装返回;不写业务流程 |
|
||
| Service | `common/service/XxxService.php` | 业务逻辑、状态流转、事务、状态常量 |
|
||
| Model | `common/model/` | 表映射、关联、获取器/修改器、`*_text` 追加字段 |
|
||
| Validate | `common/validate/` | 可复用的验证规则 |
|
||
| 定时/队列 | `common/command/`、`common/job/` | 调用 Service,不内联业务 |
|
||
|
||
## API 控制器模板
|
||
|
||
`success()/error()` 通过抛出 `HttpResponseException` 输出,catch 里必须原样抛出:
|
||
|
||
```php
|
||
class Demand extends Api
|
||
{
|
||
protected $noNeedLogin = []; // 免登录方法,支付回调等必须显式列出
|
||
protected $noNeedRight = ['*'];
|
||
|
||
/**
|
||
* 参与竞标
|
||
* @ApiMethod (POST)
|
||
* @param int $demand_id 需求ID
|
||
*/
|
||
public function joinBidding()
|
||
{
|
||
try {
|
||
$params = $this->request->post();
|
||
$validate = new Validate(['demand_id' => 'require|number'], ['demand_id.require' => '需求ID不能为空']);
|
||
if (!$validate->check($params)) {
|
||
$this->error($validate->getError());
|
||
}
|
||
$res = DemandBiddingService::join($params, $this->auth->id);
|
||
$this->success('报名成功', $res);
|
||
} catch (\Exception $e) {
|
||
if ($e instanceof HttpResponseException) {
|
||
throw $e;
|
||
}
|
||
$this->error($e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- 数组参数用 `$this->request->post('list/a', [])`;数值参数显式 `intval()`
|
||
- 需要登录的接口不要放进 `$noNeedLogin`,也不要用 `$this->auth->id ?? -1` 绕过登录
|
||
- 新增接口后在 `frontend/miniapp/config/api.js` 登记
|
||
|
||
## Service 模板
|
||
|
||
```php
|
||
class DemandService
|
||
{
|
||
use ServiceTrait; // 提供 instance()/getOne()/getList(),并静态代理模型方法
|
||
|
||
/** @var Demands */
|
||
public static $Model = Demands::class;
|
||
|
||
const STATUS_BIDDING = 1; //招标中
|
||
const STATUS_MAP = [self::STATUS_BIDDING => '招标中'];
|
||
}
|
||
```
|
||
|
||
- 方法默认 `public static`,带中文 docblock(`@param`/`@return`/`@throws`)
|
||
- 查询优先 `self::getOne($where, $with)`、`self::getList($where, $pageSize, $order, '', $with)`
|
||
- 状态值用 Service 常量或 `CommonService::NO/ONGOING/SUCCESS/FAILED`、`STATUS_ON/OFF`,**禁止**新代码出现魔法数字 `status == 2`
|
||
- 失败直接抛 `\Exception('中文原因')`(或 `exception()` 助手),不返回 `false`/错误数组
|
||
- 涉及"操作他人数据"的方法必须接收 `$userId` 并校验归属(如 `$demand->user_id == $userId`),不能只凭前端传的 ID
|
||
|
||
## 事务与并发
|
||
|
||
- 前置校验放在事务外,写操作放在事务内;catch 中 `Db::rollback()` 后 `throw $e`
|
||
- 余额、状态流转、库存/激活码分配:事务内用 `->lock(true)` 读取再判断,防止并发重复操作
|
||
- 订阅消息、企业微信、OSS、微信转账等外部调用放在 `Db::commit()` **之后**,失败只记日志不回滚业务
|
||
- 支付/转账回调必须幂等:先查订单当前状态,已处理直接返回成功
|
||
|
||
## 资金与账户
|
||
|
||
- 余额/冻结/积分/信誉分只通过 `AccountLogService::addLog()` + `AccountService::updateData()` 变更,二者在同一事务内
|
||
- 金额计算用 bcmath(`bcadd/bcsub/bcmul`,精度 2),不用浮点直接加减比较
|
||
- 结算价、订单金额从数据库商品/订单读取,不使用请求中的 `price`
|
||
|
||
## 模型
|
||
|
||
- `$name` 不带前缀;`$autoWriteTimestamp = 'integer'`,`$createTime = 'createtime'`,`$updateTime = 'updatetime'`
|
||
- JSON/数组字段用 `$type = ['images' => 'array']`
|
||
- 枚举字段配套 `getXxxList()`(文案走 `__()` 语言包)+ `getXxxTextAttr()`,并加入 `$append`
|
||
- 关联方法 camelCase,并用 `->field()` 限定字段,避免泄露手机号、openid 等敏感字段
|
||
|
||
## 日志与配置
|
||
|
||
- `Log::info(__CLASS__ . ' 描述:' . json_encode($data, JSON_UNESCAPED_UNICODE))`;禁止 `var_dump`/`echo`/`dump` 残留
|
||
- 站点配置读取 `config('site.xxx')`,环境配置读取 `Env::get()`;不在代码里硬编码密钥与域名
|
||
- 原生 SQL 必须参数绑定;`like` 关键词使用 `['like', "%{$keywords}%"]` 数组条件,不拼接字符串
|