Files
code-bang/.cursor/rules/backend-php.mdc
T
2026-10-14 14:44:01 +08:00

110 lines
4.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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}%"]` 数组条件,不拼接字符串