From ee0886b8008ac6d7147834f5a69c79d1cbbc2577 Mon Sep 17 00:00:00 2001 From: scott Date: Tue, 31 Mar 2026 10:41:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E9=A1=B5=E9=9D=A2=EF=BC=88=E5=85=8D=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E4=B8=89=E9=A1=B5=20+=20=E9=85=8D=E5=A5=97=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端: - 新增 EmptyLayout 空壳布局(无侧边栏/导航) - 新增 requestNoAuth Axios 实例(不注入 token) - 新增 integralExternal 路由模块(/integral-external/*) - permission.js 加入 whiteListPrefixes 前缀白名单跳过登录 - 新增 phoneDesensitize 手机号脱敏过滤器 - 新增三个免认证页面: · 积分订单页(/integral-external/order) · 用户积分页(/integral-external/user,手机号脱敏) · 用户积分明细子页(/integral-external/user/integral-detail) 后端: - 新增 ExternalIntegralController(无 @PreAuthorize) · GET /api/external/integral/order/list · GET /api/external/integral/user/list · POST /api/external/integral/log/list - WebSecurityConfig 加入 /api/external/integral/** permitAll 文档与工具: - 新增 coding plan、schedule、测试报告 - 新增 start-backend.sh / start-frontend.sh 本地启动脚本 - 新增 .mvn/wrapper/maven-wrapper.properties Co-Authored-By: Claude Sonnet 4.6 --- backend-adminend/src/api/integralExternal.js | 39 + backend-adminend/src/filters/user.js | 14 + backend-adminend/src/layout/EmptyLayout.vue | 18 + backend-adminend/src/permission.js | 6 +- backend-adminend/src/router/index.js | 3 + .../src/router/modules/integralExternal.js | 30 + backend-adminend/src/utils/requestNoAuth.js | 51 + .../views/integral-external/order/index.vue | 254 ++++ .../user-integral-detail/index.vue | 291 +++++ .../views/integral-external/user/index.vue | 206 ++++ backend/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes backend/.mvn/wrapper/maven-wrapper.properties | 2 + .../zbkj/admin/config/WebSecurityConfig.java | 2 + .../ExternalIntegralController.java | 92 ++ docs/integral-pages-coding-plan.md | 626 ++++++++++ docs/integral-pages-schedule.md | 106 ++ docs/integral-pages-test-report-v2.md | 168 +++ docs/integral-pages-test-report.md | 169 +++ docs/newpage.md | 14 + docs/openclaw_agent_configuration_v2.plan.md | 1019 +++++++++++++++++ docs/openclaw_agent_configuration_v3.plan.md | 946 +++++++++++++++ docs/phase1-checkpoint-report.md | 127 ++ docs/phase4-checkpoint-report.md | 89 ++ start-backend.sh | 69 ++ start-frontend.sh | 21 + 25 files changed, 4360 insertions(+), 2 deletions(-) create mode 100644 backend-adminend/src/api/integralExternal.js create mode 100644 backend-adminend/src/layout/EmptyLayout.vue create mode 100644 backend-adminend/src/router/modules/integralExternal.js create mode 100644 backend-adminend/src/utils/requestNoAuth.js create mode 100644 backend-adminend/src/views/integral-external/order/index.vue create mode 100644 backend-adminend/src/views/integral-external/user-integral-detail/index.vue create mode 100644 backend-adminend/src/views/integral-external/user/index.vue create mode 100644 backend/.mvn/wrapper/maven-wrapper.jar create mode 100644 backend/.mvn/wrapper/maven-wrapper.properties create mode 100644 backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java create mode 100644 docs/integral-pages-coding-plan.md create mode 100644 docs/integral-pages-schedule.md create mode 100644 docs/integral-pages-test-report-v2.md create mode 100644 docs/integral-pages-test-report.md create mode 100644 docs/newpage.md create mode 100644 docs/openclaw_agent_configuration_v2.plan.md create mode 100644 docs/openclaw_agent_configuration_v3.plan.md create mode 100644 docs/phase1-checkpoint-report.md create mode 100644 docs/phase4-checkpoint-report.md create mode 100755 start-backend.sh create mode 100755 start-frontend.sh diff --git a/backend-adminend/src/api/integralExternal.js b/backend-adminend/src/api/integralExternal.js new file mode 100644 index 0000000..12218df --- /dev/null +++ b/backend-adminend/src/api/integralExternal.js @@ -0,0 +1,39 @@ +/** + * 积分外部页面 API(免认证) + * 使用 requestNoAuth 实例,不注入 token,不拦截 401。 + * 对应后端:ExternalIntegralController → api/external/integral/* + */ +import requestNoAuth from '@/utils/requestNoAuth'; + +/** + * 积分订单列表 + */ +export function getExternalOrderList(params) { + return requestNoAuth({ + url: 'external/integral/order/list', + method: 'get', + params, + }); +} + +/** + * 用户积分列表(含 eb_user 积分字段) + */ +export function getExternalUserList(params) { + return requestNoAuth({ + url: 'external/integral/user/list', + method: 'get', + params, + }); +} + +/** + * 用户积分明细分页列表 + */ +export function getExternalIntegralLog(data) { + return requestNoAuth({ + url: 'external/integral/log/list', + method: 'post', + data, + }); +} diff --git a/backend-adminend/src/filters/user.js b/backend-adminend/src/filters/user.js index d6ddbf3..1e2f082 100644 --- a/backend-adminend/src/filters/user.js +++ b/backend-adminend/src/filters/user.js @@ -48,3 +48,17 @@ export function filterIsPromoter(status) { }; return statusMap[status]; } + +/** + * 手机号脱敏(中间 4 位替换为 ****) + * 适用于外部免登录页面展示,防止敏感信息泄露。 + * 例:13812345678 → 138****5678 + * @param {string|number} phone + * @return {string} + */ +export function phoneDesensitize(phone) { + if (!phone) return '-'; + const str = String(phone); + if (str.length < 7) return str; // 过短则不处理 + return str.replace(/(\d{3})\d{4}(\d+)/, '$1****$2'); +} diff --git a/backend-adminend/src/layout/EmptyLayout.vue b/backend-adminend/src/layout/EmptyLayout.vue new file mode 100644 index 0000000..b10338e --- /dev/null +++ b/backend-adminend/src/layout/EmptyLayout.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/backend-adminend/src/permission.js b/backend-adminend/src/permission.js index f93471e..b5f5326 100644 --- a/backend-adminend/src/permission.js +++ b/backend-adminend/src/permission.js @@ -18,7 +18,9 @@ import getPageTitle from '@/utils/get-page-title'; NProgress.configure({ showSpinner: false }); // NProgress Configuration -const whiteList = ['/login', '/auth-redirect']; // no redirect whitelist +// no redirect whitelist — exact match for /login, prefix match for /integral-external +const whiteList = ['/login', '/auth-redirect']; +const whiteListPrefixes = ['/integral-external']; router.beforeEach(async (to, from, next) => { // start progress bar @@ -56,7 +58,7 @@ router.beforeEach(async (to, from, next) => { } } else { /* has no token*/ - if (whiteList.indexOf(to.path) !== -1) { + if (whiteList.indexOf(to.path) !== -1 || whiteListPrefixes.some(prefix => to.path.startsWith(prefix))) { // in the free login whitelist, go directly next(); } else { diff --git a/backend-adminend/src/router/index.js b/backend-adminend/src/router/index.js index 98e5d27..0ffafc9 100644 --- a/backend-adminend/src/router/index.js +++ b/backend-adminend/src/router/index.js @@ -32,6 +32,7 @@ import maintainRouter from './modules/maintain'; import mobileRouter from './modules/mobile'; import statistic from './modules/statistic'; import designRouter from './modules/design'; +import integralExternalRouter from './modules/integralExternal'; /** * Note: sub-menu only appear when route children.length >= 1 @@ -90,6 +91,8 @@ export const constantRoutes = [ statistic, //装修 designRouter, + // 积分外部页面(免认证) + integralExternalRouter, { path: '/404', component: () => import('@/views/error-page/404'), diff --git a/backend-adminend/src/router/modules/integralExternal.js b/backend-adminend/src/router/modules/integralExternal.js new file mode 100644 index 0000000..7ba5f1d --- /dev/null +++ b/backend-adminend/src/router/modules/integralExternal.js @@ -0,0 +1,30 @@ +const EmptyLayout = () => import('@/layout/EmptyLayout'); + +const integralExternalRouter = { + path: '/integral-external', + component: EmptyLayout, + redirect: '/integral-external/order', + hidden: true, + children: [ + { + path: 'order', + component: () => import('@/views/integral-external/order/index'), + name: 'IntegralExternalOrder', + meta: { title: '积分订单' }, + }, + { + path: 'user', + component: () => import('@/views/integral-external/user/index'), + name: 'IntegralExternalUser', + meta: { title: '用户积分' }, + }, + { + path: 'user/integral-detail', + component: () => import('@/views/integral-external/user-integral-detail/index'), + name: 'IntegralExternalUserDetail', + meta: { title: '用户积分明细' }, + }, + ], +}; + +export default integralExternalRouter; diff --git a/backend-adminend/src/utils/requestNoAuth.js b/backend-adminend/src/utils/requestNoAuth.js new file mode 100644 index 0000000..5e03ffb --- /dev/null +++ b/backend-adminend/src/utils/requestNoAuth.js @@ -0,0 +1,51 @@ +/** + * 免认证 Axios 实例 + * 供积分外部页面(/integral-external/*)使用。 + * 不注入 Authori-zation token,不拦截 401 自动跳转登录页。 + */ +import axios from 'axios'; +import { Message } from 'element-ui'; +import SettingMer from '@/utils/settingMer'; + +const service = axios.create({ + baseURL: SettingMer.apiBaseURL, + timeout: 60000, +}); + +// 请求拦截器 — 不注入 token +service.interceptors.request.use( + (config) => { + // GET 请求防缓存 + if (/get/i.test(config.method)) { + config.params = config.params || {}; + config.params.temp = Date.parse(new Date()) / 1000; + } + return config; + }, + (error) => Promise.reject(error), +); + +// 响应拦截器 — 不拦截 401 跳转 +service.interceptors.response.use( + (response) => { + const res = response.data; + if (res.code !== 0 && res.code !== 200) { + Message({ + message: res.msg || res.message || '请求失败', + type: 'error', + duration: 5 * 1000, + }); + return Promise.reject(new Error(res.msg || '请求失败')); + } + return res.data; + }, + (error) => { + const msg = error.response + ? `网络请求失败 (${error.response.status})` + : '网络连接失败,请检查服务器是否启动'; + Message({ message: msg, type: 'error', duration: 5 * 1000 }); + return Promise.reject(error); + }, +); + +export default service; diff --git a/backend-adminend/src/views/integral-external/order/index.vue b/backend-adminend/src/views/integral-external/order/index.vue new file mode 100644 index 0000000..205f8ed --- /dev/null +++ b/backend-adminend/src/views/integral-external/order/index.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/backend-adminend/src/views/integral-external/user-integral-detail/index.vue b/backend-adminend/src/views/integral-external/user-integral-detail/index.vue new file mode 100644 index 0000000..922c9ea --- /dev/null +++ b/backend-adminend/src/views/integral-external/user-integral-detail/index.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/backend-adminend/src/views/integral-external/user/index.vue b/backend-adminend/src/views/integral-external/user/index.vue new file mode 100644 index 0000000..ec78ca1 --- /dev/null +++ b/backend-adminend/src/views/integral-external/user/index.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/backend/.mvn/wrapper/maven-wrapper.jar b/backend/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java b/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java index 95e27fc..0ab04c6 100644 --- a/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java +++ b/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java @@ -148,6 +148,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers("/api/admin/store/product/copy/**").permitAll() .antMatchers("/api/admin/merchandise/select").permitAll() .antMatchers("/api/admin/merchandise/update").permitAll() + // 积分模块外部免认证只读接口(供 /integral-external/* 页面调用) + .antMatchers("/api/external/integral/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() diff --git a/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java new file mode 100644 index 0000000..7198bc7 --- /dev/null +++ b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/ExternalIntegralController.java @@ -0,0 +1,92 @@ +package com.zbkj.admin.controller; + +import com.zbkj.common.page.CommonPage; +import com.zbkj.common.request.*; +import com.zbkj.common.response.StoreOrderDetailResponse; +import com.zbkj.common.response.UserIntegralRecordResponse; +import com.zbkj.common.response.UserResponse; +import com.zbkj.common.result.CommonResult; +import com.zbkj.service.service.StoreOrderService; +import com.zbkj.service.service.UserIntegralRecordService; +import com.zbkj.service.service.UserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 积分模块外部免认证接口 Controller + * 供管理后台外部页面(/integral-external/*)调用,跳过登录验证。 + * 所有接口仅提供只读查询能力,不包含任何写操作。 + * + * 安全说明:此 Controller 映射路径已在 WebSecurityConfig 中配置为 permitAll。 + * 建议生产环境配合 IP 白名单或反向代理层访问控制使用。 + */ +@Slf4j +@RestController +@RequestMapping("api/external/integral") +@Api(tags = "积分外部免认证接口") +public class ExternalIntegralController { + + @Autowired + private UserIntegralRecordService integralRecordService; + + @Autowired + private StoreOrderService storeOrderService; + + @Autowired + private UserService userService; + + /** + * 积分明细分页列表(免认证) + * 复用 UserIntegralRecordService.findAdminList,与 /admin/user/integral/list 逻辑完全一致。 + * + * @param request 搜索条件(dateLimit / keywords / uid) + * @param pageParamRequest 分页参数(page / limit) + */ + @ApiOperation(value = "积分明细分页列表(免认证)") + @RequestMapping(value = "/log/list", method = RequestMethod.POST) + public CommonResult> getIntegralLogList( + @RequestBody @Validated AdminIntegralSearchRequest request, + @Validated PageParamRequest pageParamRequest) { + CommonPage restPage = + CommonPage.restPage(integralRecordService.findAdminList(request, pageParamRequest)); + return CommonResult.success(restPage); + } + + /** + * 订单分页列表(免认证) + * 复用 StoreOrderService.getAdminList,与 /admin/store/order/list 逻辑完全一致。 + * + * @param request 搜索条件(status / dateLimit / orderNo / type) + * @param pageParamRequest 分页参数(page / limit) + */ + @ApiOperation(value = "订单分页列表(免认证)") + @GetMapping(value = "/order/list") + public CommonResult> getOrderList( + @Validated StoreOrderSearchRequest request, + @Validated PageParamRequest pageParamRequest) { + CommonPage restPage = + CommonPage.restPage(storeOrderService.getAdminList(request, pageParamRequest)); + return CommonResult.success(restPage); + } + + /** + * 用户分页列表(免认证) + * 复用 UserService.getList,与 /admin/user/list 逻辑完全一致。 + * + * @param request 搜索条件(keywords / dateLimit 等) + * @param pageParamRequest 分页参数(page / limit) + */ + @ApiOperation(value = "用户分页列表(免认证)") + @GetMapping(value = "/user/list") + public CommonResult> getUserList( + @ModelAttribute @Validated UserSearchRequest request, + @Validated PageParamRequest pageParamRequest) { + CommonPage restPage = + CommonPage.restPage(userService.getList(request, pageParamRequest)); + return CommonResult.success(restPage); + } +} diff --git a/docs/integral-pages-coding-plan.md b/docs/integral-pages-coding-plan.md new file mode 100644 index 0000000..399271e --- /dev/null +++ b/docs/integral-pages-coding-plan.md @@ -0,0 +1,626 @@ +# 积分模块新增页面 — Coding Plan + +> 版本:v1.0 +> 日期:2026-03-30 +> 范围:管理后台(backend-adminend)新增积分订单、用户积分、用户积分明细三个独立页面 + +--- + +## 1. 需求概述 + +在管理后台中新增三个独立页面,用于积分业务的外部查看与运营。所有页面需跳过用户登录验证,按后端 API 最小修改原则,尽量复用现有后端接口。 + +| 序号 | 页面 | 参考原页面 | 说明 | +|------|------|-----------|------| +| 1 | 积分订单 | `/order/index` | 新建独立页面,展示积分相关订单 | +| 2 | 用户积分 | `/user/index` | 新建独立页面,增加 `wa_users` 相关字段 | +| 3 | 用户积分明细 | 用户管理 → 账户详情 → 积分明细 | 子页面,复用 `/admin/user/integral/list` 接口 | + +--- + +## 2. 技术架构分析 + +### 2.1 技术栈 + +管理后台前端基于 Vue 2 + Vue CLI + Element UI + Vue Router (history mode) + Vuex + Axios。 + +### 2.2 现有认证机制 + +认证逻辑位于 `src/permission.js`,通过 `router.beforeEach` 全局守卫实现。未登录时除白名单路由外,一律重定向至 `/login`。 + +白名单当前值:`['/login', '/auth-redirect']` + +请求拦截器(`src/utils/request.js`)会在 header 中附加 `Authori-zation` token,后端返回 401 时自动跳转登录页。 + +### 2.3 关键参考文件 + +| 文件 | 说明 | +|------|------| +| `src/views/order/index.vue` | 订单列表页,约 40k 行,含筛选/表格/分页/操作 | +| `src/views/user/list/index.vue` | 用户管理页,含多条件筛选、用户详情弹窗 | +| `src/views/user/integral/index.vue` | 积分日志页(242 行),表格 + 搜索 + 分页 | +| `src/api/integral.js` | 积分接口:`integralListApi` → POST `/admin/user/integral/list` | +| `src/api/user.js` | 用户接口:`userListApi` → GET `/admin/user/list` | +| `src/router/modules/order.js` | 订单路由定义 | +| `src/router/modules/user.js` | 用户路由定义 | +| `src/router/index.js` | 主路由,含 `constantRoutes` 和白名单 | +| `src/permission.js` | 全局路由守卫(登录校验) | +| `src/utils/request.js` | Axios 封装,token 注入 & 401 拦截 | + +### 2.4 wa_users 表字段(需要在用户积分页展示) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | int | 主键 | +| `username` | string | 用户名 | +| `nickname` | string | 昵称 | +| `mobile` | string | 手机号 | +| `money` | BigDecimal | 账户余额 | +| `selfBonus` | BigDecimal | 个人奖金 | +| `shareBonus` | BigDecimal | 分享奖金 | +| `score` | BigDecimal | 积分 | +| `level` | int | 等级 | +| `status` | int | 状态(0=禁用, 1=启用) | +| `isVip` | int | VIP(0=否, 1=是) | +| `isResell` | int | 可转卖(0=否, 1=是) | +| `joinTime` | timestamp | 注册时间 | +| `lastTime` | timestamp | 最后登录 | + +--- + +## 3. 整体方案设计 + +### 3.1 目录结构规划 + +``` +src/ +├── views/ +│ └── integral-external/ # 新增:积分外部页面目录 +│ ├── order/ +│ │ └── index.vue # 积分订单页面 +│ ├── user/ +│ │ └── index.vue # 用户积分页面 +│ └── user-integral-detail/ +│ └── index.vue # 用户积分明细子页面 +├── api/ +│ └── integralExternal.js # 新增:积分外部页面 API 集合 +├── router/ +│ └── modules/ +│ └── integralExternal.js # 新增:积分外部路由模块 +└── layout/ + └── EmptyLayout.vue # 新增:空白布局(无侧边栏/顶栏) +``` + +### 3.2 跳过登录验证方案 + +采用**多层级免登录**策略,确保页面完全绕过认证: + +**第一层:路由白名单** + +在 `src/permission.js` 的 `whiteList` 中添加新页面路径前缀: + +```js +const whiteList = ['/login', '/auth-redirect', '/integral-external']; +``` + +同时修改白名单匹配逻辑,从精确匹配改为前缀匹配: + +```js +// 修改前 +if (whiteList.indexOf(to.path) !== -1) + +// 修改后 +if (whiteList.some(path => to.path.startsWith(path))) +``` + +**第二层:无 token 请求支持** + +新建一个不注入 token、不拦截 401 的 Axios 实例 `requestNoAuth`,供外部页面 API 使用: + +```js +// src/utils/requestNoAuth.js +import axios from 'axios'; +import { Message } from 'element-ui'; +import SettingMer from '@/utils/settingMer'; + +const service = axios.create({ + baseURL: SettingMer.apiBaseURL, + timeout: 60000, +}); + +// 不注入 token,不拦截 401 跳转 +service.interceptors.response.use( + (response) => { + const res = response.data; + if (res.code !== 0 && res.code !== 200) { + Message({ message: res.msg || '请求失败', type: 'error' }); + return Promise.reject(new Error(res.msg || '请求失败')); + } + return res.data; + }, + (error) => { + Message({ message: '网络请求失败', type: 'error' }); + return Promise.reject(error); + }, +); + +export default service; +``` + +**第三层:空白布局** + +新建 `EmptyLayout.vue`,不包含侧边栏、顶栏和权限组件,作为外部页面的容器: + +```vue + +``` + +--- + +## 4. 各页面详细设计 + +### 4.1 积分订单页面 + +**路由**:`/integral-external/order` +**参考**:`src/views/order/index.vue` + +#### 功能要点 + +从原订单页面中提取积分订单相关的核心功能,去除权限校验(`v-hasPermi`)和管理操作(编辑、发货、退款等),保留只读展示。 + +#### 筛选条件 + +| 筛选项 | 类型 | 说明 | +|--------|------|------| +| 订单状态 | RadioGroup | 全部/未支付/未发货/待收货/交易完成 等 | +| 时间选择 | DateRangePicker | 快捷选项 + 自定义范围 | +| 订单号 | Input | 精确搜索 | + +#### 表格列 + +| 列 | 字段 | 宽度 | +|----|------|------| +| 订单号 | `orderId` | 210 | +| 订单类型 | `orderType` | 110 | +| 收货人 | `realName` | 100 | +| 商品信息 | `productList` | 400 | +| 支付金额 | `payPrice` | 100 | +| 支付方式 | `payType` | 100 | +| 订单状态 | `status` | 100 | +| 创建时间 | `createTime` | 150 | + +#### API 复用 + +直接复用现有订单列表接口。需确认后端是否允许无 token 调用,若不允许,需后端新增一个免认证的订单查询接口(或在已有接口上增加免认证标注)。 + +```js +// src/api/integralExternal.js +export function getIntegralOrderList(params) { + return requestNoAuth({ + url: '/admin/order/list', // 复用原接口或后端新增免认证接口 + method: 'get', + params, + }); +} +``` + +#### 实现步骤 + +1. 复制 `order/index.vue` 为基础模板 +2. 删除所有 `v-hasPermi` 权限指令 +3. 删除操作列(编辑价格、发货、退款等按钮) +4. 删除导出功能 +5. 将 API 调用替换为 `requestNoAuth` 版本 +6. 简化订单类型筛选,只保留积分相关类型 +7. 去除 Vuex store 依赖 + +--- + +### 4.2 用户积分页面 + +**路由**:`/integral-external/user` +**参考**:`src/views/user/list/index.vue` + +#### 功能要点 + +基于用户列表页精简,增加 `wa_users` 表的积分/奖金相关字段展示,提供积分明细跳转入口。 + +#### 筛选条件 + +| 筛选项 | 类型 | 说明 | +|--------|------|------| +| 用户搜索 | Input | 姓名/手机号/用户名 | +| 时间选择 | DateRangePicker | 注册时间范围 | + +#### 表格列 + +| 列 | 字段 | 来源 | 说明 | +|----|------|------|------| +| 用户ID | `uid` | CRMEB | 系统用户ID | +| 用户昵称 | `nickname` | CRMEB | — | +| 手机号 | `phone` | CRMEB | — | +| 系统积分 | `integral` | CRMEB | CRMEB 系统积分 | +| WA用户名 | `wa_username` | wa_users | WA系统用户名 | +| 账户余额 | `wa_money` | wa_users | WA账户余额 | +| 个人奖金 | `wa_selfBonus` | wa_users | 可提现奖金 | +| 分享奖金 | `wa_shareBonus` | wa_users | 推荐奖金 | +| WA积分 | `wa_score` | wa_users | WA系统积分 | +| 用户等级 | `wa_level` | wa_users | — | +| 状态 | `wa_status` | wa_users | 启用/禁用 | +| 注册时间 | `createTime` | CRMEB | — | +| 操作 | — | — | 查看积分明细 | + +#### API 方案 + +**方案 A(推荐 — 最小后端修改)**:前端分别调用用户列表接口和 WA 用户信息接口,在前端做数据合并。 + +```js +// 复用原用户列表 +export function getUserListNoAuth(params) { + return requestNoAuth({ + url: '/admin/user/list', + method: 'get', + params, + }); +} + +// 复用前端 WA 用户信息接口(需确认是否免认证) +export function getWaUserInfo(userId) { + return requestNoAuth({ + url: '/api/front/wa/user/info', + method: 'post', + data: { userId }, + }); +} +``` + +**方案 B(若后端配合)**:后端新增一个聚合接口,一次性返回 CRMEB 用户 + wa_users 的合并数据。 + +#### 操作列 + +"查看积分明细" 按钮,点击后跳转至用户积分明细子页面,携带 `uid` 参数: + +```js +this.$router.push({ + path: '/integral-external/user/integral-detail', + query: { uid: row.uid }, +}); +``` + +#### 实现步骤 + +1. 以 `user/list/index.vue` 为参考创建精简版页面 +2. 删除所有权限指令、分组/标签/等级筛选、操作按钮(编辑、设为分销员等) +3. 删除 Tab 切换(全部/有效/无效用户) +4. 在表格中增加 wa_users 字段列 +5. 实现前端数据合并逻辑(逐行匹配或批量查询) +6. 添加"查看积分明细"操作按钮 +7. 将所有 API 替换为 `requestNoAuth` 版本 + +--- + +### 4.3 用户积分明细子页面 + +**路由**:`/integral-external/user/integral-detail` +**参考**:`src/views/user/integral/index.vue`(242 行) +**后端 API**:POST `/admin/user/integral/list`(复用) + +#### 功能要点 + +该页面完整复用原积分日志页的展示逻辑,通过 URL query 参数 `uid` 锁定指定用户,隐藏"用户搜索"字段。 + +#### 筛选条件 + +| 筛选项 | 类型 | 说明 | +|--------|------|------| +| 用户ID | 隐藏字段 | 从 URL query `uid` 自动获取 | +| 时间选择 | DateRangePicker | 日期范围 | + +#### 表格列(完全复用原页面) + +| 列 | 字段 | 说明 | +|----|------|------| +| ID | `id` | 记录ID | +| 用户ID | `uid` | — | +| 用户昵称 | `nickName` | — | +| 标题 | `title` | 积分变动标题 | +| 积分变动 | `integral` | +/- 显示 | +| 剩余积分 | `balance` | 变动后余额 | +| 类型 | `type` | 增加(1)/扣减(2) | +| 关联类型 | `linkType` | 订单/签到/系统 | +| 状态 | `status` | 订单创建/冻结期/完成/失效 | +| 备注 | `mark` | — | +| 创建时间 | `createTime` | — | + +#### API 复用 + +```js +export function getIntegralLogNoAuth(data) { + return requestNoAuth({ + url: '/admin/user/integral/list', // 直接复用原接口 + method: 'post', + data, + }); +} +``` + +#### 页面头部信息 + +在表格上方展示当前用户的积分概览卡片: + +``` +┌──────────────────────────────────────┐ +│ 用户:张三 (UID: 1001) │ +│ 积分:1,200 个人奖金:350 │ +│ [← 返回用户积分列表] │ +└──────────────────────────────────────┘ +``` + +> **字段说明**:积分取自 `eb_user` 表的 `integral` 字段(`BigDecimal`,用户剩余积分);个人奖金取自 `wa_users` 表的 `selfBonus` 字段。 + +#### 实现步骤 + +1. 复制 `user/integral/index.vue` 作为基础 +2. 从 URL query 中读取 `uid`,自动注入搜索参数 +3. 隐藏"用户搜索"和"用户ID"输入框(已通过 query 锁定) +4. 添加顶部用户信息概览卡片 +5. 添加"返回"按钮 +6. 替换 API 为 `requestNoAuth` 版本 + +--- + +## 5. 路由配置 + +### 5.1 新增路由模块 + +```js +// src/router/modules/integralExternal.js +const EmptyLayout = () => import('@/layout/EmptyLayout'); + +const integralExternalRouter = { + path: '/integral-external', + component: EmptyLayout, + redirect: '/integral-external/order', + hidden: true, // 不在侧边栏显示 + children: [ + { + path: 'order', + component: () => import('@/views/integral-external/order/index'), + name: 'IntegralExternalOrder', + meta: { title: '积分订单' }, + }, + { + path: 'user', + component: () => import('@/views/integral-external/user/index'), + name: 'IntegralExternalUser', + meta: { title: '用户积分' }, + }, + { + path: 'user/integral-detail', + component: () => import('@/views/integral-external/user-integral-detail/index'), + name: 'IntegralExternalUserDetail', + meta: { title: '用户积分明细' }, + }, + ], +}; + +export default integralExternalRouter; +``` + +### 5.2 注册路由 + +在 `src/router/index.js` 中将新模块加入 `constantRoutes`: + +```js +import integralExternalRouter from './modules/integralExternal'; + +export const constantRoutes = [ + integralExternalRouter, // 积分外部页面(免登录) + storeRouter, + orderRouter, + // ...其余路由 +]; +``` + +### 5.3 修改权限守卫 + +在 `src/permission.js` 中扩展白名单: + +```js +const whiteList = ['/login', '/auth-redirect', '/integral-external']; + +// 匹配逻辑改为前缀匹配 +if (whiteList.some(path => to.path.startsWith(path))) { + next(); +} +``` + +--- + +## 6. 后端 API 影响评估 + +### 6.1 可直接复用的接口 + +| 接口 | Method | 免认证现状 | 所需改动 | +|------|--------|-----------|---------| +| `/admin/user/integral/list` | POST | 需认证 | 需后端为外部调用新增免认证入口,或前端伪造 token | +| `/admin/user/list` | GET | 需认证 | 同上 | +| `/admin/order/list` | GET | 需认证 | 同上 | + +### 6.2 推荐的后端最小改动方案 + +按照"最小修改原则",建议后端在现有 Controller 基础上新增一套免认证的映射路径,内部直接调用相同的 Service 方法: + +``` +新路径 → 复用的 Service 方法 +/api/external/integral/order/list → OrderService.list() +/api/external/integral/user/list → UserService.list()(补充 wa_users 字段) +/api/external/integral/log/list → IntegralService.list() +``` + +只需新建一个 `ExternalIntegralController`,加 `@RestController` 免认证注解,约 50-80 行代码。 + +### 6.3 wa_users 字段集成 + +**方案 A**:后端在用户列表接口返回中直接 JOIN wa_users 表,新增字段返回。 +**方案 B**:前端先拿用户列表,再批量查 wa_users 信息,前端做合并。 + +推荐方案 A(后端改动更少,前端实现更简单)。 + +--- + +## 7. 开发任务清单 + +### Phase 1:基础设施(预计 0.5 天) + +| # | 任务 | 文件 | +|---|------|------| +| 1.1 | 创建 `EmptyLayout.vue` 空白布局 | `src/layout/EmptyLayout.vue` | +| 1.2 | 创建 `requestNoAuth.js` 免认证请求实例 | `src/utils/requestNoAuth.js` | +| 1.3 | 创建 `integralExternal.js` 路由模块 | `src/router/modules/integralExternal.js` | +| 1.4 | 注册路由到 `constantRoutes` | `src/router/index.js` | +| 1.5 | 修改 `permission.js` 白名单 | `src/permission.js` | +| 1.6 | 创建 `integralExternal.js` API 文件 | `src/api/integralExternal.js` | + +### Phase 2:积分订单页面(预计 1 天) + +| # | 任务 | +|---|------| +| 2.1 | 基于 `order/index.vue` 创建精简版积分订单页 | +| 2.2 | 去除权限校验、操作按钮、导出功能 | +| 2.3 | 接入 `requestNoAuth` 请求 | +| 2.4 | 测试筛选、分页、数据展示 | + +### Phase 3:用户积分页面(预计 1.5 天) + +| # | 任务 | +|---|------| +| 3.1 | 基于 `user/list/index.vue` 创建精简版用户积分页 | +| 3.2 | 去除高级筛选、权限、操作按钮 | +| 3.3 | 增加 wa_users 字段列(奖金、积分、余额等) | +| 3.4 | 实现数据合并逻辑(前端或后端) | +| 3.5 | 添加"查看积分明细"跳转按钮 | +| 3.6 | 测试数据展示与跳转 | + +### Phase 4:用户积分明细子页面(预计 0.5 天) + +| # | 任务 | +|---|------| +| 4.1 | 基于 `user/integral/index.vue` 创建积分明细页 | +| 4.2 | 通过 URL query 读取 uid 并锁定用户 | +| 4.3 | 添加用户积分概览卡片 | +| 4.4 | 添加返回按钮 | +| 4.5 | 接入 `requestNoAuth` 请求 | +| 4.6 | 测试分页、筛选、数据展示 | + +### Phase 5:联调与验收(预计 0.5 天) + +| # | 任务 | +|---|------| +| 5.1 | 无 token 状态下完整流程测试 | +| 5.2 | 页面间跳转逻辑验证 | +| 5.3 | 后端免认证接口联调 | +| 5.4 | 兼容性和响应式测试 | + +**总计预估工时:4 天** + +--- + +## 8. 测试方案 + +### 8.1 免登录访问测试 + +| 编号 | 测试场景 | 操作步骤 | 预期结果 | +|------|---------|---------|---------| +| A-01 | 无 token 直接访问积分订单页 | 清除浏览器所有 cookie/sessionStorage,直接访问 `/integral-external/order` | 页面正常加载,不跳转至 `/login` | +| A-02 | 无 token 直接访问用户积分页 | 同上,访问 `/integral-external/user` | 页面正常加载,不跳转至 `/login` | +| A-03 | 无 token 直接访问积分明细页 | 同上,访问 `/integral-external/user/integral-detail?uid=1` | 页面正常加载,不跳转至 `/login` | +| A-04 | 免登录页面不影响原有认证 | 无 token 访问 `/order/index`(原页面) | 仍然正常跳转至 `/login` | +| A-05 | 已登录用户访问免登录页面 | 管理员登录后访问 `/integral-external/order` | 页面正常加载,不受登录态影响 | + +### 8.2 积分订单页面测试 + +| 编号 | 测试场景 | 操作步骤 | 预期结果 | +|------|---------|---------|---------| +| B-01 | 默认加载 | 进入页面 | 表格展示订单列表,分页信息正确 | +| B-02 | 按订单状态筛选 | 依次点击"未支付""未发货""交易完成"等状态 | 表格数据按状态正确过滤,数量标签更新 | +| B-03 | 按时间范围筛选 | 选择起止日期 | 仅显示时间范围内的订单 | +| B-04 | 按订单号搜索 | 输入完整订单号,点击搜索 | 精确匹配到对应订单 | +| B-05 | 重置筛选条件 | 设置筛选条件后点击重置 | 所有筛选项恢复默认,表格展示全部数据 | +| B-06 | 分页切换 | 切换页码、修改每页显示数 | 数据正确刷新,分页器状态正确 | +| B-07 | 空数据状态 | 搜索不存在的订单号 | 表格显示空状态提示,无 JS 报错 | +| B-08 | 无操作列 | 检查表格列 | 不存在编辑、发货、退款等操作按钮 | + +### 8.3 用户积分页面测试 + +| 编号 | 测试场景 | 操作步骤 | 预期结果 | +|------|---------|---------|---------| +| C-01 | 默认加载 | 进入页面 | 用户列表正常展示,含 CRMEB 和 wa_users 字段 | +| C-02 | wa_users 字段展示 | 查看表格列 | 个人奖金(`selfBonus`)、账户余额(`money`)等 wa_users 字段正确显示 | +| C-03 | 积分字段来源验证 | 对比数据库 `eb_user.integral` 值 | 页面显示的积分与 `eb_user` 表一致 | +| C-04 | wa_users 无关联数据 | 查看无 wa_users 记录的 CRMEB 用户行 | wa_users 相关列显示 `-` 或 `0`,不报错 | +| C-05 | 用户搜索 | 输入姓名/手机号搜索 | 正确过滤,支持模糊匹配 | +| C-06 | 跳转积分明细 | 点击某用户行的"查看积分明细" | 正确跳转至 `/integral-external/user/integral-detail?uid=xxx` | +| C-07 | 分页功能 | 切换页码和每页条数 | 数据正确刷新 | +| C-08 | 无权限指令残留 | 审查页面 DOM | 不存在 `v-hasPermi` 相关的隐藏元素或报错 | + +### 8.4 用户积分明细子页面测试 + +| 编号 | 测试场景 | 操作步骤 | 预期结果 | +|------|---------|---------|---------| +| D-01 | 带 uid 参数加载 | 访问 `?uid=1001` | 自动加载 uid=1001 的积分明细,顶部概览卡片显示用户信息 | +| D-02 | 概览卡片数据验证 | 对比数据库值 | 积分值 = `eb_user.integral`,个人奖金 = `wa_users.selfBonus` | +| D-03 | 无 uid 参数访问 | 访问不带 `?uid` 参数的页面 | 页面给出"缺少用户参数"提示,或重定向至用户积分列表 | +| D-04 | 无效 uid 访问 | 访问 `?uid=999999`(不存在的用户) | 表格为空,概览卡片显示空状态,无 JS 报错 | +| D-05 | 时间范围筛选 | 选择日期范围 | 积分明细按时间正确过滤 | +| D-06 | 积分变动显示 | 查看积分变动列 | 增加显示绿色 `+`,扣减显示红色 `-` | +| D-07 | 状态与关联类型 | 查看状态和关联类型列 | 订单创建/冻结期/完成/失效 正确渲染标签颜色;订单/签到/系统 正确显示 | +| D-08 | 返回按钮 | 点击"返回用户积分列表" | 正确跳转回 `/integral-external/user` | +| D-09 | 分页功能 | 切换页码(15/30/45/60) | 数据正确刷新 | + +### 8.5 接口与数据测试 + +| 编号 | 测试场景 | 操作步骤 | 预期结果 | +|------|---------|---------|---------| +| E-01 | 免认证接口可达性 | 无 token 调用各外部接口 | 返回 200 及正确业务数据,不返回 401 | +| E-02 | 原认证接口不受影响 | 无 token 调用原 `/admin/user/list` 等接口 | 仍返回 401 | +| E-03 | 接口仅读不写 | 尝试对免认证接口发送写操作请求 | 返回 403 或方法不允许 | +| E-04 | 大数据量分页 | 请求 limit=60,数据总量 > 1000 | 分页正确,响应时间 < 3s | +| E-05 | 边界参数 | page=0、limit=-1、uid=null 等异常参数 | 接口返回友好错误信息,不产生 500 | +| E-06 | 数据脱敏验证 | 检查返回的手机号字段 | 中间 4 位做掩码处理(如 `138****8888`) | + +### 8.6 兼容性与 UI 测试 + +| 编号 | 测试场景 | 预期结果 | +|------|---------|---------| +| F-01 | Chrome 最新版 | 页面布局正常,功能正常 | +| F-02 | Firefox 最新版 | 页面布局正常,功能正常 | +| F-03 | Edge 最新版 | 页面布局正常,功能正常 | +| F-04 | 1920×1080 分辨率 | 表格列宽合理,无横向滚动条溢出 | +| F-05 | 1366×768 分辨率 | 表格可横向滚动,筛选栏自动换行 | +| F-06 | EmptyLayout 布局验证 | 页面无侧边栏、无顶部导航栏、无面包屑 | +| F-07 | 加载状态 | 数据加载中显示 loading 动画 | + +### 8.7 测试执行时间规划 + +| 阶段 | 内容 | 预计时间 | +|------|------|---------| +| 冒烟测试 | Phase 1 基础设施完成后,验证 A-01 ~ A-05 免登录链路 | 0.5h | +| 功能测试 | 每个页面开发完成后,执行对应 B/C/D 组用例 | 每页面 1~2h | +| 接口联调测试 | 后端免认证接口就绪后,执行 E 组用例 | 1h | +| 回归测试 | 全部开发完成后,执行全量用例 | 2h | +| 兼容性测试 | 回归通过后,执行 F 组用例 | 1h | + +--- + +## 9. 注意事项 + +1. **安全风险**:免登录页面直接暴露后台数据,建议后端对免认证接口做 IP 白名单或 API Key 鉴权。 +2. **数据脱敏**:用户手机号等敏感字段建议做中间位掩码处理(如 `138****8888`)。 +3. **接口幂等**:所有免认证接口仅开放读取权限(GET/查询),禁止写操作。 +4. **路由隔离**:新页面使用 `EmptyLayout`,与管理后台主布局完全隔离,避免引入侧边栏/权限组件的副作用。 +5. **组件依赖**:新页面可复用 Element UI 组件,但避免引入需要 Vuex store(如用户信息、权限)的业务组件。 diff --git a/docs/integral-pages-schedule.md b/docs/integral-pages-schedule.md new file mode 100644 index 0000000..64aad01 --- /dev/null +++ b/docs/integral-pages-schedule.md @@ -0,0 +1,106 @@ +# 积分模块新增页面 — 2 小时极速排期 + +> 关联文档:[integral-pages-coding-plan.md](./integral-pages-coding-plan.md) +> 开发人员:1 人(全栈) +> 时间窗口:2026-03-30 20:35 ~ 22:35(共 120 分钟) + +--- + +## 1. 时间线总览 + +``` +20:35 20:55 21:25 21:55 22:15 22:25 22:35 + ├─────────────┼─────────────┼─────────────┼──────────────┼────────┼────────┤ + │ Phase 1 │ Phase 2 │ Phase 3 │ Phase 4 │Phase 5 │ 收尾 │ + │ 基础设施 │ 积分订单页 │ 用户积分页 │ 积分明细页 │ 联调 │ 提交 │ + │ 20min │ 30min │ 30min │ 20min │ 10min │ 10min │ + └─────────────┴─────────────┴─────────────┴──────────────┴────────┴────────┘ +``` + +--- + +## 2. 分段任务明细 + +### Phase 1:基础设施(20:35 ~ 20:55,20 min) + +| 时间 | 任务 | 产出物 | +|------|------|--------| +| 20:35 ~ 20:40 | 创建 `EmptyLayout.vue` + `requestNoAuth.js` | 布局组件 + 免认证请求实例 | +| 20:40 ~ 20:45 | 创建路由模块 `integralExternal.js`,注册到 `constantRoutes` | 路由配置 | +| 20:45 ~ 20:50 | 修改 `permission.js` 白名单为前缀匹配 | 免登录机制生效 | +| 20:50 ~ 20:55 | 创建 API 文件 `integralExternal.js` + 快速冒烟验证(访问空页面不跳登录) | API 框架 + 冒烟通过 | + +**20:55 检查点**:无 token 访问 `/integral-external/order` 不跳转登录页 ✅ + +--- + +### Phase 2:积分订单页面(20:55 ~ 21:25,30 min) + +| 时间 | 任务 | 产出物 | +|------|------|--------| +| 20:55 ~ 21:10 | 从 `order/index.vue` 裁剪:只保留表格 + 筛选 + 分页,删除权限指令/操作列/导出 | 页面主体 | +| 21:10 ~ 21:20 | 替换 API 为 `requestNoAuth`,去除 Vuex 依赖 | 接口对接完成 | +| 21:20 ~ 21:25 | 快速自测:列表加载、状态筛选、分页切换 | 自测通过 | + +**21:25 检查点**:积分订单页数据可正常展示和筛选 ✅ + +--- + +### Phase 3:用户积分页面(21:25 ~ 21:55,30 min) + +| 时间 | 任务 | 产出物 | +|------|------|--------| +| 21:25 ~ 21:35 | 从 `user/list/index.vue` 裁剪:删除高级筛选/Tab/权限/操作按钮 | 页面主体 | +| 21:35 ~ 21:45 | 增加 wa_users 字段列(积分、个人奖金、余额等),实现数据合并 | 字段展示 | +| 21:45 ~ 21:50 | 添加"查看积分明细"跳转按钮,替换 API | 跳转功能 | +| 21:50 ~ 21:55 | 快速自测:列表加载、wa_users 字段、跳转明细 | 自测通过 | + +**21:55 检查点**:用户积分页含 wa_users 字段,点击可跳转明细页 ✅ + +--- + +### Phase 4:用户积分明细子页面(21:55 ~ 22:15,20 min) + +| 时间 | 任务 | 产出物 | +|------|------|--------| +| 21:55 ~ 22:05 | 复制 `user/integral/index.vue`,从 URL query 读取 uid 注入搜索参数 | 页面主体 | +| 22:05 ~ 22:10 | 添加顶部概览卡片(积分 from eb_user + 个人奖金 from wa_users)+ 返回按钮 | 概览卡片 | +| 22:10 ~ 22:15 | 替换 API,快速自测:明细列表、分页、返回跳转 | 自测通过 | + +**22:15 检查点**:积分明细页带 uid 参数可正常展示,概览卡片数据正确 ✅ + +--- + +### Phase 5:联调验证 + 提交(22:15 ~ 22:35,20 min) + +| 时间 | 任务 | 产出物 | +|------|------|--------| +| 22:15 ~ 22:25 | 无 token 全流程走查:订单页 → 用户页 → 点击明细 → 返回 | 流程通过 | +| 22:25 ~ 22:30 | 修复走查发现的问题 | Bug Fix | +| 22:30 ~ 22:35 | 清理 console.log,git commit | 代码提交 | + +**22:35 完成**:全部三个页面开发完成并提交 ✅ + +--- + +## 3. 极速开发策略 + +为了在 2 小时内完成,采取以下策略: + +1. **大量复制-裁剪**:不从零编写,直接复制原页面再删减,效率最高 +2. **跳过样式美化**:使用原页面样式,不做额外 UI 调整 +3. **后端接口先复用**:直接调用原有 `/admin/` 接口,免认证改造延后处理 +4. **数据合并从简**:wa_users 字段优先用前端逐行查询方式,性能优化后续迭代 +5. **测试精简**:每个页面只做核心功能冒烟,全量测试用例留给后续回归 + +--- + +## 4. 关键检查点 + +| 时间 | 检查项 | 不通过时的应对 | +|------|--------|--------------| +| 20:55 | 免登录链路跑通 | 停下排查 permission.js,这是后续一切的前提 | +| 21:25 | 订单页可展示数据 | 若裁剪受阻,直接用最小化表格(5 列 + 分页) | +| 21:55 | 用户页含 wa_users 字段 | 若合并逻辑复杂,先只展示 CRMEB 字段,wa_users 留 TODO | +| 22:15 | 明细页 uid 传参正常 | 原页面仅 242 行,风险最低 | +| 22:35 | 代码提交 | 即使有小问题也先提交,记录 TODO 后续修复 | diff --git a/docs/integral-pages-test-report-v2.md b/docs/integral-pages-test-report-v2.md new file mode 100644 index 0000000..4cfec33 --- /dev/null +++ b/docs/integral-pages-test-report-v2.md @@ -0,0 +1,168 @@ +# 积分模块新增页面 — 功能测试报告 v2 + +**测试时间:** 2026-03-31 +**测试范围:** Coding Plan 交付清单功能验证(静态分析 + 结构检查) +**测试结果:** ✅ 全部通过(11/11 项) + +--- + +## T01 — 交付文件存在性检查 + +| 文件 | 结果 | +|---|:---:| +| `src/layout/EmptyLayout.vue` | ✅ PASS | +| `src/utils/requestNoAuth.js` | ✅ PASS | +| `src/router/modules/integralExternal.js` | ✅ PASS | +| `src/router/index.js`(已注册) | ✅ PASS | +| `src/api/integralExternal.js` | ✅ PASS | +| `src/permission.js`(已修改) | ✅ PASS | +| `src/filters/user.js`(已修改) | ✅ PASS | +| `src/views/integral-external/order/index.vue` | ✅ PASS | +| `src/views/integral-external/user/index.vue` | ✅ PASS | +| `src/views/integral-external/user-integral-detail/index.vue` | ✅ PASS | +| `ExternalIntegralController.java` | ✅ PASS | + +**11/11 文件存在** + +--- + +## T02 — permission.js 白名单前缀检查 + +```js +const whiteList = ['/login', '/auth-redirect']; +const whiteListPrefixes = ['/integral-external']; +// ... +if (whiteList.indexOf(to.path) !== -1 + || whiteListPrefixes.some(prefix => to.path.startsWith(prefix))) { + next(); +} +``` + +- ✅ `whiteListPrefixes` 已定义并包含 `/integral-external` +- ✅ 使用 `startsWith` 前缀匹配(支持所有子路径) + +--- + +## T03 — router/index.js 注册检查 + +- ✅ `import integralExternalRouter from './modules/integralExternal'` 已添加 +- ✅ `integralExternalRouter` 已加入 `constantRoutes` + +--- + +## T04 — 新页面无权限指令检查 + +| 页面 | v-hasPermi | checkPermi | +|---|:---:|:---:| +| order/index.vue | ✅ 无 | ✅ 无 | +| user/index.vue | ✅ 无 | ✅ 无 | +| user-integral-detail/index.vue | ✅ 无 | ✅ 无 | + +**三个页面均不含任何权限指令,符合免认证要求。** + +--- + +## T05 — phoneDesensitize 过滤器链路 + +1. ✅ `filters/user.js` 导出 `phoneDesensitize` 函数 +2. ✅ `filters/index.js` 通过 `export * from './user'` 自动 re-export +3. ✅ `main.js` 通过 `Object.keys(filters).forEach` 全局注册所有过滤器 +4. ✅ `user/index.vue` 正确使用 `{{ scope.row.phone | phoneDesensitize }}` + +--- + +## T06 — API 函数与后端路径一致性 + +| API 函数 | 前端 URL | HTTP 方法 | +|---|---|:---:| +| `getExternalOrderList` | `external/integral/order/list` | GET | +| `getExternalUserList` | `external/integral/user/list` | GET | +| `getExternalIntegralLog` | `external/integral/log/list` | POST | + +所有 URL 与 `ExternalIntegralController` 中的映射路径完全一致。 + +--- + +## T07 — 文件语法结构检查 + +| 文件 | template | script | name 属性 | 括号平衡 | +|---|:---:|:---:|:---:|:---:| +| EmptyLayout.vue | ✅ | ✅ | ✅ | ✅ | +| order/index.vue | ✅ | ✅ | ✅ | ✅ | +| user/index.vue | ✅ | ✅ | ✅ | ✅ | +| user-integral-detail/index.vue | ✅ | ✅ | ✅ | ✅ | + +--- + +## T08 — 路由路径一致性 + +| 路由定义(子路径) | 完整路径 | 跳转来源 | +|---|---|---| +| `order` | `/integral-external/order` | 默认 redirect | +| `user` | `/integral-external/user` | — | +| `user/integral-detail` | `/integral-external/user/integral-detail` | user/index.vue `$router.push` | + +- ✅ `user/index.vue` 导航路径 `/integral-external/user/integral-detail` 与路由定义一致 + +--- + +## T09 — EmptyLayout 引用链 + +- ✅ `integralExternal.js` 动态引入 `EmptyLayout` +- ✅ `EmptyLayout.vue` 包含 ``(子页面正确渲染) + +--- + +## T10 — requestNoAuth 免认证验证 + +- ✅ `api/integralExternal.js` 使用 `requestNoAuth` 实例(非 `request`) +- ✅ `requestNoAuth.js` 请求拦截器中**无**任何 `Authorization` Header 注入逻辑 +- ✅ `requestNoAuth.js` 响应拦截器中**无** 401 重定向到登录页逻辑 + +--- + +## T11 — 后端 Java 检查 + +| 检查项 | 结果 | +|---|:---:| +| `@RestController` 注解 | ✅ PASS | +| `@RequestMapping("api/external/integral")` | ✅ PASS | +| `/order/list` → `@GetMapping` | ✅ PASS(与前端 GET 一致) | +| `/user/list` → `@GetMapping` | ✅ PASS(与前端 GET 一致) | +| `/log/list` → `@PostMapping` | ✅ PASS(与前端 POST 一致) | +| **无 `@PreAuthorize`** | ✅ PASS | +| `WebSecurityConfig` permitAll 白名单 | ✅ PASS | + +--- + +## 汇总 + +| 测试项 | 通过 | 失败 | +|---|:---:|:---:| +| T01 文件存在性(11项) | 11 | 0 | +| T02 路由白名单前缀 | 1 | 0 | +| T03 路由注册 | 1 | 0 | +| T04 无权限指令(3页) | 3 | 0 | +| T05 过滤器链路(4环节) | 4 | 0 | +| T06 API 路径一致性(3接口) | 3 | 0 | +| T07 文件语法结构(4文件) | 4 | 0 | +| T08 路由路径一致性 | 1 | 0 | +| T09 EmptyLayout 引用链 | 2 | 0 | +| T10 免认证验证(3项) | 3 | 0 | +| T11 后端 Java(7项) | 7 | 0 | +| **合计** | **40** | **0** | + +> ✅ **40/40 全部通过** — 交付物满足 Coding Plan 所有功能需求,可进入联调阶段。 + +--- + +## 待联调验证(需运行环境) + +以下项目需在实际启动前后端后验证: + +- [ ] 浏览器访问 `/integral-external/order` 不跳转登录页 +- [ ] 订单列表数据正确渲染(含商品图片) +- [ ] 用户列表手机号脱敏显示(138\*\*\*\*5678) +- [ ] 点击"查看积分明细"正确传参 uid 并跳转 +- [ ] 积分明细页概览卡片显示正确的积分 & 个人奖金 +- [ ] 返回按钮回到用户积分列表 diff --git a/docs/integral-pages-test-report.md b/docs/integral-pages-test-report.md new file mode 100644 index 0000000..b95e744 --- /dev/null +++ b/docs/integral-pages-test-report.md @@ -0,0 +1,169 @@ +# 积分模块新增页面 — 测试报告 + +> 执行时间:2026-03-30 +> 测试类型:静态代码分析(新增页面尚未开发,针对现有代码库做预检) +> 测试依据:integral-pages-coding-plan.md § 8 测试方案 + +--- + +## 总体结论 + +| 维度 | 状态 | 说明 | +|------|------|------| +| 新增页面文件 | ❌ 未创建 | 三个新页面均未开发,开发尚未启动 | +| 免登录基础设施 | ❌ 未实现 | `permission.js` / `EmptyLayout` / `requestNoAuth` 均未修改 | +| 参考页面可裁剪性 | ✅ 可行 | 原页面结构清晰,具备裁剪条件 | +| 后端接口认证机制 | ⚠️ 有阻塞 | 积分接口有 `@PreAuthorize` 强认证,需后端配合新增免认证路径 | + +--- + +## A 组:免登录访问测试 + +> 前提:`EmptyLayout.vue` / `requestNoAuth.js` / 路由 / `permission.js` 白名单均**尚未修改** + +| 编号 | 测试场景 | 结果 | 详情 | +|------|---------|------|------| +| A-01 | 无 token 访问积分订单页 | ❌ **FAIL** | `permission.js` 白名单仅含 `['/login', '/auth-redirect']`,精确 `indexOf` 匹配,`/integral-external/order` 会被重定向至 `/login` | +| A-02 | 无 token 访问用户积分页 | ❌ **FAIL** | 同 A-01,无对应白名单条目 | +| A-03 | 无 token 访问积分明细页 | ❌ **FAIL** | 同 A-01 | +| A-04 | 免登录页面不影响原有认证 | ✅ **PASS** | 原有 `/order/index` 等路径未做变更,仍需登录 | +| A-05 | 已登录用户访问免登录页面 | ⏭️ **SKIP** | 新页面路由未注册,无法访问 | + +**A 组结论**:需在 `permission.js` 第 21 行修改白名单,并将第 59 行 `indexOf` 改为 `startsWith` 前缀匹配。 + +**修改方案**: +```js +// permission.js 第 21 行 +const whiteList = ['/login', '/auth-redirect', '/integral-external']; + +// 第 59 行 +if (whiteList.some(path => to.path.startsWith(path))) { +``` + +--- + +## B 组:积分订单页面测试 + +> 参考文件:`src/views/order/index.vue`(1182 行) + +| 编号 | 测试场景 | 结果 | 详情 | +|------|---------|------|------| +| B-01 | 默认加载 | ⏭️ **SKIP** | 页面未创建 | +| B-02 | 按订单状态筛选 | ⏭️ **SKIP** | 页面未创建 | +| B-03 | 按时间范围筛选 | ⏭️ **SKIP** | 页面未创建 | +| B-04 | 按订单号搜索 | ⏭️ **SKIP** | 页面未创建 | +| B-05 | 重置筛选条件 | ⏭️ **SKIP** | 页面未创建 | +| B-06 | 分页切换 | ⏭️ **SKIP** | 页面未创建 | +| B-07 | 空数据状态 | ⏭️ **SKIP** | 页面未创建 | +| B-08 | 无操作列 | ⚠️ **PRE-CHECK** | 原页面含 **11 处** `v-hasPermi`、`发货/退款/出库` 操作按钮、导出功能,裁剪时需逐一清理 | + +**B 组预检发现**: +- `v-hasPermi` 出现 11 次,需全部移除 +- 导出按钮在第 79 行:`导出` +- `exports()` 方法在第 896 行,需连同方法一起删除 +- 原页面**无 Vuex store 直接依赖**,裁剪负担较轻 + +--- + +## C 组:用户积分页面测试 + +> 参考文件:`src/views/user/list/index.vue`(1079 行) + +| 编号 | 测试场景 | 结果 | 详情 | +|------|---------|------|------| +| C-01 | 默认加载 | ⏭️ **SKIP** | 页面未创建 | +| C-02 | wa_users 字段展示 | ⏭️ **SKIP** | 页面未创建 | +| C-03 | 积分字段来源验证 | ⚠️ **PRE-CHECK** | `integral` 字段已在原 `user/list` 表格中(第 227 行),`eb_user.integral` 字段存在(`User.java` 第 98 行),来源正确 | +| C-04 | wa_users 无关联数据 | ⚠️ **PRE-CHECK** | admin 端无现成的 wa_users API,需前端补充处理空值逻辑 | +| C-05 | 用户搜索 | ⏭️ **SKIP** | 页面未创建 | +| C-06 | 跳转积分明细 | ⏭️ **SKIP** | 页面未创建 | +| C-07 | 分页功能 | ⏭️ **SKIP** | 页面未创建 | +| C-08 | 无权限指令残留 | ⚠️ **PRE-CHECK** | 原页面含 **15 处** `v-hasPermi`,裁剪时均需移除 | + +**C 组预检发现**: +- `integral` 字段已在原用户列表接口中返回,**无需后端改动** +- admin 端**无独立的 wa_users 查询 API**,需新增或复用 `consignment.js` 中的 `selfBonusLogListApi` 辅助拼合 +- 需删除的高级筛选项:等级、分组、标签、国家/省份、消费情况、访问情况、性别、身份(共 8 个筛选项) + +--- + +## D 组:用户积分明细子页面测试 + +> 参考文件:`src/views/user/integral/index.vue`(241 行) + +| 编号 | 测试场景 | 结果 | 详情 | +|------|---------|------|------| +| D-01 | 带 uid 参数加载 | ⚠️ **PRE-CHECK** | 原页面 `searchForm.uid` 已存在,只需在 `mounted()` 从 `$route.query.uid` 注入即可 | +| D-02 | 概览卡片数据验证 | ⚠️ **PRE-CHECK** | 积分来自 `eb_user.integral` ✅;个人奖金来自 `wa_users.selfBonus`(admin 端无现成 API)⚠️ | +| D-03 | 无 uid 参数访问 | ⚠️ **PRE-CHECK** | 原页面无 uid 校验逻辑,需在 `mounted()` 添加 fallback 处理 | +| D-04 | 无效 uid 访问 | ⚠️ **PRE-CHECK** | 后端返回空列表即可,前端需处理空状态显示 | +| D-05 | 时间范围筛选 | ✅ **PRE-PASS** | 原页面已有完整 `DateRangePicker` 实现,直接复用 | +| D-06 | 积分变动显示 | ✅ **PRE-PASS** | 原页面已实现 `type===1` 绿色 `+`、否则红色 `-` 逻辑(第 65-66 行) | +| D-07 | 状态与关联类型 | ✅ **PRE-PASS** | `linkTypeFilter` / `statusFilter` / `statusTypeFilter` 三个方法完整(第 196-223 行) | +| D-08 | 返回按钮 | ⚠️ **PRE-CHECK** | 原页面无返回按钮,需手动添加 | +| D-09 | 分页功能 | ✅ **PRE-PASS** | `[15, 30, 45, 60]` 分页完整实现,直接复用 | + +**D 组结论**:参考页面仅 241 行,复用度最高(5/9 项可直接复用),是三个页面中风险最低的。 + +--- + +## E 组:接口与后端认证测试 + +| 编号 | 测试场景 | 结果 | 详情 | +|------|---------|------|------| +| E-01 | 免认证接口可达性 | ❌ **FAIL** | `UserIntegralController.getList()` 有 `@PreAuthorize("hasAuthority('admin:user:integral:list')")`,无 token 必返回 401 | +| E-02 | 原认证接口不受影响 | ✅ **PASS** | 原接口认证逻辑未变动 | +| E-03 | 接口仅读不写 | ✅ **PASS** | 积分 list 接口为 POST 查询,无写操作 | +| E-04 | 大数据量分页 | ⏭️ **SKIP** | 待联调时测试 | +| E-05 | 边界参数 | ⏭️ **SKIP** | 待联调时测试 | +| E-06 | 数据脱敏验证 | ❌ **FAIL** | 当前 admin 接口无脱敏处理,用户手机号明文返回 | + +**E 组关键发现**: +- 后端 `WebSecurityConfig` 的 `permitAll` 白名单**不包含** `/api/admin/user/integral/**` +- 需后端在 `WebSecurityConfig` 第 121 行附近新增: + ```java + .antMatchers("/api/admin/user/integral/list").permitAll() + ``` + 或新建 `ExternalIntegralController` 映射至免认证路径 + +--- + +## F 组:兼容性与 UI 测试 + +| 编号 | 测试场景 | 结果 | +|------|---------|------| +| F-01 ~ F-07 | 全部兼容性测试 | ⏭️ **SKIP** — 页面未创建,待开发完成后执行 | + +--- + +## 问题汇总(需在开发中修复) + +| 优先级 | 问题 | 影响范围 | 解决方案 | +|--------|------|---------|---------| +| 🔴 P0 | `permission.js` 白名单未更新 | A 组全部 FAIL | 修改白名单为前缀匹配 | +| 🔴 P0 | 后端积分接口有 `@PreAuthorize` 强认证 | E-01 FAIL | 后端新增免认证路径或 controller | +| 🟠 P1 | admin 端无独立 wa_users 查询 API | C-04、D-02 阻塞 | 复用寄卖模块的 `selfBonusLogListApi` 或后端新增聚合接口 | +| 🟠 P1 | 用户手机号无脱敏处理 | E-06 FAIL | 后端接口或前端 filter 处理 `138****8888` | +| 🟡 P2 | 原订单页 11 处权限指令需清理 | B-08 | 开发时逐一删除 | +| 🟡 P2 | 原用户列表页 15 处权限指令需清理 | C-08 | 开发时逐一删除 | +| 🟡 P2 | 积分明细页缺少 uid 空值校验和返回按钮 | D-03、D-08 | 开发时添加 | + +--- + +## 测试覆盖统计 + +| 组别 | 总用例 | PASS | FAIL | PRE-CHECK | SKIP | +|------|--------|------|------|-----------|------| +| A 组(免登录) | 5 | 1 | 3 | 0 | 1 | +| B 组(订单页) | 8 | 0 | 0 | 1 | 7 | +| C 组(用户积分页) | 8 | 0 | 0 | 3 | 5 | +| D 组(积分明细页) | 9 | 4 | 0 | 5 | 0 | +| E 组(接口) | 6 | 2 | 2 | 0 | 2 | +| F 组(兼容性) | 7 | 0 | 0 | 0 | 7 | +| **合计** | **43** | **7** | **5** | **9** | **22** | + +> PASS = 代码层面已满足条件;FAIL = 存在明确问题需修复;PRE-CHECK = 有条件可实现,开发时需注意;SKIP = 页面未创建,待开发完成后执行 + +--- + +*报告生成时间:2026-03-30* diff --git a/docs/newpage.md b/docs/newpage.md new file mode 100644 index 0000000..042413b --- /dev/null +++ b/docs/newpage.md @@ -0,0 +1,14 @@ +# 管理后台中积分模块新增如下页面 + +## 积分订单页面 +- 新建页面,参考原页面:/order/index + +## 用户积分页面 +- 新建页面,参考原页面:/user/index,增加wa_users的相关字段 + +### 用户积分明细子页面 +- 一个新建积分明细页面,参考原页面:“user/index 用户管理-》账户详情-》积分明细”,延用原后端api:/marketing/integral/integrallog + +## 备注 +- 所有新建页面跳过用户登陆状态验证 +- 按照后端api最小修改原则,尽量延用原后端api \ No newline at end of file diff --git a/docs/openclaw_agent_configuration_v2.plan.md b/docs/openclaw_agent_configuration_v2.plan.md new file mode 100644 index 0000000..2ff286c --- /dev/null +++ b/docs/openclaw_agent_configuration_v2.plan.md @@ -0,0 +1,1019 @@ +--- +name: OpenClaw Agent Configuration v2 +overview: 在现有 OpenClaw 环境(6 个已运行 Agent + 飞书 channel)中增量添加 4 个积分商城 Agent(integral-pm/backend/frontend/qa),采用精简 Skill 策略、明确 Agent 间通信协议、分级部署权限、版本锁定约束。 +todos: + - id: update-openclaw-json + content: 在现有 openclaw.json 中追加 4 个 Agent 和 Feishu bindings(不影响已有配置) + status: pending + - id: create-pm-workspace + content: 创建 PM Agent workspace 全套文件(IDENTITY/SOUL/AGENTS/USER/TOOLS.md) + status: pending + - id: create-backend-workspace + content: 创建 Backend Agent workspace 全套文件 + status: pending + - id: create-frontend-workspace + content: 创建 Frontend Agent workspace 全套文件 + status: pending + - id: create-qa-workspace + content: 创建 QA Agent workspace 全套文件 + status: pending + - id: register-and-verify + content: 通过 openclaw CLI 注册 4 个 Agent、安装 skills、运行 doctor 验证 + status: pending +isProject: false +--- + +# OpenClaw 多 Agent 配置方案 v2 -- 单商户积分商城 + +> **v2 变更摘要(相对 v1):** +> +> 1. 新增「Agent 间通信协议」章节,明确消息路由机制 +> 2. 飞书账号从 4 个独立应用简化为 1 个共享应用 + 消息路由 +> 3. 每个 Agent 的 Skill 精简至 ≤8 个,降低推理噪声 +> 4. QA 部署流程增加 PM 审批卡点,生产环境需人工确认 +> 5. SOUL.md 增加严格的技术栈版本锁定声明 +> 6. 新增故障恢复与任务状态管理机制 +> 7. 新增安全性约束(SSH 密钥引用、环境分级) + +--- + +## 一、现有环境概况与约束 + +### 已有 OpenClaw 配置(不可变动) + +- **运行环境:** macOS (darwin 25.3.0),OpenClaw 已安装运行 +- **配置文件:** `~/.openclaw/openclaw.json` +- **已有 Agents(6 个,保持不变):** main, miao, msh, jxy, mom, my-production +- **Channel:** Feishu(飞书),已配置 4 个 accounts: default, msh, jxy, mom +- **默认模型:** minimax-portal/MiniMax-M2.5(fallback: kimi-coding/k2p5 等) +- **Gateway:** 端口 18789, local 模式 + +### 新增部分(本方案范围) + +- **编码工具:** Cursor IDE +- **版本管理:** Gitea (`http://49.235.131.69:3000/scottpan/integral-shop.git`) +- **部署方式:** SSH 远程部署(脚本在 `backend/shell/`) +- **项目路径:** `/Users/apple/scott2026/integral-shop/single-shop-22` +- **新增 4 个 Agent:** integral-pm, integral-backend, integral-frontend, integral-qa +- **新增 4 个 workspace:** `~/.openclaw/workspace-integral-{pm,backend,frontend,qa}/` + +> 所有新增 Agent ID 使用 `integral-` 前缀,避免与已有 Agent 冲突。 + +--- + +## 二、Agent 角色设计(4 个) + +小型项目将设计职能合并到 PM,将两个前端合并为一个 Frontend Agent,QA 兼管部署验证(但部署需 PM 审批)。 + +```mermaid +flowchart TB + User[用户/飞书] -->|提需求| PM["integral-pm (PM + 设计)"] + PM -->|后端任务 + API 设计| BE[integral-backend] + PM -->|前端任务 + UI 规范| FE["integral-frontend (管理后台 + 用户端)"] + PM -->|测试计划| QA["integral-qa (测试 + 部署验证)"] + BE -->|API 就绪| FE + BE -->|提测| QA + FE -->|提测| QA + QA -->|Bug 反馈| BE + QA -->|Bug 反馈| FE + QA -->|测试报告| PM + QA -.->|部署申请| PM + PM -.->|部署审批| QA +``` + +| Agent ID | 角色 | 职责范围 | +| --------------------- | --------- | ---------------------------------- | +| **integral-pm** | 项目经理 + 设计 | 需求拆解、PRD、UI 设计规范、任务分派、进度跟踪、部署审批 | +| **integral-backend** | 后端开发 | Java/Spring Boot 接口开发、数据库变更、API 文档 | +| **integral-frontend** | 前端开发 | 管理后台 Vue + 用户端 uni-app 开发 | +| **integral-qa** | 测试 + 部署 | 测试用例、功能测试、回归测试、部署执行(需 PM 审批) | + +--- + +## 三、Agent 间通信协议(v2 新增) + +### 3.1 通信方式 + +Agent 间通过 **飞书群消息** 进行异步通信。创建一个专用飞书群「积分商城-协作」,4 个 Agent 机器人全部加入。 + +```mermaid +flowchart LR + subgraph feishuGroup ["飞书群:积分商城-协作"] + PM_bot["积分商城-PM"] + BE_bot["积分商城-后端"] + FE_bot["积分商城-前端"] + QA_bot["积分商城-QA"] + end + PM_bot <-->|@mention 路由| BE_bot + PM_bot <-->|@mention 路由| FE_bot + PM_bot <-->|@mention 路由| QA_bot + BE_bot -->|API 就绪通知| FE_bot + BE_bot -->|提测通知| QA_bot + FE_bot -->|提测通知| QA_bot + QA_bot -->|Bug 反馈| BE_bot + QA_bot -->|Bug 反馈| FE_bot +``` + +### 3.2 消息协议格式 + +所有 Agent 间通信遵循统一的结构化消息格式: + +``` +【<消息类型>】<标题> +发送方: +接收方: @ +关联任务: +--- +<消息正文> +``` + +**消息类型枚举:** + +| 类型 | 发送方 | 接收方 | 说明 | +| -------- | ----- | ----------- | -------------- | +| 任务分派 | PM | BE/FE/QA | 含 PRD 链接、验收标准 | +| API-就绪 | BE | FE | 含接口文档、变更说明 | +| 提测通知 | BE/FE | QA | 含分支名、变更范围、自测结果 | +| Bug-反馈 | QA | BE/FE | 含复现步骤、期望/实际结果 | +| 测试报告 | QA | PM | 含通过率、遗留问题 | +| 部署申请 | QA | PM | 含环境、版本、测试结论 | +| 部署审批 | PM | QA | 含审批结果、注意事项 | +| 进度更新 | 任意 | PM | 每日或里程碑节点 | + +### 3.3 任务状态机 + +```mermaid +stateDiagram-v2 + [*] --> Created: PM 创建任务 + Created --> InProgress: 开发 Agent 认领 + InProgress --> CodeReview: 代码提交 + CodeReview --> Testing: QA 接收提测 + Testing --> BugFound: 发现 Bug + BugFound --> InProgress: 开发修复 + Testing --> Passed: 测试通过 + Passed --> DeployApproval: QA 申请部署 + DeployApproval --> Deploying: PM 审批通过 + DeployApproval --> Passed: PM 驳回(需补充) + Deploying --> Done: 部署验证通过 + Deploying --> BugFound: 部署验证失败 + Done --> [*] +``` + +任务状态记录在 `tasks/.md` 文件中,PM Agent 负责状态汇总。 + +--- + +## 四、openclaw.json 增量修改 + +**原则:只追加,不修改已有配置。** + +### 4.1 在 `agents.list` 数组末尾追加 4 个 Agent + +```json +{ + "id": "integral-pm", + "name": "integral-pm", + "workspace": "/Users/apple/.openclaw/workspace-integral-pm", + "agentDir": "/Users/apple/.openclaw/agents/integral-pm/agent", + "model": "kimi-coding/k2p5" +}, +{ + "id": "integral-backend", + "name": "integral-backend", + "workspace": "/Users/apple/.openclaw/workspace-integral-backend", + "agentDir": "/Users/apple/.openclaw/agents/integral-backend/agent", + "model": "kimi-coding/k2p5" +}, +{ + "id": "integral-frontend", + "name": "integral-frontend", + "workspace": "/Users/apple/.openclaw/workspace-integral-frontend", + "agentDir": "/Users/apple/.openclaw/agents/integral-frontend/agent", + "model": "kimi-coding/k2p5" +}, +{ + "id": "integral-qa", + "name": "integral-qa", + "workspace": "/Users/apple/.openclaw/workspace-integral-qa", + "agentDir": "/Users/apple/.openclaw/agents/integral-qa/agent", + "model": "kimi-coding/k2p5" +} +``` + +> **模型分工:** OpenClaw 层统一 kimi-coding/k2p5(对话协调),Cursor CLI 层 PM 用 `--model claude-4.6-opus`,其余用 `--model auto`。 + +### 4.2 飞书配置方案(v2 简化) + +**v1 方案:** 4 个独立飞书应用 → 运维成本高 +**v2 方案:** 1 个共享飞书应用「积分商城」 + OpenClaw 内部路由 + +在飞书开放平台只创建 **1 个** 机器人应用「积分商城-Bot」,获取一组 appId/appSecret。通过 OpenClaw 的 `match` 规则中的 `metadata` 或 `keyword` 字段做 Agent 路由: + +```json +// channels.feishu.accounts 中只新增 1 个账号 +"integral-shop": { + "appId": "<飞书开放平台-积分商城Bot-appId>", + "appSecret": "<飞书开放平台-积分商城Bot-appSecret>", + "agent": "integral-pm", + "dmPolicy": "open", + "allowFrom": ["*"] +} +``` + +```json +// bindings 中追加 4 条路由,通过消息前缀关键词分发 +{ + "agentId": "integral-pm", + "match": { + "channel": "feishu", + "accountId": "integral-shop", + "keyword": "^@PM|^/pm" + } +}, +{ + "agentId": "integral-backend", + "match": { + "channel": "feishu", + "accountId": "integral-shop", + "keyword": "^@后端|^/backend" + } +}, +{ + "agentId": "integral-frontend", + "match": { + "channel": "feishu", + "accountId": "integral-shop", + "keyword": "^@前端|^/frontend" + } +}, +{ + "agentId": "integral-qa", + "match": { + "channel": "feishu", + "accountId": "integral-shop", + "keyword": "^@QA|^/qa" + } +} +``` + +> **降级路由:** 没有匹配关键词的消息默认路由到 integral-pm(PM 负责分发)。 +> +> **备选方案:** 如果 OpenClaw 不支持 `keyword` 路由,则回退到 v1 的 4 个独立飞书应用方案(见附录 A)。 + +### 4.3 不变动的部分 + +以下配置保持原样不动:`agents.defaults`、`models`、`auth`、`gateway`、`plugins`、`channels.feishu` 根级配置、`messages`、`commands`、`session`、现有 6 个 Agent 和 4 条 binding。 + +--- + +## 五、双模型架构:OpenClaw + Cursor CLI + +| Agent | OpenClaw 模型 | Cursor CLI 模型 | 说明 | +| ----------------- | ---------------- | ------------------------- | -------------------- | +| integral-pm | kimi-coding/k2p5 | `--model claude-4.6-opus` | 需求分析、架构设计需要最强推理 | +| integral-backend | kimi-coding/k2p5 | `--model auto` | Java 代码编写,Cursor 自动选 | +| integral-frontend | kimi-coding/k2p5 | `--model auto` | Vue/uni-app 编写 | +| integral-qa | kimi-coding/k2p5 | `--model auto` | 测试代码、Bug 分析 | + +--- + +## 六、Skills 精简分配(v2 核心变更) + +### 6.1 精简原则 + +- 每个 Agent 的 Skill 总数 **≤ 8 个**(含内置 Tools) +- 优先启用与角色核心职责直接相关的 Skill +- `capability-evolver` 和 `self-improving-agent` 仅分配给 PM(由 PM 统一管理 Agent 进化策略,避免 4 个 Agent 各自"进化"导致行为漂移) +- ClawHub Skill 初期不安装,待基础流程跑通后按需引入 + +### 6.2 精简后的分配矩阵 + +| Skill / Tool | integral-pm | integral-backend | integral-frontend | integral-qa | +| --------------------- | :---------: | :--------------: | :---------------: | :---------: | +| git | ● | ● | ● | ● | +| file-manager | ● | ● | ● | ● | +| web-search | ● | - | - | - | +| browser | ● | - | ● | ● | +| code-runner | - | ● | ● | ● | +| http-request | - | ● | - | ● | +| gitea-version-control | ● | ● | ● | ● | +| cursor-cli | ● (opus) | ● (auto) | ● (auto) | ● (auto) | +| maven-helper | - | ● | - | - | +| spring-boot-engineer | - | ● | - | - | +| frontend-design | ● | - | ● | - | +| proactive-agent | ● | - | - | - | +| self-improving-agent | ● | - | - | - | +| peekaboo | - | - | - | ● | +| **合计** | **8** | **8** | **8** | **8** | + +### 6.3 移除说明 + +相比 v1 移除的 Skill 及理由: + +| 移除的 Skill | 原分配 | 移除理由 | +| ------------------- | -------- | ------------------------------- | +| agent-builder | PM | 初期配置稳定后无需频繁修改 Agent,手动操作即可 | +| skill-finder | PM | 按需手动搜索即可,不必常驻 | +| kie-ai-skill | PM | AI 图片生成非核心流程,UI 规范以文字描述为主 | +| cron-scheduler | QA | 定时回归测试是后期优化项,初期手动触发 | +| capability-evolver | 全部 → PM | 集中管理进化策略,避免行为漂移 | +| self-improving-agent | 全部 → PM | 同上,由 PM 统一记录和分发改进经验 | +| api-dev (ClawHub) | BE/QA | 初期不引入,spring-boot-engineer 已覆盖基础能力 | +| code-reviewer | BE/FE | 初期不引入,代码审查由 Cursor 内置能力完成 | +| agent-browser | FE/QA | 初期不引入,browser 内置工具已满足基础需求 | +| summarize | PM/QA | 初期不引入,摘要能力由模型原生能力完成 | + +### 6.4 后续可引入的 Skill 队列 + +当基础流程稳定运行 1-2 周后,按优先级逐步引入: + +1. **P1(第 2 周):** code-reviewer → BE/FE(代码质量有明确需求时) +2. **P2(第 3 周):** cron-scheduler → QA(需要定时回归测试时) +3. **P3(按需):** api-dev, agent-browser, summarize, capability-evolver(扩展到其他 Agent) + +--- + +## 七、各 Agent Workspace 详细配置 + +### workspace 统一目录结构 + +``` +~/.openclaw/workspace-integral-/ + IDENTITY.md # Agent 身份标识 + SOUL.md # 人格、沟通风格、行为准则、版本约束 + AGENTS.md # 操作规范、工作流、编码标准、通信协议 + USER.md # 用户信息 + TOOLS.md # 环境说明、工具清单、部署信息 + MEMORY.md # 长期记忆 + memory/ # 每日记忆日志 + plans/ # PRD / 计划文档 + tasks/ # 任务文档(含状态机) +``` + +--- + +### 1. PM Agent (integral-pm) + +**workspace 路径:** `~/.openclaw/workspace-integral-pm/` + +**IDENTITY.md:** + +``` +name: 积分商城PM +emoji: clipboard +theme: professional +``` + +**SOUL.md 核心内容:** + +```markdown +## 角色定义 +项目经理兼 UI 设计指导,同时负责 Agent 团队的进化管理。 + +## 沟通风格 +- 结构化、简洁、中文为主 +- 任务分派必须使用标准消息协议格式(见 AGENTS.md) + +## 核心能力 +需求分析、任务拆解、进度追踪、UI 布局和风格指导、部署审批。 + +## 决策原则 +- MVP 优先、增量迭代 +- 技术方案交由开发 Agent 决定,PM 不干预实现细节 +- 部署审批必须确认:测试通过率 ≥ 95%、无 P0 Bug、QA 签字 + +## 设计输出 +以文字描述 + 参考截图形式交付 UI 规范,不做独立设计稿。 +管理后台遵循 Element UI 2.13 风格,用户端遵循现有积分商城 H5 风格。 + +## Agent 进化管理(v2 新增) +- 统一记录各 Agent 的错误模式和改进经验到 memory/agent-improvements.md +- 定期(每周)审阅并分发改进建议给各 Agent +- 禁止各 Agent 自行修改自身 SOUL.md 或 AGENTS.md +``` + +**AGENTS.md 核心内容:** + +```markdown +## 工作流 +1. 收到需求 -> 写 PRD 到 plans/.md +2. 拆解为子任务 -> 写入 tasks/--.md +3. 通过飞书群 @mention 分别通知 integral-backend / integral-frontend / integral-qa + +## 任务文件格式 +文件名: tasks/.md +内容包含: +- 优先级: P0/P1/P2 +- 状态: Created/InProgress/CodeReview/Testing/Passed/Deploying/Done +- 验收标准(AC) +- 依赖关系 +- 负责 Agent + +## 通信协议 +所有 Agent 间消息必须遵循以下格式: +【<消息类型>】<标题> +发送方: integral-pm +接收方: @ +关联任务: +--- +<消息正文> + +## 部署审批流程 +1. 收到 QA 的【部署申请】消息 +2. 检查:测试报告通过率 ≥ 95%、无 P0 Bug +3. 确认部署环境(测试环境直接批准,生产环境需 @用户 人工确认) +4. 回复【部署审批】消息 + +## Cursor 使用 +- 命令: agent --model claude-4.6-opus +- 用途: 需求分析、代码审阅、架构设计 +- 示例: agent --model claude-4.6-opus "分析 backend/crmeb-front 中积分模块的代码结构" + +## 每日进度 +汇总到 memory/YYYY-MM-DD.md +``` + +**TOOLS.md 核心内容:** + +```markdown +## 项目信息 +- 源码路径: /Users/apple/scott2026/integral-shop/single-shop-22 +- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git +- 编码工具: Cursor IDE (macOS) + +## 子项目结构 +- backend/ -> Java Spring Boot 后端 +- backend-adminend/ -> 管理后台 Vue 前端 +- single_uniapp22miao/ -> 用户端 uni-app H5 + +## Cursor CLI 配置 +- 模型: agent --model claude-4.6-opus +- 项目目录: /Users/apple/scott2026/integral-shop/single-shop-22 + +## 启用的 Skills (8个) +- 内置: git, file-manager, web-search, browser +- 本地: gitea-version-control, cursor-cli, frontend-design, proactive-agent +- 额外职责: self-improving-agent(统一管理 Agent 进化) +``` + +--- + +### 2. Backend Agent (integral-backend) + +**workspace 路径:** `~/.openclaw/workspace-integral-backend/` + +**IDENTITY.md:** + +``` +name: 后端开发 +emoji: gear +theme: technical +``` + +**SOUL.md 核心内容:** + +```markdown +## 角色定义 +Java 后端开发工程师,负责积分商城后端接口开发。 + +## 技术栈版本锁定(严格遵守,不可升级) +- Java: 1.8(禁止使用 Java 9+ 特性,如 var、模块化、Records) +- Spring Boot: 2.2.6.RELEASE(禁止使用 Spring Boot 3.x API) +- MyBatis Plus: 3.3.1 +- MySQL: 5.7(禁止使用 MySQL 8.0 特性,如窗口函数、CTE) +- Maven: 3.6.1 +- Redis: 5.x 兼容 API + +## 编码规范 +- 阿里 Java 开发规约 +- RESTful API 设计 +- 接口变更须给出 Swagger 格式文档并说明影响范围 +- 代码编写在 Cursor IDE 中完成 + +## 沟通风格 +技术精确,接口文档完整。变更通知必须包含:变更接口列表、请求/响应格式变化、影响的前端页面。 + +## 禁止行为 +- 禁止引入新的 Maven 依赖(除非 PM 明确批准) +- 禁止修改 pom.xml 中已有依赖的版本号 +- 禁止修改 application.yml 中的端口和数据库配置 +- 禁止自行修改本文件(SOUL.md)或 AGENTS.md +``` + +**AGENTS.md 核心内容:** + +```markdown +## 代码范围 +/Users/apple/scott2026/integral-shop/single-shop-22/backend/ + +## 模块结构 +- crmeb-admin: 管理后台 API +- crmeb-front: 用户端 API +- crmeb-service: 业务逻辑 +- crmeb-common: 公共模块 + +## 开发流程 +1. 从 PM 任务获取需求 -> 在 develop 基础上创建 feature/backend- 分支 +2. 在 Cursor 中编码: agent --model auto +3. 新增接口须同步更新 Swagger 注解 +4. 数据库变更编写 SQL 迁移脚本 -> backend/sql/-.sql +5. 本地构建验证: mvn clean compile -pl -am +6. Push 到 Gitea +7. 在飞书群通知 @integral-frontend API 变更(使用【API-就绪】消息格式) +8. 在飞书群通知 @integral-qa 提测(使用【提测通知】消息格式) + +## 通信协议 +遵循 PM 定义的标准消息格式。 + +## 故障恢复(v2 新增) +- Cursor CLI 超时/失败时:记录错误到 memory/errors.md,回退未完成的代码变更(git stash),在飞书通知 PM +- 构建失败时:分析 mvn 错误日志,尝试修复,3 次失败后上报 PM +``` + +**TOOLS.md 核心内容:** + +```markdown +## 开发环境 (macOS) +- Java: 1.8 +- Maven: 3.6.1 +- IDE: Cursor +- 项目路径: /Users/apple/scott2026/integral-shop/single-shop-22/backend + +## 本地运行 +- Admin API: mvn spring-boot:run -pl crmeb-admin (端口 8080) +- Front API: mvn spring-boot:run -pl crmeb-front (端口 8081) + +## 打包 +- Admin: mvn clean package -pl crmeb-admin -am -DskipTests +- Front: mvn clean package -pl crmeb-front -am -DskipTests + +## 版本管理 +- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git +- 分支规范: feature/backend-, bugfix/backend- + +## 启用的 Skills (8个) +- 内置: git, file-manager, code-runner, http-request +- 本地: gitea-version-control, cursor-cli, maven-helper, spring-boot-engineer +``` + +--- + +### 3. Frontend Agent (integral-frontend) + +**workspace 路径:** `~/.openclaw/workspace-integral-frontend/` + +**IDENTITY.md:** + +``` +name: 前端开发 +emoji: monitor +theme: creative +``` + +**SOUL.md 核心内容:** + +```markdown +## 角色定义 +全栈前端开发工程师,负责管理后台和用户端。 + +## 技术栈版本锁定(严格遵守,不可升级) + +### 管理后台 (backend-adminend/) +- Vue: 2.6.x(禁止使用 Vue 3 Composition API、 +``` + +**步骤 2:创建 `src/utils/requestNoAuth.js`** +```js +import axios from 'axios' + +const requestNoAuth = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, + timeout: 15000 +}) + +requestNoAuth.interceptors.response.use( + response => response.data, + error => Promise.reject(error) +) + +export default requestNoAuth +``` + +**步骤 3:创建 `src/router/modules/integralExternal.js`** +```js +import EmptyLayout from '@/layout/EmptyLayout' + +const integralExternalRouter = { + path: '/integral-external', + component: EmptyLayout, + children: [ + { path: 'order', name: 'IntegralOrder', component: () => import('@/views/integral/external/order/index') }, + { path: 'user', name: 'IntegralUser', component: () => import('@/views/integral/external/user/index') }, + { path: 'detail', name: 'IntegralDetail', component: () => import('@/views/integral/external/detail/index') } + ] +} + +export default integralExternalRouter +``` + +**步骤 4:修改 `src/permission.js` 白名单为前缀匹配** +```js +// 改为: +const whiteList = ['/login', '/auth-redirect', '/integral-external']; + +// 修改匹配逻辑(约第 55 行): +if (whiteList.some(path => to.path.startsWith(path))) { + next(); +} else { + next(`/login?redirect=${to.path}`); + NProgress.done(); +} +``` + +**步骤 5:创建 `src/api/integralExternal.js`**(基础框架) +```js +import requestNoAuth from '@/utils/requestNoAuth' + +export function getIntegralOrderList(params) { + return requestNoAuth({ url: '/api/integral/order/list', method: 'get', params }) +} + +export function getIntegralUserList(params) { + return requestNoAuth({ url: '/api/integral/user/list', method: 'get', params }) +} + +export function getIntegralDetail(params) { + return requestNoAuth({ url: '/api/integral/detail/list', method: 'get', params }) +} +``` + +--- + +## ⚠️ 重要提示 + +**免登录链路是后续 Phase 2~4 一切工作的前提**,如果 permission.js 白名单不通,所有积分外部页面都无法访问。 + +请优先确保 `permission.js` 的前缀匹配逻辑正确生效后,再进入 Phase 2 开发。 + +当前时间已到 17:30,**建议立即开始 Phase 1 任务**,完成后方可进入 Phase 2:积分订单页面开发。 diff --git a/docs/phase4-checkpoint-report.md b/docs/phase4-checkpoint-report.md new file mode 100644 index 0000000..71821f5 --- /dev/null +++ b/docs/phase4-checkpoint-report.md @@ -0,0 +1,89 @@ +# Phase 4 检查点报告 — 18:50 自动检查 + +> 生成时间:2026-03-30 18:50 +> 检查范围:`backend-adminend/src` + +--- + +## 检查结果汇总 + +| # | 检查项 | 状态 | 说明 | +|---|--------|------|------| +| 1 | 积分明细页面(从 `user/integral/index.vue` 复制并修改) | ❌ **未完成** | `views/integral/external/detail/` 目录不存在,未创建任何外部页面 | +| 2 | URL query 参数 `uid` 自动注入搜索参数 | ❌ **未完成** | 外部积分明细页面未创建,无法验证 uid 参数读取 | +| 3 | 顶部概览卡片(`eb_user.integral` + `wa_users.selfBonus`) | ❌ **未完成** | 无新增页面,概览卡片不存在 | +| 4 | 返回按钮跳回用户积分列表 | ❌ **未完成** | 页面未创建 | +| 5 | 分页和时间筛选 | ❌ **未完成** | 页面未创建 | + +--- + +## ⚠️ 根因分析 + +**Phase 4 的全部 5 项检查均未通过,根本原因是 Phase 1 基础设施仍未搭建。** + +截至本次检查,以下前置依赖均不存在: + +| 前置项 | 状态 | +|--------|------| +| `src/layout/EmptyLayout.vue` | ❌ 未创建 | +| `src/utils/requestNoAuth.js` | ❌ 未创建 | +| `src/router/modules/integralExternal.js` | ❌ 未创建 | +| `src/api/integralExternal.js` | ❌ 未创建 | +| `permission.js` 白名单前缀匹配改造 | ❌ 未修改 | +| `router/index.js` 注册 constantRoutes | ❌ 未修改 | + +Phase 1 → Phase 2 → Phase 3 → Phase 4 均为顺序依赖,无法跳过。 + +--- + +## 源文件就绪情况 + +积分明细源页面 `src/views/user/integral/index.vue` 存在(242 行),结构清晰: + +- ✅ 已有 `searchForm.uid` 字段 — 可直接从 `$route.query.uid` 注入 +- ✅ 已有时间选择器 `daterange` — 分页和时间筛选逻辑可复用 +- ✅ 已有 `integralListApi` 数据请求 — 需替换为 `requestNoAuth` 版本 +- ⬜ 需新增:顶部概览卡片(调用用户详情接口获取 `integral` 和 `selfBonus`) +- ⬜ 需新增:返回按钮(`this.$router.push('/integral-external/user')`) + +改造量确实很小(~50 行修改),**确认源页面仅 242 行,风险最低**。 + +--- + +## 能否进入 Phase 5? + +**❌ 不能进入 Phase 5(联调验证 + 提交)。** + +Phase 5 的前提是 Phase 1~4 全部完成。当前连 Phase 1 都未完成。 + +--- + +## 建议行动 + +### 方案 A:快速补救(推荐) + +如果用户仍有时间,建议按以下**压缩顺序**一次性完成 Phase 1 + Phase 4: + +1. **创建 `EmptyLayout.vue`**(1 分钟) +2. **创建 `requestNoAuth.js`**(2 分钟) +3. **修改 `permission.js` 白名单**(2 分钟) +4. **创建路由模块 + 注册 constantRoutes**(3 分钟) +5. **复制 `user/integral/index.vue` → 外部积分明细页面**(5 分钟) + - 注入 `$route.query.uid` + - 替换 API 为免认证版本 + - 添加概览卡片和返回按钮 +6. **冒烟测试**(5 分钟) + +预计总耗时:~18 分钟 + +### 方案 B:仅完成基础设施 + +如果时间紧张,优先完成 Phase 1 基础设施,确保免登录链路畅通,Phase 4 积分明细页面留到下次。 + +--- + +## 参考文档 + +- 开发计划:`docs/integral-pages-schedule.md` +- 技术方案:`docs/integral-pages-coding-plan.md` +- Phase 1 检查报告:`docs/phase1-checkpoint-report.md`(17:30 生成,全部未通过) diff --git a/start-backend.sh b/start-backend.sh new file mode 100755 index 0000000..f91891d --- /dev/null +++ b/start-backend.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ============================================ +# 启动 Backend API(Spring Boot, dev profile) +# 端口: 20600 MySQL: 127.0.0.1:3306/java_dev +# ============================================ +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/backend" + +echo "📦 Working dir: $(pwd)" + +# ── 自动定位 Java ────────────────────────── +find_java() { + # 1. 系统 java + if /usr/libexec/java_home &>/dev/null; then + echo "$(/usr/libexec/java_home)/bin/java" + return + fi + # 2. Homebrew (Apple Silicon) + for p in /opt/homebrew/opt/openjdk*/bin/java /opt/homebrew/opt/openjdk/bin/java; do + [ -x "$p" ] && echo "$p" && return + done + # 3. Homebrew (Intel) + for p in /usr/local/opt/openjdk*/bin/java /usr/local/opt/openjdk/bin/java; do + [ -x "$p" ] && echo "$p" && return + done + # 4. SDKMAN + [ -n "$SDKMAN_DIR" ] && ls "$SDKMAN_DIR/candidates/java/current/bin/java" 2>/dev/null && \ + echo "$SDKMAN_DIR/candidates/java/current/bin/java" && return + # 5. PATH(排除 macOS 占位符 /usr/bin/java) + local j + j=$(command -v java 2>/dev/null) + if [ -n "$j" ]; then + # 检测是否为 macOS 占位符(会输出 Unable to locate) + if "$j" -version 2>&1 | grep -q "Unable to locate"; then + : # 是占位符,跳过 + else + echo "$j" && return + fi + fi + echo "" +} + +JAVA_BIN=$(find_java) +if [ -z "$JAVA_BIN" ]; then + echo "" + echo "❌ 未找到 Java 运行环境。请先安装 JDK 11:" + echo " brew install openjdk@11" + echo " 然后按照提示设置 JAVA_HOME 后重试。" + exit 1 +fi + +JAVA_VER=$("$JAVA_BIN" -version 2>&1 | head -1) +echo "☕ Java: $JAVA_BIN" +echo " 版本: $JAVA_VER" +echo "" + +export JAVA_HOME="$(dirname $(dirname $JAVA_BIN))" + +echo "🚀 Starting crmeb-admin with profile=dev ..." +echo "" + +./mvnw spring-boot:run \ + -pl crmeb-admin \ + -am \ + -DskipTests \ + -Dspring-boot.run.profiles=dev \ + 2>&1 diff --git a/start-frontend.sh b/start-frontend.sh new file mode 100755 index 0000000..032f6b1 --- /dev/null +++ b/start-frontend.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# ============================================ +# 启动 Frontend Dev Server (Vue 2 + Element UI) +# 端口: 9527 API: 见 .env.development +# ============================================ +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/backend-adminend" + +echo "📦 Working dir: $(pwd)" + +# 如果 node_modules 不存在则先安装 +if [ ! -f "node_modules/.bin/vue-cli-service" ]; then + echo "📥 Installing dependencies ..." + npm install --legacy-peer-deps +fi + +echo "🚀 Starting Vue dev server on http://localhost:9527 ..." +echo "" +npm run dev