--- 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}%"]` 数组条件,不拼接字符串