feat(health): 积分规则/商品 update/delete + 标签更新端点
- 积分规则: 添加 update/delete service + handler + 路由 - 兑换商品: 添加 update/delete service + handler + 路由 - 文章标签: 添加 update service + handler + 路由 - Web 管理端: 规则/商品列表页支持编辑/删除/启用切换 - Web 管理端: 标签管理页支持编辑、删除传 version
This commit is contained in:
@@ -252,11 +252,19 @@ export const articleTagApi = {
|
||||
return data.data;
|
||||
},
|
||||
|
||||
delete: async (id: string) => {
|
||||
update: async (id: string, req: { name: string; version: number }) => {
|
||||
const { data } = await client.put<{
|
||||
success: boolean;
|
||||
data: ArticleTagItem;
|
||||
}>(`/health/article-tags/${id}`, req);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
delete: async (id: string, version: number) => {
|
||||
const { data } = await client.delete<{
|
||||
success: boolean;
|
||||
data: null;
|
||||
}>(`/health/article-tags/${id}`);
|
||||
}>(`/health/article-tags/${id}`, { data: { version } });
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -168,6 +168,20 @@ export const pointsApi = {
|
||||
return data.data;
|
||||
},
|
||||
|
||||
updateRule: async (id: string, req: Partial<CreatePointsRuleReq> & { is_active?: boolean; version: number }) => {
|
||||
const { data } = await client.put<{
|
||||
success: boolean;
|
||||
data: PointsRule;
|
||||
}>(`/health/admin/points/rules/${id}`, { data: req, version: req.version });
|
||||
return data.data;
|
||||
},
|
||||
|
||||
deleteRule: async (id: string, version: number) => {
|
||||
await client.delete(`/health/admin/points/rules/${id}`, {
|
||||
data: { version },
|
||||
});
|
||||
},
|
||||
|
||||
// Products
|
||||
listProducts: async (params?: Record<string, unknown>) => {
|
||||
const { data } = await client.get<{
|
||||
@@ -185,6 +199,20 @@ export const pointsApi = {
|
||||
return data.data;
|
||||
},
|
||||
|
||||
updateProduct: async (id: string, req: Partial<CreatePointsProductReq> & { is_active?: boolean; version: number }) => {
|
||||
const { data } = await client.put<{
|
||||
success: boolean;
|
||||
data: PointsProduct;
|
||||
}>(`/health/admin/points/products/${id}`, { data: req, version: req.version });
|
||||
return data.data;
|
||||
},
|
||||
|
||||
deleteProduct: async (id: string, version: number) => {
|
||||
await client.delete(`/health/admin/points/products/${id}`, {
|
||||
data: { version },
|
||||
});
|
||||
},
|
||||
|
||||
// Orders
|
||||
listOrders: async (params?: Record<string, unknown>) => {
|
||||
const { data } = await client.get<{
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Tag,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
articleTagApi,
|
||||
type ArticleTagItem,
|
||||
@@ -22,6 +22,7 @@ export default function ArticleTagManage() {
|
||||
const [tags, setTags] = useState<ArticleTagItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<ArticleTagItem | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const isDark = useThemeMode();
|
||||
|
||||
@@ -42,37 +43,50 @@ export default function ArticleTagManage() {
|
||||
}, [fetchTags]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditModal = (record: ArticleTagItem) => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue({ name: record.name });
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleCreate = async (values: { name: string; slug?: string; color?: string }) => {
|
||||
const handleSubmit = async (values: { name: string; slug?: string; color?: string }) => {
|
||||
try {
|
||||
const req: CreateTagReq = {
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
color: values.color,
|
||||
};
|
||||
await articleTagApi.create(req);
|
||||
message.success('标签创建成功');
|
||||
if (editing) {
|
||||
await articleTagApi.update(editing.id, { name: values.name, version: editing.version });
|
||||
message.success('标签更新成功');
|
||||
} else {
|
||||
const req: CreateTagReq = {
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
color: values.color,
|
||||
};
|
||||
await articleTagApi.create(req);
|
||||
message.success('标签创建成功');
|
||||
}
|
||||
closeModal();
|
||||
fetchTags();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ||
|
||||
'创建失败';
|
||||
(editing ? '更新失败' : '创建失败');
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const handleDelete = async (record: ArticleTagItem) => {
|
||||
try {
|
||||
await articleTagApi.delete(id);
|
||||
await articleTagApi.delete(record.id, record.version);
|
||||
message.success('标签已删除');
|
||||
fetchTags();
|
||||
} catch {
|
||||
@@ -138,13 +152,16 @@ export default function ArticleTagManage() {
|
||||
width: 80,
|
||||
render: (_: unknown, record: ArticleTagItem) => (
|
||||
<AuthButton code="health.articles.manage">
|
||||
<Popconfirm
|
||||
title="确定删除此标签?"
|
||||
description="删除后关联的文章将移除该标签"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
|
||||
</Popconfirm>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEditModal(record)} />
|
||||
<Popconfirm
|
||||
title="确定删除此标签?"
|
||||
description="删除后关联的文章将移除该标签"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
>
|
||||
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</AuthButton>
|
||||
),
|
||||
},
|
||||
@@ -183,9 +200,9 @@ export default function ArticleTagManage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 新建标签弹窗 */}
|
||||
{/* 新建/编辑标签弹窗 */}
|
||||
<Modal
|
||||
title="新建标签"
|
||||
title={editing ? '编辑标签' : '新建标签'}
|
||||
open={modalOpen}
|
||||
onCancel={closeModal}
|
||||
onOk={() => form.submit()}
|
||||
@@ -193,7 +210,7 @@ export default function ArticleTagManage() {
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreate}
|
||||
onFinish={handleSubmit}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -113,16 +114,23 @@ export default function PointsProductList() {
|
||||
sort_order?: number;
|
||||
}) => {
|
||||
try {
|
||||
const req: CreatePointsProductReq = {
|
||||
name: values.name,
|
||||
product_type: values.product_type,
|
||||
points_cost: values.points_cost,
|
||||
stock: values.stock,
|
||||
description: values.description,
|
||||
image_url: values.image_url,
|
||||
sort_order: values.sort_order,
|
||||
};
|
||||
await pointsApi.createProduct(req);
|
||||
if (editing) {
|
||||
await pointsApi.updateProduct(editing.id, {
|
||||
...values,
|
||||
version: editing.version,
|
||||
});
|
||||
} else {
|
||||
const req: CreatePointsProductReq = {
|
||||
name: values.name,
|
||||
product_type: values.product_type,
|
||||
points_cost: values.points_cost,
|
||||
stock: values.stock,
|
||||
description: values.description,
|
||||
image_url: values.image_url,
|
||||
sort_order: values.sort_order,
|
||||
};
|
||||
await pointsApi.createProduct(req);
|
||||
}
|
||||
message.success(editing ? '更新成功' : '创建成功');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
@@ -135,16 +143,10 @@ export default function PointsProductList() {
|
||||
// ---- 切换上下架 ----
|
||||
const handleToggleActive = async (record: PointsProduct) => {
|
||||
try {
|
||||
const req: CreatePointsProductReq = {
|
||||
name: record.name,
|
||||
product_type: record.product_type,
|
||||
points_cost: record.points_cost,
|
||||
stock: record.stock,
|
||||
description: record.description ?? undefined,
|
||||
image_url: record.image_url ?? undefined,
|
||||
sort_order: record.sort_order,
|
||||
};
|
||||
await pointsApi.createProduct(req);
|
||||
await pointsApi.updateProduct(record.id, {
|
||||
is_active: !record.is_active,
|
||||
version: record.version,
|
||||
});
|
||||
message.success(record.is_active ? '已下架' : '已上架');
|
||||
fetchData(page, pageSize);
|
||||
} catch {
|
||||
@@ -152,6 +154,24 @@ export default function PointsProductList() {
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 删除 ----
|
||||
const handleDelete = (record: PointsProduct) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除商品「${record.name}」吗?`,
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await pointsApi.deleteProduct(record.id, record.version);
|
||||
message.success('删除成功');
|
||||
fetchData(page, pageSize);
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ---- 列定义 ----
|
||||
const columns = [
|
||||
{
|
||||
@@ -230,6 +250,15 @@ export default function PointsProductList() {
|
||||
unCheckedChildren="下架"
|
||||
onChange={() => handleToggleActive(record)}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</AuthButton>
|
||||
),
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -101,17 +102,24 @@ export default function PointsRuleList() {
|
||||
streak_30d_bonus?: number;
|
||||
}) => {
|
||||
try {
|
||||
const req: CreatePointsRuleReq = {
|
||||
event_type: values.event_type,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
points_value: values.points_value,
|
||||
daily_cap: values.daily_cap,
|
||||
streak_7d_bonus: values.streak_7d_bonus,
|
||||
streak_14d_bonus: values.streak_14d_bonus,
|
||||
streak_30d_bonus: values.streak_30d_bonus,
|
||||
};
|
||||
await pointsApi.createRule(req);
|
||||
if (editing) {
|
||||
await pointsApi.updateRule(editing.id, {
|
||||
...values,
|
||||
version: editing.version,
|
||||
});
|
||||
} else {
|
||||
const req: CreatePointsRuleReq = {
|
||||
event_type: values.event_type,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
points_value: values.points_value,
|
||||
daily_cap: values.daily_cap,
|
||||
streak_7d_bonus: values.streak_7d_bonus,
|
||||
streak_14d_bonus: values.streak_14d_bonus,
|
||||
streak_30d_bonus: values.streak_30d_bonus,
|
||||
};
|
||||
await pointsApi.createRule(req);
|
||||
}
|
||||
message.success(editing ? '更新成功' : '创建成功');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
@@ -124,19 +132,10 @@ export default function PointsRuleList() {
|
||||
// ---- 切换启用状态 ----
|
||||
const handleToggleActive = async (record: PointsRule) => {
|
||||
try {
|
||||
// 目前后端没有 toggle 接口,重新创建等同于更新
|
||||
// 使用 create 接口覆盖同 event_type 的规则
|
||||
const req: CreatePointsRuleReq = {
|
||||
event_type: record.event_type,
|
||||
name: record.name,
|
||||
description: record.description ?? undefined,
|
||||
points_value: record.points_value,
|
||||
daily_cap: record.daily_cap,
|
||||
streak_7d_bonus: record.streak_7d_bonus,
|
||||
streak_14d_bonus: record.streak_14d_bonus,
|
||||
streak_30d_bonus: record.streak_30d_bonus,
|
||||
};
|
||||
await pointsApi.createRule(req);
|
||||
await pointsApi.updateRule(record.id, {
|
||||
is_active: !record.is_active,
|
||||
version: record.version,
|
||||
});
|
||||
message.success(record.is_active ? '已停用' : '已启用');
|
||||
fetchData();
|
||||
} catch {
|
||||
@@ -144,6 +143,24 @@ export default function PointsRuleList() {
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 删除 ----
|
||||
const handleDelete = (record: PointsRule) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除规则「${record.name}」吗?`,
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await pointsApi.deleteRule(record.id, record.version);
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ---- 列定义 ----
|
||||
const columns = [
|
||||
{
|
||||
@@ -234,6 +251,15 @@ export default function PointsRuleList() {
|
||||
unCheckedChildren="停用"
|
||||
onChange={() => handleToggleActive(record)}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</AuthButton>
|
||||
),
|
||||
|
||||
@@ -218,3 +218,15 @@ impl CreateTagReq {
|
||||
self.name = sanitize_string(&self.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateTagReq {
|
||||
pub name: String,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
impl UpdateTagReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = sanitize_string(&self.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,18 @@ pub struct OfflineEventResp {
|
||||
// 管理端:带版本号的更新/删除包装
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateRuleWithVersion {
|
||||
pub data: UpdatePointsRuleReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateProductWithVersion {
|
||||
pub data: UpdatePointsProductReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateOfflineEventWithVersion {
|
||||
pub data: UpdateOfflineEventReq,
|
||||
|
||||
@@ -29,6 +29,9 @@ pub enum HealthError {
|
||||
#[error("日常监测记录不存在")]
|
||||
DailyMonitoringNotFound,
|
||||
|
||||
#[error("积分规则不存在")]
|
||||
PointsRuleNotFound,
|
||||
|
||||
#[error("兑换商品不存在")]
|
||||
PointsProductNotFound,
|
||||
|
||||
@@ -92,6 +95,7 @@ impl From<HealthError> for AppError {
|
||||
| HealthError::FollowUpTaskNotFound
|
||||
| HealthError::ConsultationNotFound
|
||||
| HealthError::ArticleNotFound
|
||||
| HealthError::PointsRuleNotFound
|
||||
| HealthError::PointsProductNotFound
|
||||
| HealthError::PointsOrderNotFound
|
||||
| HealthError::OfflineEventNotFound
|
||||
|
||||
@@ -6,7 +6,7 @@ use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp};
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp, UpdateTagReq};
|
||||
use crate::service::article_tag_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
@@ -40,6 +40,24 @@ where
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_tag<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
mut req: Json<UpdateTagReq>,
|
||||
) -> Result<Json<ApiResponse<TagResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
req.sanitize();
|
||||
let result = article_tag_service::update_tag(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteTagReq {
|
||||
pub version: i32,
|
||||
|
||||
@@ -227,6 +227,38 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_rule<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(rule_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::points_dto::UpdateRuleWithVersion>,
|
||||
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
let mut data = wrapper.data;
|
||||
data.sanitize();
|
||||
let result = points_service::update_rule(
|
||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), data, wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_rule<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(rule_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
points_service::delete_rule(
|
||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
pub async fn admin_create_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -243,6 +275,38 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn admin_update_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(product_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::points_dto::UpdateProductWithVersion>,
|
||||
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
let mut data = wrapper.data;
|
||||
data.sanitize();
|
||||
let result = points_service::update_product(
|
||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), data, wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn admin_delete_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(product_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
points_service::delete_product(
|
||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
pub async fn admin_list_orders<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
@@ -372,7 +372,8 @@ impl HealthModule {
|
||||
)
|
||||
.route(
|
||||
"/health/article-tags/{id}",
|
||||
axum::routing::delete(article_tag_handler::delete_tag),
|
||||
axum::routing::put(article_tag_handler::update_tag)
|
||||
.delete(article_tag_handler::delete_tag),
|
||||
)
|
||||
// 积分商城 — 患者端
|
||||
.route(
|
||||
@@ -426,10 +427,20 @@ impl HealthModule {
|
||||
axum::routing::get(points_handler::list_rules)
|
||||
.post(points_handler::create_rule),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/rules/{id}",
|
||||
axum::routing::put(points_handler::update_rule)
|
||||
.delete(points_handler::delete_rule),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/products",
|
||||
axum::routing::post(points_handler::admin_create_product),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/products/{id}",
|
||||
axum::routing::put(points_handler::admin_update_product)
|
||||
.delete(points_handler::admin_delete_product),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/orders",
|
||||
axum::routing::get(points_handler::admin_list_orders),
|
||||
|
||||
@@ -9,7 +9,7 @@ use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::error::check_version;
|
||||
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp};
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp, UpdateTagReq};
|
||||
use crate::entity::article_tag;
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
@@ -66,6 +66,44 @@ pub async fn create_tag(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_tag(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateTagReq,
|
||||
) -> HealthResult<TagResp> {
|
||||
let expected_version = req.version;
|
||||
let model = article_tag::Entity::find()
|
||||
.filter(article_tag::Column::Id.eq(id))
|
||||
.filter(article_tag::Column::TenantId.eq(tenant_id))
|
||||
.filter(article_tag::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::TagNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: article_tag::ActiveModel = model.into();
|
||||
active.name = Set(req.name);
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "article_tag.updated", "article_tag")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(TagResp {
|
||||
id: m.id, name: m.name,
|
||||
created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_tag(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
|
||||
@@ -645,6 +645,89 @@ pub async fn create_product(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_product(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
product_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdatePointsProductReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<PointsProductResp> {
|
||||
let model = points_product::Entity::find()
|
||||
.filter(points_product::Column::Id.eq(product_id))
|
||||
.filter(points_product::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_product::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsProductNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_product::ActiveModel = model.into();
|
||||
if let Some(name) = req.name { active.name = Set(name); }
|
||||
if let Some(product_type) = req.product_type { active.product_type = Set(product_type); }
|
||||
if let Some(points_cost) = req.points_cost { active.points_cost = Set(points_cost); }
|
||||
if let Some(stock) = req.stock { active.stock = Set(stock); }
|
||||
if let Some(image_url) = req.image_url { active.image_url = Set(Some(image_url)); }
|
||||
if let Some(description) = req.description { active.description = Set(Some(description)); }
|
||||
if let Some(service_config) = req.service_config { active.service_config = Set(Some(service_config)); }
|
||||
if let Some(is_active) = req.is_active { active.is_active = Set(is_active); }
|
||||
if let Some(sort_order) = req.sort_order { active.sort_order = Set(sort_order); }
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_product.updated", "points_product")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(PointsProductResp {
|
||||
id: m.id, name: m.name, product_type: m.product_type,
|
||||
points_cost: m.points_cost, stock: m.stock,
|
||||
image_url: m.image_url, description: m.description,
|
||||
is_active: m.is_active, sort_order: m.sort_order,
|
||||
created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_product(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
product_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = points_product::Entity::find()
|
||||
.filter(points_product::Column::Id.eq(product_id))
|
||||
.filter(points_product::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_product::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsProductNotFound)?;
|
||||
|
||||
let _next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_product::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_product.deleted", "points_product")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 兑换(FIFO 消费积分)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1055,6 +1138,89 @@ pub async fn create_rule(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_rule(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
rule_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdatePointsRuleReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<PointsRuleResp> {
|
||||
let model = points_rule::Entity::find()
|
||||
.filter(points_rule::Column::Id.eq(rule_id))
|
||||
.filter(points_rule::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_rule::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsRuleNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_rule::ActiveModel = model.into();
|
||||
if let Some(name) = req.name { active.name = Set(name); }
|
||||
if let Some(description) = req.description { active.description = Set(Some(description)); }
|
||||
if let Some(points_value) = req.points_value { active.points_value = Set(points_value); }
|
||||
if let Some(daily_cap) = req.daily_cap { active.daily_cap = Set(daily_cap); }
|
||||
if let Some(streak_7d_bonus) = req.streak_7d_bonus { active.streak_7d_bonus = Set(streak_7d_bonus); }
|
||||
if let Some(streak_14d_bonus) = req.streak_14d_bonus { active.streak_14d_bonus = Set(streak_14d_bonus); }
|
||||
if let Some(streak_30d_bonus) = req.streak_30d_bonus { active.streak_30d_bonus = Set(streak_30d_bonus); }
|
||||
if let Some(is_active) = req.is_active { active.is_active = Set(is_active); }
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_rule.updated", "points_rule")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(PointsRuleResp {
|
||||
id: m.id, event_type: m.event_type, name: m.name,
|
||||
description: m.description, points_value: m.points_value,
|
||||
daily_cap: m.daily_cap, streak_7d_bonus: m.streak_7d_bonus,
|
||||
streak_14d_bonus: m.streak_14d_bonus, streak_30d_bonus: m.streak_30d_bonus,
|
||||
is_active: m.is_active, created_at: m.created_at,
|
||||
updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_rule(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
rule_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = points_rule::Entity::find()
|
||||
.filter(points_rule::Column::Id.eq(rule_id))
|
||||
.filter(points_rule::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_rule::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsRuleNotFound)?;
|
||||
|
||||
let _next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_rule::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_rule.deleted", "points_rule")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 线下活动
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user