Compare commits
14 Commits
6bd74bb7d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ddf5e3c5f | |||
| 72bc6502eb | |||
| 4979bb83d2 | |||
| 11f40a75e4 | |||
| bcf54c2727 | |||
|
|
6e2769c528 | ||
|
|
2f5e298d6b | ||
|
|
21f39eed85 | ||
|
|
cab69b4193 | ||
|
|
ab417bd0f2 | ||
|
|
bd226c260f | ||
|
|
d0018a0173 | ||
|
|
04d755b115 | ||
|
|
1e4c30208b |
31
.cursor/rules/Code-Generation.mdc
Normal file
31
.cursor/rules/Code-Generation.mdc
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: 代码生成双端产出与 uniapp C 端三页模板约定
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 代码生成(双端前端)
|
||||
|
||||
`CodeGenerationService::generate` 一次生成必须包含:
|
||||
|
||||
1. **后端**:Controller / Service / Model / 路由 / 建表 / PC 菜单
|
||||
2. **PC 前端 zip**:`view/<dash-case>/` → `nl-admin-view/.../views/my-gen/`
|
||||
3. **uniapp 前端 zip**:`uniapp/<dash-case>/` → `nl-admin-uniapp/src/pages-sub/my-gen/<dash-case>/`
|
||||
|
||||
模板目录:
|
||||
|
||||
- `app/template/view/` — Vben Admin
|
||||
- `app/template/uniapp/` — 移动超管(C 端)
|
||||
|
||||
# uniapp 生成物强制
|
||||
|
||||
- 兼容 **微信小程序 + App + H5**
|
||||
- **交互必须是三页**:`pages/list.vue` → `pages/detail.vue` → `pages/form.vue`
|
||||
- 列表点击进详情;新增/编辑用全屏表单;**禁止**底部抽屉做主 CRUD
|
||||
- **C 端 UI**:大留白卡片、DetailHero、InfoGroup、BottomBar、PageForm;禁止 PC 宽表
|
||||
- 复用:`AppCardList` / `AppFilterBar` / `AppSearchBar` / `AppDetailHero` / `AppInfoGroup` / `AppBottomBar` / `AppPageForm`
|
||||
- `AppFormDrawer` 不得作为生成主路径
|
||||
- zip 内附带 `PAGES_JSON_SNIPPET.md`(注册 list/detail/form 三页 + features.ts)
|
||||
|
||||
# 与移动端规则对齐
|
||||
|
||||
以 `nl-admin-uniapp` 的 `Code-Standards.mdc` 为准;本仓库生成模板不得与之冲突。
|
||||
@@ -1,8 +1,16 @@
|
||||
---
|
||||
description: nl-admin 后端(Laravel + 自动路由)代码规范
|
||||
description: nl-admin 后端(Laravel 13 + PHP 8.3 + 自动路由)代码规范
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 技术栈基线(升级后必守)
|
||||
|
||||
- **Laravel 13**(`composer.json`:`laravel/framework: ^13.0`)
|
||||
- **PHP ≥ 8.3**(Laravel 13 官方最低要求;禁止按 8.2 写法或回退依赖)
|
||||
- 应用启动走 `bootstrap/app.php` 的 fluent API(`withRouting` / `withMiddleware` / `withExceptions`),**不要**再按旧版 `Http/Kernel` 拆分中间件与异常
|
||||
- 代码风格优先 `laravel/pint`;测试栈为 PHPUnit 12
|
||||
- Laravel 13 的 PHP Attribute(如 `#[Middleware]`、`#[Authorize]`、Job 的 `#[Tries]` 等)可用,但本项目的 **HTTP 谓词仍以 PHPDoc `@Method` 为准**(`autoRouteRegister` 扫描依赖它)
|
||||
|
||||
# 基础规范(所有项目通用)
|
||||
|
||||
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
|
||||
@@ -11,6 +19,15 @@ alwaysApply: true
|
||||
4. 小程序端的抽屉全部需要用 page-container 来防止用户意外退出页面(本仓库为 API,此项约束 uniapp)
|
||||
5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化
|
||||
|
||||
# 数据库变更(强制 SQL,禁止新建 PHP Migration)
|
||||
|
||||
- **禁止**为业务/系统表 DDL 新建 `database/migrations/*.php`(Laravel migrate 不作变更载体)
|
||||
- **一律**写到 `database/sql/<YYYY-MM-DD>/`,文件名:`01_简短英文名.sql`、`02_...`(当天序号递增)
|
||||
- 例:`database/sql/2026-08-14/01_cc_carousel_add_status.sql`
|
||||
- 脚本尽量幂等(`information_schema` + `PREPARE`,风格对齐已有 `rbac_migrate_ddl.sql`)
|
||||
- 系统表结构若影响全新安装,同步回写 `public/nl_admin.sql`;业务表 `cc_*` 只放 `database/sql/`,不进 `nl_admin.sql`
|
||||
- 运维在目标库手动执行对应日期目录下的 SQL;不要指望 `php artisan migrate` 补列/补表
|
||||
|
||||
# 全局架构规范
|
||||
|
||||
## 分层架构(Controller → Service → Model,禁止跨层)
|
||||
@@ -38,7 +55,7 @@ alwaysApply: true
|
||||
|
||||
- 禁止在 Controller 直接复用业务逻辑,必须抽到 Service
|
||||
- 禁止在多个 Service 中复制粘贴相同逻辑,必须抽到 `app/Service/common/`
|
||||
- 若后续引入队列:统一放 `app/Jobs/<域>/`,实现 `ShouldQueue` + `Queueable`,必要时 `->onQueue('xxx')`(当前仓库尚未建 Jobs,按需新增)
|
||||
- 若后续引入队列:统一放 `app/Jobs/<域>/`,实现 `ShouldQueue` + `Queueable`;单 Job 可用 `->onQueue('xxx')`,多 Job 默认路由优先用 Laravel 13 的 `Queue::route(Job::class, connection: 'redis', queue: 'xxx')` 集中声明(当前仓库尚未建 Jobs,按需新增)
|
||||
|
||||
## 路由规范(自动注册,禁止手写业务路由)
|
||||
|
||||
@@ -64,9 +81,10 @@ alwaysApply: true
|
||||
|
||||
## 常量管理
|
||||
|
||||
- 枚举值必须用 PHP 8.1 enum 放 `app/Enum/`,禁止在代码中散落魔法数字
|
||||
- 枚举值必须用 PHP native `enum`(PHP 8.3+)放 `app/Enum/`,禁止在代码中散落魔法数字
|
||||
- 状态约定以现有枚举为准:`StatusEnum` / `UserStatusEnum` 为 **0=正常,1=禁用/冻结**(不要写成其他项目的 1 启用 0 禁用)
|
||||
- 新增状态类字段优先复用或扩展 `app/Enum/` 中的枚举
|
||||
- CSRF / 请求伪造防护中间件类名以 Laravel 13 为准:`PreventRequestForgery`(旧名 `VerifyCsrfToken` 仅为兼容别名,新代码不要再写)
|
||||
|
||||
## 命名规范
|
||||
|
||||
@@ -90,5 +108,11 @@ alwaysApply: true
|
||||
|
||||
## 代码生成
|
||||
|
||||
- 模板在 `app/template/`,服务在 `app/Service/core/CodeGenerationService`
|
||||
- 生成物需仍遵守本文件的分层、基类、路由注册与命名约定;生成后记得在 `routes/api.php` 的 `autoRouteRegister` 中挂上控制器
|
||||
- 模板在 `app/template/`:`view/`(PC Vben)与 `uniapp/`(移动端)两套并存;服务在 `app/Service/core/CodeGenerationService`
|
||||
- **`generate` 必须同时产出** zip 内的 `view/<dash-case>/` 与 `uniapp/<dash-case>/`,缺一不可
|
||||
- 生成物需仍遵守本文件的分层、基类、路由注册与命名约定;生成后记得在 `routes/api.php` 的 `autoRouteRegister` 中挂上控制器(当前 `genRoute` 会自动写入)
|
||||
- **uniapp 模板强制约定**(与移动端规则同口径):
|
||||
- 必须兼容 **微信小程序 + App + H5**
|
||||
- 交互必须是 **list → detail → form 三页**;禁止底部抽屉做主 CRUD;禁止 PC 宽表
|
||||
- 复用:`AppCardList` / `AppFilterBar` / `AppDetailHero` / `AppInfoGroup` / `AppBottomBar` / `AppPageForm`
|
||||
- 粘贴落点:`nl-admin-uniapp/src/pages-sub/my-gen/<dash-case>/`,并同步 `pages.json`(三页)与 `features.ts`
|
||||
|
||||
27
.cursor/rules/Database-SQL.mdc
Normal file
27
.cursor/rules/Database-SQL.mdc
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: 数据库变更一律走按日 SQL 目录,禁止新建 PHP Migration
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 数据库变更约定(lgp-admin-plus-api)
|
||||
|
||||
## 强制
|
||||
|
||||
- **禁止**为表结构变更新建 `database/migrations/*.php`
|
||||
- **一律**落在:
|
||||
|
||||
```
|
||||
database/sql/<YYYY-MM-DD>/01_简短英文名.sql
|
||||
database/sql/<YYYY-MM-DD>/02_....sql
|
||||
```
|
||||
|
||||
- 当天目录不存在则新建;序号从 `01` 递增
|
||||
- 脚本尽量幂等(查 `information_schema` + `PREPARE`)
|
||||
- 系统表(`nl_*`)若影响全新安装:同步改 `public/nl_admin.sql`
|
||||
- 业务表(`cc_*`):只写 `database/sql/`,**不进** `nl_admin.sql`
|
||||
- 上线由运维在目标库手工执行对应日期目录下的 SQL
|
||||
|
||||
## 反例
|
||||
|
||||
- `php artisan make:migration add_xxx_to_yyy` → 不要
|
||||
- 把 DDL 塞进 Seeder / 业务代码里临时跑 → 不要
|
||||
38
.cursor/rules/Dual-Backend-Sync.mdc
Normal file
38
.cursor/rules/Dual-Backend-Sync.mdc
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
description: 双后端同步:本仓库(Laravel)与 nl-admin-api-gf(GoFrame)互为镜像实现,改一端必须同步另一端
|
||||
alwaysApply: true
|
||||
---
|
||||
# 双后端同步(Laravel ↔ GoFrame)
|
||||
|
||||
本仓库(Laravel 13 版 nl-admin-api)与工作区内 `d:\myCode\nl-admin\nl-admin-api-gf`(GoFrame v2 的 Go 版同功能实现)**互为镜像**:
|
||||
接口路径、入参出参、业务规则、响应结构、数据表结构必须保持一致,两端 JWT/密码/加密互通(同库同 Redis 可互认)。
|
||||
|
||||
## 强制要求
|
||||
|
||||
- 本仓库任何**接口、业务逻辑、表结构、配置键**的改动,必须同步修改 `nl-admin-api-gf` 对应实现
|
||||
- 若本次会话无法当场同步(如用户明确说只改一端),必须在回复末尾列出「gf 侧待同步清单」(文件级 + 行为级)
|
||||
- 新增接口两端都实现完才算完成;禁止只改一端悄悄收尾
|
||||
- 改动 `config/nl.php` 的键值(如 log.no_insert、redis 前缀、jwt)时,同步改 gf 的 `manifest/config/config.yaml` 的 `nl` 段
|
||||
|
||||
## 层级对应关系(本仓库 → nl-admin-api-gf)
|
||||
|
||||
| Laravel(本仓库) | GoFrame(nl-admin-api-gf) |
|
||||
|---|---|
|
||||
| routes/api.php autoRouteRegister | internal/cmd/cmd.go 手写注册 + internal/router/genroutes |
|
||||
| app/Http/Controllers/Api(及 core)/*Controller | internal/controller/<模块>/ |
|
||||
| app/Service/*Service | internal/logic/<模块>/ |
|
||||
| app/Service/common/(JWT/Utils/加密/OSS/AI/CURD 基类) | internal/library/(auth/utils/crypto/oss/ai/curd/response/xerr) |
|
||||
| app/Models/*Model | internal/model/entity + internal/dao |
|
||||
| app/Enum/ | internal/consts/consts.go |
|
||||
| app/Http/Middleware/ApiOpLogMiddleware | internal/middleware/oplog.go |
|
||||
| config/nl.php | manifest/config/config.yaml 的 nl 段 |
|
||||
| app/template/(api/view/uniapp 代码生成模板) | resource/template/(api 模板为 Go 版,view/uniapp 语义一致) |
|
||||
|
||||
## 必须对齐的行为基线(两端一致,改动需双端验证)
|
||||
|
||||
- 鉴权:`Authorization: Bearer <JWT>`(HS256,同 secret 可互认)+ Redis `nl_login_{id}`(不设 TTL);无独立鉴权中间件差异不影响行为口径
|
||||
- 响应:`{code, message, result, type}`,成功 code=0,失败默认 500,HTTP 恒 200(sql 安装向导 `{success,...}` 除外)
|
||||
- 密码 bcrypt(PHP password_hash ↔ Go bcrypt cost12 互通);库内敏感字段 AES-256-CBC,密文前缀 `nl_ase_256_`
|
||||
- 分页 `page`/`pageSize` 默认 1/20(个人 login-log/op-log 默认 10 上限 100,兼容 `size`);软删 `deleted_at`;批量删除入参 `ids`
|
||||
- 操作日志按 `nl_api_endpoint` 的 `is_log`/`status` 落 `nl_api_op_log`,脱敏键与跳过白名单两端同步
|
||||
- 代码生成产物:后端代码 + 建表 + 路由挂载 + `nl_menu` 菜单 + zip(view/ 与 uniapp/ 双前端),两端能力必须同口径
|
||||
40
.cursor/rules/lgp-pitfalls.mdc
Normal file
40
.cursor/rules/lgp-pitfalls.mdc
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: LGP 新后端开发约定与全项目群已知陷阱
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 开发约定与已知陷阱
|
||||
|
||||
## 新后端 lgp-admin-plus-api
|
||||
|
||||
- 路由由 `UtilsService::autoRouteRegister()` **反射注册**,继承自 `BaseController` 的方法也会被注册成路由。不需要暴露的继承方法必须用 `@Method NO` 覆盖,否则每个控制器白送一套 `create/update/delete`
|
||||
- 方法名 camelCase 会转成 kebab-case 路径:`myInfo` → `my-info`
|
||||
- 缺 `@Method` 注解会注册成 `ANY`,务必显式写 `@Method GET` 或 `@Method POST`
|
||||
- 响应统一走 `jok` / `jerr`,结构固定 `{code, message, result}`,成功 `code=0`。前端拦截器按 `result` 取数据,**不要用 `data`**
|
||||
- 业务表 Model 覆盖 `$connection = 'business'`;系统表沿用默认 `mysql`
|
||||
- 免鉴权 Service 继承 `BaseNotAuthService`,不要像老项目那样硬编码路径白名单
|
||||
- 鉴权真正发生在 `BaseService::__construct`,绕过 BaseService 的方法就没有鉴权。新代码一律走 BaseService
|
||||
- `nl_menu` 的 `keep_alive` 与 `affix_tab` 输出到 meta 时是**取反**的,插数据别弄反
|
||||
- 第三方凭据用 `FieldEncryptService` 加密入库,不回显明文
|
||||
- **库表变更禁止新建 PHP Migration**:写 `database/sql/<YYYY-MM-DD>/01_名字.sql`(当天序号递增、尽量幂等);系统表同步 `public/nl_admin.sql`,业务表 `cc_*` 只放 sql 目录
|
||||
|
||||
## 老项目只读
|
||||
|
||||
`lgp-api` / `lgp-wx-api` / `lgp-vben` / `lgp-vben-new` 处于迁移期,除安全修复外不要改动。
|
||||
查业务逻辑可以读,新功能一律写在 `lgp-admin-plus-api` + `lgp-admin-plus`。
|
||||
|
||||
## 已知陷阱
|
||||
|
||||
- 老 `lgp-wx-api` 把微信 `session_key` 当 token 直接返回客户端,且 `AuthMiddleware` 先执行控制器再检查 token、只判空不验签。归并时必须改成服务端自签 token
|
||||
- 老 `lgp-api` 密码是**无盐 sha1**,新后端是 bcrypt,靠 `nl_admin.legacy_password` 惰性升级,遗留列有过期时间
|
||||
- 老 `cc_role.id=1` 不是超管,而新 `nl_role.id=1` 是超管且代码硬判断全量放行。角色迁移必须做 ID 偏移
|
||||
- 老前端 10 个模块的批量删除在静默 404(后端只有 `template` 注册了 `delete-batch`)
|
||||
- `cc_price_sheet` 的 6 个材质列是死 schema,`create()` 从不赋值、前端已注释,实际只有 `routine` 在用
|
||||
- 图片水印不是后端接口,是前端拼 OSS URL 参数 `?watermark/2/text/...`
|
||||
- 小程序 `baseURL` 硬编码在 `api/interceptor.js` 且靠注释切换,极易把测试地址发上线
|
||||
- 小程序无分包配置,uview-plus 全量进主包
|
||||
|
||||
## 安全红线
|
||||
|
||||
不要把任何密钥写进源码。老项目里七牛 AK/SK(`UploadService.php`)、微信 AppSecret(`WeChatService.php`)明文硬编码,
|
||||
`.env.example` 里还提交了真实数据库密码——这些是待清理的历史欠账,不要照抄这个做法。
|
||||
46
.cursor/rules/lgp-project-map.mdc
Normal file
46
.cursor/rules/lgp-project-map.mdc
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
description: LGP 项目群架构、各仓库角色与双品牌对应关系
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# LGP 项目地图
|
||||
|
||||
两个家具品牌共用一套代码,正从老架构迁往新架构。
|
||||
|
||||
## 新架构(开发主战场)
|
||||
|
||||
- `lgp-admin-plus` — 后台前端,vben **5.7.0** monorepo,主应用 `apps/web-antd`,上游是自研 `nl-admin-view`
|
||||
- `lgp-admin-plus-api` — 后端,**Laravel 13**,位于 `sites/lgp-api/index/lgp-admin-plus-api`
|
||||
|
||||
## 老架构(迁移期只读,迁完归档)
|
||||
|
||||
- `lgp-vben` — 老后台前端,vben 2.11.5
|
||||
- `lgp-api` — 老后台后端,Laravel 10
|
||||
- `lgp-wx-api` — 老小程序后端,Laravel 12,本期归并进新后端的 `wx` 路由组
|
||||
- `lgp-vben-new` — **已弃用**,与 `lgp-vben` 同源克隆,不要在里面做任何改动
|
||||
|
||||
## 小程序(uni-app + Vue3 + uview-plus)
|
||||
|
||||
- `lgp-wx` — 佛山铂尔曼,AppID `wx679d36842570cea7`,API `brm.wx.api.borman.top`
|
||||
- `lgp-wx-new` — 维伦家具,AppID `wx57b54060a579c31a`,API `wx.api.borman.top`
|
||||
|
||||
两者页面与业务代码基本一致,差异只有 AppID、`baseURL` 与首页视觉。改一边通常要同步另一边。
|
||||
|
||||
## 双品牌
|
||||
|
||||
同一套后台代码部署两份,靠构建模式区分:`build:brm`(佛山铂尔曼)/ `build:wl`(维伦家具)。
|
||||
两个品牌共用一套代码与数据库,任何业务改动会同时影响两家客户,上线前按品牌分别回归。
|
||||
|
||||
## 数据库
|
||||
|
||||
同一个库、两个连接(`config/database.php`):
|
||||
|
||||
- `mysql`,前缀 `nl_` — 脚手架系统表(admin / role / menu / oss_config / file / api_endpoint 等)
|
||||
- `business`,前缀 `cc_` — 业务表(catalogue / category / image / price_sheet / list / wx_user / enterprise 等)
|
||||
|
||||
业务数据零迁移,只迁账号与角色。业务 Model 必须覆盖 `protected $connection = 'business'`。
|
||||
|
||||
## 迁移范围
|
||||
|
||||
老后端注册 171 条路由但前端只调用 **79** 条;老小程序后端注册 35 条但小程序只调用 **14** 条。
|
||||
迁移目标是这 93 条,其余是反射自动注册出来的死路由,不要跟着迁。
|
||||
76
.env.example
76
.env.example
@@ -1,12 +1,11 @@
|
||||
APP_NAME=Xiaokang
|
||||
APP_NAME=lgp-admin-plus
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:X9vy9lAitu0bA6J1/vqsiOHdNnc3XOrLihHKRGYHdbY=
|
||||
APP_KEY=base64:pQ7mR2wYh4KdN8sZaJ6xVbT3fLcE9gUnO5rXi1DyBqM=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=Asia/Shanghai
|
||||
APP_URL=http://localhost
|
||||
ECS=dev
|
||||
|
||||
# 修改后
|
||||
APP_LOCALE=zh_CN
|
||||
APP_FALLBACK_LOCALE=zh_CN
|
||||
APP_FAKER_LOCALE=zh_CN
|
||||
@@ -19,14 +18,19 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 新的数据库
|
||||
# 数据库:同一个库两个连接
|
||||
# mysql 前缀 nl_ → 脚手架系统表
|
||||
# business 前缀 cc_ → 业务表(见 config/database.php 的 business 连接)
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql8
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=cc_admin
|
||||
DB_DATABASE=cc_new_stash
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=nl_
|
||||
DB_BUSINESS_PREFIX=cc_
|
||||
# 业务库既有表主用 utf8mb4_0900_ai_ci,保持一致避免新建表混排序规则
|
||||
DB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
@@ -40,46 +44,38 @@ FILESYSTEM_DISK=local
|
||||
CACHE_STORE=redis
|
||||
CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=1Panel-redis-TxPD
|
||||
REDIS_PASSWORD=redis_Pa2Y7J
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=redis_Q83eyF
|
||||
REDIS_PORT=6379
|
||||
|
||||
# 邮件发送配置
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.163.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=qiqiqiyoucili@163.com
|
||||
MAIL_PASSWORD=NZbPs32frizAaFM9
|
||||
MAIL_ENCRYPTION=ssl
|
||||
MAIL_FROM_ADDRESS=qiqiqiyoucili@163.com
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_FROM_ADDRESS="noreply@borman.top"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
# 库内敏感字段 AES 密钥(密文前缀 nl_ase_256_)
|
||||
# 一旦有数据入库就不能再改,否则历史密文无法解密
|
||||
ENCRYPT_KEY=8d41c7b0e5a29f63d84b17ce9052af38b6e0d7419c2af58e3b06d915748ca2ef
|
||||
# JWT HS256 密钥(至少 32 字符)
|
||||
JWT_SECRET=f92a5d6c4e8b1d70a35f9c2e64b8d017a3e5c9f2b48d61a07e3c95b28f4d6a1c
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
# 消息队列 配置
|
||||
#QUEUE_CONNECTION=rabbitmq
|
||||
#RABBITMQ_HOST=172.26.47.10
|
||||
#RABBITMQ_PORT=15672
|
||||
#RABBITMQ_VHOST=/
|
||||
#RABBITMQ_QUEUE=xk-api
|
||||
#RABBITMQ_USER=xk_api
|
||||
#RABBITMQ_PASSWORD=qiqi991012
|
||||
AI_DEFAULT_PROVIDER=deepseek
|
||||
SPARK_API_PASSWORD=
|
||||
DEEPSEEK_API_KEY=
|
||||
|
||||
# 小程序品牌声明(单品牌部署兜底用)
|
||||
# 双品牌共用一份部署时,仍以 nl_wx_app 表 + 请求头 X-App-Code 分流为主
|
||||
# AppSecret 不写这里,必须经后台「小程序应用配置」加密入库
|
||||
WX_DEFAULT_APP_CODE=wl
|
||||
WX_APP_ID=wx57b54060a579c31a
|
||||
WX_APP_NAME=佛山铂尔曼
|
||||
|
||||
|
||||
# BaseUrl
|
||||
#BASE_OLD_API_URL=https://app.xiaokang88.com
|
||||
BASE_OLD_API_URL=http://www.xk888.com
|
||||
BASE_OLD_SHOP_API_URL=https://shop.xiaokang88.com
|
||||
|
||||
# 密钥
|
||||
ENCRYPT_KEY=3a8f9b2d7c1e4f8a9b2d7c1e4f8a9b2d7c1e4f8a9b2d7c1e4f8a9b2d7c1e4f8a
|
||||
# JWT HS256 密钥(至少 32 字符;生产环境务必改成随机长串)
|
||||
JWT_SECRET=nl_admin_jwt_secret_key_change_me_32b
|
||||
# weilun
|
||||
# $this->appId = 'wx57b54060a579c31a';
|
||||
# $this->appSecret = 'b1b1852d6c438893bfd2cc1cdd5114dc';
|
||||
# boerman
|
||||
# $this->appId = 'wx679d36842570cea7';
|
||||
# $this->appSecret = '4f2bcb8ca2ed1aaa29ae82ce270338e3';
|
||||
|
||||
19
README.md
19
README.md
@@ -18,3 +18,22 @@ php artisan sql:run-nl-admin --force
|
||||
# 创建新数据库
|
||||
php artisan sql:run-nl-admin --database=my_new_db
|
||||
```
|
||||
|
||||
### 合并数据使用示例
|
||||
```cmd
|
||||
# 1. 补列(解除 my-info Unknown column department_id)
|
||||
# 在 Navicat 选中库 cc_new_stash,执行 database/sql/rbac_migrate_ddl.sql
|
||||
# 或:mysql ... cc_new_stash < database/sql/rbac_migrate_ddl.sql
|
||||
|
||||
# 2. 迁移菜单 / 同步路由
|
||||
php artisan lgp:menu-sync
|
||||
php artisan lgp:endpoint-sync
|
||||
|
||||
# 3. 账号角色体检(只读)
|
||||
php artisan lgp:rbac-audit
|
||||
# 等价 SQL:database/sql/rbac_audit.sql
|
||||
|
||||
# 4. 账号角色迁移(有事务/ID 偏移/幂等,仍走 artisan)
|
||||
php artisan lgp:rbac-migrate --dry-run
|
||||
php artisan lgp:rbac-migrate --force
|
||||
```
|
||||
|
||||
15
app/BaseApp/BaseBusinessModel.php
Normal file
15
app/BaseApp/BaseBusinessModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\BaseApp;
|
||||
|
||||
/**
|
||||
* 业务表模型基类
|
||||
*
|
||||
* 业务表与系统表同库不同前缀(系统表 nl_ / 业务表 cc_),
|
||||
* 故业务表走独立的 business 连接。所有 cc_* 表的 Model 都应继承本类而不是 BaseModel。
|
||||
* 代码生成器产出的 Model 默认继承 BaseModel,用于业务表时需手工改成本类。
|
||||
*/
|
||||
class BaseBusinessModel extends BaseModel
|
||||
{
|
||||
protected $connection = 'business';
|
||||
}
|
||||
@@ -14,6 +14,17 @@ class BaseController
|
||||
protected array $notRequest = [];
|
||||
protected array $updateField = [];
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* @var array<int, string> 不注册成路由的方法名
|
||||
*
|
||||
* autoRouteRegister 是反射整个类,继承来的方法也会被注册,
|
||||
* 于是每个控制器都白得一套 list/option/detail/create/update/delete。
|
||||
* 对不该有 CRUD 的控制器(比如登录),在子类里声明要排除的方法即可,
|
||||
* 不必为了挂 @Method NO 去空覆写六个方法。
|
||||
*/
|
||||
protected array $exceptRoute = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
@@ -141,13 +152,19 @@ class BaseController
|
||||
$result = [];
|
||||
foreach ($checkFields as $fieldName) {
|
||||
$checkValue = $data[$fieldName] ?? '';
|
||||
if (is_array($checkValue) && !empty($checkValue)) {
|
||||
$result[$fieldName] = $checkValue;
|
||||
if (is_array($checkValue)) {
|
||||
// 数组值单独处理:非空才收;空数组按「跳过」(与 GF 版一致),
|
||||
// 且绝不对数组做 strval,避免触发 "Array to string conversion" 警告
|
||||
if (!empty($checkValue)) {
|
||||
$result[$fieldName] = $checkValue;
|
||||
}
|
||||
} else {
|
||||
if (mb_strlen(strval($checkValue)) <= 0 && !in_array($fieldName, $this->notRequest)) {
|
||||
$requiredFields[] = $fieldName;
|
||||
} else {
|
||||
if (!empty($checkValue) || $checkValue === 0) {
|
||||
// 用字符串长度判断是否有值,避免 !empty 把合法的 0 / "0" 误当空丢弃
|
||||
// (HTTP 参数皆为字符串,status=0 会传成 "0",原写法会导致状态无法写入/改回启用)
|
||||
if (mb_strlen(strval($checkValue)) > 0) {
|
||||
$result[$fieldName] = $checkValue;
|
||||
}
|
||||
}
|
||||
|
||||
85
app/BaseApp/BaseWxService.php
Normal file
85
app/BaseApp/BaseWxService.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\BaseApp;
|
||||
|
||||
use App\Models\business\WxUserModel;
|
||||
use App\Service\common\UtilsService;
|
||||
use App\Service\wx\WxTokenService;
|
||||
|
||||
/**
|
||||
* 小程序业务基类
|
||||
*
|
||||
* 与 BaseService 的区别只在身份来源:后台是 nl_admin,小程序是 cc_wx_user。
|
||||
* 分页与查询能力复用 BaseNotAuthService,但不走它的构造(那里解的是后台 token)。
|
||||
*/
|
||||
class BaseWxService extends BaseNotAuthService
|
||||
{
|
||||
/**
|
||||
* @var bool 是否要求已登录
|
||||
*/
|
||||
protected bool $needLogin = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->utils = UtilsService::getInstance();
|
||||
$token = WxTokenService::getInstance();
|
||||
if ($this->needLogin) {
|
||||
$this->userInfo = $token->requireUser();
|
||||
} else {
|
||||
$this->userInfo = $token->resolveUser(request()->bearerToken()) ?? [];
|
||||
}
|
||||
$this->userId = (int) ($this->userInfo['id'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户是否可见价格
|
||||
*/
|
||||
protected function canSeePrice(): bool
|
||||
{
|
||||
return (bool) ($this->userInfo['show_price'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户的价格倍率
|
||||
*/
|
||||
protected function priceMultiplier(): mixed
|
||||
{
|
||||
return $this->userInfo['price_number'] ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开分享链接时继承代理商倍率
|
||||
*
|
||||
* 商品详情、套餐详情共用:只有「还没有上级、自己也不是代理商」的已登录用户会被绑定;
|
||||
* 已绑同一上级时同步最新倍率。未登录不写库,只影响本次会话价。
|
||||
*/
|
||||
protected function inheritAgentPrice(int $shareUserId): void
|
||||
{
|
||||
if ($shareUserId <= 0) {
|
||||
return;
|
||||
}
|
||||
$isAgent = (int) ($this->userInfo['is_p'] ?? 0) === 1;
|
||||
$pid = (int) ($this->userInfo['pid'] ?? 0);
|
||||
if ($isAgent) {
|
||||
return;
|
||||
}
|
||||
if ($pid !== 0 && $pid !== $shareUserId) {
|
||||
return;
|
||||
}
|
||||
$agent = WxUserModel::where('id', $shareUserId)->where('deleted_at', 0)->first();
|
||||
if (empty($agent) || (int) $agent['is_p'] !== 1) {
|
||||
return;
|
||||
}
|
||||
$this->userInfo['show_price'] = 1;
|
||||
$this->userInfo['price_number'] = $agent['price_number'];
|
||||
if ($this->userId <= 0) {
|
||||
return;
|
||||
}
|
||||
WxUserModel::where('id', $this->userId)->update([
|
||||
'pid' => $shareUserId,
|
||||
'show_price' => 1,
|
||||
'price_number' => $agent['price_number'],
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
238
app/Console/Commands/EndpointSyncCommand.php
Normal file
238
app/Console/Commands/EndpointSyncCommand.php
Normal file
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ApiEndpointModel;
|
||||
use App\Service\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Routing\Route as RoutingRoute;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* 把已注册的路由同步进 nl_api_endpoint
|
||||
*
|
||||
* 这张表同时是操作日志的匹配依据和接口级权限的授权清单,手工维护必然漏。
|
||||
* 路由本身由 autoRouteRegister 反射生成,这里再反射一次路由表,
|
||||
* 顺便把控制器方法的 PHPDoc 首行当作操作名,新增模块不用再补 SQL。
|
||||
*/
|
||||
class EndpointSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'lgp:endpoint-sync
|
||||
{--dry-run : 只预演不写库}
|
||||
{--prune : 把路由表里已不存在的接口标记为停用}';
|
||||
|
||||
protected $description = '同步路由到接口注册表 nl_api_endpoint(幂等)';
|
||||
|
||||
private bool $dryRun = false;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->dryRun = (bool) $this->option('dry-run');
|
||||
if ($this->dryRun) {
|
||||
$this->warn('预演模式:不会写入任何数据');
|
||||
}
|
||||
|
||||
$noLog = (array) config('nl.log.no_insert', []);
|
||||
$seen = [];
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach (Route::getRoutes() as $route) {
|
||||
$url = $this->normalizeUri($route->uri());
|
||||
if ($url === null) {
|
||||
continue;
|
||||
}
|
||||
$method = $this->mapMethod($route);
|
||||
$key = $url . '#' . $method;
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
|
||||
$attributes = [
|
||||
'name' => $this->resolveName($route, $url),
|
||||
'description' => $this->resolveDescription($route),
|
||||
'controller' => $this->resolveController($route),
|
||||
'is_log' => in_array($url, $noLog, true) ? 0 : 1,
|
||||
'status' => 1,
|
||||
];
|
||||
|
||||
$exists = ApiEndpointModel::where('url', $url)->where('method', $method)->first();
|
||||
if ($exists) {
|
||||
// 名称与说明可能被人在后台改过,不覆盖;只补空值与纠正控制器归属
|
||||
$diff = [];
|
||||
foreach (['controller', 'status'] as $field) {
|
||||
if ((string) $exists->{$field} !== (string) $attributes[$field]) {
|
||||
$diff[$field] = $attributes[$field];
|
||||
}
|
||||
}
|
||||
foreach (['name', 'description'] as $field) {
|
||||
if ((string) $exists->{$field} === '' && $attributes[$field] !== '') {
|
||||
$diff[$field] = $attributes[$field];
|
||||
}
|
||||
}
|
||||
if ((int) $exists->deleted_at !== 0) {
|
||||
$diff['deleted_at'] = 0;
|
||||
}
|
||||
if (!empty($diff)) {
|
||||
$updated++;
|
||||
$this->line(" ~ {$url} [{$method}] " . implode(', ', array_keys($diff)));
|
||||
if (!$this->dryRun) {
|
||||
$diff['updated_at'] = time();
|
||||
ApiEndpointModel::where('id', $exists->id)->update($diff);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$created++;
|
||||
$this->line(" + {$url} [{$method}] {$attributes['name']}");
|
||||
if (!$this->dryRun) {
|
||||
ApiEndpointModel::insert($attributes + [
|
||||
'url' => $url,
|
||||
'method' => $method,
|
||||
'created_at' => time(),
|
||||
'deleted_at' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$stale = $this->findStale(array_keys($seen));
|
||||
$this->newLine();
|
||||
$this->info("新增 {$created} 条,更新 {$updated} 条,路由已不存在 " . count($stale) . ' 条');
|
||||
|
||||
if (!empty($stale)) {
|
||||
foreach ($stale as $row) {
|
||||
$this->line(" ? {$row->url} [{$row->method}] {$row->name}");
|
||||
}
|
||||
if ($this->option('prune') && !$this->dryRun) {
|
||||
ApiEndpointModel::whereIn('id', array_column($stale, 'id'))->update([
|
||||
'status' => 0,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
$this->warn('以上接口已停用(未删除,避免历史操作日志失去关联)');
|
||||
} else {
|
||||
$this->warn('加 --prune 可把它们停用');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->dryRun) {
|
||||
PermissionService::getInstance()->clear();
|
||||
$this->info('已清空角色权限缓存');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只收 /api/ 下的业务接口;带路径参数的路由不进注册表(授权与日志都按固定路径匹配)
|
||||
*/
|
||||
private function normalizeUri(string $uri): ?string
|
||||
{
|
||||
$uri = trim($uri, '/');
|
||||
if (!str_starts_with($uri, 'api/')) {
|
||||
return null;
|
||||
}
|
||||
$uri = trim(substr($uri, 4), '/');
|
||||
if ($uri === '' || str_contains($uri, '{')) {
|
||||
return null;
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET=1 POST=2 其他=0,与 ApiOpLogMiddleware 的映射保持一致
|
||||
*/
|
||||
private function mapMethod(RoutingRoute $route): int
|
||||
{
|
||||
$methods = array_diff($route->methods(), ['HEAD']);
|
||||
if ($methods === ['GET']) {
|
||||
return 1;
|
||||
}
|
||||
if ($methods === ['POST']) {
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function resolveController(RoutingRoute $route): string
|
||||
{
|
||||
$action = $route->getAction('controller');
|
||||
if (!is_string($action) || !str_contains($action, '@')) {
|
||||
return '';
|
||||
}
|
||||
return class_basename(explode('@', $action)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取控制器方法 PHPDoc 的首行作为操作名,没有注释时回落成路径
|
||||
*/
|
||||
private function resolveName(RoutingRoute $route, string $url): string
|
||||
{
|
||||
$summary = $this->docSummary($route);
|
||||
return $summary !== '' ? mb_substr($summary, 0, 64) : $url;
|
||||
}
|
||||
|
||||
private function resolveDescription(RoutingRoute $route): string
|
||||
{
|
||||
$lines = $this->docLines($route);
|
||||
array_shift($lines);
|
||||
return mb_substr(trim(implode(' ', $lines)), 0, 255);
|
||||
}
|
||||
|
||||
private function docSummary(RoutingRoute $route): string
|
||||
{
|
||||
return $this->docLines($route)[0] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string> 去掉 @tag 行后的注释正文
|
||||
*/
|
||||
private function docLines(RoutingRoute $route): array
|
||||
{
|
||||
$action = $route->getAction('controller');
|
||||
if (!is_string($action) || !str_contains($action, '@')) {
|
||||
return [];
|
||||
}
|
||||
[$class, $method] = explode('@', $action);
|
||||
try {
|
||||
$doc = (new ReflectionMethod($class, $method))->getDocComment();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
if (!is_string($doc)) {
|
||||
return [];
|
||||
}
|
||||
$lines = [];
|
||||
foreach (explode("\n", $doc) as $line) {
|
||||
$line = trim(ltrim(trim($line), '/*'));
|
||||
if ($line === '' || str_starts_with($line, '@')) {
|
||||
continue;
|
||||
}
|
||||
$lines[] = $line;
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $seenKeys url#method 组合
|
||||
* @return array<int, object>
|
||||
*/
|
||||
private function findStale(array $seenKeys): array
|
||||
{
|
||||
$stale = [];
|
||||
$rows = ApiEndpointModel::where('deleted_at', 0)->where('status', 1)->get(['id', 'url', 'method', 'name']);
|
||||
foreach ($rows as $row) {
|
||||
if (!in_array($row->url . '#' . (int) $row->method, $seenKeys, true)) {
|
||||
$stale[] = (object) [
|
||||
'id' => (int) $row->id,
|
||||
'url' => (string) $row->url,
|
||||
'method' => (int) $row->method,
|
||||
'name' => (string) $row->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $stale;
|
||||
}
|
||||
}
|
||||
157
app/Console/Commands/MenuSyncCommand.php
Normal file
157
app/Console/Commands/MenuSyncCommand.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MenuModel;
|
||||
use App\Models\RoleMenuRelationModel;
|
||||
use App\Service\common\RedisService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* 按 config/lgp_menu.php 幂等同步 nl_menu
|
||||
*
|
||||
* name 是全局唯一键,同步以它为准:存在则按声明纠正字段,不存在则插入。
|
||||
* pid 用父节点的 name 反查,所以配置里不出现任何自增 id,反复执行结果一致。
|
||||
* 同步完必须清菜单缓存,否则前端还会拿到 Redis 里的旧树。
|
||||
*/
|
||||
class MenuSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'lgp:menu-sync
|
||||
{--dry-run : 只预演不写库}
|
||||
{--grant-super : 同步后把新菜单补进超级管理员之外的角色(默认不动角色关系)}';
|
||||
|
||||
protected $description = '同步 LGP 业务菜单到 nl_menu(幂等)';
|
||||
|
||||
private bool $dryRun = false;
|
||||
|
||||
private int $created = 0;
|
||||
|
||||
private int $updated = 0;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->dryRun = (bool) $this->option('dry-run');
|
||||
if ($this->dryRun) {
|
||||
$this->warn('预演模式:不会写入任何数据');
|
||||
}
|
||||
|
||||
$tree = config('lgp_menu', []);
|
||||
if (empty($tree)) {
|
||||
$this->error('config/lgp_menu.php 为空');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->walk($tree, 0);
|
||||
|
||||
$this->newLine();
|
||||
$this->info("新增 {$this->created} 条,更新 {$this->updated} 条");
|
||||
|
||||
if (!$this->dryRun) {
|
||||
// 菜单树按角色缓存在 Redis,不清的话前端看不到新菜单
|
||||
RedisService::getInstance()->init(config('nl.redis.menu_key'))->delAll();
|
||||
$this->info('已清空菜单缓存');
|
||||
$this->reportRoleBinding();
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $nodes
|
||||
*/
|
||||
private function walk(array $nodes, int $pid): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
// parent 显式声明时优先(用于挂到脚手架已有菜单下),否则跟随递归层级
|
||||
$parentId = isset($node['parent'])
|
||||
? $this->resolveParentId((string) $node['parent'])
|
||||
: $pid;
|
||||
$id = $this->upsert($node, $parentId);
|
||||
if (!empty($node['children'])) {
|
||||
$this->walk($node['children'], $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveParentId(string $name): int
|
||||
{
|
||||
$id = (int) MenuModel::where('name', $name)->where('deleted_at', 0)->value('id');
|
||||
if ($id === 0) {
|
||||
$this->warn("父级菜单 {$name} 不存在,该节点挂到根目录");
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $node
|
||||
* @return int 菜单 id(预演模式下返回 0,子节点会挂到根,仅影响预演输出)
|
||||
*/
|
||||
private function upsert(array $node, int $pid): int
|
||||
{
|
||||
$isDirectory = ($node['component'] ?? '') === 'BasicLayout';
|
||||
$attributes = [
|
||||
'title' => (string) $node['title'],
|
||||
'icon' => (string) ($node['icon'] ?? ''),
|
||||
'path' => (string) $node['path'],
|
||||
'component' => (string) $node['component'],
|
||||
'redirect' => (string) ($node['redirect'] ?? ''),
|
||||
// 取反字段:0 开启缓存 / 0 固定标签
|
||||
'keep_alive' => (int) ($node['keep_alive'] ?? ($isDirectory ? 0 : 1)),
|
||||
'hide_in_menu' => (int) ($node['hide_in_menu'] ?? 0),
|
||||
'affix_tab' => (int) ($node['affix_tab'] ?? 1),
|
||||
'badge' => (string) ($node['badge'] ?? ''),
|
||||
'badge_type' => (int) ($node['badge_type'] ?? 0),
|
||||
'badge_variants' => (int) ($node['badge_variants'] ?? 0),
|
||||
'iframe_src' => (string) ($node['iframe_src'] ?? ''),
|
||||
'query' => (string) ($node['query'] ?? ''),
|
||||
'pid' => $pid,
|
||||
'sort' => (int) ($node['sort'] ?? 0),
|
||||
];
|
||||
|
||||
$name = (string) $node['name'];
|
||||
$exists = MenuModel::where('name', $name)->first();
|
||||
|
||||
if ($exists) {
|
||||
$diff = array_filter(
|
||||
$attributes,
|
||||
fn ($value, $key) => (string) $exists->{$key} !== (string) $value,
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
// 已软删的菜单同步时一并复活,否则改配置也救不回来
|
||||
if ((int) $exists->deleted_at !== 0) {
|
||||
$diff['deleted_at'] = 0;
|
||||
}
|
||||
if (empty($diff)) {
|
||||
$this->line(" = {$attributes['title']} ({$name})");
|
||||
return (int) $exists->id;
|
||||
}
|
||||
$this->line(" ~ {$attributes['title']} ({$name}) " . implode(', ', array_keys($diff)));
|
||||
$this->updated++;
|
||||
if (!$this->dryRun) {
|
||||
$diff['updated_at'] = time();
|
||||
MenuModel::where('id', $exists->id)->update($diff);
|
||||
}
|
||||
return (int) $exists->id;
|
||||
}
|
||||
|
||||
$this->line(" + {$attributes['title']} ({$name})");
|
||||
$this->created++;
|
||||
if ($this->dryRun) {
|
||||
return 0;
|
||||
}
|
||||
$attributes['created_at'] = time();
|
||||
return (int) MenuModel::insertGetId($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只提示不自动授权:给谁看哪些菜单是业务决策,命令替用户决定容易造成越权
|
||||
*/
|
||||
private function reportRoleBinding(): void
|
||||
{
|
||||
if (!$this->option('grant-super')) {
|
||||
$bound = RoleMenuRelationModel::distinct()->count('role_id');
|
||||
$this->newLine();
|
||||
$this->warn("超级管理员(role_id=1)自动可见全部菜单;其余 {$bound} 个已授权角色需到「角色管理」重新勾选新菜单。");
|
||||
}
|
||||
}
|
||||
}
|
||||
396
app/Console/Commands/RbacAuditCommand.php
Normal file
396
app/Console/Commands/RbacAuditCommand.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 账号与角色迁移前的数据体检
|
||||
*
|
||||
* 老库 cc_user / cc_role / cc_user_role_relation 的结构与新库 nl_admin / nl_role 有几处不兼容,
|
||||
* 直接搬运会静默出错(登录到错误的人、普通角色变超管、账号迁过去全是禁用)。
|
||||
* 本命令只读不写,把这些隐患一次列清,供 lgp:rbac-migrate 之前决策。
|
||||
*/
|
||||
class RbacAuditCommand extends Command
|
||||
{
|
||||
protected $signature = 'lgp:rbac-audit
|
||||
{--json= : 额外把结果写成 JSON 文件,便于留档比对}';
|
||||
|
||||
protected $description = '账号与角色迁移前数据体检(只读)';
|
||||
|
||||
/** 老库连接:业务表走 business 连接(前缀 cc_) */
|
||||
private const OLD = 'business';
|
||||
|
||||
/** 新库连接:系统表走默认 mysql 连接(前缀 nl_) */
|
||||
private const NEW = 'mysql';
|
||||
|
||||
/** 阻断级问题数量,决定退出码 */
|
||||
private int $blockerCount = 0;
|
||||
|
||||
/** @var array<string, mixed> 汇总结果,供 --json 落盘 */
|
||||
private array $report = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('账号与角色迁移前数据体检');
|
||||
$this->line('老库连接:' . self::OLD . '(前缀 ' . DB::connection(self::OLD)->getTablePrefix() . ')');
|
||||
$this->line('新库连接:' . self::NEW . '(前缀 ' . DB::connection(self::NEW)->getTablePrefix() . ')');
|
||||
$this->newLine();
|
||||
|
||||
if (!$this->checkTables()) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->sectionScale();
|
||||
$this->sectionMultiRole();
|
||||
$this->sectionDuplicatePhone();
|
||||
$this->sectionInvalidPhone();
|
||||
$this->sectionRoles();
|
||||
$this->sectionRoleIdCollision();
|
||||
$this->sectionPasswordFormat();
|
||||
$this->sectionDepartment();
|
||||
$this->sectionAccountVsPhone();
|
||||
|
||||
$this->newLine();
|
||||
if ($this->blockerCount > 0) {
|
||||
$this->error("发现 {$this->blockerCount} 项阻断级问题,请先处理再执行 lgp:rbac-migrate");
|
||||
} else {
|
||||
$this->info('未发现阻断级问题,可以执行 lgp:rbac-migrate --dry-run 预演');
|
||||
}
|
||||
|
||||
if ($path = $this->option('json')) {
|
||||
$this->writeJson($path);
|
||||
}
|
||||
|
||||
return $this->blockerCount > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前置检查:两侧表都得在,否则后面每条查询都会抛异常;
|
||||
* 同时检查 nl_admin / nl_role 是否已补齐 RBAC 迁移所需列(缺列时 my-info 等接口会直接 500)
|
||||
*/
|
||||
private function checkTables(): bool
|
||||
{
|
||||
$missing = [];
|
||||
foreach (['user', 'role', 'user_role_relation', 'role_menu_relation', 'department'] as $table) {
|
||||
if (!DB::connection(self::OLD)->getSchemaBuilder()->hasTable($table)) {
|
||||
$missing[] = DB::connection(self::OLD)->getTablePrefix() . $table;
|
||||
}
|
||||
}
|
||||
foreach (['admin', 'role'] as $table) {
|
||||
if (!DB::connection(self::NEW)->getSchemaBuilder()->hasTable($table)) {
|
||||
$missing[] = DB::connection(self::NEW)->getTablePrefix() . $table;
|
||||
}
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
$this->error('缺少表:' . implode('、', $missing));
|
||||
$this->line('新库系统表请先执行 php artisan sql:run-nl-admin 安装');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 代码已依赖这些列(AdminService::selectField / LoginService 惰性升级),迁移前就必须存在
|
||||
$missingColumns = [];
|
||||
foreach ([
|
||||
'admin' => ['department_id', 'legacy_password', 'legacy_password_expire_at'],
|
||||
'role' => ['status', 'color'],
|
||||
] as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
if (!DB::connection(self::NEW)->getSchemaBuilder()->hasColumn($table, $column)) {
|
||||
$missingColumns[] = DB::connection(self::NEW)->getTablePrefix() . $table . '.' . $column;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($missingColumns)) {
|
||||
$this->error('新库缺少列:' . implode('、', $missingColumns));
|
||||
$this->line('请先执行 SQL:database/sql/rbac_migrate_ddl.sql(幂等,可重复跑)');
|
||||
$this->line('或直接跑:php artisan lgp:rbac-migrate --force (会先幂等补列再迁数据)');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移规模:先知道要搬多少行
|
||||
*/
|
||||
private function sectionScale(): void
|
||||
{
|
||||
$rows = [
|
||||
['cc_user 待迁账号', $this->oldCount('user')],
|
||||
['cc_role 待迁角色', $this->oldCount('role')],
|
||||
['cc_department 部门(复用不迁)', $this->oldCount('department')],
|
||||
['cc_user_role_relation 用户角色关系', DB::connection(self::OLD)->table('user_role_relation')->count()],
|
||||
['cc_role_menu_relation 角色菜单关系', DB::connection(self::OLD)->table('role_menu_relation')->count()],
|
||||
['nl_admin 现有账号', $this->newCount('admin')],
|
||||
['nl_role 现有角色', $this->newCount('role')],
|
||||
];
|
||||
$this->line('<comment>[1] 迁移规模</comment>');
|
||||
$this->table(['项目', '数量'], $rows);
|
||||
$this->report['scale'] = collect($rows)->mapWithKeys(fn ($r) => [$r[0] => $r[1]])->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 多角色用户:老库是中间表多对多,新库是 nl_admin.role_id 单字段,多绑的必须人工决定取哪个
|
||||
*
|
||||
* 注意:business 连接有前缀 cc_,Laravel 会把别名 `ur` 编译成 `cc_ur`,
|
||||
* 但 selectRaw 不会自动改写,必须手写带前缀的别名,否则报 Unknown column 'ur.user_id'
|
||||
*/
|
||||
private function sectionMultiRole(): void
|
||||
{
|
||||
$p = $this->oldPrefix();
|
||||
$rows = DB::connection(self::OLD)->table('user_role_relation as ur')
|
||||
->join('user as u', 'u.id', '=', 'ur.user_id')
|
||||
->where('u.deleted_at', 0)
|
||||
->groupBy('ur.user_id', 'u.account', 'u.nick_name')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->selectRaw("{$p}ur.user_id, {$p}u.account, {$p}u.nick_name, COUNT(*) AS role_count, GROUP_CONCAT({$p}ur.role_id ORDER BY {$p}ur.role_id) AS role_ids")
|
||||
->get();
|
||||
|
||||
$this->line('<comment>[2] 绑定了多个角色的用户</comment>(新库一个账号只有一个 role_id)');
|
||||
if ($rows->isEmpty()) {
|
||||
$this->info(' 无,可安全一对一迁移');
|
||||
$this->report['multi_role'] = [];
|
||||
return;
|
||||
}
|
||||
$this->blockerCount++;
|
||||
$this->table(
|
||||
['user_id', 'account', 'nick_name', '角色数', 'role_ids'],
|
||||
$rows->map(fn ($r) => [$r->user_id, $r->account, $r->nick_name, $r->role_count, $r->role_ids])->all()
|
||||
);
|
||||
$this->warn(' 需决策:取 role_id 最小者、还是给新系统补多角色支持');
|
||||
$this->report['multi_role'] = $rows->toArray();
|
||||
|
||||
// 没有任何角色关系的账号同样要处理,否则迁过去 role_id=0 什么菜单都看不到
|
||||
$noRole = DB::connection(self::OLD)->table('user as u')
|
||||
->where('u.deleted_at', 0)
|
||||
->whereNotExists(function ($q) {
|
||||
$q->select(DB::raw(1))->from('user_role_relation as ur')->whereColumn('ur.user_id', 'u.id');
|
||||
})
|
||||
->get(['id', 'account', 'nick_name']);
|
||||
if ($noRole->isNotEmpty()) {
|
||||
$this->warn(' 另有 ' . $noRole->count() . ' 个账号没有任何角色关系,迁移后 role_id=0 将看不到任何菜单');
|
||||
$this->report['no_role'] = $noRole->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复手机号:登录是 where('phone', ...)->first(),重复会登录到错误的人
|
||||
*/
|
||||
private function sectionDuplicatePhone(): void
|
||||
{
|
||||
$rows = DB::connection(self::OLD)->table('user')
|
||||
->where('deleted_at', 0)
|
||||
->groupBy('phone')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->selectRaw('phone, COUNT(*) AS c, GROUP_CONCAT(id ORDER BY id) AS ids, GROUP_CONCAT(account ORDER BY id) AS accounts')
|
||||
->get();
|
||||
|
||||
$this->line('<comment>[3] 重复手机号</comment>(新库登录按 phone 查询,重复会登错人)');
|
||||
if ($rows->isEmpty()) {
|
||||
$this->info(' 无重复');
|
||||
$this->report['duplicate_phone'] = [];
|
||||
return;
|
||||
}
|
||||
$this->blockerCount++;
|
||||
$this->table(
|
||||
['phone', '重复数', 'user_ids', 'accounts'],
|
||||
$rows->map(fn ($r) => [$r->phone, $r->c, $r->ids, $r->accounts])->all()
|
||||
);
|
||||
$this->warn(' 必须先在老库消重,迁移后还要给 nl_admin.phone 补唯一索引');
|
||||
$this->report['duplicate_phone'] = $rows->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号格式:新库 nl_admin.phone 是 char(11),超长会被截断,空值无法登录
|
||||
*/
|
||||
private function sectionInvalidPhone(): void
|
||||
{
|
||||
$rows = DB::connection(self::OLD)->table('user')
|
||||
->where('deleted_at', 0)
|
||||
->whereRaw("(phone = '' OR CHAR_LENGTH(phone) <> 11)")
|
||||
->selectRaw('id, account, nick_name, phone, CHAR_LENGTH(phone) AS len')
|
||||
->get();
|
||||
|
||||
$this->line('<comment>[4] 手机号长度异常</comment>(新库是 char(11),超长会截断)');
|
||||
if ($rows->isEmpty()) {
|
||||
$this->info(' 全部为 11 位');
|
||||
$this->report['invalid_phone'] = [];
|
||||
return;
|
||||
}
|
||||
$this->blockerCount++;
|
||||
$this->table(
|
||||
['id', 'account', 'nick_name', 'phone', '长度'],
|
||||
$rows->map(fn ($r) => [$r->id, $r->account, $r->nick_name, $r->phone, $r->len])->all()
|
||||
);
|
||||
$this->warn(' 空手机号将完全无法登录,带区号/空格的要先清洗');
|
||||
$this->report['invalid_phone'] = $rows->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 老角色清单:新库 nl_role 需要 value(英文标识),老库没这个字段,逐个要指定
|
||||
*/
|
||||
private function sectionRoles(): void
|
||||
{
|
||||
$rows = DB::connection(self::OLD)->table('role')
|
||||
->where('deleted_at', 0)
|
||||
->orderBy('id')
|
||||
->get(['id', 'name', 'desc', 'status', 'color']);
|
||||
|
||||
$counts = DB::connection(self::OLD)->table('user_role_relation as ur')
|
||||
->join('user as u', 'u.id', '=', 'ur.user_id')
|
||||
->where('u.deleted_at', 0)
|
||||
->groupBy('ur.role_id')
|
||||
->selectRaw("{$this->oldPrefix()}ur.role_id, COUNT(*) AS c")
|
||||
->pluck('c', 'role_id');
|
||||
|
||||
$menuCounts = DB::connection(self::OLD)->table('role_menu_relation')
|
||||
->groupBy('role_id')
|
||||
->selectRaw('role_id, COUNT(*) AS c')
|
||||
->pluck('c', 'role_id');
|
||||
|
||||
$this->line('<comment>[5] 老角色清单</comment>(新库 nl_role.value 必填,需为每个角色指定英文标识)');
|
||||
$this->table(
|
||||
['老 id', 'name', 'desc', 'status(0正常1禁用)', 'color', '账号数', '菜单数'],
|
||||
$rows->map(fn ($r) => [
|
||||
$r->id, $r->name, $r->desc, $r->status, $r->color,
|
||||
$counts[$r->id] ?? 0, $menuCounts[$r->id] ?? 0,
|
||||
])->all()
|
||||
);
|
||||
$this->report['old_roles'] = $rows->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色 ID 撞车:nl_role.id=1 是超级管理员且代码里硬判断 role_id===1 全量放行,
|
||||
* 老库 id=1 未必是超管,按原 ID 迁会把普通角色提权成超管
|
||||
*/
|
||||
private function sectionRoleIdCollision(): void
|
||||
{
|
||||
$this->line('<comment>[6] 角色 ID 撞车检查</comment>');
|
||||
$oldOne = DB::connection(self::OLD)->table('role')->where('id', 1)->first();
|
||||
$newOne = DB::connection(self::NEW)->table('role')->where('id', 1)->first();
|
||||
if ($oldOne && $newOne) {
|
||||
$this->table(
|
||||
['库', 'id', 'name'],
|
||||
[['老 cc_role', 1, $oldOne->name], ['新 nl_role', 1, $newOne->name]]
|
||||
);
|
||||
$this->warn(' 老 id=1 是「' . $oldOne->name . '」,新 id=1 是「' . $newOne->name . '」(硬编码全量放行)');
|
||||
$this->warn(' 迁移必须做 ID 偏移,不能按原 ID 直搬');
|
||||
$this->blockerCount++;
|
||||
} else {
|
||||
$this->info(' 老库无 id=1 角色,仍建议偏移以留出系统角色区间');
|
||||
}
|
||||
|
||||
$maxOld = (int) DB::connection(self::OLD)->table('role')->max('id');
|
||||
$this->line(' 老角色最大 id = ' . $maxOld . ',建议偏移量 100(迁后占用 101..' . (100 + $maxOld) . ')');
|
||||
$this->report['role_id_collision'] = [
|
||||
'old_role_1' => $oldOne->name ?? null,
|
||||
'new_role_1' => $newOne->name ?? null,
|
||||
'old_max_id' => $maxOld,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码格式:老库是无盐 sha1(40 位 hex),非此格式的迁过去无法惰性升级,只能走重置
|
||||
*/
|
||||
private function sectionPasswordFormat(): void
|
||||
{
|
||||
$total = $this->oldCount('user');
|
||||
$sha1 = DB::connection(self::OLD)->table('user')
|
||||
->where('deleted_at', 0)
|
||||
->whereRaw("CHAR_LENGTH(password) = 40 AND password REGEXP '^[0-9a-f]{40}$'")
|
||||
->count();
|
||||
$empty = DB::connection(self::OLD)->table('user')
|
||||
->where('deleted_at', 0)->where('password', '')->count();
|
||||
$other = $total - $sha1 - $empty;
|
||||
|
||||
$this->line('<comment>[7] 密码格式</comment>(惰性升级依赖无盐 sha1,40 位 hex)');
|
||||
$this->table(['类型', '数量'], [
|
||||
['标准 sha1(可惰性升级)', $sha1],
|
||||
['空密码(必须重置)', $empty],
|
||||
['其它格式(必须重置)', max($other, 0)],
|
||||
]);
|
||||
if ($empty > 0 || $other > 0) {
|
||||
$this->warn(' 非 sha1 的账号迁移后需强制走密码重置流程');
|
||||
}
|
||||
$this->report['password_format'] = ['sha1' => $sha1, 'empty' => $empty, 'other' => max($other, 0)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门引用:cc_user.department 指向 cc_department.id,悬空引用迁过去会显示空部门
|
||||
*/
|
||||
private function sectionDepartment(): void
|
||||
{
|
||||
$rows = DB::connection(self::OLD)->table('user as u')
|
||||
->where('u.deleted_at', 0)
|
||||
->where('u.department', '>', 0)
|
||||
->whereNotExists(function ($q) {
|
||||
$q->select(DB::raw(1))->from('department as d')
|
||||
->whereColumn('d.id', 'u.department')->where('d.deleted_at', 0);
|
||||
})
|
||||
->get(['u.id', 'u.account', 'u.department']);
|
||||
|
||||
$this->line('<comment>[8] 部门悬空引用</comment>');
|
||||
if ($rows->isEmpty()) {
|
||||
$this->info(' 无悬空引用');
|
||||
$this->report['orphan_department'] = [];
|
||||
return;
|
||||
}
|
||||
$this->table(
|
||||
['user_id', 'account', 'department(已不存在)'],
|
||||
$rows->map(fn ($r) => [$r->id, $r->account, $r->department])->all()
|
||||
);
|
||||
$this->warn(' 这些账号迁移后 department_id 建议置 0');
|
||||
$this->report['orphan_department'] = $rows->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* account 与 phone 的关系:老前端表单字段叫 account,但后端一直是 where('phone', ...),
|
||||
* 也就是用户实际输的是手机号。account 不参与登录,迁移时只留痕
|
||||
*/
|
||||
private function sectionAccountVsPhone(): void
|
||||
{
|
||||
$diff = DB::connection(self::OLD)->table('user')
|
||||
->where('deleted_at', 0)
|
||||
->whereColumn('account', '<>', 'phone')
|
||||
->count();
|
||||
|
||||
$this->line('<comment>[9] account 与 phone 不一致的账号</comment>');
|
||||
$this->line(' 共 ' . $diff . ' 个。account 从不参与登录(老 UserService::login 查的是 phone),');
|
||||
$this->line(' 迁移时把原 account 写进 nl_admin.desc 留痕即可,不新增列');
|
||||
$this->report['account_phone_diff'] = $diff;
|
||||
}
|
||||
|
||||
private function oldCount(string $table): int
|
||||
{
|
||||
return DB::connection(self::OLD)->table($table)->where('deleted_at', 0)->count();
|
||||
}
|
||||
|
||||
private function newCount(string $table): int
|
||||
{
|
||||
return DB::connection(self::NEW)->table($table)->where('deleted_at', 0)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 老库(business)表前缀,selectRaw 里引用别名时必须手动拼上
|
||||
* Laravel 会把 `as ur` 编译成 `as cc_ur`,但 selectRaw 字符串不会自动改写
|
||||
*/
|
||||
private function oldPrefix(): string
|
||||
{
|
||||
return DB::connection(self::OLD)->getTablePrefix();
|
||||
}
|
||||
|
||||
private function writeJson(string $path): void
|
||||
{
|
||||
$full = str_starts_with($path, '/') ? $path : base_path($path);
|
||||
@mkdir(dirname($full), 0755, true);
|
||||
file_put_contents(
|
||||
$full,
|
||||
json_encode([
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'blocker_count' => $this->blockerCount,
|
||||
'report' => $this->report,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
$this->info('体检结果已写入:' . $full);
|
||||
}
|
||||
}
|
||||
415
app/Console/Commands/RbacMigrateCommand.php
Normal file
415
app/Console/Commands/RbacMigrateCommand.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 账号与角色迁移:cc_user / cc_role / cc_user_role_relation → nl_admin / nl_role
|
||||
*
|
||||
* 几处必须偏移或转换的地方(照原样搬会静默出错):
|
||||
* - 角色 ID 偏移:nl_role.id=1 是超管且代码里硬判断 role_id===1 全量放行,老库 id=1 未必是超管
|
||||
* - 账号 ID 同样偏移:老 cc_user.id 从 1 起,会撞上安装包里的超管
|
||||
* - 用户角色从多对多降级成 nl_admin.role_id 单字段,多绑的取 role_id 最小者
|
||||
* - 密码是无盐 sha1,进 legacy_password 走惰性升级,password 留空
|
||||
* - nl_admin.open_id / ip_table 是 NOT NULL 且无默认值,必须显式赋值
|
||||
*
|
||||
* 幂等:按 nl_admin.desc / nl_role.desc 里的迁移留痕判断是否已迁,重复执行只补未迁的行。
|
||||
*/
|
||||
class RbacMigrateCommand extends Command
|
||||
{
|
||||
protected $signature = 'lgp:rbac-migrate
|
||||
{--dry-run : 只预演不写库}
|
||||
{--offset=100 : 角色与账号的 ID 偏移量,避开系统预留区间}
|
||||
{--legacy-days=90 : 遗留 sha1 密码的有效天数,过期强制重置}
|
||||
{--value-map= : 角色 value 映射 JSON 文件,形如 {"管理员":"manager"}}
|
||||
{--force : 跳过确认}';
|
||||
|
||||
protected $description = '账号与角色迁移(cc_user/cc_role → nl_admin/nl_role)';
|
||||
|
||||
private const OLD = 'business';
|
||||
private const NEW = 'mysql';
|
||||
|
||||
/** 迁移留痕前缀,用于幂等判断 */
|
||||
private const TAG = '[migrated:cc_user#';
|
||||
|
||||
private const ROLE_TAG = '[migrated:cc_role#';
|
||||
|
||||
private bool $dryRun = false;
|
||||
|
||||
private int $offset = 100;
|
||||
|
||||
/** @var array<int, int> 老角色 id => 新角色 id */
|
||||
private array $roleMap = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->dryRun = (bool) $this->option('dry-run');
|
||||
$this->offset = max(1, (int) $this->option('offset'));
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->warn('预演模式:不会写入任何数据');
|
||||
}
|
||||
|
||||
$this->ensureColumns();
|
||||
|
||||
if (!$this->dryRun && !$this->option('force')) {
|
||||
$this->warn('即将向 nl_role / nl_admin 写入数据。建议先跑 lgp:rbac-audit 与 --dry-run。');
|
||||
if (!$this->confirm('继续?', false)) {
|
||||
$this->line('已取消');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->dryRun) {
|
||||
DB::connection(self::NEW)->beginTransaction();
|
||||
}
|
||||
$this->migrateRoles();
|
||||
$this->migrateAdmins();
|
||||
if (!$this->dryRun) {
|
||||
DB::connection(self::NEW)->commit();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if (!$this->dryRun && DB::connection(self::NEW)->transactionLevel() > 0) {
|
||||
DB::connection(self::NEW)->rollBack();
|
||||
}
|
||||
$this->error('迁移失败已回滚:' . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->reportRoleMenu();
|
||||
|
||||
$this->newLine();
|
||||
$this->info($this->dryRun ? '预演结束' : '迁移完成');
|
||||
$this->line('后续手动步骤:');
|
||||
$this->line(' 1. 按第十一节重建 nl_menu,再按功能重绑角色菜单(老 menu_id 不能直搬)');
|
||||
$this->line(' 2. 确认手机号无重复后执行 database/sql/rbac_migrate_ddl.sql 里的 uk_phone 唯一索引');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等补列:MySQL 不支持 ADD COLUMN IF NOT EXISTS,这里查 information_schema 再决定
|
||||
*/
|
||||
private function ensureColumns(): void
|
||||
{
|
||||
$adds = [
|
||||
'admin' => [
|
||||
'department_id' => "ADD COLUMN `department_id` int NOT NULL DEFAULT 0 COMMENT '所属部门ID,对应 cc_department.id' AFTER `role_id`",
|
||||
'legacy_password' => "ADD COLUMN `legacy_password` varchar(64) NOT NULL DEFAULT '' COMMENT '老系统无盐sha1密码,登录成功后清空' AFTER `password`",
|
||||
'legacy_password_expire_at' => "ADD COLUMN `legacy_password_expire_at` int NOT NULL DEFAULT 0 COMMENT '遗留密码失效时间' AFTER `legacy_password`",
|
||||
],
|
||||
'role' => [
|
||||
'status' => "ADD COLUMN `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1禁用' AFTER `desc`",
|
||||
'color' => "ADD COLUMN `color` varchar(32) NOT NULL DEFAULT '' COMMENT '角色标签颜色' AFTER `status`",
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($adds as $table => $columns) {
|
||||
$missing = [];
|
||||
foreach ($columns as $column => $clause) {
|
||||
if (!$this->hasColumn($table, $column)) {
|
||||
$missing[] = $clause;
|
||||
}
|
||||
}
|
||||
if (empty($missing)) {
|
||||
continue;
|
||||
}
|
||||
$physical = DB::connection(self::NEW)->getTablePrefix() . $table;
|
||||
$sql = "ALTER TABLE `{$physical}` " . implode(', ', $missing);
|
||||
$this->line('补列:' . $physical . ' → ' . count($missing) . ' 列');
|
||||
if (!$this->dryRun) {
|
||||
DB::connection(self::NEW)->statement($sql);
|
||||
}
|
||||
}
|
||||
|
||||
// 安装包里超管 status=1,而列注释是「0正常 1禁用」,补上状态校验后会被锁死,先纠正
|
||||
if (!$this->dryRun) {
|
||||
DB::connection(self::NEW)->table('admin')->where('id', 1)->where('status', 1)->update(['status' => 0]);
|
||||
}
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
return DB::connection(self::NEW)->getSchemaBuilder()->hasColumn($table, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色迁移:新 id = 老 id + offset,value 缺省生成 role_{老id}
|
||||
*/
|
||||
private function migrateRoles(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line('<comment>迁移角色</comment>');
|
||||
|
||||
$valueMap = $this->loadValueMap();
|
||||
$roles = DB::connection(self::OLD)->table('role')->where('deleted_at', 0)->orderBy('id')->get();
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$newId = (int) $role->id + $this->offset;
|
||||
$this->roleMap[(int) $role->id] = $newId;
|
||||
|
||||
if (DB::connection(self::NEW)->table('role')->where('id', $newId)->exists()) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $valueMap[$role->name] ?? ('role_' . $role->id);
|
||||
$data = [
|
||||
'id' => $newId,
|
||||
'name' => mb_substr((string) $role->name, 0, 32),
|
||||
'value' => mb_substr($value, 0, 32),
|
||||
'pid' => 0,
|
||||
'desc' => $this->tagged(self::ROLE_TAG, (int) $role->id, (string) $role->desc),
|
||||
'status' => (int) $role->status,
|
||||
'color' => (string) $role->color,
|
||||
'created_at' => (int) $role->created_at,
|
||||
'updated_at' => (int) $role->updated_at,
|
||||
'deleted_at' => 0,
|
||||
];
|
||||
$rows[] = $data;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['老 id', '新 id', 'name', 'value', 'status'],
|
||||
array_map(
|
||||
fn ($r) => [$r['id'] - $this->offset, $r['id'], $r['name'], $r['value'], $r['status']],
|
||||
$rows
|
||||
)
|
||||
);
|
||||
if ($skipped > 0) {
|
||||
$this->line('已存在跳过:' . $skipped . ' 个');
|
||||
}
|
||||
|
||||
if (!$this->dryRun && !empty($rows)) {
|
||||
DB::connection(self::NEW)->table('role')->insert($rows);
|
||||
}
|
||||
$this->info('角色新增 ' . count($rows) . ' 个');
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号迁移:新 id = 老 id + offset,密码进 legacy_password 走惰性升级
|
||||
*/
|
||||
private function migrateAdmins(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line('<comment>迁移账号</comment>');
|
||||
|
||||
// 多角色降级:取 role_id 最小者,与体检报告口径一致
|
||||
$userRole = DB::connection(self::OLD)->table('user_role_relation')
|
||||
->groupBy('user_id')
|
||||
->selectRaw('user_id, MIN(role_id) AS role_id')
|
||||
->pluck('role_id', 'user_id');
|
||||
|
||||
$validDept = DB::connection(self::OLD)->table('department')
|
||||
->where('deleted_at', 0)->pluck('id')->all();
|
||||
$validDept = array_flip(array_map('intval', $validDept));
|
||||
|
||||
$legacyExpire = time() + max(1, (int) $this->option('legacy-days')) * 86400;
|
||||
|
||||
$users = DB::connection(self::OLD)->table('user')->where('deleted_at', 0)->orderBy('id')->get();
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
$problems = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$newId = (int) $user->id + $this->offset;
|
||||
if (DB::connection(self::NEW)->table('admin')->where('id', $newId)->exists()) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$phone = trim((string) $user->phone);
|
||||
if ($phone === '' || mb_strlen($phone) !== 11) {
|
||||
$problems[] = [$user->id, $user->account, $phone === '' ? '(空)' : $phone, '手机号非 11 位,已跳过'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$legacy = $this->normalizeLegacyPassword((string) $user->password);
|
||||
if ($legacy === '') {
|
||||
$problems[] = [$user->id, $user->account, $phone, '密码非标准 sha1,迁移后需走重置'];
|
||||
}
|
||||
|
||||
$oldRoleId = (int) ($userRole[$user->id] ?? 0);
|
||||
$newRoleId = $oldRoleId > 0 ? ($this->roleMap[$oldRoleId] ?? 0) : 0;
|
||||
if ($newRoleId === 0) {
|
||||
$problems[] = [$user->id, $user->account, $phone, '无角色,迁移后看不到菜单'];
|
||||
}
|
||||
|
||||
$dept = (int) $user->department;
|
||||
if ($dept > 0 && !isset($validDept[$dept])) {
|
||||
$problems[] = [$user->id, $user->account, $phone, "部门 {$dept} 不存在,已置 0"];
|
||||
$dept = 0;
|
||||
}
|
||||
|
||||
$nickName = trim((string) $user->nick_name);
|
||||
if ($nickName === '') {
|
||||
$nickName = (string) $user->account;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'id' => $newId,
|
||||
// NOT NULL 无默认值,格式对齐安装包里超管那行
|
||||
'open_id' => 'nl_' . bin2hex(random_bytes(15)),
|
||||
'avatar' => (string) $user->avatar,
|
||||
'nick_name' => mb_substr($nickName, 0, 32),
|
||||
// 新库是 bcrypt,老 sha1 没有明文无法转换,先留空由 legacy_password 兜底
|
||||
'password' => '',
|
||||
'legacy_password' => $legacy,
|
||||
'legacy_password_expire_at' => $legacy === '' ? 0 : $legacyExpire,
|
||||
'phone' => $phone,
|
||||
'email' => substr((string) $user->email, 0, 32),
|
||||
'code' => '',
|
||||
'role_id' => $newRoleId,
|
||||
'department_id' => $dept,
|
||||
'province_id' => 0,
|
||||
'city_id' => 0,
|
||||
'reg_ip' => 0,
|
||||
'last_login_time' => $this->parseLastLogin((string) $user->last_login_at),
|
||||
'ip' => (string) ($user->last_login_ip ?: '0'),
|
||||
// NOT NULL 的 json 列,不给值会直接插入失败
|
||||
'ip_table' => '[]',
|
||||
'operation_password' => '0',
|
||||
'desc' => $this->buildAdminDesc($user),
|
||||
// 老库同样是 0正常 1禁用,可以直搬;但新表默认值是 1,必须显式写
|
||||
'status' => (int) $user->status,
|
||||
'created_at' => (int) $user->created_at,
|
||||
'updated_at' => (int) $user->updated_at,
|
||||
'deleted_at' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($problems)) {
|
||||
$this->newLine();
|
||||
$this->warn('需要关注的账号:');
|
||||
$this->table(['老 id', 'account', 'phone', '说明'], $problems);
|
||||
}
|
||||
|
||||
if (!$this->dryRun && !empty($rows)) {
|
||||
foreach (array_chunk($rows, 200) as $chunk) {
|
||||
DB::connection(self::NEW)->table('admin')->insert($chunk);
|
||||
}
|
||||
// 后续新建账号从迁移区间之后继续,避免自增撞上已占用的 id
|
||||
$maxId = (int) DB::connection(self::NEW)->table('admin')->max('id');
|
||||
$physical = DB::connection(self::NEW)->getTablePrefix() . 'admin';
|
||||
DB::connection(self::NEW)->statement("ALTER TABLE `{$physical}` AUTO_INCREMENT = " . ($maxId + 1));
|
||||
}
|
||||
|
||||
if ($skipped > 0) {
|
||||
$this->line('已存在跳过:' . $skipped . ' 个');
|
||||
}
|
||||
$this->info('账号新增 ' . count($rows) . ' 个');
|
||||
}
|
||||
|
||||
/**
|
||||
* 老 sha1 必须是 40 位纯 hex 才能参与惰性升级,其它格式一律留空走重置
|
||||
*/
|
||||
private function normalizeLegacyPassword(string $password): string
|
||||
{
|
||||
$password = strtolower(trim($password));
|
||||
return preg_match('/^[0-9a-f]{40}$/', $password) === 1 ? $password : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 老 last_login_at 是 varchar,可能是时间戳字符串也可能是日期文本
|
||||
*/
|
||||
private function parseLastLogin(string $value): int
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '' || $value === '0') {
|
||||
return 0;
|
||||
}
|
||||
if (ctype_digit($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
$ts = strtotime($value);
|
||||
return $ts === false ? 0 : $ts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备注里留迁移痕迹:account 不参与登录(老 login 查的是 phone),但对不上账时要能追溯
|
||||
*/
|
||||
private function buildAdminDesc(object $user): string
|
||||
{
|
||||
$parts = [];
|
||||
$intro = trim((string) ($user->introduction ?? ''));
|
||||
$address = trim((string) ($user->address ?? ''));
|
||||
if ($intro !== '') {
|
||||
$parts[] = $intro;
|
||||
}
|
||||
if ($address !== '') {
|
||||
$parts[] = '所在地:' . $address;
|
||||
}
|
||||
$desc = implode(';', $parts);
|
||||
return $this->tagged(self::TAG, (int) $user->id, $desc, (string) $user->account);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼迁移留痕,同时作为幂等判断依据。超长时优先保留留痕
|
||||
*/
|
||||
private function tagged(string $tag, int $oldId, string $desc, string $account = ''): string
|
||||
{
|
||||
$mark = $tag . $oldId . ($account !== '' ? ' account=' . $account : '') . ']';
|
||||
$full = $desc === '' ? $mark : $mark . ' ' . $desc;
|
||||
return mb_substr($full, 0, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色菜单关系不能按 menu_id 直搬:业务页面在 vben5 里路径全变了,老 menu_id 指向的菜单不复存在。
|
||||
* 这里只导出「哪个角色能看哪些功能」的语义清单,供重建菜单后按功能重绑。
|
||||
*/
|
||||
private function reportRoleMenu(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line('<comment>角色菜单语义参照(需人工重绑,不自动迁移)</comment>');
|
||||
|
||||
$rows = DB::connection(self::OLD)->table('role_menu_relation as rm')
|
||||
->join('role as r', 'r.id', '=', 'rm.role_id')
|
||||
->join('menu as m', 'm.id', '=', 'rm.menu_id')
|
||||
->where('r.deleted_at', 0)
|
||||
->where('m.deleted_at', 0)
|
||||
->orderBy('r.id')
|
||||
->orderBy('m.order_no')
|
||||
->get(['r.id as role_id', 'r.name as role_name', 'm.title', 'm.router']);
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
$this->line(' 老库无角色菜单关系');
|
||||
return;
|
||||
}
|
||||
|
||||
$grouped = [];
|
||||
foreach ($rows as $row) {
|
||||
$key = ($this->roleMap[(int) $row->role_id] ?? 0) . '|' . $row->role_name;
|
||||
$grouped[$key][] = $row->title;
|
||||
}
|
||||
$table = [];
|
||||
foreach ($grouped as $key => $titles) {
|
||||
[$newRoleId, $roleName] = explode('|', $key, 2);
|
||||
$table[] = [$newRoleId, $roleName, count($titles), implode('、', array_slice($titles, 0, 8)) . (count($titles) > 8 ? ' …' : '')];
|
||||
}
|
||||
$this->table(['新角色 id', '角色名', '菜单数', '可见功能(截断)'], $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色 value 映射:老 cc_role 没有 value 字段,缺省用 role_{老id},
|
||||
* 想要可读的英文标识就传 --value-map
|
||||
*/
|
||||
private function loadValueMap(): array
|
||||
{
|
||||
$path = (string) $this->option('value-map');
|
||||
if ($path === '') {
|
||||
return [];
|
||||
}
|
||||
$full = str_starts_with($path, '/') ? $path : base_path($path);
|
||||
if (!file_exists($full)) {
|
||||
$this->warn('value-map 文件不存在,改用默认 role_{老id}:' . $full);
|
||||
return [];
|
||||
}
|
||||
$map = json_decode((string) file_get_contents($full), true);
|
||||
return is_array($map) ? $map : [];
|
||||
}
|
||||
}
|
||||
128
app/Core/DatabaseEncryptor.php
Normal file
128
app/Core/DatabaseEncryptor.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 库内敏感字段 AES-256-CBC 加解密
|
||||
* 密文格式:nl_ase_256_ + base64(iv + ciphertext)
|
||||
* 密钥派生:hash('sha256', ENCRYPT_KEY, true) → 32 字节
|
||||
* 读取时兼容历史前缀 ***(迁移期)
|
||||
*/
|
||||
class DatabaseEncryptor
|
||||
{
|
||||
/** 新密文前缀(入库统一用这个) */
|
||||
public const PREFIX = 'nl_ase_256_';
|
||||
|
||||
/** 历史密文前缀(仅解密兼容) */
|
||||
public const LEGACY_PREFIX = '***';
|
||||
|
||||
private string $cipherMethod = 'AES-256-CBC';
|
||||
|
||||
private string $secretKey;
|
||||
|
||||
private int $ivLength;
|
||||
|
||||
/**
|
||||
* @param string $key 来自 config('nl.encrypt_key') / env ENCRYPT_KEY
|
||||
*/
|
||||
public function __construct(string $key)
|
||||
{
|
||||
$key = trim($key);
|
||||
if ($key === '') {
|
||||
throw new RuntimeException('AES 加密密钥未配置(请设置 ENCRYPT_KEY)');
|
||||
}
|
||||
$this->secretKey = hash('sha256', $key, true);
|
||||
$ivLength = openssl_cipher_iv_length($this->cipherMethod);
|
||||
if ($ivLength === false) {
|
||||
throw new RuntimeException('不支持的加密算法:' . $this->cipherMethod);
|
||||
}
|
||||
$this->ivLength = $ivLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密后入库;空串不加密,已是密文则原样返回避免二次加密
|
||||
*
|
||||
* @param string|null $data 明文
|
||||
* @throws Exception
|
||||
*/
|
||||
public function encrypt($data): string
|
||||
{
|
||||
if ($data === null || $data === '') {
|
||||
return '';
|
||||
}
|
||||
$data = (string) $data;
|
||||
if (self::isEncrypted($data)) {
|
||||
return $data;
|
||||
}
|
||||
$iv = openssl_random_pseudo_bytes($this->ivLength);
|
||||
if ($iv === false) {
|
||||
throw new RuntimeException('生成 IV 失败');
|
||||
}
|
||||
$encryptedData = openssl_encrypt($data, $this->cipherMethod, $this->secretKey, OPENSSL_RAW_DATA, $iv);
|
||||
if ($encryptedData === false) {
|
||||
throw new RuntimeException('加密失败:' . (openssl_error_string() ?: 'unknown'));
|
||||
}
|
||||
return self::PREFIX . base64_encode($iv . $encryptedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从库内密文解密;无已知前缀视为明文原样返回
|
||||
*
|
||||
* @param string|null $base64EncodedEncryptedData 库内值
|
||||
* @throws Exception
|
||||
*/
|
||||
public function decrypt($base64EncodedEncryptedData): string
|
||||
{
|
||||
if ($base64EncodedEncryptedData === null || $base64EncodedEncryptedData === '') {
|
||||
return '';
|
||||
}
|
||||
$base64EncodedEncryptedData = (string) $base64EncodedEncryptedData;
|
||||
$prefix = self::detectPrefix($base64EncodedEncryptedData);
|
||||
if ($prefix === null) {
|
||||
return $base64EncodedEncryptedData;
|
||||
}
|
||||
$payload = substr($base64EncodedEncryptedData, strlen($prefix));
|
||||
$decodedData = base64_decode($payload, true);
|
||||
if ($decodedData === false) {
|
||||
throw new RuntimeException('Base64 解码失败');
|
||||
}
|
||||
if (strlen($decodedData) <= $this->ivLength) {
|
||||
throw new RuntimeException('密文长度非法');
|
||||
}
|
||||
$iv = substr($decodedData, 0, $this->ivLength);
|
||||
$encryptedData = substr($decodedData, $this->ivLength);
|
||||
$decryptedData = openssl_decrypt($encryptedData, $this->cipherMethod, $this->secretKey, OPENSSL_RAW_DATA, $iv);
|
||||
if ($decryptedData === false) {
|
||||
throw new RuntimeException('解密失败:' . (openssl_error_string() ?: 'unknown'));
|
||||
}
|
||||
return $decryptedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已是库内密文(含历史前缀)
|
||||
*/
|
||||
public static function isEncrypted(?string $value): bool
|
||||
{
|
||||
return self::detectPrefix($value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别密文前缀;新前缀优先
|
||||
*/
|
||||
public static function detectPrefix(?string $value): ?string
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (str_starts_with($value, self::PREFIX)) {
|
||||
return self::PREFIX;
|
||||
}
|
||||
if (str_starts_with($value, self::LEGACY_PREFIX)) {
|
||||
return self::LEGACY_PREFIX;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
22
app/Enum/AiGenerationSceneEnum.php
Normal file
22
app/Enum/AiGenerationSceneEnum.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* AI 生成场景(本项目可扩展,勿塞入其他业务域专有场景)
|
||||
*/
|
||||
enum AiGenerationSceneEnum: string
|
||||
{
|
||||
/** 根据描述直接生成代码结构 JSON */
|
||||
case CODE_GENERATION = 'code_generation';
|
||||
/** 根据模块名生成/优化需求概览与代码生成提示词 */
|
||||
case REQUIREMENT_PROMPT = 'requirement_prompt';
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::CODE_GENERATION => '代码生成',
|
||||
self::REQUIREMENT_PROMPT => '需求概览/提示词',
|
||||
};
|
||||
}
|
||||
}
|
||||
22
app/Enum/AiGenerationStatusEnum.php
Normal file
22
app/Enum/AiGenerationStatusEnum.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* AI 生成记录状态
|
||||
*/
|
||||
enum AiGenerationStatusEnum: int
|
||||
{
|
||||
case RUNNING = 0;
|
||||
case SUCCESS = 1;
|
||||
case FAIL = 2;
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::RUNNING => '进行中',
|
||||
self::SUCCESS => '成功',
|
||||
self::FAIL => '失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
112
app/Enum/OssDriverEnum.php
Normal file
112
app/Enum/OssDriverEnum.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* OSS 存储驱动枚举:管理端卡片展示与上传分发共用
|
||||
*/
|
||||
enum OssDriverEnum: string
|
||||
{
|
||||
case Local = 'local';
|
||||
case Aliyun = 'aliyun';
|
||||
case Qcloud = 'qcloud';
|
||||
case Qiniu = 'qiniu';
|
||||
case Huawei = 'huawei';
|
||||
case Aws = 'aws';
|
||||
case Minio = 'minio';
|
||||
case Baidu = 'baidu';
|
||||
|
||||
/**
|
||||
* 驱动编码(与库表 driver 一致)
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文展示名
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => '本地存储',
|
||||
self::Aliyun => '阿里云 OSS',
|
||||
self::Qcloud => '腾讯云 COS',
|
||||
self::Qiniu => '七牛云',
|
||||
self::Huawei => '华为云 OBS',
|
||||
self::Aws => 'AWS S3',
|
||||
self::Minio => 'MinIO',
|
||||
self::Baidu => '百度云 BOS',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片说明文案
|
||||
*/
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => '文件保存到应用服务器本地磁盘',
|
||||
self::Aliyun => '阿里云对象存储 OSS',
|
||||
self::Qcloud => '腾讯云对象存储 COS',
|
||||
self::Qiniu => '七牛云对象存储 Kodo',
|
||||
self::Huawei => '华为云对象存储 OBS',
|
||||
self::Aws => '亚马逊 S3 对象存储',
|
||||
self::Minio => 'S3 兼容的私有化对象存储',
|
||||
self::Baidu => '百度智能云对象存储 BOS',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端卡片图标(iconify)
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => 'lucide:hard-drive',
|
||||
self::Aliyun => 'simple-icons:alibabadotcom',
|
||||
self::Qcloud => 'simple-icons:tencentqq',
|
||||
self::Qiniu => 'lucide:cloud',
|
||||
self::Huawei => 'simple-icons:huawei',
|
||||
self::Aws => 'simple-icons:amazonaws',
|
||||
self::Minio => 'simple-icons:minio',
|
||||
self::Baidu => 'simple-icons:baidu',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要 AccessKey / SecretKey
|
||||
*/
|
||||
public function needsSecret(): bool
|
||||
{
|
||||
return $this !== self::Local;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端驱动选项列表(含图标,供卡片展示)
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
$list = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$list[] = [
|
||||
'code' => $case->code(),
|
||||
'name' => $case->label(),
|
||||
'label' => $case->label(),
|
||||
'description' => $case->description(),
|
||||
'icon' => $case->icon(),
|
||||
'needs_secret' => $case->needsSecret() ? 1 : 0,
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验驱动编码是否合法
|
||||
*/
|
||||
public static function isValid(string $code): bool
|
||||
{
|
||||
return self::tryFrom($code) !== null;
|
||||
}
|
||||
}
|
||||
29
app/Enum/WechatArticleStatusEnum.php
Normal file
29
app/Enum/WechatArticleStatusEnum.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* 公众号图文文章状态枚举
|
||||
* 发布走微信 freepublish 异步审核:提交后先置「发布中」,由 publish-status 轮询回填终态
|
||||
*/
|
||||
enum WechatArticleStatusEnum: int
|
||||
{
|
||||
// 0草稿 1发布中 2已发布 3发布失败
|
||||
case DRAFT = 0;
|
||||
case PUBLISHING = 1;
|
||||
case PUBLISHED = 2;
|
||||
case FAILED = 3;
|
||||
|
||||
/**
|
||||
* 中文展示名
|
||||
*/
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => '草稿',
|
||||
self::PUBLISHING => '发布中',
|
||||
self::PUBLISHED => '已发布',
|
||||
self::FAILED => '发布失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
31
app/Enum/WechatVersionActionEnum.php
Normal file
31
app/Enum/WechatVersionActionEnum.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* 公众号图文版本流水的操作类型枚举
|
||||
* 每次创建/修改/删除/回退/发布都会写一条版本快照,action 标记这次快照的来源
|
||||
*/
|
||||
enum WechatVersionActionEnum: int
|
||||
{
|
||||
// 1创建 2修改 3删除 4回退 5发布
|
||||
case CREATE = 1;
|
||||
case UPDATE = 2;
|
||||
case DELETE = 3;
|
||||
case ROLLBACK = 4;
|
||||
case PUBLISH = 5;
|
||||
|
||||
/**
|
||||
* 中文展示名(版本列表直接用)
|
||||
*/
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::CREATE => '创建',
|
||||
self::UPDATE => '修改',
|
||||
self::DELETE => '删除',
|
||||
self::ROLLBACK => '回退',
|
||||
self::PUBLISH => '发布',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class AdminController extends BaseController
|
||||
$this->service = AdminService::getInstance();
|
||||
$this->insertField = [ 'phone', 'nick_name', 'password', 'role_id' ];
|
||||
$this->updateField = [ 'id', 'phone', 'nick_name', 'role_id' ];
|
||||
$this->notRequest = ['qr_code', 'avatar', 'email', 'status'];
|
||||
$this->notRequest = ['qr_code', 'avatar', 'email', 'status', 'department_id', 'desc'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +73,33 @@ class AdminController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心更新资料
|
||||
* @Method POST
|
||||
*/
|
||||
public function updateProfile(): JsonResponse
|
||||
{
|
||||
return jok($this->service->updateProfile(request()->post()), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心:登录日志
|
||||
* @Method GET
|
||||
*/
|
||||
public function loginLog(): JsonResponse
|
||||
{
|
||||
return jok($this->service->loginLogList(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心:操作日志
|
||||
* @Method GET
|
||||
*/
|
||||
public function opLog(): JsonResponse
|
||||
{
|
||||
return jok($this->service->opLogList(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* @Method GET
|
||||
@@ -88,7 +115,9 @@ class AdminController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* 获取当前账号的权限码
|
||||
*
|
||||
* 码由接口路径推导(admin/list → admin:list),前端 v-access 与 TableAction 的 auth 用的就是它。
|
||||
* @Method GET
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
@@ -96,7 +125,7 @@ class AdminController extends BaseController
|
||||
public function codes(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
[],
|
||||
$this->service->codes(),
|
||||
'获取成功'
|
||||
);
|
||||
}
|
||||
|
||||
38
app/Http/Controllers/Api/AfterSaleController.php
Normal file
38
app/Http/Controllers/Api/AfterSaleController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\AfterSaleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台售后
|
||||
*/
|
||||
class AfterSaleController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = AfterSaleService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核售后
|
||||
* @Method POST
|
||||
*/
|
||||
public function audit(): JsonResponse
|
||||
{
|
||||
$pass = (int) request()->post('status', 1) === 1;
|
||||
return jok(
|
||||
$this->service->audit(
|
||||
(int) request()->post('id', 0),
|
||||
$pass,
|
||||
(string) request()->post('admin_remark', '')
|
||||
),
|
||||
$pass ? '已通过' : '已拒绝'
|
||||
);
|
||||
}
|
||||
}
|
||||
168
app/Http/Controllers/Api/AiConfigController.php
Normal file
168
app/Http/Controllers/Api/AiConfigController.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\AiConfigService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* AI 配置控制器:运行时选用 + 密钥管理
|
||||
*/
|
||||
class AiConfigController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = AiConfigService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用基类通用列表(本模块用 keyList)
|
||||
* @Method NO
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 管理页选项(平台/模型/密钥卡片数据 + 当前激活项)
|
||||
* @Method GET
|
||||
*/
|
||||
public function runtimeOptions(): JsonResponse
|
||||
{
|
||||
return jok($this->service->getRuntimeOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存默认 AI 平台/模型/密钥组合
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveRuntime(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
return jok($this->service->saveRuntime($params), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 密钥列表(卡片展示)
|
||||
* @Method GET
|
||||
*/
|
||||
public function keyList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->listKeys(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台下拉
|
||||
* @Method GET
|
||||
*/
|
||||
public function platformOption(): JsonResponse
|
||||
{
|
||||
return jok($this->service->platformOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增密钥
|
||||
* @Method POST
|
||||
*/
|
||||
public function createKey(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
return jok($this->service->createKey($params), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新密钥
|
||||
* @Method POST
|
||||
*/
|
||||
public function updateKey(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->updateKey($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除密钥
|
||||
* @Method POST
|
||||
*/
|
||||
public function deleteKey(): JsonResponse
|
||||
{
|
||||
$ids = request()->post('ids');
|
||||
if (empty($ids)) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
if (!is_array($ids)) {
|
||||
$ids = [$ids];
|
||||
}
|
||||
return jok($this->service->deleteKeys($ids), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 生成历史列表
|
||||
* @Method GET
|
||||
*/
|
||||
public function generationList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->generationList(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 生成历史详情
|
||||
* @Method GET
|
||||
*/
|
||||
public function generationDetail(): JsonResponse
|
||||
{
|
||||
$id = (int) request()->get('id', 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
return jok($this->service->generationDetail($id), '获取成功');
|
||||
}
|
||||
}
|
||||
75
app/Http/Controllers/Api/ApiEndpointController.php
Normal file
75
app/Http/Controllers/Api/ApiEndpointController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\ApiEndpointService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 接口注册表控制器:仅开放 list / update
|
||||
*/
|
||||
class ApiEndpointController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = ApiEndpointService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->list(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新接口说明 / 是否记日志 / 状态
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->update($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/CardClassController.php
Normal file
35
app/Http/Controllers/Api/CardClassController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\CardClassService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 色卡分类
|
||||
*/
|
||||
class CardClassController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = CardClassService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/CarouselController.php
Normal file
35
app/Http/Controllers/Api/CarouselController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\CarouselService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序轮播图
|
||||
*/
|
||||
class CarouselController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = CarouselService::getInstance();
|
||||
$this->insertField = ['url'];
|
||||
$this->updateField = ['id', 'url'];
|
||||
$this->notRequest = ['to_path', 'sort', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/Api/CatalogueController.php
Normal file
58
app/Http/Controllers/Api/CatalogueController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\CatalogueService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 商品图册
|
||||
*/
|
||||
class CatalogueController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = CatalogueService::getInstance();
|
||||
$this->insertField = ['title', 'category_id', 'cover', 'identifier'];
|
||||
$this->updateField = ['id', 'title', 'category_id', 'cover', 'identifier'];
|
||||
$this->notRequest = ['alias', 'pdf', 'video', 'price', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出商品表(结构化数据,前端 ExcelJS 画 xlsx)
|
||||
* @Method GET
|
||||
*/
|
||||
public function export(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportExcel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入商品表:前端已把 xlsx 拆成 rows
|
||||
* @Method POST
|
||||
*/
|
||||
public function import(): JsonResponse
|
||||
{
|
||||
$rows = request()->post('rows', []);
|
||||
$priceSheets = request()->post('price_sheets', []);
|
||||
return jok($this->service->importRows(
|
||||
is_array($rows) ? $rows : [],
|
||||
is_array($priceSheets) ? $priceSheets : [],
|
||||
), '导入完成');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/CategoryController.php
Normal file
35
app/Http/Controllers/Api/CategoryController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\CategoryService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 商品分类
|
||||
*/
|
||||
class CategoryController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = CategoryService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['url', 'pid', 'status', 'sort'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/ColorcardController.php
Normal file
35
app/Http/Controllers/Api/ColorcardController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\ColorcardService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 色卡
|
||||
*/
|
||||
class ColorcardController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = ColorcardService::getInstance();
|
||||
$this->insertField = ['card_class', 'company', 'cover'];
|
||||
$this->updateField = ['id', 'card_class', 'company'];
|
||||
$this->notRequest = ['price', 'description', 'cover', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/CompanyController.php
Normal file
35
app/Http/Controllers/Api/CompanyController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\CompanyService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 色卡所属公司
|
||||
*/
|
||||
class CompanyController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = CompanyService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['phone', 'address', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Api/DepartmentController.php
Normal file
47
app/Http/Controllers/Api/DepartmentController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\DepartmentService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DepartmentController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = DepartmentService::getInstance();
|
||||
// insertField / updateField 既是必填校验清单,也是入库字段白名单,漏写的字段不会被收进 params
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
// 这几个允许缺省:pid 缺省为顶级,status 缺省为正常
|
||||
$this->notRequest = ['pid', 'desc', 'status', 'color'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树下拉(表单选上级部门用)
|
||||
* @Method GET
|
||||
*/
|
||||
public function treeOption(): JsonResponse
|
||||
{
|
||||
$isSelect = filter_var(request()->get('is_select', false), FILTER_VALIDATE_BOOLEAN);
|
||||
return jok($this->service->getTreeOption($isSelect), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用 / 启用部门
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$id = request()->post('id');
|
||||
$status = request()->post('status');
|
||||
if (empty($id) || !in_array((string) $status, ['0', '1'], true)) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
return jok($this->service->status($id, $status), '操作成功');
|
||||
}
|
||||
}
|
||||
38
app/Http/Controllers/Api/EnterpriseController.php
Normal file
38
app/Http/Controllers/Api/EnterpriseController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\EnterpriseService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 企业管理
|
||||
*/
|
||||
class EnterpriseController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = EnterpriseService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = [
|
||||
'logo', 'contact_name', 'phone', 'address',
|
||||
'tax_no', 'settle_type', 'price_number', 'status', 'remark',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
35
app/Http/Controllers/Api/FactoryClassificationController.php
Normal file
35
app/Http/Controllers/Api/FactoryClassificationController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\FactoryClassificationService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 工厂分类
|
||||
*/
|
||||
class FactoryClassificationController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FactoryClassificationService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Api/FactoryImageController.php
Normal file
44
app/Http/Controllers/Api/FactoryImageController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\FactoryImageService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 工厂产品图
|
||||
*/
|
||||
class FactoryImageController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FactoryImageService::getInstance();
|
||||
$this->insertField = ['factory', 'url'];
|
||||
$this->updateField = ['id', 'url'];
|
||||
$this->notRequest = ['factory', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 某工厂的全部产品图(入参 factory)
|
||||
* @Method GET
|
||||
*/
|
||||
public function imageList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->imageList((int) request()->get('factory', 0)), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
54
app/Http/Controllers/Api/FactoryInfoController.php
Normal file
54
app/Http/Controllers/Api/FactoryInfoController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\FactoryInfoService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 工厂管理
|
||||
*/
|
||||
class FactoryInfoController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FactoryInfoService::getInstance();
|
||||
$this->insertField = ['name', 'classification'];
|
||||
$this->updateField = ['id', 'name', 'classification'];
|
||||
$this->notRequest = ['phone', 'cover', 'address', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出工厂表(结构化数据,前端 ExcelJS 画 xlsx)
|
||||
* @Method GET
|
||||
*/
|
||||
public function export(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportExcel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入工厂表:前端已把 xlsx 拆成 rows
|
||||
* @Method POST
|
||||
*/
|
||||
public function import(): JsonResponse
|
||||
{
|
||||
$rows = request()->post('rows', []);
|
||||
return jok($this->service->importRows(is_array($rows) ? $rows : []), '导入完成');
|
||||
}
|
||||
}
|
||||
34
app/Http/Controllers/Api/FeedbackController.php
Normal file
34
app/Http/Controllers/Api/FeedbackController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\FeedbackService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台用户反馈
|
||||
*/
|
||||
class FeedbackController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FeedbackService::getInstance();
|
||||
$this->updateField = ['id', 'reply'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复
|
||||
* @Method POST
|
||||
*/
|
||||
public function reply(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->reply((int) request()->post('id', 0), (string) request()->post('reply', '')),
|
||||
'已回复'
|
||||
);
|
||||
}
|
||||
}
|
||||
22
app/Http/Controllers/Api/FileFolderController.php
Normal file
22
app/Http/Controllers/Api/FileFolderController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\FileFolderService;
|
||||
|
||||
/**
|
||||
* 素材文件夹(标准 CRUD + 目录树)
|
||||
*/
|
||||
class FileFolderController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FileFolderService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
// pid = 0 就是根目录,sort / status 也允许留空走默认值
|
||||
$this->notRequest = ['pid', 'sort', 'status'];
|
||||
}
|
||||
}
|
||||
53
app/Http/Controllers/Api/ImageController.php
Normal file
53
app/Http/Controllers/Api/ImageController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\ImageService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 商品相册
|
||||
*/
|
||||
class ImageController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = ImageService::getInstance();
|
||||
$this->insertField = ['catalogue_id', 'url', 'type'];
|
||||
$this->updateField = ['id', 'url'];
|
||||
$this->notRequest = ['catalogue_id', 'type', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染图列表(入参 catalogue_id)
|
||||
* @Method GET
|
||||
*/
|
||||
public function renderGraph(): JsonResponse
|
||||
{
|
||||
return jok($this->service->renderGraph((int) request()->get('catalogue_id', 0)), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实物图列表(入参 catalogue_id)
|
||||
* @Method GET
|
||||
*/
|
||||
public function physicalDrawing(): JsonResponse
|
||||
{
|
||||
return jok($this->service->physicalDrawing((int) request()->get('catalogue_id', 0)), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
70
app/Http/Controllers/Api/ListController.php
Normal file
70
app/Http/Controllers/Api/ListController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\ListService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 清单管理
|
||||
*/
|
||||
class ListController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = ListService::getInstance();
|
||||
$this->insertField = ['name', 'user_id'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['remark', 'enterprise_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 某用户的清单
|
||||
* @Method GET
|
||||
*/
|
||||
public function byUser(): JsonResponse
|
||||
{
|
||||
return jok($this->service->byUser((int) request()->get('user_id', 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清单生成订单
|
||||
* @Method POST
|
||||
*/
|
||||
public function toOrder(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->toOrder((int) request()->post('id', 0), request()->post()),
|
||||
'订单已生成'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存清单明细
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveItem(): JsonResponse
|
||||
{
|
||||
return jok($this->service->saveItem(request()->post()), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除清单明细
|
||||
* @Method POST
|
||||
*/
|
||||
public function deleteItem(): JsonResponse
|
||||
{
|
||||
return jok($this->service->deleteItem(request()->post('ids', [])), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出清单报价(结构化数据 + 分享文本)
|
||||
* @Method GET
|
||||
*/
|
||||
public function exportQuote(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportQuote((int) request()->get('id', 0)));
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@ use Illuminate\Http\JsonResponse;
|
||||
|
||||
class LoginController extends BaseController
|
||||
{
|
||||
//
|
||||
/**
|
||||
* 登录控制器注册在免登录组里,继承来的 CRUD 会变成 /api/list、/api/create 这种
|
||||
* 无需鉴权、指向 LoginService(根本没有这些方法)的路由,必须排除
|
||||
*/
|
||||
protected array $exceptRoute = ['list', 'option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -54,20 +58,32 @@ class LoginController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
public function logout()
|
||||
/**
|
||||
* 退出登录
|
||||
* @Method POST
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function logout(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
[],
|
||||
$this->service->logout(),
|
||||
'退出成功'
|
||||
);
|
||||
}
|
||||
|
||||
public function codes()
|
||||
/**
|
||||
* 续签 token
|
||||
*
|
||||
* 前端拦截器在 401 时调用,此时 token 已过期,所以只能放在免登录组里,
|
||||
* 由 LoginService::refresh 自己校验签名与 Redis 会话。
|
||||
* @Method POST
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function refresh(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
[],
|
||||
'获取成功'
|
||||
$this->service->refresh(),
|
||||
'续签成功'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
113
app/Http/Controllers/Api/MaterialController.php
Normal file
113
app/Http/Controllers/Api/MaterialController.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\common\UtilsService;
|
||||
use App\Service\MaterialService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 素材库
|
||||
*
|
||||
* 素材记录只有两个来源:上传接口写入、从 OSS 反向同步补录,
|
||||
* 所以没有「手工新增」,也不需要下拉选项 —— 把继承来的 create / option 排掉。
|
||||
*/
|
||||
class MaterialController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['create', 'option'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = MaterialService::getInstance();
|
||||
// 只放行素材名与文件夹:对象键、体积、哈希都是同步结果,不接受手工改
|
||||
$this->updateField = ['id', 'name', 'folder_id'];
|
||||
$this->notRequest = ['name', 'folder_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 素材概览统计
|
||||
*
|
||||
* @Method GET
|
||||
* @throws Exception
|
||||
*/
|
||||
public function stat(): JsonResponse
|
||||
{
|
||||
return jok($this->service->stat(), '统计获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 OSS 增量同步对象
|
||||
*
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function syncFromOss(): JsonResponse
|
||||
{
|
||||
// 七牛要先查区域再列举,再加一批 insert,默认 30s 很容易被网关/PHP 掐死
|
||||
cc_set_time_limit();
|
||||
$this->insertField = ['oss_config_id'];
|
||||
$this->notRequest = ['prefix', 'marker', 'limit'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->syncFromOss($params), '同步完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 全库引用扫描
|
||||
*
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function scanReferences(): JsonResponse
|
||||
{
|
||||
// 表多、正文长时会跑几十秒,先把执行时限和内存放开
|
||||
cc_set_time_limit();
|
||||
return jok($this->service->scanReferences(request()->post()), '扫描完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 无引用素材清单
|
||||
*
|
||||
* @Method GET
|
||||
* @throws Exception
|
||||
*/
|
||||
public function unusedList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->unusedList(request()->query()), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收:删远端对象并软删记录
|
||||
*
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reclaim(): JsonResponse
|
||||
{
|
||||
$ids = request()->post('ids');
|
||||
if (empty($ids) || !is_array($ids)) {
|
||||
UtilsService::getInstance()->notFound('参数错误');
|
||||
}
|
||||
return jok($this->service->reclaim($ids), '回收完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移动到文件夹
|
||||
*
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function moveToFolder(): JsonResponse
|
||||
{
|
||||
$ids = request()->post('ids');
|
||||
if (empty($ids) || !is_array($ids)) {
|
||||
UtilsService::getInstance()->notFound('参数错误');
|
||||
}
|
||||
return jok(
|
||||
$this->service->moveToFolder($ids, (int) request()->post('folder_id', 0)),
|
||||
'移动成功'
|
||||
);
|
||||
}
|
||||
}
|
||||
79
app/Http/Controllers/Api/OrderController.php
Normal file
79
app/Http/Controllers/Api/OrderController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\OrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 订单管理
|
||||
*/
|
||||
class OrderController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = OrderService::getInstance();
|
||||
$this->insertField = ['list_id'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = [
|
||||
'receiver_name', 'receiver_phone', 'receiver_address',
|
||||
'remark', 'delivery_type',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 概览统计
|
||||
* @Method GET
|
||||
*/
|
||||
public function stat(): JsonResponse
|
||||
{
|
||||
return jok($this->service->stat());
|
||||
}
|
||||
|
||||
/**
|
||||
* 某用户的订单
|
||||
* @Method GET
|
||||
*/
|
||||
public function byUser(): JsonResponse
|
||||
{
|
||||
return jok($this->service->byUser((int) request()->get('user_id', 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核转账凭证
|
||||
* @Method POST
|
||||
*/
|
||||
public function auditPayment(): JsonResponse
|
||||
{
|
||||
return jok($this->service->auditPayment(request()->post()), '处理成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货
|
||||
* @Method POST
|
||||
*/
|
||||
public function ship(): JsonResponse
|
||||
{
|
||||
return jok($this->service->ship(request()->post()), '已发货');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @Method POST
|
||||
*/
|
||||
public function cancel(): JsonResponse
|
||||
{
|
||||
return jok($this->service->cancel((int) request()->post('id', 0)), '已取消');
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单
|
||||
* @Method POST
|
||||
*/
|
||||
public function complete(): JsonResponse
|
||||
{
|
||||
return jok($this->service->complete((int) request()->post('id', 0)), '已完成');
|
||||
}
|
||||
}
|
||||
111
app/Http/Controllers/Api/OssConfigController.php
Normal file
111
app/Http/Controllers/Api/OssConfigController.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\OssConfigService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* OSS 存储配置控制器:驱动选项 / 启用切换 / 配置 CRUD
|
||||
*/
|
||||
class OssConfigController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = OssConfigService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置列表(卡片,无密钥明文)
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->listConfigs(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增配置(覆盖基类,走加密逻辑)
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->createConfig(request()->post()), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->updateConfig($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
$ids = request()->post('ids');
|
||||
if (empty($ids)) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
if (!is_array($ids)) {
|
||||
$ids = [$ids];
|
||||
}
|
||||
return jok($this->service->deleteConfigs($ids), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱动枚举选项
|
||||
* @Method GET
|
||||
*/
|
||||
public function driverOptions(): JsonResponse
|
||||
{
|
||||
return jok($this->service->driverOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时选项(卡片 + 当前启用)
|
||||
* @Method GET
|
||||
*/
|
||||
public function runtimeOptions(): JsonResponse
|
||||
{
|
||||
return jok($this->service->getRuntimeOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用指定配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveRuntime(): JsonResponse
|
||||
{
|
||||
return jok($this->service->saveRuntime(request()->post()), '保存成功');
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Api/PackageController.php
Normal file
52
app/Http/Controllers/Api/PackageController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\PackageService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台套餐搭配
|
||||
*/
|
||||
class PackageController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = PackageService::getInstance();
|
||||
$this->insertField = ['name'];
|
||||
$this->updateField = ['id', 'name'];
|
||||
$this->notRequest = ['cover', 'subtitle', 'remark', 'is_hot', 'sort', 'status', 'package_amount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上架 / 下架
|
||||
* @Method POST
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->status((int) request()->post('id'), request()->post('status')),
|
||||
'操作成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存搭配明细并重算原价 / 补差价
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveItems(): JsonResponse
|
||||
{
|
||||
return jok($this->service->saveItems(request()->post()), '搭配已保存');
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭配搜索:带回封面和规格,无有效报价的规格 selectable=false
|
||||
* @Method GET
|
||||
*/
|
||||
public function searchCatalog(): JsonResponse
|
||||
{
|
||||
return jok($this->service->searchCatalog());
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/Api/PriceSheetController.php
Normal file
63
app/Http/Controllers/Api/PriceSheetController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\PriceSheetService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 报价单(商品规格)
|
||||
*/
|
||||
class PriceSheetController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = PriceSheetService::getInstance();
|
||||
$this->insertField = ['catalogue_id'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = ['specification', 'dimension', 'routine', 'stock', 'rows', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 某商品的全部规格行(报价单抽屉回填用)
|
||||
* @Method GET
|
||||
*/
|
||||
public function rows(): JsonResponse
|
||||
{
|
||||
return jok($this->service->rowsOf((int) request()->get('catalogue_id', 0)), '列表获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖式保存某商品的全部规格行
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveRows(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['catalogue_id'];
|
||||
$this->notRequest = ['rows'];
|
||||
$this->checkRequiredFields(request()->post());
|
||||
return jok(
|
||||
$this->service->saveRows(
|
||||
(int) request()->post('catalogue_id'),
|
||||
(array) request()->post('rows', [])
|
||||
),
|
||||
'保存成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 禁用
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class RoleController extends BaseController
|
||||
parent::__construct();
|
||||
$this->insertField = ['name', 'value', 'desc'];
|
||||
$this->updateField = ['id', 'name', 'value', 'desc'];
|
||||
$this->notRequest = ['status', 'color', 'pid'];
|
||||
$this->service = RoleService::getInstance();
|
||||
}
|
||||
|
||||
@@ -51,4 +52,52 @@ class RoleController extends BaseController
|
||||
$this->service->saveRoleMenu($roleId, $menuIds)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口授权树:按控制器分组的全部可授权接口
|
||||
* @Method GET
|
||||
*/
|
||||
public function endpointTree(): JsonResponse
|
||||
{
|
||||
return jok($this->service->endpointTree(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色已授权的接口 id
|
||||
* @Method GET
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getEndpointIdsByRoleIds(): JsonResponse
|
||||
{
|
||||
return jok($this->service->getEndpointIdsByRoleId(request()->get('role_id')));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存角色接口授权
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveRoleEndpoint(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['role_id'];
|
||||
$this->checkRequiredFields(request()->post());
|
||||
return jok(
|
||||
$this->service->saveRoleEndpoint(
|
||||
(int) request()->post('role_id'),
|
||||
(array) request()->post('endpoint_id', [])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 / 停用角色
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'status'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,19 @@ class UploadController extends BaseController
|
||||
'上传成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档上传(PDF 等)
|
||||
* @Method POST
|
||||
* @return JsonResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function file()
|
||||
{
|
||||
$file = request()->file('file');
|
||||
return jok(
|
||||
$this->service->uploadDocument($file),
|
||||
'上传成功'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
app/Http/Controllers/Api/WechatAccountController.php
Normal file
62
app/Http/Controllers/Api/WechatAccountController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\WechatAccountService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 公众号账号配置控制器:CRUD(Secret 加密入库)/ 默认账号切换 / 连通性测试
|
||||
* list/option/detail 走基类(Service 内已做脱敏),create/update 覆盖走加密逻辑
|
||||
*/
|
||||
class WechatAccountController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WechatAccountService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增账号(覆盖基类:Secret 加密、AppID 查重在 Service 内处理)
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新账号(app_secret 传空 = 不修改密钥)
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->update($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认发布账号
|
||||
* @Method POST
|
||||
*/
|
||||
public function setDefault(): JsonResponse
|
||||
{
|
||||
return jok($this->service->setDefault(request()->post()), '设置成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试连通性(appid+secret 直连微信换 token 验证)
|
||||
* @Method POST
|
||||
*/
|
||||
public function testConnection(): JsonResponse
|
||||
{
|
||||
return jok($this->service->testConnection(request()->post()), '连接成功,凭据有效');
|
||||
}
|
||||
}
|
||||
97
app/Http/Controllers/Api/WechatArticleController.php
Normal file
97
app/Http/Controllers/Api/WechatArticleController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\WechatArticleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 公众号图文控制器:CRUD / 版本流水 / 版本回退 / 一键发布 / 发布状态
|
||||
* 图文正文(content_md)体量大且含任意字符,create/update 覆盖基类直接透传给 Service 白名单处理
|
||||
*/
|
||||
class WechatArticleController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WechatArticleService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增图文(标题必填在 Service 校验;创建即落 v1 版本流水)
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图文(版本号 +1 并落「修改」流水)
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->update($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本流水分页(?article_id=)
|
||||
* @Method GET
|
||||
*/
|
||||
public function versionList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->versionList((int) request()->get('article_id', 0)), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 单版本详情(含正文快照,回退前预览)
|
||||
* @Method GET
|
||||
*/
|
||||
public function versionDetail(): JsonResponse
|
||||
{
|
||||
return jok($this->service->versionDetail((int) request()->get('id', 0)), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本回退 {id, version_id}
|
||||
* @Method POST
|
||||
*/
|
||||
public function rollback(): JsonResponse
|
||||
{
|
||||
return jok($this->service->rollback(request()->post()), '回退成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键发布 {id, content_html, account_id?}(content_html 为前端渲染好的内联样式 HTML)
|
||||
* @Method POST
|
||||
*/
|
||||
public function publish(): JsonResponse
|
||||
{
|
||||
return jok($this->service->publish(request()->post()), '已提交发布,正在审核');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询发布状态并回填(?id=)
|
||||
* @Method GET
|
||||
*/
|
||||
public function publishStatus(): JsonResponse
|
||||
{
|
||||
return jok($this->service->publishStatus((int) request()->get('id', 0)), '获取成功');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Api/WechatThemeController.php
Normal file
44
app/Http/Controllers/Api/WechatThemeController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\WechatThemeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 自定义排版主题控制器:图文编辑器「我的模板」导入/列表/更新/删除
|
||||
* list/delete 走基类;create/update 覆盖直传 Service(清洗校验都在 Service 内)
|
||||
*/
|
||||
class WechatThemeController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WechatThemeService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入模板(JSON 清洗入库)
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '导入成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板(名称/色板/样式整体覆盖)
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->update($id, $params), '更新成功');
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Api/WxAppController.php
Normal file
47
app/Http/Controllers/Api/WxAppController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\WxAppConfigService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序应用配置
|
||||
*/
|
||||
class WxAppController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAppConfigService::getInstance();
|
||||
$this->insertField = ['code', 'name', 'app_id'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = [
|
||||
'app_secret', 'mch_id', 'mch_key', 'mch_serial_no', 'mch_private_key',
|
||||
'platform_public_key', 'notify_url', 'template_code', 'package_enabled',
|
||||
'subscribe_pay_tpl', 'subscribe_ship_tpl', 'remark', 'name', 'app_id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/停用
|
||||
* @Method POST
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->status(request()->post('id'), request()->post('status')),
|
||||
'操作成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 .env 引导创建/更新一条应用壳子(不含 AppSecret)
|
||||
* @Method POST
|
||||
*/
|
||||
public function initFromEnv(): JsonResponse
|
||||
{
|
||||
return jok($this->service->initFromEnv(), '初始化成功');
|
||||
}
|
||||
}
|
||||
61
app/Http/Controllers/Api/WxCardSchemeController.php
Normal file
61
app/Http/Controllers/Api/WxCardSchemeController.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\WxCardSchemeService;
|
||||
use App\Service\business\WxTemplateSchemaService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序卡片画布方案
|
||||
*/
|
||||
class WxCardSchemeController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxCardSchemeService::getInstance();
|
||||
$this->insertField = ['name', 'code'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = [
|
||||
'preview', 'layers', 'is_preset', 'app_code', 'sort', 'name', 'code', 'status',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 另存为自定义方案
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveAs(): JsonResponse
|
||||
{
|
||||
return jok($this->service->saveAs(request()->post()), '已另存为方案');
|
||||
}
|
||||
|
||||
/**
|
||||
* 灌入 15 套内置预设
|
||||
* @Method POST
|
||||
*/
|
||||
public function initPresets(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->initPresets((bool) request()->post('overwrite', false)),
|
||||
'卡片预设初始化完成'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层白名单,画布编辑器渲染控件用
|
||||
* @Method GET
|
||||
*/
|
||||
public function schema(): JsonResponse
|
||||
{
|
||||
return jok([
|
||||
'types' => WxTemplateSchemaService::LAYER_TYPES,
|
||||
'binds' => WxTemplateSchemaService::LAYER_BINDS,
|
||||
'fonts' => WxTemplateSchemaService::LAYER_FONTS,
|
||||
'faces' => WxTemplateSchemaService::LAYER_FACES,
|
||||
'assets' => WxTemplateSchemaService::DECO_ASSETS,
|
||||
]);
|
||||
}
|
||||
}
|
||||
88
app/Http/Controllers/Api/WxTemplateController.php
Normal file
88
app/Http/Controllers/Api/WxTemplateController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\WxTemplateSchemaService;
|
||||
use App\Service\business\WxTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序装修模板
|
||||
*/
|
||||
class WxTemplateController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxTemplateService::getInstance();
|
||||
$this->insertField = ['name', 'code'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = [
|
||||
'preview', 'style_tag', 'tokens', 'layout', 'app_code', 'sort', 'name', 'code',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/停用
|
||||
* @Method POST
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->status(request()->post('id'), request()->post('status')),
|
||||
'操作成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认
|
||||
* @Method POST
|
||||
*/
|
||||
public function setDefault(): JsonResponse
|
||||
{
|
||||
return jok($this->service->setDefault((int) request()->post('id', 0)), '已设为默认');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模板 JSON
|
||||
* @Method POST
|
||||
*/
|
||||
public function export(): JsonResponse
|
||||
{
|
||||
return jok($this->service->export((array) request()->post('ids', [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入模板 JSON
|
||||
* @Method POST
|
||||
*/
|
||||
public function import(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->import((array) request()->post('payload', []), (bool) request()->post('overwrite', false)),
|
||||
'导入完成'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用内置 4 套轻奢预设初始化模板库,并顺带灌 15 套卡片方案
|
||||
* @Method POST
|
||||
*/
|
||||
public function initPresets(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->initPresets((bool) request()->post('overwrite', false)),
|
||||
'初始化完成'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌与布局的可选项,装修工作室渲染表单用
|
||||
* @Method GET
|
||||
*/
|
||||
public function schema(): JsonResponse
|
||||
{
|
||||
return jok(WxTemplateSchemaService::getInstance()->describe());
|
||||
}
|
||||
}
|
||||
107
app/Http/Controllers/Api/WxUserController.php
Normal file
107
app/Http/Controllers/Api/WxUserController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\WxUserService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 微信用户管理
|
||||
*/
|
||||
class WxUserController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 微信用户只能由小程序授权登录产生,后台没有新建入口
|
||||
*/
|
||||
protected array $exceptRoute = ['create'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxUserService::getInstance();
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = ['nick_name', 'phone', 'enterprise_id', 'show_price', 'price_number', 'is_p'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 / 取消代理商身份
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateUserIsP(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id'];
|
||||
$this->notRequest = ['is_p'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok(
|
||||
$this->service->updateUserIsP($params['id'], $params['is_p'] ?? null),
|
||||
'操作成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置价格倍率
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateShowPrice(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'number'];
|
||||
$this->notRequest = [];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->updateShowPrice($params['id'], $params['number']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 单独开关价格可见性
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateShowPriceStatus(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'show_price'];
|
||||
$this->notRequest = [];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->updateShowPriceStatus($params['id'], $params['show_price']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定所属企业
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function bindUser(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id', 'enterprise_id'];
|
||||
$this->notRequest = [];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->bindUser($params['id'], $params['enterprise_id']), '绑定成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 经销商绑定专属装修模板(template_code 空=跟随品牌默认)
|
||||
* @Method POST
|
||||
* @throws Exception
|
||||
*/
|
||||
public function bindTemplate(): JsonResponse
|
||||
{
|
||||
$this->insertField = ['id'];
|
||||
$this->notRequest = ['template_code'];
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok(
|
||||
$this->service->bindTemplate($params['id'], $params['template_code'] ?? ''),
|
||||
'模板绑定成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 该代理商名下的下级用户
|
||||
* @Method GET
|
||||
*/
|
||||
public function children(): JsonResponse
|
||||
{
|
||||
return jok($this->service->children((int) request()->get('id', 0)), '列表获取成功');
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Wx/AddressController.php
Normal file
39
app/Http/Controllers/Wx/AddressController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxAddressService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序收货地址
|
||||
*/
|
||||
class AddressController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAddressService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认地址
|
||||
* @Method GET
|
||||
*/
|
||||
public function defaultOne(): JsonResponse
|
||||
{
|
||||
return jok($this->service->defaultOne());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认
|
||||
* @Method POST
|
||||
*/
|
||||
public function setDefault(): JsonResponse
|
||||
{
|
||||
return jok($this->service->setDefault((int) request()->post('id', 0)), '已设为默认');
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/AfterSaleController.php
Normal file
30
app/Http/Controllers/Wx/AfterSaleController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxAfterSaleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序售后
|
||||
*/
|
||||
class AfterSaleController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAfterSaleService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请售后
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '已提交');
|
||||
}
|
||||
}
|
||||
36
app/Http/Controllers/Wx/AuthController.php
Normal file
36
app/Http/Controllers/Wx/AuthController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxAuthService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序登录(免鉴权)
|
||||
*/
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 继承的 CRUD 会被 autoRouteRegister 反射成路由,登录组里绝不能白送这些
|
||||
*/
|
||||
protected array $exceptRoute = ['list', 'option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAuthService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信 code 登录
|
||||
* @Method POST
|
||||
*/
|
||||
public function login(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->login(request()->post()),
|
||||
'登录成功'
|
||||
);
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Wx/ColorcardController.php
Normal file
39
app/Http/Controllers/Wx/ColorcardController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxColorcardService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序色卡(免登录)
|
||||
*/
|
||||
class ColorcardController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxColorcardService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 色卡分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function classList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->classList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 色卡公司
|
||||
* @Method GET
|
||||
*/
|
||||
public function companyList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->companyList());
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FactoryController.php
Normal file
30
app/Http/Controllers/Wx/FactoryController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFactoryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序工厂(免登录)
|
||||
*/
|
||||
class FactoryController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFactoryService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 工厂分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function classList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->classList());
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FavoriteController.php
Normal file
30
app/Http/Controllers/Wx/FavoriteController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFavoriteService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序收藏
|
||||
*/
|
||||
class FavoriteController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFavoriteService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收藏
|
||||
* @Method POST
|
||||
*/
|
||||
public function toggle(): JsonResponse
|
||||
{
|
||||
return jok($this->service->toggle((int) request()->post('catalogue_id', 0)));
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FeedbackController.php
Normal file
30
app/Http/Controllers/Wx/FeedbackController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFeedbackService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序反馈
|
||||
*/
|
||||
class FeedbackController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFeedbackService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交反馈
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '已提交');
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Wx/HomeController.php
Normal file
39
app/Http/Controllers/Wx/HomeController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxHomeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序首页(免鉴权)
|
||||
*/
|
||||
class HomeController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['list', 'option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxHomeService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页轮播
|
||||
* @Method GET
|
||||
*/
|
||||
public function carousel(): JsonResponse
|
||||
{
|
||||
return jok($this->service->carousel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 一级分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function categoryList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->categoryList((int) request()->get('pid', 0)));
|
||||
}
|
||||
}
|
||||
105
app/Http/Controllers/Wx/ListController.php
Normal file
105
app/Http/Controllers/Wx/ListController.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxListService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序清单
|
||||
*/
|
||||
class ListController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxListService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的清单
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清单详情
|
||||
* @Method GET
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jok($this->service->detail((int) request()->get('id', 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建清单
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 改清单名称与备注
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->update((int) request()->post('id', 0), request()->post()),
|
||||
'保存成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除清单
|
||||
* @Method POST
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
return jok($this->service->delete(request()->post('ids', [])), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除清单明细
|
||||
* @Method POST
|
||||
*/
|
||||
public function deleteItem(): JsonResponse
|
||||
{
|
||||
return jok($this->service->deleteItem(request()->post('ids', [])), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入清单
|
||||
* @Method POST
|
||||
*/
|
||||
public function toCart(): JsonResponse
|
||||
{
|
||||
return jok($this->service->toCart(request()->post()), '添加成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 改明细的规格与数量
|
||||
* @Method POST
|
||||
*/
|
||||
public function updateItem(): JsonResponse
|
||||
{
|
||||
return jok($this->service->updateItem(request()->post()), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出清单报价
|
||||
* @Method GET
|
||||
*/
|
||||
public function exportQuote(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportQuote((int) request()->get('id', 0)));
|
||||
}
|
||||
}
|
||||
84
app/Http/Controllers/Wx/OrderController.php
Normal file
84
app/Http/Controllers/Wx/OrderController.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxOrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序订单
|
||||
*/
|
||||
class OrderController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxOrderService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的订单
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情
|
||||
* @Method GET
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jok($this->service->detail((int) request()->get('id', 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清单转订单
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->createFromList(request()->post()), '下单成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传转账凭证
|
||||
* @Method POST
|
||||
*/
|
||||
public function voucher(): JsonResponse
|
||||
{
|
||||
return jok($this->service->submitVoucher(request()->post()), '已提交,等待审核');
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起微信支付
|
||||
* @Method POST
|
||||
*/
|
||||
public function wechatPay(): JsonResponse
|
||||
{
|
||||
return jok($this->service->wechatPay(request()->post()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @Method POST
|
||||
*/
|
||||
public function cancel(): JsonResponse
|
||||
{
|
||||
return jok($this->service->cancel((int) request()->post('id', 0)), '已取消');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
* @Method POST
|
||||
*/
|
||||
public function complete(): JsonResponse
|
||||
{
|
||||
return jok($this->service->complete((int) request()->post('id', 0)), '已完成');
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Wx/PackageController.php
Normal file
51
app/Http/Controllers/Wx/PackageController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxPackageService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序套餐(热门/列表/详情免登录,一件加入走 Service 内登录校验)
|
||||
*/
|
||||
class PackageController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxPackageService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页热门套餐
|
||||
* @Method GET
|
||||
*/
|
||||
public function hot(): JsonResponse
|
||||
{
|
||||
return jok($this->service->hot());
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐详情,p_user_id 为代理商分享来源(与商品详情同一套)
|
||||
* @Method GET
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jok($this->service->detail(
|
||||
(int) request()->get('id', 0),
|
||||
(int) request()->get('p_user_id', 0)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一件加入清单
|
||||
* @Method POST
|
||||
*/
|
||||
public function toList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->toList(request()->post()), '已加入清单');
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Wx/ProductController.php
Normal file
52
app/Http/Controllers/Wx/ProductController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxHomeService;
|
||||
use App\Service\wx\WxProductService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序商品
|
||||
*/
|
||||
class ProductController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxProductService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品列表
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品详情,p_user_id 为代理商分享来源
|
||||
* @Method GET
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jok($this->service->detail(
|
||||
(int) request()->get('id', 0),
|
||||
(int) request()->get('p_user_id', 0)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按父级取分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function categoryListByPid(): JsonResponse
|
||||
{
|
||||
return jok(WxHomeService::getInstance()->categoryList((int) request()->get('pid', 0)));
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/Wx/ThemeController.php
Normal file
56
app/Http/Controllers/Wx/ThemeController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxPayService;
|
||||
use App\Service\wx\WxThemeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序主题下发与支付回调(均免鉴权)
|
||||
*/
|
||||
class ThemeController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['list', 'option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxThemeService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前主题
|
||||
* @Method GET
|
||||
*/
|
||||
public function theme(): JsonResponse
|
||||
{
|
||||
return jok($this->service->current((string) request()->get('code', '')));
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选风格
|
||||
* @Method GET
|
||||
*/
|
||||
public function themeGallery(): JsonResponse
|
||||
{
|
||||
return jok($this->service->gallery());
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付回调
|
||||
*
|
||||
* 必须用原始报文验签:先 json_decode 再 encode 回去,字节顺序变了签名就对不上。
|
||||
* @Method POST
|
||||
*/
|
||||
public function payNotify(): JsonResponse
|
||||
{
|
||||
$headers = [];
|
||||
foreach (['wechatpay-timestamp', 'wechatpay-nonce', 'wechatpay-signature', 'wechatpay-serial'] as $key) {
|
||||
$headers[$key] = (string) request()->header($key, '');
|
||||
}
|
||||
$result = WxPayService::getInstance()->handleNotify($headers, request()->getContent());
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/Wx/UserController.php
Normal file
60
app/Http/Controllers/Wx/UserController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxUserCenterService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序「我的」
|
||||
*/
|
||||
class UserController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['list', 'option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxUserCenterService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的信息
|
||||
* @Method GET
|
||||
*/
|
||||
public function myInfo(): JsonResponse
|
||||
{
|
||||
return jok($this->service->myInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* @Method POST
|
||||
*/
|
||||
public function bandPhone(): JsonResponse
|
||||
{
|
||||
return jok($this->service->bandPhone(request()->post()), '绑定成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改昵称
|
||||
* @Method POST
|
||||
*/
|
||||
public function updateNickName(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->updateNickName((string) request()->post('nick_name', '')),
|
||||
'保存成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已绑定企业详情
|
||||
* @Method GET
|
||||
*/
|
||||
public function enterprise(): JsonResponse
|
||||
{
|
||||
return jok($this->service->enterprise());
|
||||
}
|
||||
}
|
||||
37
app/Http/Controllers/Wx/WxUploadController.php
Normal file
37
app/Http/Controllers/Wx/WxUploadController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxUploadService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序上传(转账凭证等)。
|
||||
*
|
||||
* 走 nl.wx 中间件鉴权(小程序登录态),路径 wx/upload/image。
|
||||
* 仅暴露 image 方法,避免反射注册把 BaseController 的 create/update/delete 挂成路由。
|
||||
*/
|
||||
class WxUploadController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxUploadService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(转账凭证)
|
||||
* @Method POST
|
||||
*/
|
||||
public function image(): JsonResponse
|
||||
{
|
||||
$file = request()->file('file');
|
||||
return jok(
|
||||
$this->service->uploadImage($file),
|
||||
'上传成功'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
namespace App\Http\Controllers\core;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\common\ai\AiCodeGenService;
|
||||
use App\Service\common\ai\AiRequirementPromptService;
|
||||
use App\Service\common\JWTService;
|
||||
use App\Service\core\CodeGenerationService;
|
||||
use App\Service\LoginService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class CodeGenerationController extends BaseController
|
||||
@@ -14,6 +16,11 @@ class CodeGenerationController extends BaseController
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// 代码生成会写后端源码、改路由、执行建表 DDL,属超管工具,必须登录后使用。
|
||||
// 本控制器的 CodeGenerationService 未继承 BaseService,故在此显式校验登录态
|
||||
// (Bearer JWT + Redis 会话),口径与 BaseService 构造中的鉴权一致;
|
||||
// autoRouteRegister 用 ReflectionClass 读方法、不实例化控制器,故不会在启动时误触发鉴权
|
||||
JWTService::getInstance()->getToken()->getUserInfo();
|
||||
$this->service = CodeGenerationService::getInstance();
|
||||
}
|
||||
|
||||
@@ -156,8 +163,34 @@ class CodeGenerationController extends BaseController
|
||||
}
|
||||
|
||||
$codeGenData = $this->service->convertTableToCodeGenData($tableName, $className, $classComment, $icon, $sort, $pid);
|
||||
|
||||
|
||||
return jok($codeGenData);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 提示词生成代码结构(调用工厂 Agent,返回可灌入表单的 JSON)
|
||||
* @Method POST
|
||||
*/
|
||||
public function aiGenerate(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
return jok(
|
||||
AiCodeGenService::getInstance()->generate($params),
|
||||
'AI 生成成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块名生成 / 优化需求概览与代码生成提示词(写入生成历史,供前端替换导入)
|
||||
* @Method POST
|
||||
*/
|
||||
public function aiRequirementPrompt(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
return jok(
|
||||
AiRequirementPromptService::getInstance()->generate($params),
|
||||
'需求概览生成成功'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
68
app/Http/Middleware/ApiAuthMiddleware.php
Normal file
68
app/Http/Middleware/ApiAuthMiddleware.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enum\ErrorEnum;
|
||||
use App\Service\common\JWTService;
|
||||
use App\Service\PermissionService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 登录态 + 接口级权限
|
||||
*
|
||||
* 原来鉴权只发生在 BaseService 的构造函数里,任何不经过 BaseService 的方法就是裸奔的;
|
||||
* 而 autoRouteRegister 会把继承来的方法也注册成路由,等于白送一批未受保护的入口。
|
||||
* 这里把校验前移到中间件,路由一进来就拦,BaseService 里那层保留做兜底。
|
||||
*/
|
||||
class ApiAuthMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$path = $this->normalizePath($request->path());
|
||||
if (in_array($path, (array) config('nl.api.white_list', []), true)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$userInfo = JWTService::getInstance()->getToken()->getUserInfo();
|
||||
} catch (\Throwable $e) {
|
||||
return $this->deny($e->getMessage() ?: '请先登录', ErrorEnum::NOT_AUTH);
|
||||
}
|
||||
if (empty($userInfo['id'])) {
|
||||
return $this->deny('请先登录', ErrorEnum::NOT_AUTH);
|
||||
}
|
||||
|
||||
$roleId = (int) ($userInfo['role_id'] ?? 0);
|
||||
if (!PermissionService::getInstance()->allows($roleId, $path)) {
|
||||
return $this->deny('没有该操作的权限,请联系管理员', ErrorEnum::NOT_PERMISSION);
|
||||
}
|
||||
|
||||
// 后续无需再解 token 的地方可以直接取
|
||||
$request->attributes->set('nl_user', $userInfo);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$path = trim($path, '/');
|
||||
if (str_starts_with($path, 'api/')) {
|
||||
$path = substr($path, 4);
|
||||
}
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 恒 200、业务码表达失败,与前端 request.ts 拦截器的约定一致
|
||||
*/
|
||||
private function deny(string $message, ErrorEnum $code): Response
|
||||
{
|
||||
return response()->json([
|
||||
'code' => $code->value,
|
||||
'message' => $message,
|
||||
'result' => [],
|
||||
'type' => 'error',
|
||||
]);
|
||||
}
|
||||
}
|
||||
159
app/Http/Middleware/ApiOpLogMiddleware.php
Normal file
159
app/Http/Middleware/ApiOpLogMiddleware.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\ApiEndpointModel;
|
||||
use App\Models\ApiOpLogModel;
|
||||
use App\Service\common\UserAgentService;
|
||||
use Closure;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API 操作日志中间件:响应结束后按接口注册表决定是否落库
|
||||
*/
|
||||
class ApiOpLogMiddleware
|
||||
{
|
||||
/**
|
||||
* 透传请求;真正写日志在 terminate,避免拖慢接口响应
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应结束后写操作日志
|
||||
* 匹配:去掉 /api/ 前缀的 url,且 method=0(ANY) 或等于请求方法,is_log=1、status=1
|
||||
*/
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
try {
|
||||
$path = $this->normalizePath($request->path());
|
||||
if ($path === '') {
|
||||
return;
|
||||
}
|
||||
$noInsert = config('nl.log.no_insert', []);
|
||||
if (is_array($noInsert)) {
|
||||
$full = 'api/' . $path;
|
||||
if (in_array($path, $noInsert, true)
|
||||
|| in_array($full, $noInsert, true)
|
||||
|| in_array('/' . $full, $noInsert, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$methodCode = $this->mapMethod($request->method());
|
||||
$endpoint = ApiEndpointModel::where('url', $path)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->where('is_log', 1)
|
||||
->where(function ($q) use ($methodCode) {
|
||||
$q->where('method', 0)->orWhere('method', $methodCode);
|
||||
})
|
||||
->orderByDesc('method')
|
||||
->first();
|
||||
if (!$endpoint) {
|
||||
return;
|
||||
}
|
||||
$userId = $this->resolveUserId($request);
|
||||
$body = $response->getContent();
|
||||
$result = json_decode((string) $body, true);
|
||||
if (!is_array($result)) {
|
||||
$result = ['raw' => mb_substr((string) $body, 0, 2000)];
|
||||
}
|
||||
$code = $result['code'] ?? null;
|
||||
$type = ((int) $code === 0) ? 0 : 1;
|
||||
$ua = UserAgentService::getInstance();
|
||||
ApiOpLogModel::insert([
|
||||
'user_id' => $userId,
|
||||
'url' => $path,
|
||||
'method' => $methodCode,
|
||||
'controller' => (string) ($endpoint->controller ?? ''),
|
||||
'ip' => (string) get_ip(),
|
||||
'param' => json_encode($this->collectParams($request), JSON_UNESCAPED_UNICODE),
|
||||
'result' => json_encode($result, JSON_UNESCAPED_UNICODE),
|
||||
'type' => $type,
|
||||
'result_code' => (string) ($code ?? ''),
|
||||
'platform_type' => 0,
|
||||
'belong_id' => 0,
|
||||
'user_type' => 0,
|
||||
'equipment' => $ua->parseEquipment(),
|
||||
'browser' => $ua->parseBrowser(),
|
||||
'created_at' => time(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径:去掉 api/ 前缀
|
||||
*/
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$path = trim($path, '/');
|
||||
if (str_starts_with($path, 'api/')) {
|
||||
$path = substr($path, 4);
|
||||
}
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 方法映射:GET=1 POST=2 其他=0
|
||||
*/
|
||||
private function mapMethod(string $method): int
|
||||
{
|
||||
return match (strtoupper($method)) {
|
||||
'GET' => 1,
|
||||
'POST' => 2,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Bearer JWT 解析 user_id;失败返回 0(不抛鉴权异常)
|
||||
*/
|
||||
private function resolveUserId(Request $request): int
|
||||
{
|
||||
try {
|
||||
$token = $request->bearerToken();
|
||||
if (empty($token)) {
|
||||
return 0;
|
||||
}
|
||||
$secret = (string) config('nl.jwt.secret', '');
|
||||
if (strlen($secret) < 32) {
|
||||
$secret = hash('sha256', $secret !== '' ? $secret : 'nl_admin_jwt_fallback_secret');
|
||||
}
|
||||
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
|
||||
$data = \App\Service\common\JWTService::getInstance()->extractUserData($decoded);
|
||||
return (int) ($data['id'] ?? 0);
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集请求参数并脱敏密码类字段
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function collectParams(Request $request): array
|
||||
{
|
||||
$all = array_merge($request->query(), $request->request->all());
|
||||
$maskKeys = [
|
||||
'password', 'old_password', 'new_password', 'confirm_password',
|
||||
'access_key', 'secret_key', 'api_key', 'token',
|
||||
// 小程序登录凭证与商户密钥同样不能落进日志表
|
||||
'code', 'phone_code', 'session_key', 'app_secret', 'mch_key', 'mch_private_key',
|
||||
];
|
||||
foreach ($maskKeys as $k) {
|
||||
if (array_key_exists($k, $all) && $all[$k] !== '' && $all[$k] !== null) {
|
||||
$all[$k] = '******';
|
||||
}
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
41
app/Http/Middleware/WxAuthMiddleware.php
Normal file
41
app/Http/Middleware/WxAuthMiddleware.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enum\ErrorEnum;
|
||||
use App\Service\wx\WxTokenService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 小程序登录态
|
||||
*
|
||||
* 老项目的 AuthMiddleware 是先 $next($request) 跑完控制器、再检查 token,
|
||||
* 而且只判空不验签——等于没有鉴权。这里在进控制器之前就拦。
|
||||
*/
|
||||
class WxAuthMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = WxTokenService::getInstance()->resolveUser($request->bearerToken());
|
||||
if (empty($user)) {
|
||||
return response()->json([
|
||||
'code' => ErrorEnum::NOT_AUTH->value,
|
||||
'message' => '请先登录',
|
||||
'result' => [],
|
||||
'type' => 'error',
|
||||
]);
|
||||
}
|
||||
if ((int) ($user['status'] ?? 0) === 1) {
|
||||
return response()->json([
|
||||
'code' => ErrorEnum::NOT_AUTH->value,
|
||||
'message' => '账号已被停用,请联系客服',
|
||||
'result' => [],
|
||||
'type' => 'error',
|
||||
]);
|
||||
}
|
||||
$request->attributes->set('wx_user', $user);
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
63
app/Http/Middleware/WxHttpsRewriteMiddleware.php
Normal file
63
app/Http/Middleware/WxHttpsRewriteMiddleware.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 小程序响应 HTTPS 改写
|
||||
*
|
||||
* 对齐老 lgp-wx-api ApiMiddleware::checkHttp:成功包(code=0)里递归把
|
||||
* result 中的 http:// 换成 https://,满足微信小程序对图片等资源的 HTTPS 要求。
|
||||
* 仅挂在 /api/wx/*,不改管理端接口;不写库,只做出站改写。
|
||||
*/
|
||||
class WxHttpsRewriteMiddleware
|
||||
{
|
||||
/**
|
||||
* 处理完控制器后改写成功响应的 result
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
if (!$response instanceof JsonResponse) {
|
||||
return $response;
|
||||
}
|
||||
$payload = $response->getData(true);
|
||||
if (!is_array($payload)) {
|
||||
return $response;
|
||||
}
|
||||
// 与 jok 约定一致:成功 code 为 0;字符串 "0" 一并兼容
|
||||
if (($payload['code'] ?? null) != 0 || !array_key_exists('result', $payload)) {
|
||||
return $response;
|
||||
}
|
||||
$payload['result'] = $this->checkHttp($payload['result']);
|
||||
$response->setData($payload);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归把字符串/数组/对象中的 http:// 换成 https://
|
||||
* 不用 PHPUnit 的 isObject,对象先转数组再走
|
||||
*/
|
||||
private function checkHttp(mixed $data): mixed
|
||||
{
|
||||
if (is_object($data)) {
|
||||
$data = json_decode(json_encode($data), true);
|
||||
}
|
||||
if (is_string($data)) {
|
||||
return str_replace('http://', 'https://', $data);
|
||||
}
|
||||
if (is_array($data)) {
|
||||
array_walk_recursive($data, function (&$value) {
|
||||
if (is_string($value)) {
|
||||
$value = str_replace('http://', 'https://', $value);
|
||||
}
|
||||
});
|
||||
return $data;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
33
app/Jobs/ScanMediaReferencesJob.php
Normal file
33
app/Jobs/ScanMediaReferencesJob.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Service\MaterialService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
/**
|
||||
* 引用扫描任务
|
||||
*
|
||||
* 与接口版调的是同一个 Service 方法。留这个 Job 是为了以后能挂定时(比如每天夜里扫一次):
|
||||
* ref_count 越新,回收判断越准,靠人记得点按钮迟早会漏。
|
||||
* 这里不改任何队列配置,默认驱动下 dispatch 等于同步执行。
|
||||
*/
|
||||
class ScanMediaReferencesJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* @param array $params 预留给 MaterialService::scanReferences 的扫描参数
|
||||
*/
|
||||
public function __construct(public array $params = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// 全库逐表逐字段扫,默认 30 秒执行时限根本不够
|
||||
cc_set_time_limit();
|
||||
MaterialService::getInstance()->scanReferences($this->params);
|
||||
}
|
||||
}
|
||||
50
app/Jobs/SyncOssObjectsJob.php
Normal file
50
app/Jobs/SyncOssObjectsJob.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Service\MaterialService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
/**
|
||||
* OSS 对象同步任务
|
||||
*
|
||||
* 同步本身是同步接口(前端点一次跑一批),这个 Job 只是把同一个 Service 方法
|
||||
* 包成可入队的形式:bucket 里几万个对象时不该让人一直点按钮,
|
||||
* 以后把队列驱动切成 redis 就能 dispatch 它自己跑完全量。
|
||||
* 这里不改任何队列配置,默认驱动下 dispatch 等于同步执行。
|
||||
*/
|
||||
class SyncOssObjectsJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* @param array $params 与 MaterialService::syncFromOss 同构:oss_config_id / prefix / marker / limit
|
||||
*/
|
||||
public function __construct(public array $params = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 一路续拉到 finished
|
||||
*
|
||||
* next_marker 跟上一轮一样说明驱动没有推进(配置或权限异常),必须跳出,
|
||||
* 否则这里会变成一个不停打 OSS 的死循环。
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
cc_set_time_limit();
|
||||
$service = MaterialService::getInstance();
|
||||
$params = $this->params;
|
||||
$marker = (string) ($params['marker'] ?? '');
|
||||
while (true) {
|
||||
$params['marker'] = $marker;
|
||||
$result = $service->syncFromOss($params);
|
||||
$next = (string) ($result['next_marker'] ?? '');
|
||||
if ((bool) ($result['finished'] ?? true) || $next === '' || $next === $marker) {
|
||||
break;
|
||||
}
|
||||
$marker = $next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
use App\Models\business\DepartmentModel;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class AdminModel extends BaseModel
|
||||
@@ -20,4 +21,15 @@ class AdminModel extends BaseModel
|
||||
{
|
||||
return $this->hasOne(RoleModel::class, 'id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*
|
||||
* 部门表在 business 连接(cc_ 前缀),Eloquent 关联可以跨连接,
|
||||
* 但不能和本表做 SQL join,需要 join 时只能分两次查再在 PHP 里拼。
|
||||
*/
|
||||
public function department(): HasOne
|
||||
{
|
||||
return $this->hasOne(DepartmentModel::class, 'id', 'department_id');
|
||||
}
|
||||
}
|
||||
|
||||
15
app/Models/ApiEndpointModel.php
Normal file
15
app/Models/ApiEndpointModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 接口注册表模型:维护 URL / 操作名 / 是否记入操作日志
|
||||
*/
|
||||
class ApiEndpointModel extends BaseModel
|
||||
{
|
||||
protected $table = 'api_endpoint';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
24
app/Models/FileFolderModel.php
Normal file
24
app/Models/FileFolderModel.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 素材文件夹(nl_file_folder)
|
||||
*
|
||||
* 只做素材的逻辑归类,与对象在 OSS 上的实际路径无关:
|
||||
* 移动素材如果跟着改对象键,已经发出去的历史地址会全部 404。
|
||||
*/
|
||||
class FileFolderModel extends BaseModel
|
||||
{
|
||||
protected $table = 'file_folder';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'pid' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
@@ -4,14 +4,94 @@ namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 文件 / 素材表(nl_file)
|
||||
*
|
||||
* 原表只是上传流水(user_id + url)。素材库扩了对象键、体积、哈希与引用计数之后,
|
||||
* 它同时充当素材主表,type / source 的取值被同步、回收、前端筛选三处共用,
|
||||
* 所以固化成常量,免得三边各写一套魔法数字、还各写错一个。
|
||||
*/
|
||||
class FileModel extends BaseModel
|
||||
{
|
||||
/*
|
||||
* type 取值。0~4 是老表原有语义,5、6 是素材库补的:
|
||||
* 图册 PDF、报价 Excel 这类文件原先全落在「其他」里没法筛。
|
||||
*/
|
||||
public const TYPE_IMAGE = 0;
|
||||
public const TYPE_VIDEO = 1;
|
||||
public const TYPE_AUDIO = 2;
|
||||
public const TYPE_EXCEL = 3;
|
||||
public const TYPE_ARCHIVE = 4;
|
||||
public const TYPE_DOCUMENT = 5;
|
||||
public const TYPE_OTHER = 6;
|
||||
|
||||
/** source:经上传接口进来的 */
|
||||
public const SOURCE_UPLOAD = 0;
|
||||
/** source:从 OSS 反向列举补录的历史文件 */
|
||||
public const SOURCE_OSS = 1;
|
||||
|
||||
protected $table = 'file';
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 数值列显式转型:MySQL 驱动会把 bigint / int 读成字符串,
|
||||
* 前端拿 size 做体积换算、拿 ref_count 判断能否回收时会被字符串坑到
|
||||
*
|
||||
* created_at / updated_at 不在此列:BaseModel 已经用访问器格式化成了日期串,
|
||||
* 再加 cast 只会让两套逻辑互相打架
|
||||
*/
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'oss_config_id' => 'integer',
|
||||
'folder_id' => 'integer',
|
||||
'type' => 'integer',
|
||||
'size' => 'integer',
|
||||
'width' => 'integer',
|
||||
'height' => 'integer',
|
||||
'ref_count' => 'integer',
|
||||
'last_scan_at' => 'integer',
|
||||
'source' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 从对象键取出能落库的扩展名(小写、不带点、最长 20)
|
||||
*
|
||||
* 七牛历史上会把处理参数写进文件名:
|
||||
* b_xxx.jpeg~tplv-a9rns2rl98-downsize_watermark_1_5
|
||||
* pathinfo 会把整段 ~ 后面都当成扩展名,varchar(20) 直接截断报错,
|
||||
* 整批 insert 跟着失败。这里先剥查询串和 ~ 后缀,再取真正的后缀。
|
||||
* 微信保存的 .pic / .pic_hd 按 jpg 认,否则会被打成「其他」。
|
||||
*/
|
||||
public static function extOfKey(string $key): string
|
||||
{
|
||||
$name = basename(str_replace('\\', '/', $key));
|
||||
$cut = strpbrk($name, '?~#');
|
||||
if ($cut !== false) {
|
||||
$name = substr($name, 0, strlen($name) - strlen($cut));
|
||||
}
|
||||
$ext = strtolower(trim((string) pathinfo($name, PATHINFO_EXTENSION), " \t\n\r\0\x0B."));
|
||||
if ($ext === 'pic' || $ext === 'pic_hd') {
|
||||
return 'jpg';
|
||||
}
|
||||
return $ext === '' ? '' : substr($ext, 0, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展名归类到 type
|
||||
*
|
||||
* 从 OSS 反向同步时手上只有对象键,没有上传时的 MIME,只能按扩展名判断
|
||||
*/
|
||||
public static function typeOfExt(string $ext): int
|
||||
{
|
||||
return match (strtolower(trim($ext, " \t\n\r\0\x0B."))) {
|
||||
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'avif', 'heic', 'pic', 'pic_hd' => self::TYPE_IMAGE,
|
||||
'mp4', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'webm', 'm3u8', 'ts' => self::TYPE_VIDEO,
|
||||
'mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a', 'amr' => self::TYPE_AUDIO,
|
||||
'xls', 'xlsx', 'csv' => self::TYPE_EXCEL,
|
||||
'zip', 'rar', '7z', 'tar', 'gz', 'bz2' => self::TYPE_ARCHIVE,
|
||||
'pdf', 'doc', 'docx', 'ppt', 'pptx', 'txt', 'md' => self::TYPE_DOCUMENT,
|
||||
default => self::TYPE_OTHER,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
15
app/Models/LoginLogModel.php
Normal file
15
app/Models/LoginLogModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 登录日志模型(仅 created_at,无软删)
|
||||
*/
|
||||
class LoginLogModel extends BaseModel
|
||||
{
|
||||
protected $table = 'login_log';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
14
app/Models/RoleEndpointRelationModel.php
Normal file
14
app/Models/RoleEndpointRelationModel.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
class RoleEndpointRelationModel extends BaseModel
|
||||
{
|
||||
protected $table = 'role_endpoint_relation';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
}
|
||||
15
app/Models/SystemConfigModel.php
Normal file
15
app/Models/SystemConfigModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 系统配置表模型(kv 存储)
|
||||
*/
|
||||
class SystemConfigModel extends BaseModel
|
||||
{
|
||||
protected $table = 'system_config';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
20
app/Models/WxAppModel.php
Normal file
20
app/Models/WxAppModel.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 小程序应用配置(nl_wx_app)
|
||||
*
|
||||
* app_secret / mch_key / mch_private_key 是密文列,读取必须走 WxAppService 解密,
|
||||
* 任何接口都不要直接把这三列返回给前端。
|
||||
*/
|
||||
class WxAppModel extends BaseModel
|
||||
{
|
||||
protected $table = 'wx_app';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $hidden = ['app_secret', 'mch_key', 'mch_private_key'];
|
||||
}
|
||||
15
app/Models/ai/AiApiKeyModel.php
Normal file
15
app/Models/ai/AiApiKeyModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\ai;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* AI API 密钥表模型(仅表名与字段约定,查询逻辑放 Service)
|
||||
*/
|
||||
class AiApiKeyModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ai_api_key';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/ai/AiGenerationModel.php
Normal file
15
app/Models/ai/AiGenerationModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\ai;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* AI 生成历史表模型(仅表名与字段约定)
|
||||
*/
|
||||
class AiGenerationModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ai_generation';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/ai/AiModelModel.php
Normal file
15
app/Models/ai/AiModelModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\ai;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* AI 模型表模型(仅表名与字段约定,查询逻辑放 Service)
|
||||
*/
|
||||
class AiModelModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ai_model';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/ai/AiPlatformModel.php
Normal file
15
app/Models/ai/AiPlatformModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\ai;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* AI 平台表模型(仅表名与字段约定,查询逻辑放 Service)
|
||||
*/
|
||||
class AiPlatformModel extends BaseModel
|
||||
{
|
||||
protected $table = 'ai_platform';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
32
app/Models/business/AfterSaleModel.php
Normal file
32
app/Models/business/AfterSaleModel.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 售后申请(cc_after_sale)
|
||||
*/
|
||||
class AfterSaleModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'after_sale';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public const STATUS_PENDING = 0;
|
||||
|
||||
public const STATUS_APPROVED = 1;
|
||||
|
||||
public const STATUS_REJECTED = 2;
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OrderModel::class, 'order_id', 'id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WxUserModel::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
15
app/Models/business/CardClassModel.php
Normal file
15
app/Models/business/CardClassModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 色卡分类(cc_card_class)
|
||||
*/
|
||||
class CardClassModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'card_class';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/business/CarouselModel.php
Normal file
15
app/Models/business/CarouselModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 小程序轮播图(cc_carousel)
|
||||
*/
|
||||
class CarouselModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'carousel';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
35
app/Models/business/CatalogueModel.php
Normal file
35
app/Models/business/CatalogueModel.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 商品图册(cc_catalogue)
|
||||
*
|
||||
* 老项目关系名写成 cateGory,序列化出来是 cate_gory,前端还得记这个拼写。
|
||||
* 新后端统一叫 category,列表里另外拍平成 category_name。
|
||||
*/
|
||||
class CatalogueModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'catalogue';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CategoryModel::class, 'category_id', 'id');
|
||||
}
|
||||
|
||||
public function priceSheet(): HasMany
|
||||
{
|
||||
return $this->hasMany(PriceSheetModel::class, 'catalogue_id', 'id');
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(ImageModel::class, 'catalogue_id', 'id');
|
||||
}
|
||||
}
|
||||
26
app/Models/business/CategoryModel.php
Normal file
26
app/Models/business/CategoryModel.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 商品分类(cc_category)
|
||||
*/
|
||||
class CategoryModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'category';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'pid', 'id');
|
||||
}
|
||||
|
||||
public function catalogues(): HasMany
|
||||
{
|
||||
return $this->hasMany(CatalogueModel::class, 'category_id', 'id');
|
||||
}
|
||||
}
|
||||
28
app/Models/business/ColorcardModel.php
Normal file
28
app/Models/business/ColorcardModel.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 色卡(cc_colorcard)
|
||||
*
|
||||
* 外键列名是 card_class / company(不带 _id),沿用老表不改。
|
||||
*/
|
||||
class ColorcardModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'colorcard';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function cardClassInfo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CardClassModel::class, 'card_class', 'id');
|
||||
}
|
||||
|
||||
public function companyInfo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CompanyModel::class, 'company', 'id');
|
||||
}
|
||||
}
|
||||
15
app/Models/business/CompanyModel.php
Normal file
15
app/Models/business/CompanyModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 色卡所属公司(cc_company)
|
||||
*/
|
||||
class CompanyModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'company';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
18
app/Models/business/DepartmentModel.php
Normal file
18
app/Models/business/DepartmentModel.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 部门表模型
|
||||
*
|
||||
* 直接复用老系统已有的 cc_department 表(结构正好够用:id/name/desc/status/pid/color/时间戳),
|
||||
* 部门数据零迁移。账号侧通过 nl_admin.department_id 关联。
|
||||
*/
|
||||
class DepartmentModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'department';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
23
app/Models/business/EnterpriseModel.php
Normal file
23
app/Models/business/EnterpriseModel.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 企业(cc_enterprise)
|
||||
*
|
||||
* 老表只有 name / logo,企业管理模块会补齐联系人、税号、结算方式、默认价格倍率等列。
|
||||
*/
|
||||
class EnterpriseModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'enterprise';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(WxUserModel::class, 'enterprise_id', 'id');
|
||||
}
|
||||
}
|
||||
15
app/Models/business/FactoryClassificationModel.php
Normal file
15
app/Models/business/FactoryClassificationModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 工厂分类(cc_factory_classification)
|
||||
*/
|
||||
class FactoryClassificationModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'factory_classification';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
23
app/Models/business/FactoryImageModel.php
Normal file
23
app/Models/business/FactoryImageModel.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 工厂产品图(cc_factory_image)
|
||||
*
|
||||
* 外键列名就叫 factory(不是 factory_id),沿用老表不改。
|
||||
*/
|
||||
class FactoryImageModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'factory_image';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function factoryInfo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(FactoryInfoModel::class, 'factory', 'id');
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user