feat(shop): 新增用户管理模块- 添加用户记录表模型定义,包含用户基本信息、认证信息、统计信息等字段

- 实现用户记录表的增删改查接口,支持分页查询和批量删除功能
- 在医生申请页面集成用户信息提交功能,支持个人和企业用户类型- 优化用户表单验证逻辑,添加年龄必填校验- 调整用户类型单选框默认值为个人用户
- 添加证件上传功能,支持企业用户上传资质证明
- 移除文章分类和详情相关页面组件
- 调整请求工具中后台配置API地址的日志输出为注释状态
This commit is contained in:
2025-10-07 12:42:14 +08:00
parent e3fc34db8f
commit 1005fd0718
13 changed files with 351 additions and 339 deletions

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopUser, ShopUserParam } from './model';
/**
* 分页查询用户记录表
*/
export async function pageShopUser(params: ShopUserParam) {
const res = await request.get<ApiResult<PageResult<ShopUser>>>(
'/shop/shop-user/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询用户记录表列表
*/
export async function listShopUser(params?: ShopUserParam) {
const res = await request.get<ApiResult<ShopUser[]>>(
'/shop/shop-user',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加用户记录表
*/
export async function addShopUser(data: ShopUser) {
const res = await request.post<ApiResult<unknown>>(
'http://127.0.0.1:9200/api/shop/shop-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改用户记录表
*/
export async function updateShopUser(data: ShopUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除用户记录表
*/
export async function removeShopUser(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-user/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除用户记录表
*/
export async function removeBatchShopUser(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-user/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据userId查询用户记录表
*/
export async function getShopUser(userId: number) {
const res = await request.get<ApiResult<ShopUser>>(
'/shop/shop-user/' + userId
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,165 @@
import type { PageParam } from '@/api';
/**
* 用户记录表
*/
export interface ShopUser {
// 用户id
userId?: number;
// 用户类型 0个人用户 1企业用户 2其他
type?: number;
// 账号
username?: string;
// 密码
password?: string;
// 昵称
nickname?: string;
// 手机号
phone?: string;
// 性别 1男 2女
sex?: number;
// 职务
position?: string;
// 注册来源客户端 (APP、H5、MP-WEIXIN等)
platform?: string;
// 邮箱
email?: string;
// 邮箱是否验证, 0否, 1是
emailVerified?: number;
// 别名
alias?: string;
// 真实姓名
realName?: string;
// 证件号码
idCard?: string;
// 出生日期
birthday?: string;
// 所在国家
country?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 街道地址
address?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 用户可用余额
balance?: string;
// 已提现金额
cashedMoney?: string;
// 用户可用积分
points?: number;
// 用户总支付的金额
payMoney?: string;
// 实际消费的金额(不含退款)
expendMoney?: string;
// 密码
payPassword?: string;
// 会员等级ID
gradeId?: number;
// 行业分类
category?: string;
// 个人简介
introduction?: string;
// 机构id
organizationId?: number;
// 会员分组ID
groupId?: number;
// 头像
avatar?: string;
// 背景图
bgImage?: string;
// 用户编码
userCode?: string;
// 是否已实名认证
certification?: number;
// 年龄
age?: number;
// 是否线下会员
offline?: string;
// 关注数
followers?: number;
// 粉丝数
fans?: number;
// 点赞数
likes?: number;
// 评论数
commentNumbers?: number;
// 是否推荐
recommend?: number;
// 微信openid
openid?: string;
// 微信公众号openid
officeOpenid?: string;
// 微信unionID
unionid?: string;
// 客户端ID
clientId?: string;
// 不允许办卡
notAllowVip?: string;
// 是否管理员
isAdmin?: string;
// 是否企业管理员
isOrganizationAdmin?: string;
// 累计登录次数
loginNum?: number;
// 企业ID
companyId?: number;
// 可管理的场馆
merchants?: string;
// 商户ID
merchantId?: number;
// 商户名称
merchantName?: string;
// 商户头像
merchantAvatar?: string;
// 第三方系统的用户ID
uid?: number;
// 专家角色
expertType?: string;
// 过期时间
expireTime?: number;
// 最后结算时间
settlementTime?: string;
// 资质
aptitude?: string;
// 行业类型(父级)
industryParent?: string;
// 行业类型(子级)
industryChild?: string;
// 头衔
title?: string;
// 安装的产品ID
templateId?: number;
// 插件安装状态(仅对超超管判断) 0未安装 1已安装
installed?: number;
// 特长
speciality?: string;
// 备注
comments?: string;
// 状态, 0在线, 1离线
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
// 上传证件1
uploadImg1?: string;
}
/**
* 用户记录表搜索条件
*/
export interface ShopUserParam extends PageParam {
userId?: number;
keywords?: string;
}

View File

@@ -2,14 +2,14 @@ import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Input, Form, Radio, InputNumber, TextArea, ConfigProvider} from '@nutui/nutui-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {Image} from '@tarojs/components';
import FixedButton from "@/components/FixedButton";
import {useUser} from "@/hooks/useUser";
import {updateUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
import {listUserRole, updateUserRole} from "@/api/system/userRole";
import {DictData} from "@/api/system/dict-data/model";
import {listDictData} from "@/api/system/dict-data";
import {addShopUser, getShopUser} from "@/api/shop/shopUser";
import {ShopUser} from "@/api/shop/shopUser/model";
import {uploadFile} from "@/api/system/file";
import {useUser} from "@/hooks/useUser";
const customTheme = {
nutuiInputnumberButtonWidth: '30px',
@@ -21,97 +21,49 @@ const customTheme = {
}
const AddApply = () => {
const {user, loginUser} = useUser()
const {user} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<User>()
const [FormData, setFormData] = useState<ShopUser>()
const [userType, setUserType] = useState<DictData[]>()
const formRef = useRef<any>(null)
const reload = async () => {
listDictData({dictCode: 'UserType'}).then((data) => {
setUserType(data)
const userId = Taro.getStorageSync('UserId')
if(!userId || userId == '') return
const shopUser = await getShopUser(Number(Taro.getStorageSync('UserId')))
if(shopUser){
console.log(shopUser.type,'shopUsershopUsershopUsershopUsershopUsershopUsershopUser')
setFormData({
...shopUser,
type : Number(shopUser.type)
})
}
const userType = await listDictData({dictCode: 'UserType'})
if(userType){
setUserType(userType)
}
}
// 提交表单
const submitSucceed = async (values: any) => {
if (!FormData?.age || FormData?.age == 0) {
Taro.showToast({
title: `请填写年龄`,
icon: 'error'
});
return;
}
try {
// 验证必填字段
if (!values.phone && !FormData?.phone) {
Taro.showToast({
title: '请先获取手机号',
icon: 'error'
});
return;
}
// 验证昵称:必须填写且不能是默认的微信昵称
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
title: '请填写昵称',
icon: 'error'
});
return;
}
// 检查是否为默认的微信昵称(常见的默认昵称)
const defaultNicknames = ['微信用户', 'WeChat User', '微信昵称'];
if (defaultNicknames.includes(nickname.trim())) {
Taro.showToast({
title: '请填写真实昵称,不能使用默认昵称',
icon: 'error'
});
return;
}
// 验证昵称长度
if (nickname.trim().length < 2) {
Taro.showToast({
title: '昵称至少需要2个字符',
icon: 'error'
});
return;
}
if (!values.avatar && !FormData?.avatar) {
Taro.showToast({
title: '请上传头像',
icon: 'error'
});
return;
}
console.log(values,FormData)
const roles = await listUserRole({userId: user?.userId})
console.log(roles, 'roles...')
return false;
// 准备提交的数据
await updateUser({
userId: user?.userId,
nickname: values.realName || FormData?.nickname,
phone: values.phone || FormData?.phone,
avatar: values.avatar || FormData?.avatar,
refereeId: values.refereeId || FormData?.refereeId
});
await addShopDealerUser({
userId: user?.userId,
await addShopUser({
userId: Taro.getStorageSync('UserId'),
type: values.type,
username: user?.username,
nickname: user?.nickname,
realName: values.realName || FormData?.nickname,
mobile: values.phone || FormData?.phone,
refereeId: values.refereeId || FormData?.refereeId
phone: values.phone || FormData?.phone
})
if (roles.length > 0) {
await updateUserRole({
...roles[0],
roleId: 1848
})
}
Taro.showToast({
title: `注册成功`,
icon: 'success'
@@ -136,17 +88,25 @@ const AddApply = () => {
console.log(error, 'err...')
}
const uploadImg1 = () => {
uploadFile().then(data => {
setFormData({
...FormData,
uploadImg1: data.url
})
});
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [user?.userId]); // 依赖用户ID当用户变化时重新加载
}, []); // 依赖用户ID当用户变化时重新加载
// 当FormData变化时同步更新表单字段值
useEffect(() => {
if (formRef.current && FormData) {
formRef.current.setFieldsValue({
refereeId: FormData.refereeId,
phone: FormData.phone,
avatar: FormData.avatar,
realName: FormData.nickname
@@ -172,7 +132,7 @@ const AddApply = () => {
{JSON.stringify(FormData)}
<Form.Item label="用户类型" name="type" initialValue={FormData?.type} required>
<Radio.Group
defaultValue="1"
defaultValue="0"
direction="horizontal"
value={FormData?.type}
onChange={(value: any) => {
@@ -193,10 +153,10 @@ const AddApply = () => {
<Input
placeholder={'请填写真实姓名'}
value={FormData?.realName || ''}
onChange={(e) => {
onChange={(value) => {
setFormData({
...FormData,
realName: e.detail.value
realName: value
})
}}
/>
@@ -214,17 +174,17 @@ const AddApply = () => {
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="年龄" name="age" initialValue={FormData?.age || 18} required>
<Form.Item label="年龄" name="age" initialValue={FormData?.age} required>
<ConfigProvider theme={customTheme}>
<InputNumber
value={FormData?.age || 18}
value={FormData?.age}
onChange={(value) => {
setFormData({
...FormData,
age: Number(value)
})
}}
min={1}
min={0}
max={120}
step={1}
/>
@@ -240,6 +200,34 @@ const AddApply = () => {
value={FormData?.introduction || ''}
/>
</Form.Item>
{FormData?.type == 1 && (
<>
<Form.Item label="证书编号" name="userCode" required>
<Input
placeholder={'请填证书编号'}
value={FormData?.userCode || ''}
onChange={(value) => {
setFormData({
...FormData,
idCard: value
})
}}
/>
</Form.Item>
<Form.Item
label={'上传证件'}
name="uploadImg1"
required
rules={[{message: '请上传医师资格证书'}
]}
>
<div onClick={uploadImg1}>
<Image src={FormData.uploadImg1 || ''} mode={'scaleToFill'} lazyLoad={false} />
</div>
</Form.Item>
</>
)}
</CellGroup>
</Form>

View File

@@ -1,25 +0,0 @@
import {Image, Cell} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
const ArticleList = (props: any) => {
return (
<>
<div className={'px-3'}>
{props.data.map((item, index) => {
return (
<Cell
title={item.title}
extra={
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
}
key={index}
onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})}
/>
)
})}
</div>
</>
)
}
export default ArticleList

View File

@@ -1,59 +0,0 @@
import {useEffect, useState} from "react";
import {Tabs, Loading} from '@nutui/nutui-react-taro'
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import ArticleList from "./ArticleList";
const ArticleTabs = (props: any) => {
const [loading, setLoading] = useState<boolean>(true)
const [tab1value, setTab1value] = useState<string | number>('0')
const [list, setList] = useState<CmsArticle[]>([])
const reload = async (value) => {
const {data} = props
pageCmsArticle({
categoryId: data[value].navigationId,
page: 1,
status: 0,
limit: 10
}).then((res) => {
res && setList(res?.list || [])
})
.catch(err => {
console.log(err)
})
.finally(() => {
setTab1value(value)
setLoading(false)
})
}
useEffect(() => {
reload(0).then()
}, []);
if (loading) {
return (
<Loading className={'px-2'}></Loading>
)
}
return (
<>
<Tabs
value={tab1value}
onChange={(value) => {
reload(value).then()
}}
>
{props.data?.map((item, index) => {
return (
<Tabs.TabPane title={item.categoryName} key={index}/>
)
})}
</Tabs>
<ArticleList data={list}/>
</>
)
}
export default ArticleTabs

View File

@@ -1,31 +0,0 @@
import { useEffect, useState } from 'react'
import { Swiper } from '@nutui/nutui-react-taro'
import {CmsAd} from "@/api/cms/cmsAd/model";
import {Image} from '@nutui/nutui-react-taro'
import {getCmsAd} from "@/api/cms/cmsAd";
const MyPage = () => {
const [item, setItem] = useState<CmsAd>()
const reload = () => {
getCmsAd(439).then(data => {
setItem(data)
})
}
useEffect(() => {
reload()
}, [])
return (
<>
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
{item?.imageList?.map((item) => (
<Swiper.Item key={item}>
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
</Swiper.Item>
))}
</Swiper>
</>
)
}
export default MyPage

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '文章列表',
navigationBarTextStyle: 'black'
})

View File

@@ -1,71 +0,0 @@
import Taro from '@tarojs/taro'
import {useShareAppMessage} from "@tarojs/taro"
import {Loading} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
import {getCmsNavigation, listCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import ArticleList from './components/ArticleList'
import ArticleTabs from "./components/ArticleTabs";
import './index.scss'
function Category() {
const {params} = useRouter();
const [categoryId, setCategoryId] = useState<number>(0)
const [category, setCategory] = useState<CmsNavigation[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [nav, setNav] = useState<CmsNavigation>()
const [list, setList] = useState<CmsArticle[]>([])
const reload = async () => {
// 1.加载远程数据
const id = Number(params.id || 4328)
const nav = await getCmsNavigation(id)
const categoryList = await listCmsNavigation({parentId: id})
const shopGoods = await pageCmsArticle({categoryId: id})
// 2.赋值
setCategoryId(id)
setNav(nav)
setList(shopGoods?.list || [])
setCategory(categoryList)
Taro.setNavigationBarTitle({
title: `${nav?.categoryName}`
})
};
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []);
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_通源堂健康生态平台`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
},
fail: function () {
console.log('分享失败');
}
};
});
if (loading) {
return (
<Loading className={'px-2'}></Loading>
)
}
if(category.length > 0){
return <ArticleTabs data={category}/>
}
return <ArticleList data={list}/>
}
export default Category

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '文章详情'
})

View File

@@ -1,53 +0,0 @@
import Taro from '@tarojs/taro'
import {useEffect, useState} from 'react'
import {useRouter} from '@tarojs/taro'
import {Loading} from '@nutui/nutui-react-taro'
import {View, RichText} from '@tarojs/components'
import {wxParse} from "@/utils/common";
import {getCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model"
import Line from "@/components/Gap";
import './index.scss'
function Detail() {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
// 文章详情
const [item, setItem] = useState<CmsArticle>()
const reload = async () => {
const item = await getCmsArticle(Number(params.id))
if (item) {
item.content = wxParse(item.content)
setItem(item)
Taro.setNavigationBarTitle({
title: `${item?.categoryName}`
})
}
}
useEffect(() => {
reload().then(() => {
setLoading(false)
});
}, []);
if (loading) {
return (
<Loading className={'px-2'}></Loading>
)
}
return (
<div className={'bg-white'}>
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
<View className={'content p-4'}>
<RichText nodes={item?.content}/>
</View>
<Line height={44}/>
</div>
)
}
export default Detail

View File

@@ -61,10 +61,10 @@ const getBaseUrl = (): string => {
if (configStr) {
// 如果是字符串,需要解析为对象
const config = typeof configStr === 'string' ? JSON.parse(configStr) : configStr;
console.log('获取后台配置API地址:', config);
// console.log('获取后台配置API地址:', config);
// 注意属性名是 ApiUrl首字母大写不是 apiUrl
if (config && config.ApiUrl) {
console.log('使用后台配置的API地址:', config.ApiUrl);
// console.log('使用后台配置的API地址:', config.ApiUrl);
return config.ApiUrl;
}
}