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:
scott
2026-03-31 10:41:30 +08:00
parent fe9e1916fa
commit ee0886b800
25 changed files with 4360 additions and 2 deletions

View File

@@ -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>