feat: 新增积分外部页面(免认证三页 + 配套基础设施)
前端: - 新增 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 <noreply@anthropic.com>
This commit is contained in:
39
backend-adminend/src/api/integralExternal.js
Normal file
39
backend-adminend/src/api/integralExternal.js
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -48,3 +48,17 @@ export function filterIsPromoter(status) {
|
|||||||
};
|
};
|
||||||
return statusMap[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');
|
||||||
|
}
|
||||||
|
|||||||
18
backend-adminend/src/layout/EmptyLayout.vue
Normal file
18
backend-adminend/src/layout/EmptyLayout.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div class="integral-external-layout">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'EmptyLayout',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.integral-external-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,7 +18,9 @@ import getPageTitle from '@/utils/get-page-title';
|
|||||||
|
|
||||||
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
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) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
// start progress bar
|
// start progress bar
|
||||||
@@ -56,7 +58,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* has no token*/
|
/* 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
|
// in the free login whitelist, go directly
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import maintainRouter from './modules/maintain';
|
|||||||
import mobileRouter from './modules/mobile';
|
import mobileRouter from './modules/mobile';
|
||||||
import statistic from './modules/statistic';
|
import statistic from './modules/statistic';
|
||||||
import designRouter from './modules/design';
|
import designRouter from './modules/design';
|
||||||
|
import integralExternalRouter from './modules/integralExternal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: sub-menu only appear when route children.length >= 1
|
* Note: sub-menu only appear when route children.length >= 1
|
||||||
@@ -90,6 +91,8 @@ export const constantRoutes = [
|
|||||||
statistic,
|
statistic,
|
||||||
//装修
|
//装修
|
||||||
designRouter,
|
designRouter,
|
||||||
|
// 积分外部页面(免认证)
|
||||||
|
integralExternalRouter,
|
||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
component: () => import('@/views/error-page/404'),
|
component: () => import('@/views/error-page/404'),
|
||||||
|
|||||||
30
backend-adminend/src/router/modules/integralExternal.js
Normal file
30
backend-adminend/src/router/modules/integralExternal.js
Normal file
@@ -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;
|
||||||
51
backend-adminend/src/utils/requestNoAuth.js
Normal file
51
backend-adminend/src/utils/requestNoAuth.js
Normal file
@@ -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;
|
||||||
254
backend-adminend/src/views/integral-external/order/index.vue
Normal file
254
backend-adminend/src/views/integral-external/order/index.vue
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox relative">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="container">
|
||||||
|
<el-form size="small" label-width="100px">
|
||||||
|
<el-form-item label="订单类型:">
|
||||||
|
<el-radio-group v-model="tableFrom.type" type="button" class="mr20" size="small" @change="seachList">
|
||||||
|
<el-radio-button v-for="(item, i) in typeOptions" :key="i" :label="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单状态:">
|
||||||
|
<el-radio-group v-model="tableFrom.status" type="button" @change="seachList">
|
||||||
|
<el-radio-button label="all">全部</el-radio-button>
|
||||||
|
<el-radio-button label="unPaid">未支付</el-radio-button>
|
||||||
|
<el-radio-button label="notShipped">未发货</el-radio-button>
|
||||||
|
<el-radio-button label="spike">待收货</el-radio-button>
|
||||||
|
<el-radio-button label="bargain">待评价</el-radio-button>
|
||||||
|
<el-radio-button label="complete">交易完成</el-radio-button>
|
||||||
|
<el-radio-button label="refunding">退款中</el-radio-button>
|
||||||
|
<el-radio-button label="refunded">已退款</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间选择:" class="width100">
|
||||||
|
<el-radio-group
|
||||||
|
v-model="tableFrom.dateLimit"
|
||||||
|
type="button"
|
||||||
|
class="mr20"
|
||||||
|
size="small"
|
||||||
|
@change="selectChange(tableFrom.dateLimit)"
|
||||||
|
>
|
||||||
|
<el-radio-button v-for="(item, i) in fromList.fromTxt" :key="i" :label="item.val">
|
||||||
|
{{ item.text }}
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="timeVal"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
format="yyyy-MM-dd"
|
||||||
|
size="small"
|
||||||
|
type="daterange"
|
||||||
|
placement="bottom-end"
|
||||||
|
placeholder="自定义时间"
|
||||||
|
style="width: 220px"
|
||||||
|
@change="onchangeTime"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单号:" class="width100">
|
||||||
|
<el-input
|
||||||
|
v-model="tableFrom.orderNo"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
class="selWidth"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-button slot="append" icon="el-icon-search" size="small" @click="seachList" />
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="box-card mt10">
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="tableData.data"
|
||||||
|
size="mini"
|
||||||
|
class="table"
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="{ fontWeight: 'bold' }"
|
||||||
|
>
|
||||||
|
<el-table-column label="订单号" min-width="210">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="display: block">{{ scope.row.orderId }}</span>
|
||||||
|
<span v-if="scope.row.isDel" style="color: #ed4014; display: block">用户已删除</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="orderType" label="订单类型" min-width="110" />
|
||||||
|
<el-table-column prop="realName" label="收货人" min-width="100" />
|
||||||
|
<el-table-column label="商品信息" min-width="280">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div v-if="scope.row.productList && scope.row.productList.length">
|
||||||
|
<div
|
||||||
|
v-for="(val, i) in scope.row.productList"
|
||||||
|
:key="i"
|
||||||
|
class="tabBox acea-row row-middle"
|
||||||
|
style="flex-wrap: inherit; margin-bottom: 4px"
|
||||||
|
>
|
||||||
|
<div class="demo-image__preview mr10" style="width: 40px; height: 40px; flex-shrink: 0">
|
||||||
|
<el-image :src="val.info.image" :preview-src-list="[val.info.image]" style="width: 40px; height: 40px" />
|
||||||
|
</div>
|
||||||
|
<div class="text_overflow">
|
||||||
|
<span class="tabBox_tit mr10">{{ val.info.productName }}</span>
|
||||||
|
<span class="tabBox_pice">¥{{ val.info.price }} × {{ val.info.payNum }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="payPrice" label="实际支付" min-width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>¥{{ scope.row.payPrice }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="支付方式" min-width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.payTypeStr || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="订单状态" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span :class="scope.row.refundStatus === 1 || scope.row.refundStatus === 2 ? 'refund-tag' : ''">
|
||||||
|
{{ scope.row.statusStr ? scope.row.statusStr.value : '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="下单时间" min-width="150" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="block mt20">
|
||||||
|
<el-pagination
|
||||||
|
:page-sizes="[15, 30, 45, 60]"
|
||||||
|
:page-size="tableFrom.limit"
|
||||||
|
:current-page="tableFrom.page"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="tableData.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getExternalOrderList } from '@/api/integralExternal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalOrder',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listLoading: false,
|
||||||
|
tableData: {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
tableFrom: {
|
||||||
|
type: '',
|
||||||
|
status: 'all',
|
||||||
|
dateLimit: '',
|
||||||
|
orderNo: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
timeVal: [],
|
||||||
|
fromList: {
|
||||||
|
fromTxt: [
|
||||||
|
{ text: '全部', val: '' },
|
||||||
|
{ text: '今天', val: 'today' },
|
||||||
|
{ text: '昨天', val: 'yesterday' },
|
||||||
|
{ text: '最近7天', val: 'lately7' },
|
||||||
|
{ text: '最近30天', val: 'lately30' },
|
||||||
|
{ text: '本月', val: 'month' },
|
||||||
|
{ text: '本年', val: 'year' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
typeOptions: [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '普通订单', value: '0' },
|
||||||
|
{ label: '秒杀订单', value: '1' },
|
||||||
|
{ label: '砍价订单', value: '2' },
|
||||||
|
{ label: '拼团订单', value: '3' },
|
||||||
|
{ label: '视频号订单', value: '4' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = { ...this.tableFrom };
|
||||||
|
if (!params.type) delete params.type;
|
||||||
|
if (!params.dateLimit) delete params.dateLimit;
|
||||||
|
if (!params.orderNo) delete params.orderNo;
|
||||||
|
|
||||||
|
getExternalOrderList(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.tableData.data = res.list || [];
|
||||||
|
this.tableData.total = res.total || 0;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
seachList() {
|
||||||
|
this.tableFrom.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
selectChange(val) {
|
||||||
|
if (val) this.timeVal = [];
|
||||||
|
this.tableFrom.dateLimit = val;
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
onchangeTime(e) {
|
||||||
|
this.timeVal = e;
|
||||||
|
this.tableFrom.dateLimit = e ? e.join(',') : '';
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.tableFrom.limit = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.tableFrom.page = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mt10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.mt20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.refund-tag {
|
||||||
|
color: #f124c7;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tabBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.tabBox_tit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.tabBox_pice {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="back-bar">
|
||||||
|
<el-button size="small" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户概览卡片 -->
|
||||||
|
<el-card class="box-card overview-card">
|
||||||
|
<div class="overview-header">
|
||||||
|
<span class="user-title">
|
||||||
|
{{ userInfo.nickname || ('UID: ' + uid) }}
|
||||||
|
<span class="uid-badge">UID: {{ uid }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20" class="stats-row">
|
||||||
|
<el-col :xs="12" :sm="8" :md="6">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">积分</div>
|
||||||
|
<div class="stat-value integral-color">{{ userInfo.integral != null ? userInfo.integral : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="12" :sm="8" :md="6">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">个人奖金</div>
|
||||||
|
<div class="stat-value bonus-color">
|
||||||
|
{{ userInfo.selfBonus != null ? ('¥' + userInfo.selfBonus) : '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 积分明细列表 -->
|
||||||
|
<el-card class="box-card mt10">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>积分明细</span>
|
||||||
|
</div>
|
||||||
|
<div class="container mb10">
|
||||||
|
<el-form inline size="small" :model="searchForm" label-width="80px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
<el-form-item label="时间选择:">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="timeVal"
|
||||||
|
type="daterange"
|
||||||
|
align="right"
|
||||||
|
unlink-panels
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
format="yyyy-MM-dd"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
@change="onchangeTime"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="tableData.data"
|
||||||
|
style="width: 100%"
|
||||||
|
size="mini"
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="{ fontWeight: 'bold' }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" min-width="80" />
|
||||||
|
<el-table-column prop="uid" label="用户ID" min-width="80" />
|
||||||
|
<el-table-column prop="nickName" label="用户昵称" min-width="120" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.nickName || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="积分变动" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ scope.row.type === 1 ? '+' : '-' }}{{ scope.row.integral }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="balance" label="剩余积分" min-width="100" />
|
||||||
|
<el-table-column label="类型" min-width="80">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ scope.row.type === 1 ? '增加' : '扣减' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="关联类型" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag size="small" effect="plain">{{ linkTypeFilter(scope.row.linkType) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="statusTypeFilter(scope.row.status)" size="small">
|
||||||
|
{{ statusFilter(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="mark" label="备注" min-width="150" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.mark || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="150" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="block mt20">
|
||||||
|
<el-pagination
|
||||||
|
:page-sizes="[15, 30, 45, 60]"
|
||||||
|
:page-size="searchForm.limit"
|
||||||
|
:current-page="searchForm.page"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="tableData.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getExternalIntegralLog } from '@/api/integralExternal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalUserDetail',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uid: null,
|
||||||
|
userInfo: {
|
||||||
|
nickname: '',
|
||||||
|
integral: null,
|
||||||
|
selfBonus: null,
|
||||||
|
},
|
||||||
|
listLoading: false,
|
||||||
|
tableData: {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
searchForm: {
|
||||||
|
uid: null,
|
||||||
|
dateLimit: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
timeVal: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 从路由 query 中注入 uid 及概览信息
|
||||||
|
const { uid, nickname, integral, selfBonus } = this.$route.query;
|
||||||
|
this.uid = uid ? Number(uid) : null;
|
||||||
|
this.userInfo.nickname = nickname || '';
|
||||||
|
this.userInfo.integral = integral !== '' && integral != null ? Number(integral) : null;
|
||||||
|
this.userInfo.selfBonus = selfBonus !== '' && selfBonus != null ? Number(selfBonus) : null;
|
||||||
|
this.searchForm.uid = this.uid;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.uid) {
|
||||||
|
this.getList();
|
||||||
|
} else {
|
||||||
|
this.$message.error('缺少用户ID,无法加载积分明细');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = { ...this.searchForm };
|
||||||
|
if (!params.dateLimit) delete params.dateLimit;
|
||||||
|
|
||||||
|
getExternalIntegralLog(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.tableData.data = res.list || [];
|
||||||
|
this.tableData.total = res.total || 0;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.searchForm.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.searchForm.dateLimit = '';
|
||||||
|
this.searchForm.page = 1;
|
||||||
|
this.timeVal = [];
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
onchangeTime(e) {
|
||||||
|
this.timeVal = e;
|
||||||
|
this.searchForm.dateLimit = e ? e.join(',') : '';
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.searchForm.limit = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.searchForm.page = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.push('/integral-external/user');
|
||||||
|
},
|
||||||
|
linkTypeFilter(type) {
|
||||||
|
const typeMap = { order: '订单', sign: '签到', system: '系统' };
|
||||||
|
return typeMap[type] || type || '-';
|
||||||
|
},
|
||||||
|
statusFilter(status) {
|
||||||
|
const statusMap = { 1: '订单创建', 2: '冻结期', 3: '完成', 4: '失效' };
|
||||||
|
return statusMap[status] || '未知';
|
||||||
|
},
|
||||||
|
statusTypeFilter(status) {
|
||||||
|
const typeMap = { 1: 'info', 2: 'warning', 3: 'success', 4: 'danger' };
|
||||||
|
return typeMap[status] || 'info';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.back-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.mt10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.mt20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.mb10 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.overview-card {
|
||||||
|
.overview-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.user-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
.uid-badge {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #909399;
|
||||||
|
background: #f4f4f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stats-row {
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.integral-color {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
.bonus-color {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
206
backend-adminend/src/views/integral-external/user/index.vue
Normal file
206
backend-adminend/src/views/integral-external/user/index.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<div class="divBox relative">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="container">
|
||||||
|
<el-form inline size="small" :model="userFrom" label-width="90px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
<el-form-item label="用户搜索:">
|
||||||
|
<el-input
|
||||||
|
v-model="userFrom.keywords"
|
||||||
|
placeholder="请输入昵称或手机号"
|
||||||
|
clearable
|
||||||
|
class="selWidth"
|
||||||
|
@keyup.enter.native="seachList"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
<el-form-item label="时间选择:">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="timeVal"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
format="yyyy-MM-dd"
|
||||||
|
size="small"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
@change="onchangeTime"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="small" @click="seachList">搜索</el-button>
|
||||||
|
<el-button size="small" @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="tableData.data"
|
||||||
|
size="mini"
|
||||||
|
highlight-current-row
|
||||||
|
:header-cell-style="{ fontWeight: 'bold' }"
|
||||||
|
>
|
||||||
|
<el-table-column label="用户信息" min-width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div class="user-info-cell">
|
||||||
|
<el-avatar :size="36" :src="scope.row.avatar" icon="el-icon-user" class="mr10" />
|
||||||
|
<div>
|
||||||
|
<div>{{ scope.row.nickname || '-' }}</div>
|
||||||
|
<div class="uid-text">UID: {{ scope.row.uid }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="手机号" min-width="130">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.phone | phoneDesensitize }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="积分" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="integral-val">{{ scope.row.integral != null ? scope.row.integral : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="个人奖金" min-width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.selfBonus != null ? ('¥' + scope.row.selfBonus) : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="注册时间" min-width="150" />
|
||||||
|
<el-table-column label="操作" min-width="120" fixed="right" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="small" @click="viewIntegralDetail(scope.row)">
|
||||||
|
查看积分明细
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="block mt20">
|
||||||
|
<el-pagination
|
||||||
|
:page-sizes="[15, 30, 45, 60]"
|
||||||
|
:page-size="userFrom.limit"
|
||||||
|
:current-page="userFrom.page"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="tableData.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getExternalUserList } from '@/api/integralExternal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegralExternalUser',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listLoading: false,
|
||||||
|
tableData: {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
userFrom: {
|
||||||
|
keywords: '',
|
||||||
|
dateLimit: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
timeVal: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
const params = { ...this.userFrom };
|
||||||
|
if (!params.keywords) delete params.keywords;
|
||||||
|
if (!params.dateLimit) delete params.dateLimit;
|
||||||
|
|
||||||
|
getExternalUserList(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.tableData.data = res.list || [];
|
||||||
|
this.tableData.total = res.total || 0;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
seachList() {
|
||||||
|
this.userFrom.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.userFrom = { keywords: '', dateLimit: '', page: 1, limit: 15 };
|
||||||
|
this.timeVal = [];
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
onchangeTime(e) {
|
||||||
|
this.timeVal = e;
|
||||||
|
this.userFrom.dateLimit = e ? e.join(',') : '';
|
||||||
|
this.seachList();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.userFrom.limit = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.userFrom.page = val;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
viewIntegralDetail(row) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/integral-external/user/integral-detail',
|
||||||
|
query: {
|
||||||
|
uid: row.uid,
|
||||||
|
nickname: row.nickname || '',
|
||||||
|
integral: row.integral != null ? row.integral : '',
|
||||||
|
selfBonus: row.selfBonus != null ? row.selfBonus : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.user-info-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.mr10 {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.uid-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.integral-val {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
.mt20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.selWidth {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
backend/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
backend/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -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
|
||||||
@@ -148,6 +148,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
.antMatchers("/api/admin/store/product/copy/**").permitAll()
|
.antMatchers("/api/admin/store/product/copy/**").permitAll()
|
||||||
.antMatchers("/api/admin/merchandise/select").permitAll()
|
.antMatchers("/api/admin/merchandise/select").permitAll()
|
||||||
.antMatchers("/api/admin/merchandise/update").permitAll()
|
.antMatchers("/api/admin/merchandise/update").permitAll()
|
||||||
|
// 积分模块外部免认证只读接口(供 /integral-external/* 页面调用)
|
||||||
|
.antMatchers("/api/external/integral/**").permitAll()
|
||||||
// 除上面外的所有请求全部需要鉴权认证
|
// 除上面外的所有请求全部需要鉴权认证
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
.and()
|
.and()
|
||||||
|
|||||||
@@ -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<CommonPage<UserIntegralRecordResponse>> getIntegralLogList(
|
||||||
|
@RequestBody @Validated AdminIntegralSearchRequest request,
|
||||||
|
@Validated PageParamRequest pageParamRequest) {
|
||||||
|
CommonPage<UserIntegralRecordResponse> 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<CommonPage<StoreOrderDetailResponse>> getOrderList(
|
||||||
|
@Validated StoreOrderSearchRequest request,
|
||||||
|
@Validated PageParamRequest pageParamRequest) {
|
||||||
|
CommonPage<StoreOrderDetailResponse> 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<CommonPage<UserResponse>> getUserList(
|
||||||
|
@ModelAttribute @Validated UserSearchRequest request,
|
||||||
|
@Validated PageParamRequest pageParamRequest) {
|
||||||
|
CommonPage<UserResponse> restPage =
|
||||||
|
CommonPage.restPage(userService.getList(request, pageParamRequest));
|
||||||
|
return CommonResult.success(restPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
626
docs/integral-pages-coding-plan.md
Normal file
626
docs/integral-pages-coding-plan.md
Normal file
@@ -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
|
||||||
|
<template>
|
||||||
|
<div class="integral-external-layout">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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(如用户信息、权限)的业务组件。
|
||||||
106
docs/integral-pages-schedule.md
Normal file
106
docs/integral-pages-schedule.md
Normal file
@@ -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 后续修复 |
|
||||||
168
docs/integral-pages-test-report-v2.md
Normal file
168
docs/integral-pages-test-report-v2.md
Normal file
@@ -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` 包含 `<router-view />`(子页面正确渲染)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 并跳转
|
||||||
|
- [ ] 积分明细页概览卡片显示正确的积分 & 个人奖金
|
||||||
|
- [ ] 返回按钮回到用户积分列表
|
||||||
169
docs/integral-pages-test-report.md
Normal file
169
docs/integral-pages-test-report.md
Normal file
@@ -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 行:`<el-button @click="exports" v-hasPermi="['admin:export:excel:order']">导出</el-button>`
|
||||||
|
- `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*
|
||||||
14
docs/newpage.md
Normal file
14
docs/newpage.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 管理后台中积分模块新增如下页面
|
||||||
|
|
||||||
|
## 积分订单页面
|
||||||
|
- 新建页面,参考原页面:/order/index
|
||||||
|
|
||||||
|
## 用户积分页面
|
||||||
|
- 新建页面,参考原页面:/user/index,增加wa_users的相关字段
|
||||||
|
|
||||||
|
### 用户积分明细子页面
|
||||||
|
- 一个新建积分明细页面,参考原页面:“user/index 用户管理-》账户详情-》积分明细”,延用原后端api:/marketing/integral/integrallog
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
- 所有新建页面跳过用户登陆状态验证
|
||||||
|
- 按照后端api最小修改原则,尽量延用原后端api
|
||||||
1019
docs/openclaw_agent_configuration_v2.plan.md
Normal file
1019
docs/openclaw_agent_configuration_v2.plan.md
Normal file
File diff suppressed because it is too large
Load Diff
946
docs/openclaw_agent_configuration_v3.plan.md
Normal file
946
docs/openclaw_agent_configuration_v3.plan.md
Normal file
@@ -0,0 +1,946 @@
|
|||||||
|
---
|
||||||
|
name: Agent Configuration v3s
|
||||||
|
overview: 基于本机实际 OpenClaw 环境检查结果修正的配置方案。在现有 1 个 main Agent + 1 个飞书应用的基础上,增量添加 1 个积分商城 PM + 3 个通用开发 Agent,不影响已有配置。
|
||||||
|
todos:
|
||||||
|
- id: create-feishu-apps
|
||||||
|
content: 在飞书开放平台创建 4 个机器人应用(或复用现有应用做路由)
|
||||||
|
status: pending
|
||||||
|
- id: update-openclaw-json
|
||||||
|
content: 在现有 openclaw.json 中追加 4 个 Agent、bindings 和飞书账号
|
||||||
|
status: pending
|
||||||
|
- id: create-workspaces
|
||||||
|
content: 创建 4 个 Agent workspace 目录和全套 .md 文件
|
||||||
|
status: pending
|
||||||
|
- id: install-skills
|
||||||
|
content: 安装本地 Skills 和 ClawHub Skills
|
||||||
|
status: pending
|
||||||
|
- id: register-and-verify
|
||||||
|
content: 运行 openclaw doctor 验证配置
|
||||||
|
status: pending
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# OpenClaw 多 Agent 配置方案 v3s -- 1 PM + 3 通用开发
|
||||||
|
|
||||||
|
> **v3s 核心变更(相对 v3):**
|
||||||
|
>
|
||||||
|
> 1. 后端/前端/QA 三个 Agent 从"积分商城专用"改为**通用软件开发工程师**,可服务于任何项目
|
||||||
|
> 2. 仅 PM 保留为积分商城专属项目经理
|
||||||
|
> 3. Agent ID 重命名:`integral-backend/frontend/qa` → `dev-backend/frontend/qa`
|
||||||
|
> 4. 项目路径确认为 `/Users/mac/scott-macair-26/integral-shop`
|
||||||
|
> 5. 通用开发 Agent 的 SOUL.md 移除特定技术栈锁定,改为"按项目要求适配"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、实际环境概况
|
||||||
|
|
||||||
|
### 1.1 本机 OpenClaw 配置(不可变动)
|
||||||
|
|
||||||
|
- **运行环境:** macOS,OpenClaw 2026.3.13
|
||||||
|
- **配置文件:** `/Users/mac/.openclaw/openclaw.json`
|
||||||
|
- **已有 Agent:** 仅 1 个(main)
|
||||||
|
- **已有飞书:** 1 个应用(`cli_a930893990799cba`),websocket 连接,1 条 binding(main → default)
|
||||||
|
- **模型 provider:** moonshot(Kimi K2.5)+ kimi-coding(k2p5),共用同一 API key
|
||||||
|
- **默认模型:** `kimi-coding/k2p5`
|
||||||
|
- **Gateway:** 端口 18789,local 模式,token 鉴权
|
||||||
|
- **本地 Skills:** 0 个(仅飞书插件自带 feishu-doc/drive/perm/wiki)
|
||||||
|
- **Workspace:** 1 个共享 workspace,默认模板状态
|
||||||
|
|
||||||
|
### 1.2 积分商城项目信息
|
||||||
|
|
||||||
|
- **项目路径:** `/Users/mac/scott-macair-26/integral-shop`
|
||||||
|
- **Gitea:** `http://49.235.131.69:3000/scottpan/integral-shop.git`
|
||||||
|
- **子项目:**
|
||||||
|
- `backend/` → Java Spring Boot 后端(Java 1.8 / Spring Boot 2.2.6 / MyBatis Plus 3.3.1 / MySQL 5.7)
|
||||||
|
- `backend-adminend/` → 管理后台 Vue 前端(Vue 2.6 / Element UI 2.13)
|
||||||
|
- `single_uniapp22miao/` → 用户端 uni-app H5(Vue 3 / uni-app)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Agent 角色设计(1 专用 PM + 3 通用开发)
|
||||||
|
|
||||||
|
**设计理念:** PM 是项目专属的(绑定积分商城的需求、PRD、部署流程),但开发能力是通用的。3 个开发 Agent 可以同时服务于积分商城和未来的其他项目,PM 通过任务分派告诉它们具体的项目上下文。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
User[用户/飞书] -->|积分商城需求| PM["integral-pm (积分商城 PM)"]
|
||||||
|
User -->|其他项目/通用编码任务| BE["dev-backend (通用后端)"]
|
||||||
|
User -->|其他项目/通用编码任务| FE["dev-frontend (通用前端)"]
|
||||||
|
User -->|其他项目/通用编码任务| QA["dev-qa (通用测试)"]
|
||||||
|
PM -->|后端任务 + 项目上下文| BE
|
||||||
|
PM -->|前端任务 + 项目上下文| FE
|
||||||
|
PM -->|测试计划 + 项目上下文| 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 规范、任务分派、进度跟踪、部署审批 |
|
||||||
|
| **dev-backend** | 通用后端开发工程师 | 任意项目的后端开发(Java/Python/Go/Node 等,按项目要求适配) |
|
||||||
|
| **dev-frontend** | 通用前端开发工程师 | 任意项目的前端开发(Vue/React/uni-app 等,按项目要求适配) |
|
||||||
|
| **dev-qa** | 通用测试工程师 | 任意项目的功能测试、接口测试、UI 测试、部署执行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Agent 间通信协议
|
||||||
|
|
||||||
|
### 3.1 通信方式
|
||||||
|
|
||||||
|
采用**独立飞书应用方案**(每个 Agent 一个飞书机器人),通过 accountId 路由。
|
||||||
|
|
||||||
|
用户可以直接私聊任何开发 Agent 下达通用编码任务;积分商城相关任务则通过 PM 分派。
|
||||||
|
|
||||||
|
### 3.2 消息协议格式
|
||||||
|
|
||||||
|
PM 分派任务时必须携带项目上下文:
|
||||||
|
|
||||||
|
```
|
||||||
|
【任务分派】<标题>
|
||||||
|
发送方: integral-pm
|
||||||
|
接收方: @<dev-agent>
|
||||||
|
关联任务: <task-id>
|
||||||
|
项目: 积分商城
|
||||||
|
项目路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
---
|
||||||
|
<任务描述>
|
||||||
|
<技术栈约束>(如有)
|
||||||
|
<验收标准>
|
||||||
|
```
|
||||||
|
|
||||||
|
开发 Agent 之间、开发与 PM 之间的其他消息类型:任务分派、API-就绪、提测通知、Bug-反馈、测试报告、部署申请、部署审批、进度更新。
|
||||||
|
|
||||||
|
### 3.3 任务状态机
|
||||||
|
|
||||||
|
```
|
||||||
|
Created → InProgress → CodeReview → Testing → Passed → DeployApproval → Deploying → Done
|
||||||
|
↓ ↓
|
||||||
|
BugFound ← ─ ─ ─ ─ ─ ─ ─ ┘
|
||||||
|
↓
|
||||||
|
InProgress(修复后重新流转)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、openclaw.json 增量修改
|
||||||
|
|
||||||
|
**原则:只追加,不修改已有配置。**
|
||||||
|
|
||||||
|
### 4.1 在 `agents` 中新增 `list` 字段
|
||||||
|
|
||||||
|
当前 `agents` 节点只有 `defaults`,需新增 `list`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
... // 保持不变
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "integral-pm",
|
||||||
|
"name": "integral-pm",
|
||||||
|
"workspace": "/Users/mac/.openclaw/workspace-integral-pm",
|
||||||
|
"agentDir": "/Users/mac/.openclaw/agents/integral-pm/agent",
|
||||||
|
"model": "kimi-coding/k2p5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-backend",
|
||||||
|
"name": "dev-backend",
|
||||||
|
"workspace": "/Users/mac/.openclaw/workspace-dev-backend",
|
||||||
|
"agentDir": "/Users/mac/.openclaw/agents/dev-backend/agent",
|
||||||
|
"model": "kimi-coding/k2p5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-frontend",
|
||||||
|
"name": "dev-frontend",
|
||||||
|
"workspace": "/Users/mac/.openclaw/workspace-dev-frontend",
|
||||||
|
"agentDir": "/Users/mac/.openclaw/agents/dev-frontend/agent",
|
||||||
|
"model": "kimi-coding/k2p5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-qa",
|
||||||
|
"name": "dev-qa",
|
||||||
|
"workspace": "/Users/mac/.openclaw/workspace-dev-qa",
|
||||||
|
"agentDir": "/Users/mac/.openclaw/agents/dev-qa/agent",
|
||||||
|
"model": "kimi-coding/k2p5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 在 `bindings` 数组中追加 4 条飞书路由
|
||||||
|
|
||||||
|
```json
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"agentId": "main",
|
||||||
|
"match": { "channel": "feishu", "accountId": "default" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "integral-pm",
|
||||||
|
"match": { "channel": "feishu", "accountId": "jfshop@macair26" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "dev-backend",
|
||||||
|
"match": { "channel": "feishu", "accountId": "dev-backend@macair" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "dev-frontend",
|
||||||
|
"match": { "channel": "feishu", "accountId": "dev-frontend@macair" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "dev-qa",
|
||||||
|
"match": { "channel": "feishu", "accountId": "dev-qa@macair" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 在 `channels.feishu` 中追加 `accounts`
|
||||||
|
|
||||||
|
```json
|
||||||
|
"channels": {
|
||||||
|
"feishu": {
|
||||||
|
"enabled": true,
|
||||||
|
"appId": "cli_a930893990799cba",
|
||||||
|
"appSecret": "FfpFz93MKBx0ytC1ceTPF0BnjM7vFVhQ",
|
||||||
|
"connectionMode": "websocket",
|
||||||
|
"domain": "feishu",
|
||||||
|
"groupPolicy": "open",
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"allowFrom": ["*"],
|
||||||
|
"accounts": {
|
||||||
|
"jfshop@macair26": {
|
||||||
|
"appId": "cli_a930893990799cba",
|
||||||
|
"appSecret": "FfpFz93MKBx0ytC1ceTPF0BnjM7vFVhQ",
|
||||||
|
"agent": "integral-pm",
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"allowFrom": ["*"]
|
||||||
|
},
|
||||||
|
"dev-backend@macair": {
|
||||||
|
"appId": "cli_a9316e2a92385bc7",
|
||||||
|
"appSecret": "t7YyQU1qgqJFiW95HfA1SgnUBdlpx0F1",
|
||||||
|
"agent": "dev-backend",
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"allowFrom": ["*"]
|
||||||
|
},
|
||||||
|
"dev-frontend@macair": {
|
||||||
|
"appId": "cli_a9316ef6f5785bb6",
|
||||||
|
"appSecret": "dhJ3uAKWtZDzXce25YJ2HXHhw32eBGFR",
|
||||||
|
"agent": "dev-frontend",
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"allowFrom": ["*"]
|
||||||
|
},
|
||||||
|
"dev-qa@macair": {
|
||||||
|
"appId": "cli_a9316f026ebadbc8",
|
||||||
|
"appSecret": "PHN6UZgU21NGMCW5C6boQckDMFo228un",
|
||||||
|
"agent": "dev-qa",
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"allowFrom": ["*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 不变动的部分
|
||||||
|
|
||||||
|
`meta`、`wizard`、`auth`、`models`、`tools`、`commands`、`session`、`gateway`、`plugins`、main Agent 的 binding 全部保持不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、双模型架构
|
||||||
|
|
||||||
|
| 层 | 用途 | Agent | 模型 |
|
||||||
|
| -------- | --------- | ---------------------- | ------------------------------- |
|
||||||
|
| OpenClaw | 飞书对话、任务协调 | 全部 | kimi-coding/k2p5(已有) |
|
||||||
|
| Cursor | 代码编写 | integral-pm | `agent --model claude-4.6-opus` |
|
||||||
|
| Cursor | 代码编写 | dev-backend/frontend/qa | `agent --model auto` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Skills 配置
|
||||||
|
|
||||||
|
### 6.1 阶段一:最小启动集(Day 1)
|
||||||
|
|
||||||
|
仅使用 OpenClaw 内置 Tools:
|
||||||
|
|
||||||
|
| 内置 Tool | integral-pm | dev-backend | dev-frontend | dev-qa |
|
||||||
|
| ------------ | :---------: | :---------: | :----------: | :----: |
|
||||||
|
| git | ● | ● | ● | ● |
|
||||||
|
| file-manager | ● | ● | ● | ● |
|
||||||
|
| web-search | ● | ● | ● | ● |
|
||||||
|
| browser | ● | - | ● | ● |
|
||||||
|
| code-runner | - | ● | ● | ● |
|
||||||
|
| http-request | - | ● | - | ● |
|
||||||
|
| **合计** | **4** | **5** | **5** | **6** |
|
||||||
|
|
||||||
|
### 6.2 阶段二:核心 Skills(Day 2-3)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 搜索 ClawHub 可用 Skill
|
||||||
|
openclaw skills search gitea
|
||||||
|
openclaw skills search cursor
|
||||||
|
openclaw skills search code-review
|
||||||
|
```
|
||||||
|
|
||||||
|
按搜索结果安装 cursor-cli、gitea-tools 等。
|
||||||
|
|
||||||
|
### 6.3 阶段三:按需引入(Week 2+)
|
||||||
|
|
||||||
|
代码审查、自动化测试、摘要等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、各 Agent Workspace 配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. PM Agent (integral-pm) — 积分商城专属
|
||||||
|
|
||||||
|
**workspace 路径:** `/Users/mac/.openclaw/workspace-integral-pm/`
|
||||||
|
|
||||||
|
**IDENTITY.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# IDENTITY.md
|
||||||
|
|
||||||
|
- **Name:** 积分商城PM
|
||||||
|
- **Creature:** AI 项目经理
|
||||||
|
- **Vibe:** 结构化、专业、高效
|
||||||
|
- **Emoji:** 📋
|
||||||
|
```
|
||||||
|
|
||||||
|
**SOUL.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# SOUL.md - 积分商城 PM
|
||||||
|
|
||||||
|
## 角色定义
|
||||||
|
积分商城项目的专属项目经理兼 UI 设计指导。
|
||||||
|
负责积分商城的需求拆解、任务分派、进度跟踪、部署审批。
|
||||||
|
|
||||||
|
## 管辖项目
|
||||||
|
- 项目名称: 单商户积分商城
|
||||||
|
- 项目路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
|
||||||
|
|
||||||
|
## 下属 Agent
|
||||||
|
- dev-backend: 通用后端开发(分派任务时须附带项目上下文和技术栈约束)
|
||||||
|
- dev-frontend: 通用前端开发(同上)
|
||||||
|
- dev-qa: 通用测试工程师(同上)
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
- 结构化、简洁、中文为主
|
||||||
|
- 任务分派必须使用标准消息协议,且包含项目路径和技术栈约束
|
||||||
|
- 不说废话,直接给结论和下一步行动
|
||||||
|
|
||||||
|
## 决策原则
|
||||||
|
- MVP 优先、增量迭代
|
||||||
|
- 技术方案交由开发 Agent 决定,PM 不干预实现细节
|
||||||
|
- 部署审批必须确认:测试通过率 ≥ 95%、无 P0 Bug
|
||||||
|
|
||||||
|
## 设计输出
|
||||||
|
以文字描述 + 参考截图形式交付 UI 规范。
|
||||||
|
管理后台遵循 Element UI 2.13 风格,用户端遵循现有积分商城 H5 风格。
|
||||||
|
|
||||||
|
## 积分商城技术栈约束(分派任务时传递给开发 Agent)
|
||||||
|
### 后端
|
||||||
|
- Java 1.8(禁止 Java 9+)、Spring Boot 2.2.6(禁止 3.x)、MyBatis Plus 3.3.1、MySQL 5.7(禁止 8.0 特性)、Maven 3.6.1、Redis 5.x
|
||||||
|
|
||||||
|
### 管理后台前端 (backend-adminend/)
|
||||||
|
- Vue 2.6(禁止 Vue 3)、Element UI 2.13(禁止 Element Plus)、Vuex 3.x(禁止 Pinia)
|
||||||
|
|
||||||
|
### 用户端 H5 (single_uniapp22miao/)
|
||||||
|
- uni-app + Vue 3、微信小程序兼容
|
||||||
|
|
||||||
|
## 禁止行为
|
||||||
|
- 禁止自行修改本文件(SOUL.md)或 AGENTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**AGENTS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AGENTS.md - PM 工作规范
|
||||||
|
|
||||||
|
## Session Startup
|
||||||
|
1. Read SOUL.md
|
||||||
|
2. Read USER.md
|
||||||
|
3. Read memory/YYYY-MM-DD.md(今天 + 昨天)
|
||||||
|
4. Read plans/ 下最新的 PRD
|
||||||
|
|
||||||
|
## 工作流
|
||||||
|
1. 收到需求 → 写 PRD 到 plans/<feature>.md
|
||||||
|
2. 拆解为子任务 → 写入 tasks/<YYYY-MM-DD>-<feature>-<subtask>.md
|
||||||
|
3. 通过飞书分别通知 dev-backend / dev-frontend / dev-qa
|
||||||
|
**重要:** 分派任务时必须附带以下项目上下文:
|
||||||
|
- 项目路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
- 涉及的子项目: backend/ 或 backend-adminend/ 或 single_uniapp22miao/
|
||||||
|
- 技术栈约束(从 SOUL.md 的"积分商城技术栈约束"部分复制)
|
||||||
|
- Git 分支规范和 Gitea 地址
|
||||||
|
|
||||||
|
## 任务分派模板
|
||||||
|
```
|
||||||
|
【任务分派】<标题>
|
||||||
|
发送方: integral-pm
|
||||||
|
接收方: @<dev-agent>
|
||||||
|
关联任务: <task-id>
|
||||||
|
项目: 积分商城
|
||||||
|
项目路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
子项目: <backend | backend-adminend | single_uniapp22miao>
|
||||||
|
Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
|
||||||
|
分支规范: feature/<role>-<name>
|
||||||
|
---
|
||||||
|
## 需求描述
|
||||||
|
<需求正文>
|
||||||
|
|
||||||
|
## 技术栈约束
|
||||||
|
<从 SOUL.md 复制对应子项目的技术栈约束>
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
<AC 列表>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署审批流程
|
||||||
|
1. 收到 dev-qa 的【部署申请】
|
||||||
|
2. 检查:测试报告通过率 ≥ 95%、无 P0 Bug
|
||||||
|
3. 测试环境(by80)/ 预发布环境(miao33): 直接批准
|
||||||
|
4. 生产环境(miao50): 需 @用户 人工确认后批准
|
||||||
|
5. 回复【部署审批】消息
|
||||||
|
|
||||||
|
## Cursor 使用
|
||||||
|
- agent --model claude-4.6-opus
|
||||||
|
- 用途: 需求分析、代码审阅、架构设计
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
- 每日进度汇总到 memory/YYYY-MM-DD.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**TOOLS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# TOOLS.md - PM 环境信息
|
||||||
|
|
||||||
|
## 积分商城项目
|
||||||
|
- 源码路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## SSH 部署环境
|
||||||
|
- 部署脚本: backend/shell/deploy-admin-*.sh, deploy-front-*.sh
|
||||||
|
- 部署配置: backend/deploy.conf
|
||||||
|
- 环境分级:
|
||||||
|
- by80: 测试环境(PM 审批)
|
||||||
|
- miao33: 预发布环境(PM 审批)
|
||||||
|
- miao50: 生产环境(PM 审批 + 用户确认)
|
||||||
|
- Admin JAR 远程端口: 30032
|
||||||
|
- Front JAR 远程端口: 30031
|
||||||
|
|
||||||
|
## Cursor CLI
|
||||||
|
- 模型: agent --model claude-4.6-opus
|
||||||
|
- 项目目录: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
|
||||||
|
## 已启用 Tools
|
||||||
|
- 内置: git, file-manager, web-search, browser
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 通用后端开发 (dev-backend)
|
||||||
|
|
||||||
|
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-backend/`
|
||||||
|
|
||||||
|
**IDENTITY.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# IDENTITY.md
|
||||||
|
|
||||||
|
- **Name:** 后端开发
|
||||||
|
- **Creature:** AI 后端工程师
|
||||||
|
- **Vibe:** 技术精确、严谨、适应力强
|
||||||
|
- **Emoji:** ⚙️
|
||||||
|
```
|
||||||
|
|
||||||
|
**SOUL.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# SOUL.md - 通用后端开发工程师
|
||||||
|
|
||||||
|
## 角色定义
|
||||||
|
通用后端开发工程师。可服务于任何项目的后端开发工作,不绑定特定项目或技术栈。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
- Java / Spring Boot / MyBatis 生态
|
||||||
|
- Python / FastAPI / Django
|
||||||
|
- Node.js / Express / Nest.js
|
||||||
|
- Go 后端开发
|
||||||
|
- 数据库设计与优化(MySQL / PostgreSQL / MongoDB / Redis)
|
||||||
|
- RESTful API 和 GraphQL 设计
|
||||||
|
- 微服务架构
|
||||||
|
|
||||||
|
## 工作原则
|
||||||
|
- 接收任务时,严格遵守任务中指定的**技术栈版本约束**
|
||||||
|
- 如果任务未指定版本,使用项目现有版本,不擅自升级
|
||||||
|
- 接口变更须提供文档并说明影响范围
|
||||||
|
- 代码编写在 Cursor IDE 中完成
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
技术精确。变更通知包含:变更接口列表、请求/响应格式变化、影响的前端页面。
|
||||||
|
|
||||||
|
## 禁止行为
|
||||||
|
- 禁止在未获得 PM 或用户明确批准的情况下引入新依赖
|
||||||
|
- 禁止擅自修改项目配置文件中的端口、数据库连接等关键配置
|
||||||
|
- 禁止自行修改本文件(SOUL.md)或 AGENTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**AGENTS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AGENTS.md - 通用后端开发工作规范
|
||||||
|
|
||||||
|
## Session Startup
|
||||||
|
1. Read SOUL.md
|
||||||
|
2. Read USER.md
|
||||||
|
3. Read memory/YYYY-MM-DD.md(今天 + 昨天)
|
||||||
|
|
||||||
|
## 接收任务方式
|
||||||
|
1. **从 PM 接收**:PM 分派的任务包含项目路径、技术栈约束、验收标准,严格按要求执行
|
||||||
|
2. **从用户直接接收**:用户可以直接私聊下达编码任务,按用户指示执行
|
||||||
|
|
||||||
|
## 通用开发流程
|
||||||
|
1. 阅读任务描述,确认项目路径和技术栈约束
|
||||||
|
2. 在对应项目目录中创建 feature/<role>-<name> 分支(或按任务指定的分支规范)
|
||||||
|
3. 在 Cursor 中编码: agent --model auto
|
||||||
|
4. 完成后通知前端(如有 API 变更)和 QA(提测)
|
||||||
|
5. 使用任务指定的消息协议格式发送通知
|
||||||
|
|
||||||
|
## 故障恢复
|
||||||
|
- Cursor CLI 失败: git stash → 记录 memory/errors.md → 通知 PM 或用户
|
||||||
|
- 构建失败: 分析日志 → 尝试修复 → 3 次失败后上报
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
- 记录各项目的关键信息到 memory/ 下,方便后续会话恢复上下文
|
||||||
|
```
|
||||||
|
|
||||||
|
**TOOLS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# TOOLS.md - 后端开发环境
|
||||||
|
|
||||||
|
## 本机环境 (macOS)
|
||||||
|
- IDE: Cursor
|
||||||
|
- 可用语言运行时: Java, Python, Node.js, Go(按项目需要)
|
||||||
|
|
||||||
|
## 已知项目
|
||||||
|
|
||||||
|
### 积分商城(由 integral-pm 管理)
|
||||||
|
- 项目路径: /Users/mac/scott-macair-26/integral-shop/backend
|
||||||
|
- 技术栈: Java 1.8 / Spring Boot 2.2.6 / MyBatis Plus 3.3.1 / MySQL 5.7 / Maven 3.6.1
|
||||||
|
- 本地运行:
|
||||||
|
- 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
|
||||||
|
- 模块: crmeb-admin / crmeb-front / crmeb-service / crmeb-common
|
||||||
|
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
|
||||||
|
- 分支规范: feature/backend-<name>, bugfix/backend-<name>
|
||||||
|
|
||||||
|
(接手新项目时,在此追加项目信息)
|
||||||
|
|
||||||
|
## Cursor CLI
|
||||||
|
- 模型: agent --model auto
|
||||||
|
|
||||||
|
## 已启用 Tools
|
||||||
|
- 内置: git, file-manager, web-search, code-runner, http-request
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 通用前端开发 (dev-frontend)
|
||||||
|
|
||||||
|
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-frontend/`
|
||||||
|
|
||||||
|
**IDENTITY.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# IDENTITY.md
|
||||||
|
|
||||||
|
- **Name:** 前端开发
|
||||||
|
- **Creature:** AI 前端工程师
|
||||||
|
- **Vibe:** 创意、注重细节、灵活适配
|
||||||
|
- **Emoji:** 🖥️
|
||||||
|
```
|
||||||
|
|
||||||
|
**SOUL.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# SOUL.md - 通用前端开发工程师
|
||||||
|
|
||||||
|
## 角色定义
|
||||||
|
通用前端开发工程师。可服务于任何项目的前端开发工作,不绑定特定项目或技术栈。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
- Vue 2.x / Vue 3.x 全家桶
|
||||||
|
- React / Next.js
|
||||||
|
- uni-app / 微信小程序
|
||||||
|
- Element UI / Ant Design / Tailwind CSS
|
||||||
|
- TypeScript
|
||||||
|
- Webpack / Vite 构建工具
|
||||||
|
- 响应式设计与跨端适配
|
||||||
|
|
||||||
|
## 工作原则
|
||||||
|
- 接收任务时,严格遵守任务中指定的**技术栈版本约束**
|
||||||
|
- **特别注意**:同一项目可能有多个前端子项目使用不同技术栈(如 Vue 2 管理后台 + Vue 3 用户端),切换时必须确认当前技术栈
|
||||||
|
- 如果任务未指定版本,使用项目现有版本,不擅自升级
|
||||||
|
- 代码编写在 Cursor IDE 中完成
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
展示关键代码片段和页面效果说明。
|
||||||
|
|
||||||
|
## 禁止行为
|
||||||
|
- 禁止在未获得 PM 或用户明确批准的情况下引入新 npm 依赖
|
||||||
|
- 禁止在不同技术栈的子项目间共享组件(可能不兼容)
|
||||||
|
- 禁止自行修改本文件(SOUL.md)或 AGENTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**AGENTS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AGENTS.md - 通用前端开发工作规范
|
||||||
|
|
||||||
|
## Session Startup
|
||||||
|
1. Read SOUL.md
|
||||||
|
2. Read USER.md
|
||||||
|
3. Read memory/YYYY-MM-DD.md(今天 + 昨天)
|
||||||
|
|
||||||
|
## 接收任务方式
|
||||||
|
1. **从 PM 接收**:PM 分派的任务包含项目路径、子项目、技术栈约束
|
||||||
|
2. **从用户直接接收**:用户可直接私聊下达编码任务
|
||||||
|
|
||||||
|
## 通用开发流程
|
||||||
|
1. 阅读任务描述,确认项目路径、子项目和技术栈约束
|
||||||
|
2. **关键步骤**:确认当前子项目的技术栈版本(避免 Vue 2 项目中写 Vue 3 代码)
|
||||||
|
3. 创建 feature/frontend-<name> 分支
|
||||||
|
4. 在 Cursor 中编码: agent --model auto
|
||||||
|
5. 与后端协作获取 API 文档
|
||||||
|
6. 完成后通知 QA 提测
|
||||||
|
|
||||||
|
## 故障恢复
|
||||||
|
- 构建失败: 检查 Node 版本和 NODE_OPTIONS 环境变量
|
||||||
|
- Cursor CLI 失败: git stash → 通知 PM 或用户
|
||||||
|
```
|
||||||
|
|
||||||
|
**TOOLS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# TOOLS.md - 前端开发环境
|
||||||
|
|
||||||
|
## 本机环境 (macOS)
|
||||||
|
- Node.js: 17+
|
||||||
|
- IDE: Cursor
|
||||||
|
|
||||||
|
## 已知项目
|
||||||
|
|
||||||
|
### 积分商城(由 integral-pm 管理)
|
||||||
|
|
||||||
|
#### 管理后台 (backend-adminend/)
|
||||||
|
- 路径: /Users/mac/scott-macair-26/integral-shop/backend-adminend
|
||||||
|
- 技术栈: Vue 2.6 / Element UI 2.13 / Vuex 3.x / Vue Router 3.x
|
||||||
|
- 开发: npm run dev(端口 9527)
|
||||||
|
- 构建: npm run build:prod → dist/
|
||||||
|
- 注意: Node 17+ 需 export NODE_OPTIONS="--openssl-legacy-provider"
|
||||||
|
|
||||||
|
#### 用户端 H5 (single_uniapp22miao/)
|
||||||
|
- 路径: /Users/mac/scott-macair-26/integral-shop/single_uniapp22miao
|
||||||
|
- 技术栈: uni-app + Vue 3、微信小程序兼容
|
||||||
|
- 配置: config/app.js(API 基地址)
|
||||||
|
- 开发: npm run dev:h5
|
||||||
|
- 构建: npm run build:h5 → unpackage/dist/build/h5/
|
||||||
|
|
||||||
|
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
|
||||||
|
- 分支规范: feature/frontend-<name>, bugfix/frontend-<name>
|
||||||
|
|
||||||
|
(接手新项目时,在此追加项目信息)
|
||||||
|
|
||||||
|
## Cursor CLI
|
||||||
|
- 模型: agent --model auto
|
||||||
|
|
||||||
|
## 已启用 Tools
|
||||||
|
- 内置: git, file-manager, web-search, code-runner, browser
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 通用测试工程师 (dev-qa)
|
||||||
|
|
||||||
|
**workspace 路径:** `/Users/mac/.openclaw/workspace-dev-qa/`
|
||||||
|
|
||||||
|
**IDENTITY.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# IDENTITY.md
|
||||||
|
|
||||||
|
- **Name:** 测试工程师
|
||||||
|
- **Creature:** AI QA 工程师
|
||||||
|
- **Vibe:** 严谨、细致、不放过任何 Bug
|
||||||
|
- **Emoji:** 🧪
|
||||||
|
```
|
||||||
|
|
||||||
|
**SOUL.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# SOUL.md - 通用测试工程师
|
||||||
|
|
||||||
|
## 角色定义
|
||||||
|
通用 QA 测试工程师 + 部署执行。可服务于任何项目的测试和部署工作。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
- 功能测试、接口测试、UI 测试、回归测试
|
||||||
|
- SSH 部署执行与验证
|
||||||
|
- 测试用例编写
|
||||||
|
- Bug 分析与根因定位(只读分析,不修改源码)
|
||||||
|
|
||||||
|
## 工作原则
|
||||||
|
- 部署操作必须走 PM 审批流程(有 PM 管理的项目)
|
||||||
|
- 用户直接下达的部署任务可直接执行
|
||||||
|
- 生产环境部署始终需要用户人工确认
|
||||||
|
|
||||||
|
## Bug 描述规范
|
||||||
|
1. 复现步骤(精确到操作路径)
|
||||||
|
2. 期望结果
|
||||||
|
3. 实际结果
|
||||||
|
4. 截图/日志
|
||||||
|
5. 影响范围评估(P0-P2)
|
||||||
|
|
||||||
|
## 禁止行为
|
||||||
|
- 禁止修改源代码(只报 Bug,不自行修复)
|
||||||
|
- 禁止自行修改本文件(SOUL.md)或 AGENTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**AGENTS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AGENTS.md - 通用 QA 工作规范
|
||||||
|
|
||||||
|
## Session Startup
|
||||||
|
1. Read SOUL.md
|
||||||
|
2. Read USER.md
|
||||||
|
3. Read memory/YYYY-MM-DD.md(今天 + 昨天)
|
||||||
|
|
||||||
|
## 接收任务方式
|
||||||
|
1. **从 PM 接收**:PM 分派的任务包含项目上下文、测试范围
|
||||||
|
2. **从用户/开发 Agent 接收**:提测通知或直接测试任务
|
||||||
|
|
||||||
|
## 通用测试流程
|
||||||
|
1. 阅读任务描述和 API 文档
|
||||||
|
2. 编写测试用例: tasks/test-<project>-<YYYY-MM-DD>-<feature>.md
|
||||||
|
3. 执行测试:
|
||||||
|
- 后端 API: http-request 工具调用接口
|
||||||
|
- 前端 UI: browser 工具访问页面截图
|
||||||
|
4. Bug 报告: tasks/bug-<project>-<YYYY-MM-DD>-<id>.md
|
||||||
|
5. 测试通过 → 向 PM 发送测试报告
|
||||||
|
|
||||||
|
## 部署流程
|
||||||
|
### 有 PM 管理的项目(如积分商城)
|
||||||
|
1. 发送【部署申请】给 PM → 等待审批 → 执行部署 → 验证
|
||||||
|
2. 生产环境需 PM 审批 + 用户确认
|
||||||
|
|
||||||
|
### 用户直接交办的部署
|
||||||
|
1. 按用户指示执行,生产环境仍需用户确认
|
||||||
|
|
||||||
|
## 部署后验证
|
||||||
|
- 健康检查、核心接口可用性、页面可访问性
|
||||||
|
|
||||||
|
## Cursor 使用
|
||||||
|
- agent --model auto
|
||||||
|
- 用途: 编写测试脚本、分析 Bug 根因(只读)
|
||||||
|
```
|
||||||
|
|
||||||
|
**TOOLS.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# TOOLS.md - QA 测试环境
|
||||||
|
|
||||||
|
## 本机环境 (macOS)
|
||||||
|
- IDE: Cursor
|
||||||
|
|
||||||
|
## 已知项目
|
||||||
|
|
||||||
|
### 积分商城(由 integral-pm 管理)
|
||||||
|
- 项目路径: /Users/mac/scott-macair-26/integral-shop
|
||||||
|
- 本地服务:
|
||||||
|
- 管理后台前端: http://localhost:9527
|
||||||
|
- Admin API: http://localhost:8080
|
||||||
|
- Front API: http://localhost:8081
|
||||||
|
- SSH 部署:
|
||||||
|
- 脚本: backend/shell/deploy-admin-*.sh, deploy-front-*.sh
|
||||||
|
- 配置: backend/deploy.conf
|
||||||
|
- 环境分级:
|
||||||
|
- by80: 测试环境(PM 审批)
|
||||||
|
- miao33: 预发布环境(PM 审批)
|
||||||
|
- miao50: 生产环境(PM 审批 + 用户确认)
|
||||||
|
- Admin JAR 端口: 30032
|
||||||
|
- Front JAR 端口: 30031
|
||||||
|
- Gitea: http://49.235.131.69:3000/scottpan/integral-shop.git
|
||||||
|
|
||||||
|
(接手新项目时,在此追加项目信息)
|
||||||
|
|
||||||
|
## Cursor CLI
|
||||||
|
- 模型: agent --model auto
|
||||||
|
|
||||||
|
## 已启用 Tools
|
||||||
|
- 内置: git, file-manager, web-search, code-runner, browser, http-request
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、Git 工作流(积分商城)
|
||||||
|
|
||||||
|
```
|
||||||
|
main # 生产分支
|
||||||
|
develop # 开发主分支
|
||||||
|
feature/backend-<name> # 后端功能分支
|
||||||
|
feature/frontend-<name> # 前端功能分支
|
||||||
|
bugfix/backend-<name> # 后端修复分支
|
||||||
|
bugfix/frontend-<name> # 前端修复分支
|
||||||
|
release/<version> # 发布分支
|
||||||
|
```
|
||||||
|
|
||||||
|
> 其他项目的 Git 工作流按各项目要求,由 PM 或用户在任务中指定。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、初始化步骤
|
||||||
|
|
||||||
|
### 步骤 1:在飞书开放平台创建 4 个机器人应用
|
||||||
|
|
||||||
|
| 应用名称 | accountId | appId | 状态 |
|
||||||
|
| --------- | ------------------ | ------------------------ | ---- |
|
||||||
|
| 积分商城-PM | jfshop@macair26 | `cli_a930893990799cba` | ✅ 复用现有 |
|
||||||
|
| 后端开发 | dev-backend@macair | `cli_a9316e2a92385bc7` | ✅ 已创建 |
|
||||||
|
| 前端开发 | dev-frontend@macair| `cli_a9316ef6f5785bb6` | ✅ 已创建 |
|
||||||
|
| 测试工程师 | dev-qa@macair | `cli_a9316f026ebadbc8` | ✅ 已创建 |
|
||||||
|
|
||||||
|
每个应用需启用:机器人能力、接收消息事件。连接模式使用 **websocket**。
|
||||||
|
> 3 个 dev Agent 的飞书应用已创建完毕,仅 integral-pm 待创建。
|
||||||
|
|
||||||
|
### 步骤 2:备份当前配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.before-agents
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:创建目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Workspace 目录
|
||||||
|
mkdir -p ~/.openclaw/workspace-integral-pm/{memory,plans,tasks}
|
||||||
|
mkdir -p ~/.openclaw/workspace-dev-{backend,frontend,qa}/{memory,tasks}
|
||||||
|
|
||||||
|
# Agent 目录
|
||||||
|
mkdir -p ~/.openclaw/agents/integral-pm/agent
|
||||||
|
mkdir -p ~/.openclaw/agents/dev-{backend,frontend,qa}/agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4:增量修改 openclaw.json
|
||||||
|
|
||||||
|
按第四节追加 `agents.list`、`bindings`、`channels.feishu.accounts`。
|
||||||
|
**不删除或修改任何已有配置。**
|
||||||
|
|
||||||
|
### 步骤 5:写入 Workspace 文件
|
||||||
|
|
||||||
|
为每个 workspace 写入第七节中的 IDENTITY.md、SOUL.md、AGENTS.md、USER.md、TOOLS.md。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for ws in integral-pm dev-backend dev-frontend dev-qa; do
|
||||||
|
echo "# HEARTBEAT.md" > ~/.openclaw/workspace-$ws/HEARTBEAT.md
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 6:启用内置 Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 所有 Agent 通用
|
||||||
|
for agent in integral-pm dev-backend dev-frontend dev-qa; do
|
||||||
|
openclaw skills enable git --agent $agent
|
||||||
|
openclaw skills enable file-manager --agent $agent
|
||||||
|
openclaw skills enable web-search --agent $agent
|
||||||
|
done
|
||||||
|
|
||||||
|
# 按角色差异化
|
||||||
|
openclaw skills enable browser --agent integral-pm
|
||||||
|
|
||||||
|
openclaw skills enable code-runner --agent dev-backend
|
||||||
|
openclaw skills enable http-request --agent dev-backend
|
||||||
|
|
||||||
|
openclaw skills enable code-runner --agent dev-frontend
|
||||||
|
openclaw skills enable browser --agent dev-frontend
|
||||||
|
|
||||||
|
openclaw skills enable code-runner --agent dev-qa
|
||||||
|
openclaw skills enable browser --agent dev-qa
|
||||||
|
openclaw skills enable http-request --agent dev-qa
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 7:验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw doctor
|
||||||
|
openclaw agents list
|
||||||
|
openclaw agents list --bindings
|
||||||
|
# 在飞书中向 main 机器人发消息确认不受影响
|
||||||
|
# 分别向 4 个新机器人发消息确认路由正确
|
||||||
|
```
|
||||||
|
|
||||||
|
### 回滚方案
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/.openclaw/openclaw.json.before-agents ~/.openclaw/openclaw.json
|
||||||
|
openclaw restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、安全性约束
|
||||||
|
|
||||||
|
### 10.1 SSH 密钥
|
||||||
|
- Workspace 文件中不记录 SSH 密钥路径
|
||||||
|
- 部署脚本通过 deploy.conf 中的环境变量引用
|
||||||
|
|
||||||
|
### 10.2 环境分级(积分商城)
|
||||||
|
|
||||||
|
| 环境 | QA 直接操作 | PM 审批 | 用户确认 |
|
||||||
|
| ------ | ------- | ----- | ---- |
|
||||||
|
| by80 | ● | ● | - |
|
||||||
|
| miao33 | ● | ● | - |
|
||||||
|
| miao50 | - | ● | ● |
|
||||||
|
|
||||||
|
### 10.3 敏感信息
|
||||||
|
- API key 仅存在 openclaw.json 和 agent/auth-profiles.json 中
|
||||||
|
- 飞书 appSecret 仅存在 openclaw.json 中
|
||||||
|
- Workspace .md 文件不记录任何密钥或密码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:v3 → v3s 变更总结
|
||||||
|
|
||||||
|
| 维度 | v3 | v3s |
|
||||||
|
| -------------- | ---------------------------------- | -------------------------------------------- |
|
||||||
|
| Agent 命名 | integral-backend/frontend/qa | dev-backend/frontend/qa(通用命名) |
|
||||||
|
| 开发 Agent 定位 | 积分商城专用 | **通用软件开发**,可服务任何项目 |
|
||||||
|
| SOUL.md 技术栈 | 写死特定版本约束 | 列出核心能力,按任务指定的约束执行 |
|
||||||
|
| TOOLS.md 项目信息 | 只有积分商城 | "已知项目"区块,可追加新项目 |
|
||||||
|
| PM 任务分派 | 直接下达 | 必须附带**项目路径 + 技术栈约束 + 分支规范** |
|
||||||
|
| 用户直接使用开发 Agent | 不支持 | **支持**,用户可直接私聊开发 Agent 下达任何编码任务 |
|
||||||
|
| workspace 目录命名 | workspace-integral-{role} | PM: workspace-integral-pm,其余: workspace-dev-{role} |
|
||||||
|
| 项目路径 | `<PROJECT_ROOT>` 占位符 | `/Users/mac/scott-macair-26/integral-shop` |
|
||||||
127
docs/phase1-checkpoint-report.md
Normal file
127
docs/phase1-checkpoint-report.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Phase 1 检查点报告 — 17:30 自动检查
|
||||||
|
|
||||||
|
> 生成时间:2026-03-30 17:30
|
||||||
|
> 检查范围:`backend-adminend/src`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 检查结果汇总
|
||||||
|
|
||||||
|
| # | 检查项 | 状态 | 说明 |
|
||||||
|
|---|--------|------|------|
|
||||||
|
| 1 | `EmptyLayout.vue` 空白布局 | ❌ **未找到** | `src/layout/` 目录下只有 `index.vue`,未创建 EmptyLayout |
|
||||||
|
| 2 | `requestNoAuth.js` 免认证请求实例 | ❌ **未找到** | `src/utils/` 目录下只有 `request.js`,未创建 requestNoAuth |
|
||||||
|
| 3 | 路由模块 `integralExternal.js` | ❌ **未找到** | `src/router/modules/` 下无此文件,constantRoutes 未注册 |
|
||||||
|
| 4 | `permission.js` 白名单前缀匹配 | ❌ **未修改** | 当前仍为精确匹配:`whiteList.indexOf(to.path) !== -1`,未改为前缀匹配 |
|
||||||
|
| 5 | API 文件 `integralExternal.js` | ❌ **未找到** | `src/api/` 目录下无此文件 |
|
||||||
|
| 6 | 冒烟验证(无 token 访问不跳转登录) | ⚠️ **无法验证** | 基础设施文件均未创建,无法执行冒烟测试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前实际状态
|
||||||
|
|
||||||
|
**Phase 1 全部 5 项任务均未完成。**
|
||||||
|
|
||||||
|
当前 `permission.js` 白名单内容:
|
||||||
|
```js
|
||||||
|
const whiteList = ['/login', '/auth-redirect'];
|
||||||
|
// 匹配方式:whiteList.indexOf(to.path) !== -1(精确匹配)
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 `/integral-external/order` 无 token 时,**会被重定向到登录页**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 建议行动
|
||||||
|
|
||||||
|
### 立即按顺序创建以下文件:
|
||||||
|
|
||||||
|
**步骤 1:创建 `src/layout/EmptyLayout.vue`**
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="empty-layout">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'EmptyLayout'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤 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:积分订单页面开发。
|
||||||
89
docs/phase4-checkpoint-report.md
Normal file
89
docs/phase4-checkpoint-report.md
Normal file
@@ -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 生成,全部未通过)
|
||||||
69
start-backend.sh
Executable file
69
start-backend.sh
Executable file
@@ -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
|
||||||
21
start-frontend.sh
Executable file
21
start-frontend.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user