diff --git a/.cursor/rules/lgp-pitfalls.mdc b/.cursor/rules/lgp-pitfalls.mdc new file mode 100644 index 0000000..1530a70 --- /dev/null +++ b/.cursor/rules/lgp-pitfalls.mdc @@ -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//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` 里还提交了真实数据库密码——这些是待清理的历史欠账,不要照抄这个做法。 diff --git a/.cursor/rules/lgp-project-map.mdc b/.cursor/rules/lgp-project-map.mdc new file mode 100644 index 0000000..bd54f17 --- /dev/null +++ b/.cursor/rules/lgp-project-map.mdc @@ -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 条,其余是反射自动注册出来的死路由,不要跟着迁。 diff --git a/App.vue b/App.vue index d7b19c1..d073032 100644 --- a/App.vue +++ b/App.vue @@ -1,11 +1,17 @@ + + diff --git a/pages/order/list.vue b/pages/order/list.vue new file mode 100644 index 0000000..ba46815 --- /dev/null +++ b/pages/order/list.vue @@ -0,0 +1,350 @@ + + + + + diff --git a/pages/product/detail.vue b/pages/product/detail.vue index 5569516..5327f2a 100644 --- a/pages/product/detail.vue +++ b/pages/product/detail.vue @@ -78,13 +78,14 @@ bgColor="#F7F8FA" > - + + {{ item.name }} @@ -96,6 +97,42 @@ + + + + 选择规格 + 返回清单 + + + + + {{ spec.specification }} + {{ spec.dimension }} + + + ¥{{ spec.routine[0] }} + + + + 该商品暂无规格,将按默认规格加入 + + + + 数量 + + - + {{ specPicker.quantity }} + + + + + + @@ -131,6 +168,14 @@ export default { p_user_id: 0, listModal: false, listOption: [], + // 规格/数量选择:加入清单时必须带上 price_sheet_id,否则转订单会被 OrderCoreService 拒 + specPicker: { + visible: false, + listId: 0, + sheets: [], + priceSheetId: 0, + quantity: 1, + }, product: { title: '', cover: '', @@ -158,7 +203,6 @@ export default { previewImages(url) { let w = this.product.title; w = urlSafeBase64Encode(w); - console.log(`${url}?watermark/2/text/${w}/fontsize/800`); uni.previewImage({ current: 0, urls: [`${url}?watermark/2/text/${w}/fontsize/800`], @@ -169,10 +213,35 @@ export default { }, showModal() { this.getListOption(); + this.specPicker.visible = false; this.listModal = true; }, - addToCart(id) { - toCartApi(id, this.id).then(() => { + // 选定某个清单后进入规格选择步骤;该商品的 price_sheet 作为可选规格 + onPickList(listId) { + const sheets = (this.product.price_sheet || []).map((s) => ({ + id: s.id, + specification: s.specification, + dimension: s.dimension, + routine: s.routine || [], + })); + this.specPicker.listId = listId; + this.specPicker.sheets = sheets; + this.specPicker.priceSheetId = sheets.length > 0 ? sheets[0].id : 0; + this.specPicker.quantity = 1; + this.specPicker.visible = true; + }, + changeQty(delta) { + const next = this.specPicker.quantity + delta; + if (next < 1) return; + this.specPicker.quantity = next; + }, + // 确定加入:带 price_sheet_id 与 quantity 提交,后端按规格去重累加 + confirmAddToCart() { + const { listId, priceSheetId, quantity } = this.specPicker; + toCartApi(listId, this.id, { + price_sheet_id: priceSheetId, + quantity, + }).then(() => { this.listModal = false; uni.showToast({ title: '已添加到清单', @@ -468,4 +537,110 @@ export default { align-items: center; justify-content: center; } + +/* 规格选择步骤 */ +.spec-picker { + padding: 10rpx 0 20rpx; +} +.spec-picker-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 6rpx 20rpx; +} +.spec-picker-title { + font-size: 30rpx; + font-weight: 600; + color: #222; +} +.spec-picker-back { + font-size: 24rpx; + color: #B4854D; +} +.spec-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx 20rpx; + background: #fff; + border: 1rpx solid #EEE; + border-radius: 10rpx; + margin-bottom: 16rpx; +} +.spec-row.active { + border-color: #B4854D; + background: rgba(180, 133, 77, 0.06); +} +.spec-row-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} +.spec-row-name { + font-size: 28rpx; + color: #222; + font-weight: 500; +} +.spec-row-dim { + font-size: 22rpx; + color: #999; + margin-top: 6rpx; +} +.spec-row-price { + flex-shrink: 0; + margin-left: 20rpx; +} +.spec-row-curr { + font-size: 28rpx; + color: #B4854D; + font-weight: 600; +} +.spec-empty { + text-align: center; + color: #999; + font-size: 24rpx; + padding: 40rpx 0; +} +.qty-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx 6rpx; + margin-top: 10rpx; +} +.qty-label { + font-size: 28rpx; + color: #333; +} +.qty-ctrl { + display: flex; + align-items: center; + gap: 30rpx; +} +.qty-btn { + width: 56rpx; + height: 56rpx; + line-height: 52rpx; + text-align: center; + border-radius: 50%; + background: #F2F2F2; + color: #333; + font-size: 36rpx; +} +.qty-num { + font-size: 30rpx; + color: #222; + min-width: 40rpx; + text-align: center; +} +.spec-confirm-btn { + margin-top: 24rpx; + background: #B4854D; + color: #fff; + font-size: 30rpx; + border-radius: 48rpx; + height: 88rpx; + line-height: 88rpx; +} \ No newline at end of file diff --git a/pages/product/product.vue b/pages/product/product.vue index 96d8014..2fc5a31 100644 --- a/pages/product/product.vue +++ b/pages/product/product.vue @@ -135,33 +135,19 @@