Compare commits
7 Commits
b973bee0b3
...
53e30aebaa
Author | SHA1 | Date | |
---|---|---|---|
53e30aebaa | |||
79de0c59ab | |||
0b8897ecc4 | |||
95ae012aab | |||
88ac93c477 | |||
7a73b09b4c | |||
7728cc456a |
2
yudao-admin-vue3/.vscode/settings.json
vendored
2
yudao-admin-vue3/.vscode/settings.json
vendored
@ -86,7 +86,7 @@
|
|||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "Vue.volar"
|
||||||
},
|
},
|
||||||
"i18n-ally.localesPaths": ["src/locales"],
|
"i18n-ally.localesPaths": ["src/locales"],
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
|
@ -242,5 +242,8 @@ export enum DICT_TYPE {
|
|||||||
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
||||||
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
||||||
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
|
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
|
||||||
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
|
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
|
||||||
|
|
||||||
|
// ========== 通用模块 ==========
|
||||||
|
ZERO_OR_ONE = 'zero_or_one'
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,126 @@
|
|||||||
|
<!-- 商品发布 - 库存价格 -->
|
||||||
|
<template>
|
||||||
|
<el-form ref="formRef" :disabled="isDetail" :model="formData" :rules="rules" label-width="120px">
|
||||||
|
<el-form-item label="付费会员专属" >
|
||||||
|
<el-switch
|
||||||
|
v-model="formData.onlyPaidMemberView"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0" />
|
||||||
|
<el-text class="mx-1" size="small">开启后仅付费会员可以看见并购买此商品</el-text>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品推荐">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="formData.recommendHot"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
name="productRecommend">
|
||||||
|
热卖单品
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="formData.recommendBenefit"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
name="productRecommend">
|
||||||
|
促销单品
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="formData.recommendBest"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
name="productRecommend">
|
||||||
|
精品推荐
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="formData.recommendNew"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
name="productRecommend">
|
||||||
|
首发新品
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="formData.recommendGood"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
name="productRecommend">
|
||||||
|
优品推荐
|
||||||
|
</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 商品属性添加 Form 表单 -->
|
||||||
|
<ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { copyValueToTarget } from '@/utils'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import {
|
||||||
|
getPropertyList,
|
||||||
|
PropertyAndValues,
|
||||||
|
RuleConfig,
|
||||||
|
SkuList
|
||||||
|
} from '@/views/mall/product/spu/components/index'
|
||||||
|
import ProductAttributes from './ProductAttributes.vue'
|
||||||
|
import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
|
||||||
|
import type { Spu } from '@/api/mall/product/spu'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProductSpuSkuForm' })
|
||||||
|
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
propFormData: {
|
||||||
|
type: Object as PropType<Spu>,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
isDetail: propTypes.bool.def(false) // 是否作为详情组件
|
||||||
|
})
|
||||||
|
const attributesAddFormRef = ref() // 添加商品属性表单
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
const propertyList = ref<PropertyAndValues[]>([]) // 商品属性列表
|
||||||
|
const skuListRef = ref() // 商品属性列表 Ref
|
||||||
|
const formData = reactive<Spu>({
|
||||||
|
recommendHot: 0, // 热卖推荐
|
||||||
|
recommendBenefit: 0, // 优惠推荐
|
||||||
|
recommendBest: 0, // 精品推荐
|
||||||
|
recommendNew: 0, // 新品推荐
|
||||||
|
recommendGood: 0, // 优品推荐
|
||||||
|
onlyPaidMemberView: 0 // 仅限付费会员可见
|
||||||
|
})
|
||||||
|
const rules = reactive({
|
||||||
|
// specType: [required],
|
||||||
|
// subCommissionType: [required]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 将传进来的值赋值给 formData */
|
||||||
|
watch(
|
||||||
|
() => props.propFormData,
|
||||||
|
(data) => {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copyValueToTarget(formData, data)
|
||||||
|
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||||
|
propertyList.value = getPropertyList(data)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 表单校验 */
|
||||||
|
const emit = defineEmits(['update:activeName'])
|
||||||
|
const validate = async () => {
|
||||||
|
if (!formRef) return
|
||||||
|
try {
|
||||||
|
// 校验通过更新数据
|
||||||
|
Object.assign(props.propFormData, formData)
|
||||||
|
} catch (e) {
|
||||||
|
throw e // 目的截断之后的校验
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ validate })
|
||||||
|
|
||||||
|
</script>
|
@ -33,6 +33,14 @@
|
|||||||
:propFormData="formData"
|
:propFormData="formData"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="营销设置" name="marketing">
|
||||||
|
<MarketingForm
|
||||||
|
ref="marketingRef"
|
||||||
|
v-model:activeName="activeName"
|
||||||
|
:is-detail="isDetail"
|
||||||
|
:propFormData="formData"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
<el-tab-pane label="其它设置" name="other">
|
<el-tab-pane label="其它设置" name="other">
|
||||||
<OtherForm
|
<OtherForm
|
||||||
ref="otherRef"
|
ref="otherRef"
|
||||||
@ -41,6 +49,7 @@
|
|||||||
:propFormData="formData"
|
:propFormData="formData"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<el-form>
|
<el-form>
|
||||||
<el-form-item style="float: right">
|
<el-form-item style="float: right">
|
||||||
@ -61,6 +70,7 @@ import DescriptionForm from './DescriptionForm.vue'
|
|||||||
import OtherForm from './OtherForm.vue'
|
import OtherForm from './OtherForm.vue'
|
||||||
import SkuForm from './SkuForm.vue'
|
import SkuForm from './SkuForm.vue'
|
||||||
import DeliveryForm from './DeliveryForm.vue'
|
import DeliveryForm from './DeliveryForm.vue'
|
||||||
|
import MarketingForm from './MarketingForm.vue'
|
||||||
import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
|
import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductSpuForm' })
|
defineOptions({ name: 'ProductSpuForm' })
|
||||||
@ -79,6 +89,7 @@ const skuRef = ref() // 商品规格 Ref
|
|||||||
const deliveryRef = ref() // 物流设置 Ref
|
const deliveryRef = ref() // 物流设置 Ref
|
||||||
const descriptionRef = ref() // 商品详情 Ref
|
const descriptionRef = ref() // 商品详情 Ref
|
||||||
const otherRef = ref() // 其他设置 Ref
|
const otherRef = ref() // 其他设置 Ref
|
||||||
|
const marketingRef = ref() // 营销设置 Ref
|
||||||
// SPU 表单数据
|
// SPU 表单数据
|
||||||
const formData = ref<ProductSpuApi.Spu>({
|
const formData = ref<ProductSpuApi.Spu>({
|
||||||
name: '', // 商品名称
|
name: '', // 商品名称
|
||||||
@ -92,6 +103,12 @@ const formData = ref<ProductSpuApi.Spu>({
|
|||||||
brandId: undefined, // 商品品牌
|
brandId: undefined, // 商品品牌
|
||||||
specType: false, // 商品规格
|
specType: false, // 商品规格
|
||||||
subCommissionType: false, // 分销类型
|
subCommissionType: false, // 分销类型
|
||||||
|
recommendHot: 0, // 热卖推荐
|
||||||
|
recommendBenefit: 0, // 优惠推荐
|
||||||
|
recommendBest: 0, // 精品推荐
|
||||||
|
recommendNew: 0, // 新品推荐
|
||||||
|
recommendGood: 0, // 优品推荐
|
||||||
|
onlyPaidMemberView: 0, // 仅限付费会员可见
|
||||||
skus: [
|
skus: [
|
||||||
{
|
{
|
||||||
price: 0, // 商品价格
|
price: 0, // 商品价格
|
||||||
@ -141,6 +158,7 @@ const getDetail = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
formData.value = res
|
formData.value = res
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
@ -158,6 +176,7 @@ const submitForm = async () => {
|
|||||||
await unref(deliveryRef)?.validate()
|
await unref(deliveryRef)?.validate()
|
||||||
await unref(descriptionRef)?.validate()
|
await unref(descriptionRef)?.validate()
|
||||||
await unref(otherRef)?.validate()
|
await unref(otherRef)?.validate()
|
||||||
|
await unref(marketingRef)?.validate()
|
||||||
// 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
|
// 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
|
||||||
const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
|
const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
|
||||||
deepCopyFormData.skus!.forEach((item) => {
|
deepCopyFormData.skus!.forEach((item) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<!-- 商品中心 - 商品列表 -->
|
<!-- 商品中心 - 商品列表 -->
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【商品】商品 SPU 与 SKU" url="https://doc.iocoder.cn/mall/product-spu-sku/" />
|
<!-- <doc-alert title="【商品】商品 SPU 与 SKU" url="https://doc.iocoder.cn/mall/product-spu-sku/" /> -->
|
||||||
|
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
:inline="true"
|
:inline="true"
|
||||||
:model="queryParams"
|
:model="queryParams"
|
||||||
class="-mb-15px"
|
class="-mb-15px"
|
||||||
label-width="68px"
|
label-width="auto"
|
||||||
>
|
>
|
||||||
<el-form-item label="商品名称" prop="name">
|
<el-form-item label="商品名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
@ -31,6 +31,16 @@
|
|||||||
placeholder="请选择商品分类"
|
placeholder="请选择商品分类"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="仅付费会员可见" prop="isPaidMember">
|
||||||
|
<el-select v-model="queryParams.isPaidMember" style="width: 100px" placeholder="请选择" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.ZERO_OR_ONE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="创建时间" prop="createTime">
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="queryParams.createTime"
|
v-model="queryParams.createTime"
|
||||||
@ -234,6 +244,7 @@
|
|||||||
import { TabsPaneContext } from 'element-plus'
|
import { TabsPaneContext } from 'element-plus'
|
||||||
import { createImageViewer } from '@/components/ImageViewer'
|
import { createImageViewer } from '@/components/ImageViewer'
|
||||||
import { dateFormatter } from '@/utils/formatTime'
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { defaultProps, handleTree, treeToString } from '@/utils/tree'
|
import { defaultProps, handleTree, treeToString } from '@/utils/tree'
|
||||||
import { ProductSpuStatusEnum } from '@/utils/constants'
|
import { ProductSpuStatusEnum } from '@/utils/constants'
|
||||||
import { fenToYuan } from '@/utils'
|
import { fenToYuan } from '@/utils'
|
||||||
@ -286,7 +297,8 @@ const queryParams = ref({
|
|||||||
tabType: 0,
|
tabType: 0,
|
||||||
name: '',
|
name: '',
|
||||||
categoryId: undefined,
|
categoryId: undefined,
|
||||||
createTime: undefined
|
createTime: undefined,
|
||||||
|
isPaidMember: undefined
|
||||||
}) // 查询参数
|
}) // 查询参数
|
||||||
const queryFormRef = ref() // 搜索的表单Ref
|
const queryFormRef = ref() // 搜索的表单Ref
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@
|
|||||||
v-if="checkPermi(['member:user:update-balance'])"
|
v-if="checkPermi(['member:user:update-balance'])"
|
||||||
command="handleUpdateBlance"
|
command="handleUpdateBlance"
|
||||||
>
|
>
|
||||||
修改余额(WIP)
|
修改余额
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,4 +53,6 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode FAVORITE_EXISTS = new ErrorCode(1_008_008_000, "该商品已经被收藏");
|
ErrorCode FAVORITE_EXISTS = new ErrorCode(1_008_008_000, "该商品已经被收藏");
|
||||||
ErrorCode FAVORITE_NOT_EXISTS = new ErrorCode(1_008_008_001, "商品收藏不存在");
|
ErrorCode FAVORITE_NOT_EXISTS = new ErrorCode(1_008_008_001, "商品收藏不存在");
|
||||||
|
|
||||||
|
ErrorCode PARAMETER_ERROR = new ErrorCode(1_008_008_002, "参数错误");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.AppProductSpuController;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRecommendPageReqVo;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRespVO;
|
||||||
import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
|
import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||||
@ -87,7 +90,13 @@ public class ProductSpuController {
|
|||||||
}
|
}
|
||||||
// 查询商品 SKU
|
// 查询商品 SKU
|
||||||
List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
|
List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
|
||||||
return success(ProductSpuConvert.INSTANCE.convert(spu, skus));
|
ProductSpuRespVO convert = ProductSpuConvert.INSTANCE.convert(spu, skus);
|
||||||
|
convert.setRecommendHot(spu.getRecommendHot());
|
||||||
|
convert.setRecommendBenefit(spu.getRecommendBenefit());
|
||||||
|
convert.setRecommendBest(spu.getRecommendBest());
|
||||||
|
convert.setRecommendNew(spu.getRecommendNew());
|
||||||
|
convert.setRecommendGood(spu.getRecommendGood());
|
||||||
|
return success(convert);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list-all-simple")
|
@GetMapping("/list-all-simple")
|
||||||
|
@ -51,8 +51,12 @@ public class ProductSpuPageReqVO extends PageParam {
|
|||||||
@Schema(description = "商品分类编号", example = "1")
|
@Schema(description = "商品分类编号", example = "1")
|
||||||
private Long categoryId;
|
private Long categoryId;
|
||||||
|
|
||||||
|
@Schema(description = "是否是付费会员", example = "1")
|
||||||
|
private Integer isPaidMember;
|
||||||
|
|
||||||
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
|
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] createTime;
|
private LocalDateTime[] createTime;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -65,6 +66,29 @@ public class ProductSpuRespVO {
|
|||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "是否热卖推荐")
|
||||||
|
@ExcelProperty("是否热卖推荐")
|
||||||
|
private Integer recommendHot;
|
||||||
|
|
||||||
|
@Schema(description = "是否促销推荐")
|
||||||
|
@ExcelProperty("是否优惠推荐")
|
||||||
|
private Integer recommendBenefit;
|
||||||
|
|
||||||
|
@Schema(description = "是否精品推荐")
|
||||||
|
@ExcelProperty("是否精品推荐")
|
||||||
|
private Integer recommendBest;
|
||||||
|
|
||||||
|
@Schema(description = "是否新品推荐")
|
||||||
|
@ExcelProperty("是否新品推荐")
|
||||||
|
private Integer recommendNew;
|
||||||
|
|
||||||
|
@Schema(description = "是否优品推荐")
|
||||||
|
@ExcelProperty("是否优品推荐")
|
||||||
|
private Integer recommendGood;
|
||||||
|
|
||||||
|
@Schema(description = "是否仅付费会员可见")
|
||||||
|
@ExcelProperty("是否仅付费会员可见")
|
||||||
|
private Integer onlyPaidMemberView;
|
||||||
// ========== SKU 相关字段 =========
|
// ========== SKU 相关字段 =========
|
||||||
|
|
||||||
@Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
@Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||||
|
@ -99,4 +99,27 @@ public class ProductSpuSaveReqVO {
|
|||||||
@Valid
|
@Valid
|
||||||
private List<ProductSkuSaveReqVO> skus;
|
private List<ProductSkuSaveReqVO> skus;
|
||||||
|
|
||||||
|
@Schema(description = "是否热卖推荐")
|
||||||
|
@Valid
|
||||||
|
private Integer recommendHot;
|
||||||
|
|
||||||
|
@Schema(description = "是否促销推荐")
|
||||||
|
@Valid
|
||||||
|
private Integer recommendBenefit;
|
||||||
|
|
||||||
|
@Schema(description = "是否精品推荐")
|
||||||
|
@Valid
|
||||||
|
private Integer recommendBest;
|
||||||
|
|
||||||
|
@Schema(description = "是否新品推荐")
|
||||||
|
@Valid
|
||||||
|
private Integer recommendNew;
|
||||||
|
|
||||||
|
@Schema(description = "是否优品推荐")
|
||||||
|
@Valid
|
||||||
|
private Integer recommendGood;
|
||||||
|
|
||||||
|
@Schema(description = "是否仅付费会员可见")
|
||||||
|
@Valid
|
||||||
|
private Integer onlyPaidMemberView;
|
||||||
}
|
}
|
||||||
|
@ -10,16 +10,21 @@ import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
|
|||||||
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
|
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
|
||||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRecommendPageReqVo;
|
||||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRespVO;
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRespVO;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||||
|
import cn.iocoder.yudao.module.product.enums.ErrorCodeConstants;
|
||||||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
||||||
import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
|
import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
|
||||||
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
|
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
|
||||||
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
|
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
|
||||||
|
import cn.iocoder.yudao.module.product.service.spu.ProductSpuServiceImpl;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.apache.commons.math3.stat.descriptive.summary.Product;
|
||||||
|
import org.apache.poi.ss.formula.functions.T;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@ -33,6 +38,7 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_ENABLE;
|
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_ENABLE;
|
||||||
@ -91,6 +97,23 @@ public class AppProductSpuController {
|
|||||||
return success(voPageResult);
|
return success(voPageResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get-recommend-page")
|
||||||
|
@Operation(summary = "获得推荐类型对应的商品 SPU 分页")
|
||||||
|
public CommonResult<PageResult<AppProductSpuRespVO>> getRecommendPage(@Valid AppProductSpuRecommendPageReqVo recommendPageVo){
|
||||||
|
List<ProductSpuDO> recommendProductList = productSpuService.getRecommendProductIdList(recommendPageVo);
|
||||||
|
PageResult<ProductSpuDO> pageResult = getPaginatedList(recommendProductList, recommendPageVo.getPageNo(), recommendPageVo.getPageSize());
|
||||||
|
if (CollUtil.isEmpty(pageResult.getList())) {
|
||||||
|
return success(PageResult.empty(pageResult.getTotal()));
|
||||||
|
}
|
||||||
|
// 拼接返回
|
||||||
|
pageResult.getList().forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
|
||||||
|
PageResult<AppProductSpuRespVO> voPageResult = BeanUtils.toBean(pageResult, AppProductSpuRespVO.class);
|
||||||
|
// 处理 vip 价格
|
||||||
|
MemberLevelRespDTO memberLevel = getMemberLevel();
|
||||||
|
voPageResult.getList().forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
|
||||||
|
return success(voPageResult);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/get-detail")
|
@GetMapping("/get-detail")
|
||||||
@Operation(summary = "获得商品 SPU 明细")
|
@Operation(summary = "获得商品 SPU 明细")
|
||||||
@Parameter(name = "id", description = "编号", required = true)
|
@Parameter(name = "id", description = "编号", required = true)
|
||||||
@ -148,5 +171,28 @@ public class AppProductSpuController {
|
|||||||
return price - newPrice;
|
return price - newPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑分页
|
||||||
|
* @param list
|
||||||
|
* @param pageSize
|
||||||
|
* @param pageNumber
|
||||||
|
* @return java.util.List<T>
|
||||||
|
*/
|
||||||
|
private PageResult<ProductSpuDO> getPaginatedList(List<ProductSpuDO> list, int pageNumber, int pageSize) {
|
||||||
|
// 计算起始索引
|
||||||
|
int fromIndex = (pageNumber - 1) * pageSize;
|
||||||
|
// 计算结束索引
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, list.size());
|
||||||
|
PageResult<ProductSpuDO> pageResult = new PageResult<>();
|
||||||
|
pageResult.setTotal((long)list.size());
|
||||||
|
// 如果起始索引超出范围,返回空列表
|
||||||
|
if (fromIndex >= list.size() || fromIndex < 0) {
|
||||||
|
return pageResult;
|
||||||
|
}
|
||||||
|
// 逻辑分页
|
||||||
|
List<ProductSpuDO> result = list.subList(fromIndex, toIndex);
|
||||||
|
pageResult.setList(result);
|
||||||
|
return pageResult;
|
||||||
|
}
|
||||||
// TODO 芋艿:商品的浏览记录;
|
// TODO 芋艿:商品的浏览记录;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,9 @@ public class AppProductSpuPageReqVO extends PageParam {
|
|||||||
@Schema(description = "排序方式", example = "true")
|
@Schema(description = "排序方式", example = "true")
|
||||||
private Boolean sortAsc;
|
private Boolean sortAsc;
|
||||||
|
|
||||||
|
@Schema(description = "是否是付费会员", example = "1")
|
||||||
|
private Integer isPaidMember;
|
||||||
|
|
||||||
@AssertTrue(message = "排序字段不合法")
|
@AssertTrue(message = "排序字段不合法")
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean isSortFieldValid() {
|
public boolean isSortFieldValid() {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.module.product.controller.app.spu.vo;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "用户 App - 推荐商品 SPU 分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class AppProductSpuRecommendPageReqVo extends PageParam {
|
||||||
|
@Schema(description = "热卖推荐", example = "1")
|
||||||
|
private Integer recommendHot;
|
||||||
|
@Schema(description = "促销推荐", example = "1")
|
||||||
|
private Integer recommendBenefit;
|
||||||
|
@Schema(description = "精品推荐", example = "1")
|
||||||
|
private Integer recommendBest;
|
||||||
|
@Schema(description = "新品推荐", example = "1")
|
||||||
|
private Integer recommendNew;
|
||||||
|
@Schema(description = "优品推荐", example = "1")
|
||||||
|
private Integer recommendGood;
|
||||||
|
@Schema(description = "是否是付费会员", example = "1")
|
||||||
|
private Integer isPaidMember;
|
||||||
|
}
|
@ -6,13 +6,16 @@ import cn.iocoder.yudao.module.product.dal.dataobject.brand.ProductBrandDO;
|
|||||||
import cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
||||||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +91,36 @@ public class ProductSpuDO extends BaseDO {
|
|||||||
*/
|
*/
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否热卖推荐
|
||||||
|
*/
|
||||||
|
private Integer recommendHot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否优惠推荐
|
||||||
|
*/
|
||||||
|
private Integer recommendBenefit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否精品推荐
|
||||||
|
*/
|
||||||
|
private Integer recommendBest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否新品推荐
|
||||||
|
*/
|
||||||
|
private Integer recommendNew;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否优品推荐
|
||||||
|
*/
|
||||||
|
private Integer recommendGood;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否仅付费会员可见
|
||||||
|
*/
|
||||||
|
private Integer onlyPaidMemberView;
|
||||||
|
|
||||||
// ========== SKU 相关字段 =========
|
// ========== SKU 相关字段 =========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +31,7 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
|||||||
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
|
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
|
||||||
.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime())
|
.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime())
|
||||||
.orderByDesc(ProductSpuDO::getSort)
|
.orderByDesc(ProductSpuDO::getSort)
|
||||||
|
.eqIfPresent(ProductSpuDO::getOnlyPaidMemberView, reqVO.getIsPaidMember())
|
||||||
.orderByDesc(ProductSpuDO::getId);
|
.orderByDesc(ProductSpuDO::getId);
|
||||||
appendTabQuery(tabType, queryWrapper);
|
appendTabQuery(tabType, queryWrapper);
|
||||||
return selectPage(reqVO, queryWrapper);
|
return selectPage(reqVO, queryWrapper);
|
||||||
@ -61,7 +62,9 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
|||||||
.inIfPresent(ProductSpuDO::getCategoryId, categoryIds);
|
.inIfPresent(ProductSpuDO::getCategoryId, categoryIds);
|
||||||
// 上架状态 且有库存
|
// 上架状态 且有库存
|
||||||
query.eq(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus());
|
query.eq(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus());
|
||||||
|
if (pageReqVO.getIsPaidMember() == null || pageReqVO.getIsPaidMember() == 0) {
|
||||||
|
query.eq(ProductSpuDO::getOnlyPaidMemberView, 0);
|
||||||
|
}
|
||||||
// 排序逻辑
|
// 排序逻辑
|
||||||
if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_SALES_COUNT)) {
|
if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_SALES_COUNT)) {
|
||||||
query.last(String.format(" ORDER BY (sales_count + virtual_sales_count) %s, sort DESC, id DESC",
|
query.last(String.format(" ORDER BY (sales_count + virtual_sales_count) %s, sort DESC, id DESC",
|
||||||
|
@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReq
|
|||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuSaveReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuSaveReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateStatusReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateStatusReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRecommendPageReqVo;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
|
||||||
@ -131,4 +132,10 @@ public interface ProductSpuService {
|
|||||||
@Async
|
@Async
|
||||||
void updateBrowseCount(Long id, int incrCount);
|
void updateBrowseCount(Long id, int incrCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐类型的 SPU ID列表
|
||||||
|
* @param pageVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<ProductSpuDO> getRecommendProductIdList(AppProductSpuRecommendPageReqVo pageVo);
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,15 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
|||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.category.vo.ProductCategoryListReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.category.vo.ProductCategoryListReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSkuSaveReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSkuSaveReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuSaveReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuSaveReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateStatusReqVO;
|
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateStatusReqVO;
|
||||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
||||||
|
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRecommendPageReqVo;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO;
|
||||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||||
import cn.iocoder.yudao.module.product.dal.mysql.spu.ProductSpuMapper;
|
import cn.iocoder.yudao.module.product.dal.mysql.spu.ProductSpuMapper;
|
||||||
@ -160,6 +163,37 @@ public class ProductSpuServiceImpl implements ProductSpuService {
|
|||||||
productSpuMapper.updateBrowseCount(id , incrCount);
|
productSpuMapper.updateBrowseCount(id , incrCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProductSpuDO> getRecommendProductIdList(AppProductSpuRecommendPageReqVo pageVo) {
|
||||||
|
LambdaQueryWrapperX<ProductSpuDO> wrapperX = new LambdaQueryWrapperX<>();
|
||||||
|
Integer recommendHot = pageVo.getRecommendHot();
|
||||||
|
Integer recommendBenefit = pageVo.getRecommendBenefit();
|
||||||
|
Integer recommendBest = pageVo.getRecommendBest();
|
||||||
|
Integer recommendNew = pageVo.getRecommendNew();
|
||||||
|
Integer recommendGood = pageVo.getRecommendGood();
|
||||||
|
if (recommendHot != null && recommendHot != 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getRecommendHot, recommendHot);
|
||||||
|
}
|
||||||
|
if (recommendBenefit != null && recommendBenefit != 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getRecommendBenefit, recommendBenefit);
|
||||||
|
}
|
||||||
|
if (recommendBest != null && recommendBest != 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getRecommendBest, recommendBest);
|
||||||
|
}
|
||||||
|
if (recommendNew != null && recommendNew != 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getRecommendNew, recommendNew);
|
||||||
|
}
|
||||||
|
if (recommendGood != null && recommendGood != 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getRecommendGood, recommendGood);
|
||||||
|
}
|
||||||
|
if (pageVo.getIsPaidMember() == null || pageVo.getIsPaidMember() == 0){
|
||||||
|
wrapperX.eq(ProductSpuDO::getOnlyPaidMemberView, 0);
|
||||||
|
}
|
||||||
|
wrapperX.eq(ProductSpuDO::getStatus, 1);
|
||||||
|
wrapperX.last(" ORDER BY (sales_count + virtual_sales_count) DESC, sort DESC, id DESC");
|
||||||
|
return productSpuMapper.selectList(wrapperX);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteSpu(Long id) {
|
public void deleteSpu(Long id) {
|
||||||
|
Loading…
Reference in New Issue
Block a user