@@ -25,3 +25,4 @@ server/dukang-api/assets/fonts/*.ttf
|
|||||||
server/dukang-api/assets/fonts/*.ttc
|
server/dukang-api/assets/fonts/*.ttc
|
||||||
server/dukang-api/assets/fonts/*.otf
|
server/dukang-api/assets/fonts/*.otf
|
||||||
deploy/auto-release.env
|
deploy/auto-release.env
|
||||||
|
deploy/backups/
|
||||||
|
|||||||
@@ -1,133 +1,37 @@
|
|||||||
# 杜康好客 · AGENTS.md
|
# 杜康好客 · AGENTS.md
|
||||||
|
|
||||||
> AI 编码入口。人类协作者仍读 [`agent.md`](./agent.md);本文件供 Cursor / Codex / Copilot 等自动加载。
|
> AI 编码入口。人类读 [`agent.md`](./agent.md)。启动、端口、测试账号见 [`README.md`](./README.md)。
|
||||||
|
|
||||||
## 项目概览
|
杜康酒业 O2O:**购酒 → 发券 → 门店核销**。Monorepo + NestJS 单体(`/api/v1`,`{ code, message, data }`)。
|
||||||
|
|
||||||
杜康酒业 O2O 平台:**购酒 → 发券 → 门店核销**。Monorepo + 单体 NestJS(方案三:模块 OWNER)。
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `apps/` | `h5-user` · `h5-shop` · `h5-partner` · `admin-web` · `mini-user` |
|
||||||
|
| `packages/` | `shared-types`(枚举/DTO)· `domain`(纯规则,无 IO)· `shared-ui` |
|
||||||
|
| `server/dukang-api/` | 唯一 API |
|
||||||
|
| `pages/` | UI 原型,只读 |
|
||||||
|
|
||||||
| 阶段 | 手册 | 说明 |
|
前端:[`apps/AGENTS.md`](./apps/AGENTS.md)。后端模块/表 OWNER:[`server/dukang-api/AGENTS.md`](./server/dukang-api/AGENTS.md)。
|
||||||
|------|------|------|
|
|
||||||
| **V3.0 唯一需求源** | [`杜康好客-v3-PRD.md`](./杜康好客-v3-PRD.md) | 产品需求与三波交付(**只看这个**) |
|
|
||||||
| **现状对照** | [`杜康好客-v3-现状对照.md`](./杜康好客-v3-现状对照.md) | 已完成 / 冲突 / 缺口审计 |
|
|
||||||
| **V3 实现验收** | [`杜康好客-v3编码手册.md`](./杜康好客-v3编码手册.md) | 核销规则、分工、闭环 |
|
|
||||||
| ~~V2 / preV1~~ | 历史参考(**已压缩**;V2 全文见 git 2026-08-06 前) | **不再作为需求依据** |
|
|
||||||
|
|
||||||
**禁止**:臆造 PRD 未定义规则;依赖 `doc/` 下过时文档;跨 OWNER 直写他人 Prisma 表。
|
## 文档(冲突时)
|
||||||
|
|
||||||
## 启动命令
|
1. `杜康好客-v3-PRD.md` — **唯一需求事实源**(编码时禁止改业务规则;需求变更只改它)
|
||||||
|
2. `杜康好客-v3-现状对照.md` — 进度
|
||||||
```bash
|
3. `杜康好客-v3编码手册.md` — 验收、核销、分工
|
||||||
# 基础设施
|
4. `conventions.md` — 协作边界
|
||||||
cd deploy && docker compose up -d # dukang-v1:MySQL :6016 Redis :6017
|
5. V2 手册 §四–§六 — 架构/DB/API **参考**(与 V3 冲突则忽略)
|
||||||
|
|
||||||
# 依赖与数据库
|
|
||||||
pnpm install
|
|
||||||
cp server/dukang-api/.env.example server/dukang-api/.env
|
|
||||||
pnpm db:generate && pnpm db:validate
|
|
||||||
cd server/dukang-api && npx prisma db push && pnpm prisma:seed
|
|
||||||
|
|
||||||
# 开发(分终端)
|
|
||||||
pnpm dev:api # http://localhost:3010/api/v1
|
|
||||||
pnpm dev:user # :5173
|
|
||||||
pnpm dev:shop # :5174
|
|
||||||
pnpm dev:partner # :5175
|
|
||||||
pnpm dev:admin # preV1 HQ 替代(内部工具,非 V2 小程序)
|
|
||||||
|
|
||||||
# 验证
|
|
||||||
node scripts/smoke-v3.mjs
|
|
||||||
node scripts/smoke-prev1.mjs
|
|
||||||
pnpm lint && pnpm test
|
|
||||||
```
|
|
||||||
|
|
||||||
Mock 验证码:`999888`。测试账号见 [`README.md`](./README.md)。
|
|
||||||
|
|
||||||
> **端口冲突**:`h5-partner` 与 `admin-web` 同为 5175,勿同时 `dev:partner` + `dev:admin`。
|
|
||||||
|
|
||||||
## 文档优先级(冲突时)
|
|
||||||
|
|
||||||
1. `杜康好客-v3-PRD.md` — **唯一需求事实源**
|
|
||||||
2. `杜康好客-v3-现状对照.md` — 实现进度审计
|
|
||||||
3. `杜康好客-v3编码手册.md` — 实现验收、核销、分工
|
|
||||||
4. `conventions.md` — 协作与模块边界
|
|
||||||
5. V2 手册 §四 §五 §六 — 仅作架构/DB/API **参考**(与 V3 冲突时忽略)
|
|
||||||
6. `pages/{user,shop,partner,hq}/` — UI 参照
|
6. `pages/{user,shop,partner,hq}/` — UI 参照
|
||||||
|
|
||||||
> **V2 / preV1 手册不再作为需求依据。**
|
**禁止**:臆造 PRD 未定义规则;依赖 `doc/` 过时文档;`apps/*` import `server/*`;跨模块 `prisma` 写他人表。V2 / preV1 手册不再作需求依据。
|
||||||
|
|
||||||
## 团队与 OWNER 边界(2 人)
|
## OWNER(2026-07 起暂由 jacy 代管全部)
|
||||||
|
|
||||||
| 负责人 | Git 账号 | 职责 |
|
`jacy-dukang` 可改 `apps/*`、`packages/*`、全部后端模块。逻辑边界仍按 A/B/C/D,便于子 Agent 隔离。Prisma 迁移由 jacy 主导。
|
||||||
|--------|----------|------|
|
|
||||||
| **Jacy**(管理员) | `jacy-dukang` | C 端、admin-web、后端主模块、packages、Prisma 迁移主 Review |
|
|
||||||
| **刘景尧** | `刘景尧` | 合伙人 H5、门店 H5、`store` / `redeem` 模块(**暂由 jacy-dukang 代管**) |
|
|
||||||
|
|
||||||
> **临时分工(2026-07)**:刘景尧任务暂由 `jacy-dukang` 全权负责;逻辑 OWNER B+D 路径可改,Prisma 迁移由 jacy 主导。
|
- 跨模块只 inject 对方 **exported Service**
|
||||||
|
- Mock 只在 `integrations/*`;微信/配送回调只进 `callbacks/`
|
||||||
|
|
||||||
逻辑模块边界仍按 A/B/C/D 划分(便于 Agent 隔离),**当前人员合并**如下:
|
## 不可偏离
|
||||||
|
|
||||||
| 逻辑 OWNER | 负责人 | 可改路径 | 后端 Module |
|
|
||||||
|------------|--------|----------|-------------|
|
|
||||||
| **A + C + Lead + B + D** | jacy-dukang | `apps/*`, `packages/*`, `callbacks/`, `jobs/`, `common/`, `integrations/` | 全部后端模块 |
|
|
||||||
| ~~B + D~~ | ~~刘景尧~~ | — | 恢复分工前由 jacy 代管 |
|
|
||||||
|
|
||||||
**Prisma 迁移**:jacy-dukang 主导 Review;涉及 `store_*` / 核销表时仍建议刘景尧知会(恢复分工后共同 Review)。
|
|
||||||
|
|
||||||
### 跨模块规则(R1–R8 摘要)
|
|
||||||
|
|
||||||
- 只 inject 对方 Module **exports 的 Service**,禁止 `prisma.xxx` 写他人表
|
|
||||||
- `apps/*` 禁止 import `server/*` 源码;只走 HTTP + `packages/shared-types`
|
|
||||||
- 业务纯规则 → `packages/domain`;枚举/DTO → `packages/shared-types`
|
|
||||||
- 微信/配送回调入口 **仅** `callbacks/`;Mock 实现 **仅** `integrations/*`
|
|
||||||
|
|
||||||
## Cursor 规范
|
|
||||||
|
|
||||||
| 类型 | 路径 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| Rules | `.cursor/rules/` | 自动加载的编码规范 |
|
|
||||||
| 编码 Skill | `.cursor/skills/dukang-coding/` | @dukang-coding |
|
|
||||||
| V3.0 需求 Skill | `.cursor/skills/dukang-v3/` | @dukang-v3(PRD、Wave、REQ/ACC) |
|
|
||||||
| 需求 Skill | `.cursor/skills/dukang-project/` | @dukang-project |
|
|
||||||
| preV1 Skill | `.cursor/skills/dukang-prev1/` | Mock / Feature Flag |
|
|
||||||
| 任务卡 Skill | `.cursor/skills/dukang-task-card/` | DLV-W* / P1-* / M*-* |
|
|
||||||
| 发布 Skill | `.cursor/skills/dukang-release/` | @杜康发布 / @dukang-release |
|
|
||||||
|
|
||||||
## 子 Agent(按职责选用)
|
|
||||||
|
|
||||||
在 Cursor Agent 输入 `/` 选择:
|
|
||||||
|
|
||||||
| 子 Agent | 负责人 | 适用场景 |
|
|
||||||
|----------|--------|----------|
|
|
||||||
| `v3-delivery-lead` | jacy-dukang | V3.0 三波交付统筹、DLV 拆分、跨 Owner 协调 |
|
|
||||||
| `v3-req-reviewer` | — | V3.0 需求只读审查、REQ/ACC 缺口分析 |
|
|
||||||
| `owner-a-user-trade` | jacy-dukang | C 端、订单、支付、权益、埋点 |
|
|
||||||
| `owner-c-catalog-ops` | jacy-dukang | admin-web、开城商品、结算、运营 |
|
|
||||||
| `backend-lead` | jacy-dukang | packages、callbacks、jobs、Prisma 横切 |
|
|
||||||
| `owner-b-partner-store` | 刘景尧 | 合伙人 H5、门店 CRUD/审核 |
|
|
||||||
| `owner-d-shop-redeem` | 刘景尧 | 门店 H5、核销 |
|
|
||||||
| `boundary-reviewer` | — | PR 前只读审查跨模块违规 |
|
|
||||||
|
|
||||||
## Skills(按需 @)
|
|
||||||
|
|
||||||
| Skill | 何时用 |
|
|
||||||
|-------|--------|
|
|
||||||
| `dukang-v3` | V3.0 PRD、现状对照、Wave/REQ/ACC |
|
|
||||||
| `dukang-coding` | 实现功能、修 Bug(通用编码流程) |
|
|
||||||
| `dukang-project` | PRD/需求评审、原型对照、任务卡规划(**不用于日常编码**) |
|
|
||||||
| `dukang-prev1` | Mock 开关、preV1 裁剪、Feature Flag |
|
|
||||||
| `dukang-task-card` | 领取任务卡 `DLV-W*-*` / `P1-*` / `M*-*` 并按 DoD 交付 |
|
|
||||||
| `dukang-release`(杜康发布) | `dev_jacy`→`dev` 合并 + `deploy.sh` 发版 |
|
|
||||||
|
|
||||||
## 快速指令
|
|
||||||
|
|
||||||
- 现状审计:`@dukang-v3` + [`杜康好客-v3-现状对照.md`](./杜康好客-v3-现状对照.md)
|
|
||||||
- 实现 DLV 任务:`@dukang-coding 实现 DLV-W1-M2`
|
|
||||||
- 波次规划:`/v3-delivery-lead 拆分 Wave 2 任务`
|
|
||||||
- 需求缺口审查:`/v3-req-reviewer 审查门店提现`
|
|
||||||
- PR 前审查:`/boundary-reviewer`
|
|
||||||
- 原型参照:`pages/{user,shop,partner}/`(只读)
|
|
||||||
|
|
||||||
## 核心业务常量(不可偏离)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
权益额 = benefit_amount ?? price
|
权益额 = benefit_amount ?? price
|
||||||
@@ -137,38 +41,10 @@ C 端门店仅 status=OPEN
|
|||||||
订单 Tab:待付款 | 已付款 | 已完成
|
订单 Tab:待付款 | 已付款 | 已完成
|
||||||
```
|
```
|
||||||
|
|
||||||
**iOS 微信 H5 JSSDK**:登录/OAuth 后禁止仅 SPA 跳转再调扫码;见 `packages/weixin-sdk/GOTCHAS.md`、知识库「门店端 · 踩坑」。
|
微信:iOS H5 登录后禁止仅 SPA 跳转再扫码(`packages/weixin-sdk/GOTCHAS.md`)。小程序 `open-type` 按钮祖先禁止 `stopPropagation`;weapp 标题只用 `navigationBarTitleText`。`h5-partner` 与 `admin-web` 同端口 5175,勿同时开。
|
||||||
**微信小程序 open-type**:`chooseAvatar` 等 Button 的祖先禁止 `stopPropagation`(会编成 catchtap);见知识库「C 端 · 踩坑」、`.cursor/rules/mini-user-weapp-opentype.mdc`。
|
|
||||||
**微信小程序页面标题**:weapp 只用原生 `navigationBarTitleText`,不要再画一层与导航栏重复的 `SubPageHeader` title;见 `.cursor/rules/mini-user-weapp-nav-title.mdc`。
|
|
||||||
|
|
||||||
## 环境与发版
|
## 按需加载
|
||||||
|
|
||||||
| 环境 | 分支 | 目录 | 端口 | 域名 |
|
- 编码 `/dukang-coding` · V3 需求 `/dukang-v3` · 发版 `/dukang-release`(只说「发版」= 生产)
|
||||||
|------|------|------|------|------|
|
- 子 Agent:`/owner-a-user-trade` 等(见 `.cursor/agents/`)
|
||||||
| local | `dev_jacy` | 本机 Docker 6016/6017 | API `:3010` | — |
|
- DoD:验收项满足;不跨模块直写表;类型在 `shared-types`、规则在 `domain`;相关 lint/单测过
|
||||||
| **staging 测试** | `dev` | `/opt/dukang-staging` | 8190–8194 | `*-test.dukanghaoke.com` |
|
|
||||||
| **production 生产** | `main` | `/opt/dukang` | 8090–8094 | `*.dukanghaoke.com` |
|
|
||||||
|
|
||||||
- 日常发布 → `deploy/deploy-staging.sh`(`dev`)
|
|
||||||
- 生产发布 → `deploy/deploy-prod.sh`(`main`,验收后)
|
|
||||||
- Staging:正式微信 + 全 Mock;配置见 `.env.staging.example`
|
|
||||||
- 详情:`@dukang-release` / `.cursor/skills/dukang-release/SKILL.md`
|
|
||||||
|
|
||||||
## 提交与 PR
|
|
||||||
|
|
||||||
- Conventional Commits:`feat(trade):`、`fix(redeem):`;scope = 端或模块
|
|
||||||
- 跨模块 PR → jacy-dukang + 刘景尧 共同 Review(若涉及双方模块)
|
|
||||||
- 改 API/表 → 同步 v3-PRD + v3编码手册 + `shared-types`
|
|
||||||
- 不提交 `.env`、`dist/`、`node_modules/`
|
|
||||||
|
|
||||||
## 完成定义(DoD)
|
|
||||||
|
|
||||||
- [ ] 任务卡验收项全部满足
|
|
||||||
- [ ] 未跨模块直写 Prisma 表
|
|
||||||
- [ ] 枚举/DTO 在 `shared-types`;纯规则在 `domain`
|
|
||||||
- [ ] `pnpm lint` 无新增错误;相关 domain 单测通过
|
|
||||||
|
|
||||||
## 嵌套指引
|
|
||||||
|
|
||||||
- 后端细节:[`server/dukang-api/AGENTS.md`](./server/dukang-api/AGENTS.md)
|
|
||||||
- 前端四 App:[`apps/AGENTS.md`](./apps/AGENTS.md)
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone =
|
|||||||
cityName?: string;
|
cityName?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
partnerAccount?: { companyName?: string };
|
partnerAccount?: { companyName?: string };
|
||||||
|
primaryAccount?: {
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
const coupon = detail.coupon as { couponNo?: string; order?: { orderNo?: string } } | undefined;
|
const coupon = detail.coupon as { couponNo?: string; order?: { orderNo?: string } } | undefined;
|
||||||
@@ -46,6 +51,15 @@ export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone =
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="门店地址">{store?.address ?? '—'}</Descriptions.Item>
|
<Descriptions.Item label="门店地址">{store?.address ?? '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="合伙人">{store?.partnerAccount?.companyName ?? '—'}</Descriptions.Item>
|
<Descriptions.Item label="合伙人">{store?.partnerAccount?.companyName ?? '—'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款户名">
|
||||||
|
{store?.primaryAccount?.bankAccountName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款账号">
|
||||||
|
{store?.primaryAccount?.bankAccountNo || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户行">
|
||||||
|
{store?.primaryAccount?.bankBranch || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="权益券号">{coupon?.couponNo ?? '—'}</Descriptions.Item>
|
<Descriptions.Item label="权益券号">{coupon?.couponNo ?? '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="关联订单">{coupon?.order?.orderNo ?? '—'}</Descriptions.Item>
|
<Descriptions.Item label="关联订单">{coupon?.order?.orderNo ?? '—'}</Descriptions.Item>
|
||||||
{Array.isArray(detail.allocations) && (detail.allocations as unknown[]).length > 0 ? (
|
{Array.isArray(detail.allocations) && (detail.allocations as unknown[]).length > 0 ? (
|
||||||
|
|||||||
@@ -174,11 +174,13 @@ function PackageDetailCard({
|
|||||||
pkg,
|
pkg,
|
||||||
change,
|
change,
|
||||||
changes,
|
changes,
|
||||||
|
imageSize = 72,
|
||||||
}: {
|
}: {
|
||||||
title?: string;
|
title?: string;
|
||||||
pkg: StorePackageItemDto | StorePackageViewDto;
|
pkg: StorePackageItemDto | StorePackageViewDto;
|
||||||
change?: keyof typeof CHANGE_LABELS;
|
change?: keyof typeof CHANGE_LABELS;
|
||||||
changes?: FieldChange[];
|
changes?: FieldChange[];
|
||||||
|
imageSize?: number;
|
||||||
}) {
|
}) {
|
||||||
const images = normalizeStorePackageImageUrls(pkg);
|
const images = normalizeStorePackageImageUrls(pkg);
|
||||||
const meta = change ? CHANGE_LABELS[change] : null;
|
const meta = change ? CHANGE_LABELS[change] : null;
|
||||||
@@ -217,7 +219,7 @@ function PackageDetailCard({
|
|||||||
<Image.PreviewGroup>
|
<Image.PreviewGroup>
|
||||||
<Space wrap size={8}>
|
<Space wrap size={8}>
|
||||||
{images.map((url) => (
|
{images.map((url) => (
|
||||||
<Image key={url} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
<Image key={url} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Image.PreviewGroup>
|
</Image.PreviewGroup>
|
||||||
@@ -265,6 +267,7 @@ export type StorePackageAuditPanelProps = {
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
/** 是否在面板顶部显示通过/驳回(抽屉 extra 另有按钮时可关) */
|
/** 是否在面板顶部显示通过/驳回(抽屉 extra 另有按钮时可关) */
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
onAudited?: () => void;
|
onAudited?: () => void;
|
||||||
onDetailLoaded?: (detail: StorePackageAuditDetailDto | null) => void;
|
onDetailLoaded?: (detail: StorePackageAuditDetailDto | null) => void;
|
||||||
};
|
};
|
||||||
@@ -273,6 +276,7 @@ export type StorePackageAuditPanelProps = {
|
|||||||
export default function StorePackageAuditPanel({
|
export default function StorePackageAuditPanel({
|
||||||
requestId,
|
requestId,
|
||||||
showActions = true,
|
showActions = true,
|
||||||
|
fullscreen = false,
|
||||||
onAudited,
|
onAudited,
|
||||||
onDetailLoaded,
|
onDetailLoaded,
|
||||||
}: StorePackageAuditPanelProps) {
|
}: StorePackageAuditPanelProps) {
|
||||||
@@ -389,6 +393,7 @@ export default function StorePackageAuditPanel({
|
|||||||
title={`套餐 ${index + 1}`}
|
title={`套餐 ${index + 1}`}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
change={changeByKey.get(packageKey(pkg, index))}
|
change={changeByKey.get(packageKey(pkg, index))}
|
||||||
|
imageSize={fullscreen ? 160 : 72}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@@ -407,6 +412,7 @@ export default function StorePackageAuditPanel({
|
|||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
change={changeByKey.get(packageKey(pkg, index))}
|
change={changeByKey.get(packageKey(pkg, index))}
|
||||||
changes={changesByKey.get(packageKey(pkg, index))}
|
changes={changesByKey.get(packageKey(pkg, index))}
|
||||||
|
imageSize={fullscreen ? 160 : 72}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -196,10 +196,10 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
|||||||
'/benefit/ledgers': 'benefit',
|
'/benefit/ledgers': 'benefit',
|
||||||
'/redeem-records': 'benefit',
|
'/redeem-records': 'benefit',
|
||||||
'/redeem-pending': 'benefit',
|
'/redeem-pending': 'benefit',
|
||||||
'/redeem/debug': 'benefit',
|
'/redeem/debug': 'benefit_debug',
|
||||||
'deliveries-group': 'deliveries',
|
'deliveries-group': 'deliveries',
|
||||||
'/deliveries': 'deliveries',
|
'/deliveries': 'deliveries',
|
||||||
'/deliveries/xiaofeixia': 'deliveries',
|
'/deliveries/xiaofeixia': 'deliveries_debug',
|
||||||
'/tickets': 'tickets',
|
'/tickets': 'tickets',
|
||||||
'/tickets/support': 'tech_support',
|
'/tickets/support': 'tech_support',
|
||||||
'dev-plan-group': 'dev_plan',
|
'dev-plan-group': 'dev_plan',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
|
Image,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
DEV_PLAN_TASK_TYPE_LABELS,
|
DEV_PLAN_TASK_TYPE_LABELS,
|
||||||
SUPPORT_TICKET_STATUS_LABELS,
|
SUPPORT_TICKET_STATUS_LABELS,
|
||||||
type DevPlanTaskDto,
|
type DevPlanTaskDto,
|
||||||
|
type DevPlanTaskExportFormat,
|
||||||
type DevPlanTaskStatusDto,
|
type DevPlanTaskStatusDto,
|
||||||
type DevPlanTaskTypeDto,
|
type DevPlanTaskTypeDto,
|
||||||
type DevPlanVersionDto,
|
type DevPlanVersionDto,
|
||||||
@@ -25,7 +27,9 @@ import {
|
|||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { fmtTime } from '../lib/constants';
|
import { fmtTime } from '../lib/constants';
|
||||||
|
import { downloadBase64File } from '../lib/exportExcel';
|
||||||
import { useAdminList } from '../lib/useAdminList';
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
|
import OssUpload from '../components/OssUpload';
|
||||||
|
|
||||||
const TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map((v) => ({
|
const TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map((v) => ({
|
||||||
value: v,
|
value: v,
|
||||||
@@ -56,6 +60,8 @@ export default function DevPlanTasksPage() {
|
|||||||
const [editing, setEditing] = useState<DevPlanTaskDto | null>(null);
|
const [editing, setEditing] = useState<DevPlanTaskDto | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dispatching, setDispatching] = useState(false);
|
const [dispatching, setDispatching] = useState(false);
|
||||||
|
const [exportFormat, setExportFormat] = useState<DevPlanTaskExportFormat>('xlsx');
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [dispatchForm] = Form.useForm<{ supplement?: string }>();
|
const [dispatchForm] = Form.useForm<{ supplement?: string }>();
|
||||||
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
||||||
@@ -88,24 +94,30 @@ export default function DevPlanTasksPage() {
|
|||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
form.setFieldsValue({ content: '', type: 'BUG', status: 'TODO', supportTicketId: undefined });
|
form.setFieldsValue({ content: '', type: 'BUG', status: 'TODO', supportTicketId: undefined, attachmentUrls: [''] });
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row: DevPlanTaskDto) {
|
function openEdit(row: DevPlanTaskDto) {
|
||||||
setEditing(row);
|
setEditing(row);
|
||||||
form.setFieldsValue({ content: row.content, type: row.type, status: row.status });
|
form.setFieldsValue({
|
||||||
|
content: row.content,
|
||||||
|
type: row.type,
|
||||||
|
status: row.status,
|
||||||
|
attachmentUrls: row.attachmentUrls?.length ? row.attachmentUrls : [''],
|
||||||
|
});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
const attachmentUrls = (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean);
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(values),
|
body: JSON.stringify({ ...values, attachmentUrls }),
|
||||||
});
|
});
|
||||||
message.success('已更新');
|
message.success('已更新');
|
||||||
} else {
|
} else {
|
||||||
@@ -115,6 +127,7 @@ export default function DevPlanTasksPage() {
|
|||||||
content: values.content,
|
content: values.content,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
supportTicketId: values.supportTicketId || undefined,
|
supportTicketId: values.supportTicketId || undefined,
|
||||||
|
attachmentUrls,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
message.success('已创建');
|
message.success('已创建');
|
||||||
@@ -199,6 +212,34 @@ export default function DevPlanTasksPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportTasks(scope: 'filter' | 'selected') {
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const result = await request<{
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
contentBase64: string;
|
||||||
|
count: number;
|
||||||
|
}>('/admin/dev-plan/tasks/export', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
scope,
|
||||||
|
format: exportFormat,
|
||||||
|
ids: scope === 'selected' ? selectedRowKeys : undefined,
|
||||||
|
status: filters.status || undefined,
|
||||||
|
type: filters.type || undefined,
|
||||||
|
keyword: filters.keyword || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
downloadBase64File(result.contentBase64, result.filename, result.mimeType);
|
||||||
|
message.success(`已导出 ${result.count} 条任务`);
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '导出失败');
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<DevPlanTaskDto> = [
|
const columns: ColumnsType<DevPlanTaskDto> = [
|
||||||
{ title: '任务号', dataIndex: 'taskNo', width: 160 },
|
{ title: '任务号', dataIndex: 'taskNo', width: 160 },
|
||||||
{
|
{
|
||||||
@@ -216,6 +257,22 @@ export default function DevPlanTasksPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ title: '内容', dataIndex: 'content', ellipsis: true },
|
{ title: '内容', dataIndex: 'content', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '附件',
|
||||||
|
width: 100,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.attachmentUrls?.length ? (
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
<Space size={4}>
|
||||||
|
{row.attachmentUrls.slice(0, 3).map((url) => (
|
||||||
|
<Image key={url} src={url} width={32} height={32} style={{ objectFit: 'cover' }} />
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
),
|
||||||
|
},
|
||||||
{ title: '来源工单', dataIndex: 'supportTicketNo', width: 140, render: (v) => v || '—' },
|
{ title: '来源工单', dataIndex: 'supportTicketNo', width: 140, render: (v) => v || '—' },
|
||||||
{ title: '创建人', dataIndex: 'creatorName', width: 90 },
|
{ title: '创建人', dataIndex: 'creatorName', width: 90 },
|
||||||
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
@@ -243,7 +300,24 @@ export default function DevPlanTasksPage() {
|
|||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
开发计划 · 任务列表
|
开发计划 · 任务列表
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Space>
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
value={exportFormat}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onChange={setExportFormat}
|
||||||
|
options={[
|
||||||
|
{ value: 'markdown', label: 'Markdown' },
|
||||||
|
{ value: 'docx', label: 'Word' },
|
||||||
|
{ value: 'xlsx', label: 'Excel' },
|
||||||
|
{ value: 'pdf', label: 'PDF' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button loading={exporting} disabled={!selectedRowKeys.length} onClick={() => void exportTasks('selected')}>
|
||||||
|
导出已勾选
|
||||||
|
</Button>
|
||||||
|
<Button loading={exporting} onClick={() => void exportTasks('filter')}>
|
||||||
|
导出全部筛选
|
||||||
|
</Button>
|
||||||
<Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
|
<Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
|
||||||
批量编辑
|
批量编辑
|
||||||
</Button>
|
</Button>
|
||||||
@@ -333,6 +407,29 @@ export default function DevPlanTasksPage() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
<Form.Item label="附件图片">
|
||||||
|
<Form.List name="attachmentUrls">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<Space key={field.key} align="start" style={{ display: 'flex', marginBottom: 8 }}>
|
||||||
|
<Form.Item {...field} style={{ flex: 1, marginBottom: 0 }}>
|
||||||
|
<OssUpload bizType="SUPPORT_TICKET_ATTACHMENT" mediaType="IMAGE" />
|
||||||
|
</Form.Item>
|
||||||
|
{fields.length > 1 ? (
|
||||||
|
<Button type="link" danger onClick={() => remove(field.name)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Button type="dashed" onClick={() => add('')} block>
|
||||||
|
添加图片
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ type BillRow = {
|
|||||||
settlementMethod: string;
|
settlementMethod: string;
|
||||||
status: string;
|
status: string;
|
||||||
paidAt?: string | null;
|
paidAt?: string | null;
|
||||||
|
paymentRef?: string | null;
|
||||||
|
fulfillmentProvider?: {
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankName?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type BillItem = {
|
type BillItem = {
|
||||||
@@ -140,14 +147,30 @@ export default function LogisticsBillsPage() {
|
|||||||
}, [summaryPeriod]);
|
}, [summaryPeriod]);
|
||||||
|
|
||||||
function confirmPay(ids: string[], amountHint?: number) {
|
function confirmPay(ids: string[], amountHint?: number) {
|
||||||
|
let paymentRef = '';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认结算?',
|
title: '确认结算?',
|
||||||
content: `将确认 ${ids.length} 笔物流对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。充值模式将扣减余额,挂账模式标记已打款。`,
|
content: (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
将确认 {ids.length} 笔物流对账单
|
||||||
|
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。充值模式将扣减余额,挂账模式标记已打款。
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="打款凭证号(可选)"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onChange={(e) => {
|
||||||
|
paymentRef = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
okText: '确认结算',
|
okText: '确认结算',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||||
if (ids.length === 1) {
|
if (ids.length === 1) {
|
||||||
await request(`/admin/logistics-bills/${ids[0]}/confirm`, { method: 'POST' });
|
await request(`/admin/logistics-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||||
} else {
|
} else {
|
||||||
await request('/admin/logistics-bills/batch-confirm', {
|
await request('/admin/logistics-bills/batch-confirm', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -217,6 +240,7 @@ export default function LogisticsBillsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
|
const providerBank = (r: BillRow) => r.fulfillmentProvider;
|
||||||
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
||||||
const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.logisticsAmount), 0);
|
const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.logisticsAmount), 0);
|
||||||
|
|
||||||
@@ -248,6 +272,18 @@ export default function LogisticsBillsPage() {
|
|||||||
width: 110,
|
width: 110,
|
||||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '收款户名',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, r) => providerBank(r)?.bankAccountName || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收款账号',
|
||||||
|
width: 140,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, r) => providerBank(r)?.bankAccountNo || '—',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@@ -550,7 +586,31 @@ export default function LogisticsBillsPage() {
|
|||||||
<Descriptions.Item label="结算时间">
|
<Descriptions.Item label="结算时间">
|
||||||
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款凭证">
|
||||||
|
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
{providerBank(detail) ? (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
|
收款账户
|
||||||
|
</Typography.Title>
|
||||||
|
<Descriptions column={1} size="small" bordered style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="户名">
|
||||||
|
{providerBank(detail)?.bankAccountName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户银行">
|
||||||
|
{providerBank(detail)?.bankName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户支行">
|
||||||
|
{providerBank(detail)?.bankBranch || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号">
|
||||||
|
{providerBank(detail)?.bankAccountNo || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
Descriptions,
|
||||||
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -31,8 +33,22 @@ type Row = {
|
|||||||
periodStart: string;
|
periodStart: string;
|
||||||
periodEnd: string;
|
periodEnd: string;
|
||||||
rejectReason?: string | null;
|
rejectReason?: string | null;
|
||||||
partner?: { companyName?: string; phone?: string };
|
paymentRef?: string | null;
|
||||||
partnerAccount?: { companyName?: string; phone?: string };
|
paidAt?: string | null;
|
||||||
|
partner?: {
|
||||||
|
companyName?: string;
|
||||||
|
phone?: string;
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
};
|
||||||
|
partnerAccount?: {
|
||||||
|
companyName?: string;
|
||||||
|
phone?: string;
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type PartnerOption = { id: string; companyName: string; phone?: string };
|
type PartnerOption = { id: string; companyName: string; phone?: string };
|
||||||
@@ -77,6 +93,8 @@ export default function PartnerBillsPage() {
|
|||||||
const [rejectForm] = Form.useForm();
|
const [rejectForm] = Form.useForm();
|
||||||
const [rejecting, setRejecting] = useState(false);
|
const [rejecting, setRejecting] = useState(false);
|
||||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||||
|
const [detail, setDetail] = useState<Row | null>(null);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||||
@@ -127,17 +145,30 @@ export default function PartnerBillsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markPaid(ids: string[], amountHint?: number) {
|
function markPaid(ids: string[], amountHint?: number) {
|
||||||
|
let paymentRef = '';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认打款?',
|
title: '确认打款?',
|
||||||
content: `将标记 ${ids.length} 笔账单为已打款${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。`,
|
content: (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
将标记 {ids.length} 笔账单为已打款
|
||||||
|
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="打款凭证号(可选)"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onChange={(e) => {
|
||||||
|
paymentRef = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
okText: '确认打款',
|
okText: '确认打款',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||||
if (ids.length === 1) {
|
if (ids.length === 1) {
|
||||||
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, {
|
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, { method: 'POST', body });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await request('/admin/partner-bills/batch-mark-paid', {
|
await request('/admin/partner-bills/batch-mark-paid', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -182,6 +213,12 @@ export default function PartnerBillsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openDetail(id: string) {
|
||||||
|
const d = await request<Row>(`/admin/partner-bills/${id}`);
|
||||||
|
setDetail(d);
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
async function exportExcel() {
|
async function exportExcel() {
|
||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
@@ -201,6 +238,7 @@ export default function PartnerBillsPage() {
|
|||||||
|
|
||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
const partnerName = (r: Row) => r.partner?.companyName || r.partnerAccount?.companyName || '—';
|
const partnerName = (r: Row) => r.partner?.companyName || r.partnerAccount?.companyName || '—';
|
||||||
|
const partnerBank = (r: Row) => r.partnerAccount ?? r.partner;
|
||||||
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
||||||
const canSend = selectedRows.filter((r) => r.status === 'PENDING_REVIEW' || r.status === 'REJECTED');
|
const canSend = selectedRows.filter((r) => r.status === 'PENDING_REVIEW' || r.status === 'REJECTED');
|
||||||
const canPay = selectedRows.filter((r) => r.status === 'UNPAID');
|
const canPay = selectedRows.filter((r) => r.status === 'UNPAID');
|
||||||
@@ -238,6 +276,18 @@ export default function PartnerBillsPage() {
|
|||||||
width: 110,
|
width: 110,
|
||||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '收款户名',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, r) => partnerBank(r)?.bankAccountName || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收款账号',
|
||||||
|
width: 140,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, r) => partnerBank(r)?.bankAccountNo || '—',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@@ -259,6 +309,9 @@ export default function PartnerBillsPage() {
|
|||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={0} wrap>
|
<Space size={0} wrap>
|
||||||
|
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||||
|
明细
|
||||||
|
</Button>
|
||||||
{(row.status === 'PENDING_REVIEW' || row.status === 'REJECTED') && (
|
{(row.status === 'PENDING_REVIEW' || row.status === 'REJECTED') && (
|
||||||
<Button type="link" size="small" onClick={() => sendBills([row.id])}>
|
<Button type="link" size="small" onClick={() => sendBills([row.id])}>
|
||||||
发送
|
发送
|
||||||
@@ -465,6 +518,59 @@ export default function PartnerBillsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title="合伙人账单明细"
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
width={560}
|
||||||
|
>
|
||||||
|
{detail && (
|
||||||
|
<>
|
||||||
|
<Descriptions column={1} size="small" bordered>
|
||||||
|
<Descriptions.Item label="账单号">{detail.billNo}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="合伙人">{partnerName(detail)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账期">
|
||||||
|
{String(detail.periodStart).slice(0, 10)} ~ {String(detail.periodEnd).slice(0, 10)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="订单佣金">
|
||||||
|
¥{Number(detail.orderCommission).toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="核销佣金">
|
||||||
|
¥{Number(detail.redeemCommission).toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="应付合计">
|
||||||
|
¥{Number(detail.totalAmount).toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
<Tag color={STATUS_COLORS[detail.status] || 'default'}>
|
||||||
|
{STATUS_LABELS[detail.status] || detail.status}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款时间">
|
||||||
|
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款凭证">
|
||||||
|
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
|
收款账户
|
||||||
|
</Typography.Title>
|
||||||
|
<Descriptions column={1} size="small" bordered>
|
||||||
|
<Descriptions.Item label="户名">
|
||||||
|
{partnerBank(detail)?.bankAccountName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号">
|
||||||
|
{partnerBank(detail)?.bankAccountNo || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户行">
|
||||||
|
{partnerBank(detail)?.bankBranch || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ type Row = {
|
|||||||
settlementRate?: number | null;
|
settlementRate?: number | null;
|
||||||
payoutCount?: number | null;
|
payoutCount?: number | null;
|
||||||
store?: { id: string; name: string; cityName: string; phone?: string | null };
|
store?: { id: string; name: string; cityName: string; phone?: string | null };
|
||||||
|
bankAccount?: {
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoreOption = { id: string; name: string; phone: string };
|
type StoreOption = { id: string; name: string; phone: string };
|
||||||
@@ -55,6 +60,11 @@ const STATUS_COLORS: Record<string, string> = {
|
|||||||
REJECTED: 'red',
|
REJECTED: 'red',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function settlementStatusColor(kind: StoreSettlementKind, status: string) {
|
||||||
|
if (status === 'PAID') return kind === 'WITHDRAW' ? 'cyan' : 'green';
|
||||||
|
return STATUS_COLORS[status] || 'default';
|
||||||
|
}
|
||||||
|
|
||||||
const KIND_COLORS: Record<StoreSettlementKind, string> = {
|
const KIND_COLORS: Record<StoreSettlementKind, string> = {
|
||||||
T1_BILL: 'blue',
|
T1_BILL: 'blue',
|
||||||
WITHDRAW: 'purple',
|
WITHDRAW: 'purple',
|
||||||
@@ -118,14 +128,30 @@ export default function StoreBillsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function confirmPay(ids: string[], amountHint?: number) {
|
function confirmPay(ids: string[], amountHint?: number) {
|
||||||
|
let paymentRef = '';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认打款?',
|
title: '确认打款?',
|
||||||
content: `将确认 ${ids.length} 笔 T+1 门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`,
|
content: (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
将确认 {ids.length} 笔 T+1 门店对账单
|
||||||
|
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="打款凭证号(可选)"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onChange={(e) => {
|
||||||
|
paymentRef = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
okText: '确认打款',
|
okText: '确认打款',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||||
if (ids.length === 1) {
|
if (ids.length === 1) {
|
||||||
await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST' });
|
await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||||
} else {
|
} else {
|
||||||
setBatchLoading(true);
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -247,7 +273,9 @@ export default function StoreBillsPage() {
|
|||||||
dataIndex: 'kind',
|
dataIndex: 'kind',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (k: StoreSettlementKind) => (
|
render: (k: StoreSettlementKind) => (
|
||||||
<Tag color={KIND_COLORS[k]}>{STORE_SETTLEMENT_KIND_LABELS[k] ?? k}</Tag>
|
<Tag color={KIND_COLORS[k]} style={{ minWidth: 72, textAlign: 'center' }}>
|
||||||
|
{STORE_SETTLEMENT_KIND_LABELS[k] ?? k}
|
||||||
|
</Tag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,6 +299,20 @@ export default function StoreBillsPage() {
|
|||||||
{ title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true },
|
{ title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true },
|
||||||
{ title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
|
{ title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
|
||||||
{ title: '城市', dataIndex: ['store', 'cityName'], width: 90 },
|
{ title: '城市', dataIndex: ['store', 'cityName'], width: 90 },
|
||||||
|
{
|
||||||
|
title: '收款户名',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountName || '—' : '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收款账号',
|
||||||
|
width: 140,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountNo || '—' : '—',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '笔数',
|
title: '笔数',
|
||||||
width: 80,
|
width: 80,
|
||||||
@@ -288,7 +330,9 @@ export default function StoreBillsPage() {
|
|||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (s: string, row) => (
|
render: (s: string, row) => (
|
||||||
<Tag color={STATUS_COLORS[s] || 'default'}>{storeSettlementStatusLabel(row.kind, s)}</Tag>
|
<Tag color={settlementStatusColor(row.kind, s)}>
|
||||||
|
{storeSettlementStatusLabel(row.kind, s)}
|
||||||
|
</Tag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -339,7 +383,8 @@ export default function StoreBillsPage() {
|
|||||||
门店账单
|
门店账单
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口
|
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。
|
||||||
|
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
||||||
<Typography.Text type="danger">
|
<Typography.Text type="danger">
|
||||||
@@ -503,7 +548,32 @@ export default function StoreBillsPage() {
|
|||||||
<Descriptions.Item label="打款时间">
|
<Descriptions.Item label="打款时间">
|
||||||
{detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
|
{detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款凭证">
|
||||||
|
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
{(detail.storeAccount as {
|
||||||
|
bankAccountName?: string;
|
||||||
|
bankAccountNo?: string;
|
||||||
|
bankBranch?: string;
|
||||||
|
} | null) ? (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
|
收款账户
|
||||||
|
</Typography.Title>
|
||||||
|
<Descriptions column={1} size="small" bordered>
|
||||||
|
<Descriptions.Item label="户名">
|
||||||
|
{(detail.storeAccount as { bankAccountName?: string }).bankAccountName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号">
|
||||||
|
{(detail.storeAccount as { bankAccountNo?: string }).bankAccountNo || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户行">
|
||||||
|
{(detail.storeAccount as { bankBranch?: string }).bankBranch || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
核销明细
|
核销明细
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ function InfoChangeImageDiff({
|
|||||||
field,
|
field,
|
||||||
live,
|
live,
|
||||||
proposed,
|
proposed,
|
||||||
|
imageSize = 72,
|
||||||
}: {
|
}: {
|
||||||
field: string;
|
field: string;
|
||||||
live: unknown;
|
live: unknown;
|
||||||
proposed: unknown;
|
proposed: unknown;
|
||||||
|
imageSize?: number;
|
||||||
}) {
|
}) {
|
||||||
const liveUrls =
|
const liveUrls =
|
||||||
field === 'coverUrl'
|
field === 'coverUrl'
|
||||||
@@ -83,7 +85,7 @@ function InfoChangeImageDiff({
|
|||||||
<Image.PreviewGroup>
|
<Image.PreviewGroup>
|
||||||
<Space wrap size={8}>
|
<Space wrap size={8}>
|
||||||
{liveUrls.map((url) => (
|
{liveUrls.map((url) => (
|
||||||
<Image key={`live-${url}`} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
<Image key={`live-${url}`} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Image.PreviewGroup>
|
</Image.PreviewGroup>
|
||||||
@@ -99,7 +101,7 @@ function InfoChangeImageDiff({
|
|||||||
<Image.PreviewGroup>
|
<Image.PreviewGroup>
|
||||||
<Space wrap size={8}>
|
<Space wrap size={8}>
|
||||||
{proposedUrls.map((url) => (
|
{proposedUrls.map((url) => (
|
||||||
<Image key={`new-${url}`} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
<Image key={`new-${url}`} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Image.PreviewGroup>
|
</Image.PreviewGroup>
|
||||||
@@ -128,6 +130,7 @@ function InfoChangeAuditPanel({
|
|||||||
const [rejectOpen, setRejectOpen] = useState(false);
|
const [rejectOpen, setRejectOpen] = useState(false);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
const [infoFullscreen, setInfoFullscreen] = useState(false);
|
||||||
|
|
||||||
async function reload(nextPage = page, nextStatus = status) {
|
async function reload(nextPage = page, nextStatus = status) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -286,12 +289,19 @@ function InfoChangeAuditPanel({
|
|||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={detail ? `${detail.storeName || detail.storeId} · 信息变更` : '信息变更详情'}
|
title={detail ? `${detail.storeName || detail.storeId} · 信息变更` : '信息变更详情'}
|
||||||
width={680}
|
width={infoFullscreen ? '100%' : 680}
|
||||||
open={detailOpen}
|
open={detailOpen}
|
||||||
onClose={() => setDetailOpen(false)}
|
onClose={() => {
|
||||||
|
setDetailOpen(false);
|
||||||
|
setInfoFullscreen(false);
|
||||||
|
}}
|
||||||
extra={
|
extra={
|
||||||
detail?.status === 'PENDING' ? (
|
|
||||||
<Space>
|
<Space>
|
||||||
|
<Button onClick={() => setInfoFullscreen((v) => !v)}>
|
||||||
|
{infoFullscreen ? '退出全屏' : '全屏查看'}
|
||||||
|
</Button>
|
||||||
|
{detail?.status === 'PENDING' ? (
|
||||||
|
<>
|
||||||
<Button onClick={() => void audit(detail.id, 'APPROVE')}>通过</Button>
|
<Button onClick={() => void audit(detail.id, 'APPROVE')}>通过</Button>
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
@@ -303,8 +313,9 @@ function InfoChangeAuditPanel({
|
|||||||
>
|
>
|
||||||
驳回
|
驳回
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
) : null
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
@@ -328,7 +339,12 @@ function InfoChangeAuditPanel({
|
|||||||
label={STORE_INFO_CHANGEABLE_FIELD_LABELS[d.field] ?? d.field}
|
label={STORE_INFO_CHANGEABLE_FIELD_LABELS[d.field] ?? d.field}
|
||||||
>
|
>
|
||||||
{d.field === 'coverUrl' || d.field === 'envPhotoUrls' ? (
|
{d.field === 'coverUrl' || d.field === 'envPhotoUrls' ? (
|
||||||
<InfoChangeImageDiff field={d.field} live={d.live} proposed={d.proposed} />
|
<InfoChangeImageDiff
|
||||||
|
field={d.field}
|
||||||
|
live={d.live}
|
||||||
|
proposed={d.proposed}
|
||||||
|
imageSize={infoFullscreen ? 160 : 72}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<Typography.Text delete type="secondary">
|
<Typography.Text delete type="secondary">
|
||||||
@@ -388,6 +404,7 @@ export default function StorePackageAuditsPage() {
|
|||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [activeRequestId, setActiveRequestId] = useState<string | null>(null);
|
const [activeRequestId, setActiveRequestId] = useState<string | null>(null);
|
||||||
const [drawerTitle, setDrawerTitle] = useState('套餐变更详情');
|
const [drawerTitle, setDrawerTitle] = useState('套餐变更详情');
|
||||||
|
const [packageFullscreen, setPackageFullscreen] = useState(false);
|
||||||
|
|
||||||
async function reload(nextPage = page, nextStatus = status) {
|
async function reload(nextPage = page, nextStatus = status) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -554,14 +571,23 @@ export default function StorePackageAuditsPage() {
|
|||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={drawerTitle}
|
title={drawerTitle}
|
||||||
width={880}
|
width={packageFullscreen ? '100%' : 880}
|
||||||
open={detailOpen}
|
open={detailOpen}
|
||||||
onClose={() => setDetailOpen(false)}
|
onClose={() => {
|
||||||
|
setDetailOpen(false);
|
||||||
|
setPackageFullscreen(false);
|
||||||
|
}}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => setPackageFullscreen((v) => !v)}>
|
||||||
|
{packageFullscreen ? '退出全屏' : '全屏查看'}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{activeRequestId ? (
|
{activeRequestId ? (
|
||||||
<StorePackageAuditPanel
|
<StorePackageAuditPanel
|
||||||
requestId={activeRequestId}
|
requestId={activeRequestId}
|
||||||
|
fullscreen={packageFullscreen}
|
||||||
onAudited={() => {
|
onAudited={() => {
|
||||||
setDetailOpen(false);
|
setDetailOpen(false);
|
||||||
void reload(page, status);
|
void reload(page, status);
|
||||||
|
|||||||
@@ -218,8 +218,13 @@ export default function SupportTicketsPage() {
|
|||||||
|
|
||||||
setReviewOpen(false);
|
setReviewOpen(false);
|
||||||
const summary = [detail!.title, detail!.content].filter(Boolean).join('\n').slice(0, 500);
|
const summary = [detail!.title, detail!.content].filter(Boolean).join('\n').slice(0, 500);
|
||||||
|
const ticketUrls = (detail!.attachmentUrls ?? []).filter(Boolean);
|
||||||
tasksForm.setFieldsValue({
|
tasksForm.setFieldsValue({
|
||||||
tasks: [{ content: summary || detail!.title, type: mapSupportTicketTypeToDevPlanTask(detail!.ticketType) }],
|
tasks: [{
|
||||||
|
content: summary || detail!.title,
|
||||||
|
type: mapSupportTicketTypeToDevPlanTask(detail!.ticketType),
|
||||||
|
attachmentUrls: ticketUrls,
|
||||||
|
}],
|
||||||
dispatchToWecom: localStorage.getItem(DISPATCH_WECOM_STORAGE_KEY) !== 'false',
|
dispatchToWecom: localStorage.getItem(DISPATCH_WECOM_STORAGE_KEY) !== 'false',
|
||||||
dispatchSupplement: DEFAULT_DISPATCH_SUPPLEMENT,
|
dispatchSupplement: DEFAULT_DISPATCH_SUPPLEMENT,
|
||||||
});
|
});
|
||||||
@@ -364,7 +369,11 @@ export default function SupportTicketsPage() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
decision: 'APPROVE',
|
decision: 'APPROVE',
|
||||||
note: note?.trim() || undefined,
|
note: note?.trim() || undefined,
|
||||||
tasks: values.tasks.map((t) => ({ content: t.content.trim(), type: t.type })),
|
tasks: values.tasks.map((t) => ({
|
||||||
|
content: t.content.trim(),
|
||||||
|
type: t.type,
|
||||||
|
attachmentUrls: t.attachmentUrls?.length ? t.attachmentUrls : detail!.attachmentUrls,
|
||||||
|
})),
|
||||||
dispatchToWecom: !!values.dispatchToWecom,
|
dispatchToWecom: !!values.dispatchToWecom,
|
||||||
dispatchSupplement: values.dispatchSupplement?.trim() || undefined,
|
dispatchSupplement: values.dispatchSupplement?.trim() || undefined,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ type Row = {
|
|||||||
wineryAmount: number;
|
wineryAmount: number;
|
||||||
status: string;
|
status: string;
|
||||||
paidAt?: string | null;
|
paidAt?: string | null;
|
||||||
|
paymentRef?: string | null;
|
||||||
|
wineryBank?: {
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankName?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type BillItem = {
|
type BillItem = {
|
||||||
@@ -127,14 +134,30 @@ export default function WineryBillsPage() {
|
|||||||
(profile?.permissionKeys ?? []).includes('system_settings_winery_bank');
|
(profile?.permissionKeys ?? []).includes('system_settings_winery_bank');
|
||||||
|
|
||||||
function confirmPay(ids: string[], amountHint?: number) {
|
function confirmPay(ids: string[], amountHint?: number) {
|
||||||
|
let paymentRef = '';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认打款?',
|
title: '确认打款?',
|
||||||
content: `将确认 ${ids.length} 笔酒厂对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`,
|
content: (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
将确认 {ids.length} 笔酒厂对账单
|
||||||
|
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="打款凭证号(可选)"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onChange={(e) => {
|
||||||
|
paymentRef = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
okText: '确认打款',
|
okText: '确认打款',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||||
if (ids.length === 1) {
|
if (ids.length === 1) {
|
||||||
await request(`/admin/winery-bills/${ids[0]}/confirm`, { method: 'POST' });
|
await request(`/admin/winery-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||||
} else {
|
} else {
|
||||||
await request('/admin/winery-bills/batch-confirm', {
|
await request('/admin/winery-bills/batch-confirm', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -399,7 +422,31 @@ export default function WineryBillsPage() {
|
|||||||
{displayWineryStatus(detail.status, detail.wineryAmount).label}
|
{displayWineryStatus(detail.status, detail.wineryAmount).label}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="打款时间">{detail.paidAt ? fmtTime(detail.paidAt) : '—'}</Descriptions.Item>
|
<Descriptions.Item label="打款时间">{detail.paidAt ? fmtTime(detail.paidAt) : '—'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款凭证">
|
||||||
|
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
{detail.wineryBank ? (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
|
收款账户(酒厂)
|
||||||
|
</Typography.Title>
|
||||||
|
<Descriptions column={1} size="small" bordered>
|
||||||
|
<Descriptions.Item label="户名">
|
||||||
|
{detail.wineryBank.bankAccountName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户银行">
|
||||||
|
{detail.wineryBank.bankName || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开户支行">
|
||||||
|
{detail.wineryBank.bankBranch || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号">
|
||||||
|
{detail.wineryBank.bankAccountNo || '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
订单明细
|
订单明细
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|||||||
+10
-1
@@ -24,6 +24,8 @@
|
|||||||
| 3.4.16 | 08-09 | 联系电话分离;取消自动 ST;套餐 UI;合伙人列表/入驻客服门槛;HQ 门店列表表格 | [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md) |
|
| 3.4.16 | 08-09 | 联系电话分离;取消自动 ST;套餐 UI;合伙人列表/入驻客服门槛;HQ 门店列表表格 | [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md) |
|
||||||
| 3.5.3 | 08-20 | 门店照片/企微审核/分享;核销金额序列化与提现补建;测试流水计结算;核销浮动提示;权益金额门店图标 | [`v3.5.3`](./杜康好客-v3.5.3-开发文档.md) |
|
| 3.5.3 | 08-20 | 门店照片/企微审核/分享;核销金额序列化与提现补建;测试流水计结算;核销浮动提示;权益金额门店图标 | [`v3.5.3`](./杜康好客-v3.5.3-开发文档.md) |
|
||||||
| 3.5.4 | 08-21 | 商品 SPU+SKU 规格;DK 码/独立 69 码/规格主图;下单兼容回落;发票品类;权益文案与门店详情/收银台 UI;客户端版本号 | [`v3.5.4`](./杜康好客-v3.5.4-开发文档.md) |
|
| 3.5.4 | 08-21 | 商品 SPU+SKU 规格;DK 码/独立 69 码/规格主图;下单兼容回落;发票品类;权益文案与门店详情/收银台 UI;客户端版本号 | [`v3.5.4`](./杜康好客-v3.5.4-开发文档.md) |
|
||||||
|
| 3.5.5 | 08-23 | HQ 订单监控导出 Excel/PDF + 列表页布局优化 | [`v3.5.5`](./杜康好客-v3.5.5-开发文档.md) |
|
||||||
|
| 3.5.6 | 08-23 | HQ 任务导出;核销/财务银行信息;工单图片入任务;运营去调试;门店审核全屏 | [`v3.5.6`](./杜康好客-v3.5.6-开发文档.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,6 +90,13 @@
|
|||||||
**FIN-001~003**:白名单未出账提现 · 单店日限 ¥5000 · T+0 审完预警。
|
**FIN-001~003**:白名单未出账提现 · 单店日限 ¥5000 · T+0 审完预警。
|
||||||
合伙人 **T+30** 独立月账/打款。
|
合伙人 **T+30** 独立月账/打款。
|
||||||
|
|
||||||
|
**HQ 财务打款信息**(`admin-web` 财务四页,v3.5.6):
|
||||||
|
|
||||||
|
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
||||||
|
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
||||||
|
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
||||||
|
- 核销详情展示核销门店主账户收款信息
|
||||||
|
|
||||||
### 3.4~3.8 Wave 能力
|
### 3.4~3.8 Wave 能力
|
||||||
|
|
||||||
- **3.4** 门店一号多店、主账号、店员子账号(W2)
|
- **3.4** 门店一号多店、主账号、店员子账号(W2)
|
||||||
@@ -101,7 +110,7 @@
|
|||||||
| § | 主题 | 文档 |
|
| § | 主题 | 文档 |
|
||||||
|---|------|------|
|
|---|------|------|
|
||||||
| 3.9 | 门店套餐 ≤10 条、独立审核 | v3.4.10 |
|
| 3.9 | 门店套餐 ≤10 条、独立审核 | v3.4.10 |
|
||||||
| 3.10 | 开发计划/任务/版本/技术支持联动 | v3.4.11 |
|
| 3.10 | 开发计划/任务/版本/技术支持联动;任务导出 md/docx/xlsx/pdf;任务附件 | v3.4.11 / v3.5.6 |
|
||||||
| 3.11 | 企微智能机器人 + 消息推送 Webhook | v3.4.11 |
|
| 3.11 | 企微智能机器人 + 消息推送 Webhook | v3.4.11 |
|
||||||
| 3.12 | **登录手机号 ≠ 对外联系电话**;体验优化 | v3.4.16 |
|
| 3.12 | **登录手机号 ≠ 对外联系电话**;体验优化 | v3.4.16 |
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,11 @@
|
|||||||
| 3.5.3 | [`门店照片/企微审核/分享 + 核销金额与提现补修`](./杜康好客-v3.5.3-开发文档.md) | ✅ 生产 |
|
| 3.5.3 | [`门店照片/企微审核/分享 + 核销金额与提现补修`](./杜康好客-v3.5.3-开发文档.md) | ✅ 生产 |
|
||||||
| 3.5.4 | [`商品规格 SPU+SKU + 发票/文案 UI`](./杜康好客-v3.5.4-开发文档.md) | ✅ 生产(weapp 版本号需单独上传) |
|
| 3.5.4 | [`商品规格 SPU+SKU + 发票/文案 UI`](./杜康好客-v3.5.4-开发文档.md) | ✅ 生产(weapp 版本号需单独上传) |
|
||||||
| 3.5.5 | [`HQ 订单导出 + 订单监控页优化`](./杜康好客-v3.5.5-开发文档.md) | 🔶 开发完成,待发版 |
|
| 3.5.5 | [`HQ 订单导出 + 订单监控页优化`](./杜康好客-v3.5.5-开发文档.md) | 🔶 开发完成,待发版 |
|
||||||
|
| 3.5.6 | [`HQ 任务导出/财务银行信息/工单图片/运营去调试/审核全屏`](./杜康好客-v3.5.6-开发文档.md) | ✅ 已交付 |
|
||||||
|
|
||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-08-23 | v3.5.6:开发计划任务导出;核销/财务银行信息与打款凭证;工单图片入任务;运营去调试;门店审核全屏 |
|
||||||
| 2026-08-23 | HQ 订单监控:筛选/勾选导出 Excel(xlsx)与 PDF;`POST /admin/orders/export` |
|
| 2026-08-23 | HQ 订单监控:筛选/勾选导出 Excel(xlsx)与 PDF;`POST /admin/orders/export` |
|
||||||
| 2026-08-21 | v3.5.4 发生产:规格表/回填 SKU/DK 码/主图列/发票品类;H5+API 200 |
|
| 2026-08-21 | v3.5.4 发生产:规格表/回填 SKU/DK 码/主图列/发票品类;H5+API 200 |
|
||||||
| 2026-08-20 | v3.5.3 增补:Decimal 序列化/提现补建、测试流水计结算、核销浮动提示、好客权益金额图标 |
|
| 2026-08-20 | v3.5.3 增补:Decimal 序列化/提现补建、测试流水计结算、核销浮动提示、好客权益金额图标 |
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# 杜康好客 · v3.5.6 开发文档
|
||||||
|
|
||||||
|
> **2026-08-23** · admin-web / API
|
||||||
|
> **主题**:HQ 运营与财务体验增强(任务导出、核销/账单银行信息、工单图片、权限、审核全屏)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 版本目标
|
||||||
|
|
||||||
|
1. **开发计划** 任务列表支持导出 Markdown / DOCX / Excel / PDF。
|
||||||
|
2. **好客权益 · 核销详情** 展示核销门店收款账户(户名/账号/开户行)。
|
||||||
|
3. **门店账单** 保留「已打款」「已结算」文案,用颜色与类型标签区分;页头说明两类终态含义。
|
||||||
|
4. **财务四页**(门店/合伙人/酒厂/物流)统一展示收款账户与打款凭证;确认打款可填凭证号。
|
||||||
|
5. **技术支持 → 开发任务** 审批创建任务时带入工单图片;新建任务支持上传图片。
|
||||||
|
6. **运营权限** 去掉调试模块(核销调试、小飞侠联调),仅超级管理员可见。
|
||||||
|
7. **门店审核对比** 支持全屏查看详情。
|
||||||
|
|
||||||
|
**不做**:C 端 / 合伙人 / 门店端改动;不升小程序版本号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能清单
|
||||||
|
|
||||||
|
### 2.1 开发计划任务导出
|
||||||
|
|
||||||
|
| # | 项 | 说明 |
|
||||||
|
|---|----|------|
|
||||||
|
| 1 | API | `POST /api/v1/admin/dev-plan/tasks/export` |
|
||||||
|
| 2 | 范围 | `scope=filter` 按筛选;`scope=selected` 仅勾选 `ids` |
|
||||||
|
| 3 | 格式 | `markdown` / `docx` / `xlsx` / `pdf` |
|
||||||
|
| 4 | 列 | 任务编号、类型、状态、内容、关联工单、创建人、创建时间、完成时间、附件 |
|
||||||
|
| 5 | 权限 | `dev_plan` |
|
||||||
|
|
||||||
|
### 2.2 核销详情 · 门店收款账户
|
||||||
|
|
||||||
|
| # | 项 | 说明 |
|
||||||
|
|---|----|------|
|
||||||
|
| 6 | API | `GET /admin/redeem-records/:id` 增加 `store.primaryAccount` |
|
||||||
|
| 7 | UI | `RedeemRecordDetailDescriptions` 增加「收款账户」区块 |
|
||||||
|
|
||||||
|
### 2.3 门店账单状态区分
|
||||||
|
|
||||||
|
| 类型 | 终态 | 颜色 | 含义 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| T+1 出账 | 已打款 | 绿 | HQ 确认已对公打款 |
|
||||||
|
| 手动提现 | 已结算 | 青 | HQ 审核通过并完成结算 |
|
||||||
|
|
||||||
|
两类单据终态枚举均为 `PAID`,故文案不同;本版不改文案,仅强化视觉区分。
|
||||||
|
|
||||||
|
### 2.4 财务四页 · 银行卡打款信息
|
||||||
|
|
||||||
|
| 页 | 收款账户来源 | 打款凭证 |
|
||||||
|
|----|-------------|----------|
|
||||||
|
| 门店账单 | 主 `StoreAccount` | `StoreBill.paymentRef`(T+1);提现沿用 `StoreWithdrawRequest` |
|
||||||
|
| 合伙人账单 | `PartnerAccount` | `PartnerBill.paymentRef` |
|
||||||
|
| 酒厂账单 | `WINERY_BANK_*` 系统配置 | `WineryBill.paymentRef` |
|
||||||
|
| 物流对账 | `FulfillmentProvider` | `LogisticsBill.paymentRef`(挂账模式) |
|
||||||
|
|
||||||
|
列表 / 详情 / 导出均展示户名、账号、开户行(及酒厂开户银行);确认打款弹窗可选填凭证号。
|
||||||
|
|
||||||
|
### 2.5 工单 → 任务图片
|
||||||
|
|
||||||
|
| # | 项 | 说明 |
|
||||||
|
|---|----|------|
|
||||||
|
| 8 | DB | `dev_plan_task.attachment_urls` JSON |
|
||||||
|
| 9 | 审批创建 | 默认拷贝工单 `attachmentUrls` |
|
||||||
|
| 10 | 新建任务 | `DevPlanTasksPage` 支持 `OssUpload` |
|
||||||
|
|
||||||
|
### 2.6 运营去掉调试模块
|
||||||
|
|
||||||
|
| 菜单 | 路由 | 新权限键 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 核销调试 | `/redeem/debug` | `benefit_debug` |
|
||||||
|
| 小飞侠联调 | `/deliveries/xiaofeixia` | `deliveries_debug` |
|
||||||
|
|
||||||
|
仅 `SUPER_ADMIN` 默认拥有;调试 API 同步鉴权。
|
||||||
|
|
||||||
|
### 2.7 门店审核全屏
|
||||||
|
|
||||||
|
`StorePackageAuditsPage` 套餐对比 / 信息变更 Drawer 支持「全屏查看」切换 `width=100%`;全屏时图片放大。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Prisma 迁移
|
||||||
|
|
||||||
|
- `store_bill.payment_ref`
|
||||||
|
- `partner_bill.payment_ref`
|
||||||
|
- `winery_bill.payment_ref`
|
||||||
|
- `logistics_bill.payment_ref`
|
||||||
|
- `dev_plan_task.attachment_urls`
|
||||||
|
|
||||||
|
脚本:`prisma/migrate-finance-payment-ref-v356.sql`、`prisma/migrate-dev-plan-attachments-v356.sql`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 发版步骤
|
||||||
|
|
||||||
|
| 步骤 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 1 | `pnpm install`(新增 `docx`) |
|
||||||
|
| 2 | 执行 Prisma 迁移 |
|
||||||
|
| 3 | `pnpm build`(API + admin-web) |
|
||||||
|
| 4 | 发 `dukang-api` + `admin-web` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键路径
|
||||||
|
|
||||||
|
| 域 | 路径 |
|
||||||
|
|----|------|
|
||||||
|
| 任务导出 | `server/.../dev-plan/admin-dev-plan-export.util.ts` |
|
||||||
|
| 核销详情 | `admin-redeem.service.ts` + `RedeemRecordDetailDescriptions.tsx` |
|
||||||
|
| 门店账单 | `StoreBillsPage.tsx` + `settlement.service.ts` |
|
||||||
|
| 合伙人/酒厂/物流 | `PartnerBillsPage` / `WineryBillsPage` / `LogisticsBillsPage` |
|
||||||
|
| 工单图片 | `dev-plan.service.ts` + `SupportTicketsPage.tsx` + `DevPlanTasksPage.tsx` |
|
||||||
|
| 权限 | `hq-permissions.ts` + `AdminLayout.tsx` |
|
||||||
|
| 审核全屏 | `StorePackageAuditsPage.tsx` + `StorePackageAuditPanel.tsx` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 验收清单
|
||||||
|
|
||||||
|
- [ ] 开发计划:勾选/筛选导出 md/docx/xlsx/pdf,中文正常
|
||||||
|
- [ ] 核销详情:有主账户时展示户名/账号/开户行
|
||||||
|
- [ ] 门店账单:T+1「已打款」绿色、提现「已结算」青色;类型 Tag 醒目
|
||||||
|
- [ ] 财务四页:列表/详情/导出含收款账户;确认打款可填凭证;已打款后可见凭证
|
||||||
|
- [ ] 工单审批创建任务:图片带入;新建任务可上传图
|
||||||
|
- [ ] 运营账号无「核销调试」「小飞侠联调」菜单;直链 403
|
||||||
|
- [ ] 门店审核对比可全屏;图片放大可预览
|
||||||
@@ -92,6 +92,8 @@ export interface DevPlanTaskDto {
|
|||||||
|
|
||||||
completedAt?: string | null;
|
completedAt?: string | null;
|
||||||
|
|
||||||
|
attachmentUrls?: string[] | null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -148,6 +150,8 @@ export interface CreateDevPlanTaskInput {
|
|||||||
|
|
||||||
supportTicketId?: string;
|
supportTicketId?: string;
|
||||||
|
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +164,8 @@ export interface UpdateDevPlanTaskInput {
|
|||||||
|
|
||||||
status?: DevPlanTaskStatusDto;
|
status?: DevPlanTaskStatusDto;
|
||||||
|
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -228,7 +234,23 @@ export interface DevPlanLinkedTaskSummary {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 技术支持工单类型 → 开发任务类型 */
|
export type DevPlanTaskExportFormat = 'markdown' | 'docx' | 'xlsx' | 'pdf';
|
||||||
|
|
||||||
|
export interface DevPlanTaskExportRequest {
|
||||||
|
scope: 'filter' | 'selected';
|
||||||
|
format: DevPlanTaskExportFormat;
|
||||||
|
ids?: string[];
|
||||||
|
status?: string;
|
||||||
|
type?: string;
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevPlanTaskExportResponse {
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
contentBase64: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapSupportTicketTypeToDevPlanTask(
|
export function mapSupportTicketTypeToDevPlanTask(
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
{ key: 'partners', label: '开城管理', group: '业务' },
|
{ key: 'partners', label: '开城管理', group: '业务' },
|
||||||
{ key: 'finance', label: '财务', group: '业务' },
|
{ key: 'finance', label: '财务', group: '业务' },
|
||||||
{ key: 'benefit', label: '好客权益', group: '业务' },
|
{ key: 'benefit', label: '好客权益', group: '业务' },
|
||||||
|
{ key: 'benefit_debug', label: '核销调试', group: '业务' },
|
||||||
{ key: 'deliveries', label: '配送单', group: '业务' },
|
{ key: 'deliveries', label: '配送单', group: '业务' },
|
||||||
|
{ key: 'deliveries_debug', label: '小飞侠联调', group: '业务' },
|
||||||
{ key: 'tickets', label: '工单中心', group: '业务' },
|
{ key: 'tickets', label: '工单中心', group: '业务' },
|
||||||
{ key: 'tech_support', label: '技术支持', group: '业务' },
|
{ key: 'tech_support', label: '技术支持', group: '业务' },
|
||||||
{ key: 'invoices', label: '发票管理', group: '业务' },
|
{ key: 'invoices', label: '发票管理', group: '业务' },
|
||||||
@@ -41,6 +43,11 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
||||||
|
|
||||||
/** 危险操作:非超管角色默认不含,需在权限分配中显式勾选;超管默认拥有 */
|
/** 危险操作:非超管角色默认不含,需在权限分配中显式勾选;超管默认拥有 */
|
||||||
|
export const HQ_DEBUG_PERMISSION_KEYS = [
|
||||||
|
'benefit_debug',
|
||||||
|
'deliveries_debug',
|
||||||
|
] as const satisfies readonly HqPermissionKey[];
|
||||||
|
|
||||||
export const HQ_DANGEROUS_PERMISSION_KEYS = [
|
export const HQ_DANGEROUS_PERMISSION_KEYS = [
|
||||||
'users_delete',
|
'users_delete',
|
||||||
'orders_delete',
|
'orders_delete',
|
||||||
@@ -51,8 +58,14 @@ export function isHqDangerousPermission(key: string): boolean {
|
|||||||
return (HQ_DANGEROUS_PERMISSION_KEYS as readonly string[]).includes(key);
|
return (HQ_DANGEROUS_PERMISSION_KEYS as readonly string[]).includes(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isHqDebugPermission(key: string): boolean {
|
||||||
|
return (HQ_DEBUG_PERMISSION_KEYS as readonly string[]).includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
export function hqBasePermissionKeys(): HqPermissionKey[] {
|
export function hqBasePermissionKeys(): HqPermissionKey[] {
|
||||||
return HQ_PERMISSION_CATALOG.map((p) => p.key).filter((k) => !isHqDangerousPermission(k));
|
return HQ_PERMISSION_CATALOG.map((p) => p.key).filter(
|
||||||
|
(k) => !isHqDangerousPermission(k) && !isHqDebugPermission(k),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 系统配置 registry group → 权限 key */
|
/** 系统配置 registry group → 权限 key */
|
||||||
@@ -100,7 +113,7 @@ export const HQ_ADMIN_ROLES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
||||||
SUPER_ADMIN: hqBasePermissionKeys(),
|
SUPER_ADMIN: [...hqBasePermissionKeys(), ...HQ_DEBUG_PERMISSION_KEYS],
|
||||||
OPS: [
|
OPS: [
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'users',
|
'users',
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export interface RejectSupportTicketRequest {
|
|||||||
export interface CreateDevPlanTaskFromTicketInput {
|
export interface CreateDevPlanTaskFromTicketInput {
|
||||||
content: string;
|
content: string;
|
||||||
type: 'BUG' | 'REQUIREMENT' | 'OPTIMIZATION';
|
type: 'BUG' | 'REQUIREMENT' | 'OPTIMIZATION';
|
||||||
|
attachmentUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReviewSupportTicketRequest {
|
export interface ReviewSupportTicketRequest {
|
||||||
|
|||||||
Generated
+111
-44
@@ -68,13 +68,13 @@ importers:
|
|||||||
version: 18.3.7(@types/react@18.3.31)
|
version: 18.3.7(@types/react@18.3.31)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
version: 4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
version: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
apps/h5-partner:
|
apps/h5-partner:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -120,13 +120,13 @@ importers:
|
|||||||
version: 18.3.7(@types/react@18.3.31)
|
version: 18.3.7(@types/react@18.3.31)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
version: 4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
version: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
apps/h5-shop:
|
apps/h5-shop:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -160,13 +160,13 @@ importers:
|
|||||||
version: 18.3.7(@types/react@18.3.31)
|
version: 18.3.7(@types/react@18.3.31)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
version: 4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
version: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
apps/mini-user:
|
apps/mini-user:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -193,7 +193,7 @@ importers:
|
|||||||
version: 4.2.0(@swc/helpers@0.5.23)
|
version: 4.2.0(@swc/helpers@0.5.23)
|
||||||
'@tarojs/plugin-framework-react':
|
'@tarojs/plugin-framework-react':
|
||||||
specifier: 4.2.0
|
specifier: 4.2.0
|
||||||
version: 4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))
|
version: 4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))
|
||||||
'@tarojs/plugin-platform-h5':
|
'@tarojs/plugin-platform-h5':
|
||||||
specifier: 4.2.0
|
specifier: 4.2.0
|
||||||
version: 4.2.0(@swc/helpers@0.5.23)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))
|
version: 4.2.0(@swc/helpers@0.5.23)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))
|
||||||
@@ -242,13 +242,13 @@ importers:
|
|||||||
version: 4.2.0(@swc/helpers@0.5.23)
|
version: 4.2.0(@swc/helpers@0.5.23)
|
||||||
'@tarojs/vite-runner':
|
'@tarojs/vite-runner':
|
||||||
specifier: 4.2.0
|
specifier: 4.2.0
|
||||||
version: 4.2.0(@swc/helpers@0.5.23)(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
version: 4.2.0(@swc/helpers@0.5.23)(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.3.3
|
specifier: ^18.3.3
|
||||||
version: 18.3.31
|
version: 18.3.31
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
version: 4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
babel-preset-taro:
|
babel-preset-taro:
|
||||||
specifier: 4.2.0
|
specifier: 4.2.0
|
||||||
version: 4.2.0(@babel/core@7.29.7)(@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7))(@babel/preset-react@7.29.7(@babel/core@7.29.7))(@swc/helpers@0.5.23)
|
version: 4.2.0(@babel/core@7.29.7)(@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7))(@babel/preset-react@7.29.7(@babel/core@7.29.7))(@swc/helpers@0.5.23)
|
||||||
@@ -257,7 +257,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
version: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
packages/client-logging:
|
packages/client-logging:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -272,7 +272,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.1(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
version: 1.6.1(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
packages/shared-types:
|
packages/shared-types:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -358,6 +358,9 @@ importers:
|
|||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.4
|
version: 0.14.4
|
||||||
|
docx:
|
||||||
|
specifier: ^9.5.0
|
||||||
|
version: 9.7.1
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.4.2
|
specifier: ^17.4.2
|
||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
@@ -388,7 +391,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
specifier: ^10.4.0
|
specifier: ^10.4.0
|
||||||
version: 10.4.9
|
version: 10.4.9(@swc/core@1.3.96(@swc/helpers@0.5.23))
|
||||||
'@nestjs/schematics':
|
'@nestjs/schematics':
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
||||||
@@ -415,7 +418,7 @@ importers:
|
|||||||
version: 5.22.0
|
version: 5.22.0
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@20.19.43)(typescript@5.9.3)
|
version: 10.9.2(@swc/core@1.3.96(@swc/helpers@0.5.23))(@types/node@20.19.43)(typescript@5.9.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -2634,6 +2637,9 @@ packages:
|
|||||||
'@types/node@22.20.0':
|
'@types/node@22.20.0':
|
||||||
resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
|
resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
|
||||||
|
|
||||||
|
'@types/node@25.9.5':
|
||||||
|
resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==}
|
||||||
|
|
||||||
'@types/pdfkit@0.17.6':
|
'@types/pdfkit@0.17.6':
|
||||||
resolution: {integrity: sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==}
|
resolution: {integrity: sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==}
|
||||||
|
|
||||||
@@ -3650,6 +3656,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
docx@9.7.1:
|
||||||
|
resolution: {integrity: sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
|
||||||
@@ -4280,6 +4290,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hash.js@1.1.7:
|
||||||
|
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||||
|
|
||||||
hashery@1.5.1:
|
hashery@1.5.1:
|
||||||
resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
|
resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -4934,6 +4947,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
minimalistic-assert@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -5015,6 +5031,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@5.1.16:
|
||||||
|
resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
@@ -6532,6 +6553,9 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
undici-types@7.24.6:
|
||||||
|
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||||
|
|
||||||
unescape@1.0.1:
|
unescape@1.0.1:
|
||||||
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
|
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -6813,10 +6837,17 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
xml-js@1.6.11:
|
||||||
|
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
xml2js@0.6.2:
|
xml2js@0.6.2:
|
||||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xml@1.0.1:
|
||||||
|
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -8258,7 +8289,7 @@ snapshots:
|
|||||||
bullmq: 5.79.2
|
bullmq: 5.79.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@nestjs/cli@10.4.9':
|
'@nestjs/cli@10.4.9(@swc/core@1.3.96(@swc/helpers@0.5.23))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||||
'@angular-devkit/schematics': 17.3.11(chokidar@3.6.0)
|
'@angular-devkit/schematics': 17.3.11(chokidar@3.6.0)
|
||||||
@@ -8268,7 +8299,7 @@ snapshots:
|
|||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
cli-table3: 0.6.5
|
cli-table3: 0.6.5
|
||||||
commander: 4.1.1
|
commander: 4.1.1
|
||||||
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1)
|
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23)))
|
||||||
glob: 10.4.5
|
glob: 10.4.5
|
||||||
inquirer: 8.2.6
|
inquirer: 8.2.6
|
||||||
node-emoji: 1.11.0
|
node-emoji: 1.11.0
|
||||||
@@ -8277,8 +8308,10 @@ snapshots:
|
|||||||
tsconfig-paths: 4.2.0
|
tsconfig-paths: 4.2.0
|
||||||
tsconfig-paths-webpack-plugin: 4.2.0
|
tsconfig-paths-webpack-plugin: 4.2.0
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
webpack: 5.97.1
|
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))
|
||||||
webpack-node-externals: 3.0.0
|
webpack-node-externals: 3.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@swc/core': 1.3.96(@swc/helpers@0.5.23)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@minify-html/node'
|
- '@minify-html/node'
|
||||||
- '@swc/css'
|
- '@swc/css'
|
||||||
@@ -9084,7 +9117,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))':
|
'@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tarojs/helper': 4.2.0(@swc/helpers@0.5.23)
|
'@tarojs/helper': 4.2.0(@swc/helpers@0.5.23)
|
||||||
'@tarojs/runtime': 4.2.0
|
'@tarojs/runtime': 4.2.0
|
||||||
@@ -9094,9 +9127,9 @@ snapshots:
|
|||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vitejs/plugin-react': 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
'@vitejs/plugin-react': 4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)
|
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)
|
||||||
|
|
||||||
'@tarojs/plugin-platform-h5@4.2.0(@swc/helpers@0.5.23)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))':
|
'@tarojs/plugin-platform-h5@4.2.0(@swc/helpers@0.5.23)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@tarojs/helper@4.2.0(@swc/helpers@0.5.23))(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15))':
|
||||||
@@ -9217,7 +9250,7 @@ snapshots:
|
|||||||
rollup: 3.30.0
|
rollup: 3.30.0
|
||||||
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)
|
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(postcss@8.5.15)
|
||||||
|
|
||||||
'@tarojs/vite-runner@4.2.0(@swc/helpers@0.5.23)(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))':
|
'@tarojs/vite-runner@4.2.0(@swc/helpers@0.5.23)(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
@@ -9230,7 +9263,7 @@ snapshots:
|
|||||||
'@tarojs/runner-utils': 4.2.0(@swc/helpers@0.5.23)
|
'@tarojs/runner-utils': 4.2.0(@swc/helpers@0.5.23)
|
||||||
'@tarojs/runtime': 4.2.0
|
'@tarojs/runtime': 4.2.0
|
||||||
'@tarojs/shared': 4.2.0
|
'@tarojs/shared': 4.2.0
|
||||||
'@vitejs/plugin-legacy': 4.1.1(terser@5.48.0)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
'@vitejs/plugin-legacy': 4.1.1(terser@5.48.0)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
acorn-walk: 8.3.5
|
acorn-walk: 8.3.5
|
||||||
autoprefixer: 10.5.2(postcss@8.5.15)
|
autoprefixer: 10.5.2(postcss@8.5.15)
|
||||||
babel-plugin-transform-taroapi: 4.2.0(@babel/core@7.29.7)
|
babel-plugin-transform-taroapi: 4.2.0(@babel/core@7.29.7)
|
||||||
@@ -9253,8 +9286,8 @@ snapshots:
|
|||||||
regenerator-runtime: 0.11.1
|
regenerator-runtime: 0.11.1
|
||||||
sax: 1.2.4
|
sax: 1.2.4
|
||||||
stylelint: 16.26.1(typescript@5.9.3)
|
stylelint: 16.26.1(typescript@5.9.3)
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
vite-plugin-static-copy: 0.17.1(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
|
vite-plugin-static-copy: 0.17.1(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
- '@types/babel__core'
|
- '@types/babel__core'
|
||||||
@@ -9400,6 +9433,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@25.9.5':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.24.6
|
||||||
|
|
||||||
'@types/pdfkit@0.17.6':
|
'@types/pdfkit@0.17.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.43
|
'@types/node': 20.19.43
|
||||||
@@ -9548,7 +9585,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.62.1
|
'@typescript-eslint/types': 8.62.1
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@vitejs/plugin-legacy@4.1.1(terser@5.48.0)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))':
|
'@vitejs/plugin-legacy@4.1.1(terser@5.48.0)(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/preset-env': 7.29.7(@babel/core@7.29.7)
|
'@babel/preset-env': 7.29.7(@babel/core@7.29.7)
|
||||||
@@ -9558,11 +9595,11 @@ snapshots:
|
|||||||
regenerator-runtime: 0.13.11
|
regenerator-runtime: 0.13.11
|
||||||
systemjs: 6.15.1
|
systemjs: 6.15.1
|
||||||
terser: 5.48.0
|
terser: 5.48.0
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))':
|
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
|
||||||
@@ -9570,7 +9607,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -10670,6 +10707,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
||||||
|
docx@9.7.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.9.5
|
||||||
|
hash.js: 1.1.7
|
||||||
|
jszip: 3.10.1
|
||||||
|
nanoid: 5.1.16
|
||||||
|
xml: 1.0.1
|
||||||
|
xml-js: 1.6.11
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
@@ -11205,7 +11251,7 @@ snapshots:
|
|||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1):
|
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -11220,7 +11266,7 @@ snapshots:
|
|||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
webpack: 5.97.1
|
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))
|
||||||
|
|
||||||
form-data@4.0.6:
|
form-data@4.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11474,6 +11520,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
hash.js@1.1.7:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
minimalistic-assert: 1.0.1
|
||||||
|
|
||||||
hashery@1.5.1:
|
hashery@1.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
hookified: 1.15.1
|
hookified: 1.15.1
|
||||||
@@ -12085,6 +12136,8 @@ snapshots:
|
|||||||
|
|
||||||
mimic-response@1.0.1: {}
|
mimic-response@1.0.1: {}
|
||||||
|
|
||||||
|
minimalistic-assert@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.7
|
brace-expansion: 5.0.7
|
||||||
@@ -12172,6 +12225,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.15: {}
|
nanoid@3.3.15: {}
|
||||||
|
|
||||||
|
nanoid@5.1.16: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
@@ -13589,13 +13644,15 @@ snapshots:
|
|||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
terser-webpack-plugin@5.6.1(webpack@5.97.1):
|
terser-webpack-plugin@5.6.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jest-worker: 27.5.1
|
jest-worker: 27.5.1
|
||||||
schema-utils: 4.3.3
|
schema-utils: 4.3.3
|
||||||
terser: 5.48.0
|
terser: 5.48.0
|
||||||
webpack: 5.97.1
|
webpack: 5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23))
|
||||||
|
optionalDependencies:
|
||||||
|
'@swc/core': 1.3.96(@swc/helpers@0.5.23)
|
||||||
|
|
||||||
terser@5.48.0:
|
terser@5.48.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13681,7 +13738,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3):
|
ts-node@10.9.2(@swc/core@1.3.96(@swc/helpers@0.5.23))(@types/node@20.19.43)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
'@tsconfig/node10': 1.0.12
|
'@tsconfig/node10': 1.0.12
|
||||||
@@ -13698,6 +13755,8 @@ snapshots:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@swc/core': 1.3.96(@swc/helpers@0.5.23)
|
||||||
|
|
||||||
tsconfig-paths-webpack-plugin@4.2.0:
|
tsconfig-paths-webpack-plugin@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13779,6 +13838,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
undici-types@7.24.6: {}
|
||||||
|
|
||||||
unescape@1.0.1:
|
unescape@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
extend-shallow: 2.0.1
|
extend-shallow: 2.0.1
|
||||||
@@ -13892,13 +13953,13 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite-node@1.6.1(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0):
|
vite-node@1.6.1(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- less
|
- less
|
||||||
@@ -13910,26 +13971,26 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite-plugin-static-copy@0.17.1(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)):
|
vite-plugin-static-copy@0.17.1(vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
fs-extra: 11.3.6
|
fs-extra: 11.3.6
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
|
|
||||||
vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0):
|
vite@5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
rollup: 4.62.2
|
rollup: 4.62.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.0
|
'@types/node': 25.9.5
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.101.0
|
sass: 1.101.0
|
||||||
terser: 5.48.0
|
terser: 5.48.0
|
||||||
|
|
||||||
vitest@1.6.1(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0):
|
vitest@1.6.1(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 1.6.1
|
'@vitest/expect': 1.6.1
|
||||||
'@vitest/runner': 1.6.1
|
'@vitest/runner': 1.6.1
|
||||||
@@ -13948,11 +14009,11 @@ snapshots:
|
|||||||
strip-literal: 2.1.1
|
strip-literal: 2.1.1
|
||||||
tinybench: 2.9.0
|
tinybench: 2.9.0
|
||||||
tinypool: 0.8.4
|
tinypool: 0.8.4
|
||||||
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
vite-node: 1.6.1(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
|
vite-node: 1.6.1(@types/node@25.9.5)(sass@1.101.0)(terser@5.48.0)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.0
|
'@types/node': 25.9.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
@@ -13983,7 +14044,7 @@ snapshots:
|
|||||||
|
|
||||||
webpack-sources@3.5.0: {}
|
webpack-sources@3.5.0: {}
|
||||||
|
|
||||||
webpack@5.97.1:
|
webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint-scope': 3.7.7
|
'@types/eslint-scope': 3.7.7
|
||||||
'@types/estree': 1.0.9
|
'@types/estree': 1.0.9
|
||||||
@@ -14005,7 +14066,7 @@ snapshots:
|
|||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
schema-utils: 3.3.0
|
schema-utils: 3.3.0
|
||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
terser-webpack-plugin: 5.6.1(webpack@5.97.1)
|
terser-webpack-plugin: 5.6.1(@swc/core@1.3.96(@swc/helpers@0.5.23))(webpack@5.97.1(@swc/core@1.3.96(@swc/helpers@0.5.23)))
|
||||||
watchpack: 2.5.2
|
watchpack: 2.5.2
|
||||||
webpack-sources: 3.5.0
|
webpack-sources: 3.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -14129,11 +14190,17 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.21.1: {}
|
ws@8.21.1: {}
|
||||||
|
|
||||||
|
xml-js@1.6.11:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.6.0
|
||||||
|
|
||||||
xml2js@0.6.2:
|
xml2js@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.6.0
|
sax: 1.6.0
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xml@1.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@11.0.1: {}
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlchars@2.2.0: {}
|
xmlchars@2.2.0: {}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"bullmq": "^5.12.0",
|
"bullmq": "^5.12.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"docx": "^9.5.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- v3.5.6: 开发计划任务附件
|
||||||
|
ALTER TABLE `dev_plan_task` ADD COLUMN `attachment_urls` JSON NULL AFTER `support_ticket_id`;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- v3.5.6: 财务账单打款凭证
|
||||||
|
ALTER TABLE `store_bill` ADD COLUMN `payment_ref` VARCHAR(128) NULL AFTER `status`;
|
||||||
|
ALTER TABLE `partner_bill` ADD COLUMN `payment_ref` VARCHAR(128) NULL AFTER `status`;
|
||||||
|
ALTER TABLE `winery_bill` ADD COLUMN `payment_ref` VARCHAR(128) NULL AFTER `status`;
|
||||||
|
ALTER TABLE `logistics_bill` ADD COLUMN `payment_ref` VARCHAR(128) NULL AFTER `status`;
|
||||||
@@ -733,6 +733,7 @@ model DevPlanTask {
|
|||||||
status DevPlanTaskStatus @default(TODO)
|
status DevPlanTaskStatus @default(TODO)
|
||||||
creatorHqAccountId BigInt @map("creator_hq_account_id") @db.UnsignedBigInt
|
creatorHqAccountId BigInt @map("creator_hq_account_id") @db.UnsignedBigInt
|
||||||
supportTicketId BigInt? @map("support_ticket_id") @db.UnsignedBigInt
|
supportTicketId BigInt? @map("support_ticket_id") @db.UnsignedBigInt
|
||||||
|
attachmentUrls Json? @map("attachment_urls")
|
||||||
lastDispatchedAt DateTime? @map("last_dispatched_at") @db.DateTime(3)
|
lastDispatchedAt DateTime? @map("last_dispatched_at") @db.DateTime(3)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||||
completedAt DateTime? @map("completed_at") @db.DateTime(3)
|
completedAt DateTime? @map("completed_at") @db.DateTime(3)
|
||||||
@@ -1073,6 +1074,7 @@ model LogisticsBill {
|
|||||||
settlementMethod LogisticsSettlementMethod @map("settlement_method")
|
settlementMethod LogisticsSettlementMethod @map("settlement_method")
|
||||||
pricingSnapshotJson String? @map("pricing_snapshot_json") @db.Text
|
pricingSnapshotJson String? @map("pricing_snapshot_json") @db.Text
|
||||||
status FinancePayStatus @default(UNPAID)
|
status FinancePayStatus @default(UNPAID)
|
||||||
|
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||||
|
|
||||||
@@ -1215,6 +1217,7 @@ model PartnerBill {
|
|||||||
redeemCommission Decimal @default(0) @map("redeem_commission") @db.Decimal(10, 2)
|
redeemCommission Decimal @default(0) @map("redeem_commission") @db.Decimal(10, 2)
|
||||||
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
||||||
status PartnerBillStatus @default(PENDING_REVIEW)
|
status PartnerBillStatus @default(PENDING_REVIEW)
|
||||||
|
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
||||||
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
||||||
sentAt DateTime? @map("sent_at") @db.DateTime(3)
|
sentAt DateTime? @map("sent_at") @db.DateTime(3)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
@@ -1863,6 +1866,7 @@ model StoreBill {
|
|||||||
settlementRate Decimal @map("settlement_rate") @db.Decimal(5, 4)
|
settlementRate Decimal @map("settlement_rate") @db.Decimal(5, 4)
|
||||||
payoutAmount Decimal @map("payout_amount") @db.Decimal(10, 2)
|
payoutAmount Decimal @map("payout_amount") @db.Decimal(10, 2)
|
||||||
status FinancePayStatus @default(UNPAID)
|
status FinancePayStatus @default(UNPAID)
|
||||||
|
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||||
|
|
||||||
@@ -1946,6 +1950,7 @@ model WineryBill {
|
|||||||
wineryRate Decimal @map("winery_rate") @db.Decimal(5, 4)
|
wineryRate Decimal @map("winery_rate") @db.Decimal(5, 4)
|
||||||
wineryAmount Decimal @map("winery_amount") @db.Decimal(10, 2)
|
wineryAmount Decimal @map("winery_amount") @db.Decimal(10, 2)
|
||||||
status FinancePayStatus @default(UNPAID)
|
status FinancePayStatus @default(UNPAID)
|
||||||
|
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export type StoreBankAccountSnapshot = {
|
||||||
|
bankAccountName: string | null;
|
||||||
|
bankAccountNo: string | null;
|
||||||
|
bankBranch: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadStorePrimaryBank(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
storeId: bigint,
|
||||||
|
): Promise<StoreBankAccountSnapshot | null> {
|
||||||
|
const binding = await prisma.storeAccountStore.findFirst({
|
||||||
|
where: { storeId, storeAccount: { isPrimary: 1 } },
|
||||||
|
include: {
|
||||||
|
storeAccount: {
|
||||||
|
select: { bankAccountName: true, bankAccountNo: true, bankBranch: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (binding) return binding.storeAccount;
|
||||||
|
const fallback = await prisma.storeAccountStore.findFirst({
|
||||||
|
where: { storeId },
|
||||||
|
orderBy: { storeAccountId: 'asc' },
|
||||||
|
include: {
|
||||||
|
storeAccount: {
|
||||||
|
select: { bankAccountName: true, bankAccountNo: true, bankBranch: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return fallback?.storeAccount ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadStorePrimaryBanksMap(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
storeIds: bigint[],
|
||||||
|
): Promise<Map<string, StoreBankAccountSnapshot>> {
|
||||||
|
const map = new Map<string, StoreBankAccountSnapshot>();
|
||||||
|
if (!storeIds.length) return map;
|
||||||
|
const bindings = await prisma.storeAccountStore.findMany({
|
||||||
|
where: { storeId: { in: storeIds } },
|
||||||
|
include: {
|
||||||
|
storeAccount: {
|
||||||
|
select: {
|
||||||
|
isPrimary: true,
|
||||||
|
bankAccountName: true,
|
||||||
|
bankAccountNo: true,
|
||||||
|
bankBranch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ storeAccount: { isPrimary: 'desc' } }, { storeAccountId: 'asc' }],
|
||||||
|
});
|
||||||
|
for (const binding of bindings) {
|
||||||
|
const key = String(binding.storeId);
|
||||||
|
if (map.has(key)) continue;
|
||||||
|
map.set(key, {
|
||||||
|
bankAccountName: binding.storeAccount.bankAccountName,
|
||||||
|
bankAccountNo: binding.storeAccount.bankAccountNo,
|
||||||
|
bankBranch: binding.storeAccount.bankBranch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadWineryBankConfig(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
): Promise<StoreBankAccountSnapshot & { bankName?: string | null }> {
|
||||||
|
const keys = [
|
||||||
|
'WINERY_BANK_ACCOUNT_NAME',
|
||||||
|
'WINERY_BANK_NAME',
|
||||||
|
'WINERY_BANK_BRANCH',
|
||||||
|
'WINERY_BANK_ACCOUNT_NO',
|
||||||
|
];
|
||||||
|
const rows = await prisma.systemConfig.findMany({
|
||||||
|
where: { configKey: { in: keys } },
|
||||||
|
select: { configKey: true, value: true },
|
||||||
|
});
|
||||||
|
const map = new Map(rows.map((r) => [r.configKey, r.value]));
|
||||||
|
return {
|
||||||
|
bankAccountName: map.get('WINERY_BANK_ACCOUNT_NAME') ?? null,
|
||||||
|
bankName: map.get('WINERY_BANK_NAME') ?? null,
|
||||||
|
bankBranch: map.get('WINERY_BANK_BRANCH') ?? null,
|
||||||
|
bankAccountNo: map.get('WINERY_BANK_ACCOUNT_NO') ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import {
|
||||||
|
DEV_PLAN_TASK_STATUS_LABELS,
|
||||||
|
DEV_PLAN_TASK_TYPE_LABELS,
|
||||||
|
type DevPlanTaskStatusDto,
|
||||||
|
type DevPlanTaskTypeDto,
|
||||||
|
} from '@dukang/shared-types';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import PDFDocument from 'pdfkit';
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
HeadingLevel,
|
||||||
|
ImageRun,
|
||||||
|
Packer,
|
||||||
|
Paragraph,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TextRun,
|
||||||
|
WidthType,
|
||||||
|
} from 'docx';
|
||||||
|
|
||||||
|
const EXPORT_HEADERS = [
|
||||||
|
'任务编号',
|
||||||
|
'类型',
|
||||||
|
'状态',
|
||||||
|
'内容',
|
||||||
|
'关联工单',
|
||||||
|
'创建人',
|
||||||
|
'创建时间',
|
||||||
|
'完成时间',
|
||||||
|
'附件',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type DevPlanTaskExportRow = {
|
||||||
|
taskNo: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
content: string;
|
||||||
|
supportTicketNo: string;
|
||||||
|
creatorName: string;
|
||||||
|
createdAt: string;
|
||||||
|
completedAt: string;
|
||||||
|
attachmentUrls: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DevPlanExportFormat = 'markdown' | 'docx' | 'xlsx' | 'pdf';
|
||||||
|
|
||||||
|
export function mapTaskToExportRow(task: {
|
||||||
|
taskNo: string;
|
||||||
|
type: DevPlanTaskTypeDto;
|
||||||
|
status: DevPlanTaskStatusDto;
|
||||||
|
content: string;
|
||||||
|
supportTicketNo?: string | null;
|
||||||
|
creatorName?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
completedAt?: string | null;
|
||||||
|
attachmentUrls?: string[] | null;
|
||||||
|
}): DevPlanTaskExportRow {
|
||||||
|
return {
|
||||||
|
taskNo: task.taskNo,
|
||||||
|
type: DEV_PLAN_TASK_TYPE_LABELS[task.type] ?? task.type,
|
||||||
|
status: DEV_PLAN_TASK_STATUS_LABELS[task.status] ?? task.status,
|
||||||
|
content: task.content,
|
||||||
|
supportTicketNo: task.supportTicketNo ?? '',
|
||||||
|
creatorName: task.creatorName ?? '',
|
||||||
|
createdAt: task.createdAt.slice(0, 19).replace('T', ' '),
|
||||||
|
completedAt: task.completedAt ? task.completedAt.slice(0, 19).replace('T', ' ') : '',
|
||||||
|
attachmentUrls: task.attachmentUrls ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowToCells(row: DevPlanTaskExportRow): string[] {
|
||||||
|
return [
|
||||||
|
row.taskNo,
|
||||||
|
row.type,
|
||||||
|
row.status,
|
||||||
|
row.content,
|
||||||
|
row.supportTicketNo,
|
||||||
|
row.creatorName,
|
||||||
|
row.createdAt,
|
||||||
|
row.completedAt,
|
||||||
|
row.attachmentUrls.join(' '),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePdfFontPath(): string {
|
||||||
|
const candidates = [
|
||||||
|
process.env.EXPORT_PDF_FONT_PATH,
|
||||||
|
path.join(process.cwd(), 'assets', 'fonts', 'NotoSansSC-Regular.otf'),
|
||||||
|
path.join(process.cwd(), 'assets', 'fonts', 'simhei.ttf'),
|
||||||
|
path.join(process.cwd(), 'assets', 'fonts', 'msyh.ttc'),
|
||||||
|
path.join(process.cwd(), 'dist', 'assets', 'fonts', 'simhei.ttf'),
|
||||||
|
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
|
||||||
|
'C:\\Windows\\Fonts\\simhei.ttf',
|
||||||
|
'C:\\Windows\\Fonts\\msyh.ttc',
|
||||||
|
].filter(Boolean) as string[];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (fs.existsSync(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
throw new Error('未找到可用于 PDF 的中文字体,请将字体文件放到 server/dukang-api/assets/fonts/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDevPlanMarkdown(rows: DevPlanTaskExportRow[]): Buffer {
|
||||||
|
const lines: string[] = ['# 开发计划任务导出', ''];
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
lines.push(`## ${index + 1}. ${row.taskNo} [${row.type}] ${row.status}`);
|
||||||
|
lines.push('');
|
||||||
|
lines.push(row.content);
|
||||||
|
lines.push('');
|
||||||
|
if (row.supportTicketNo) lines.push(`- 关联工单:${row.supportTicketNo}`);
|
||||||
|
if (row.creatorName) lines.push(`- 创建人:${row.creatorName}`);
|
||||||
|
lines.push(`- 创建时间:${row.createdAt}`);
|
||||||
|
if (row.completedAt) lines.push(`- 完成时间:${row.completedAt}`);
|
||||||
|
if (row.attachmentUrls.length) {
|
||||||
|
lines.push('- 附件:');
|
||||||
|
for (const url of row.attachmentUrls) {
|
||||||
|
lines.push(` `);
|
||||||
|
lines.push(` ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
});
|
||||||
|
return Buffer.from(lines.join('\n'), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchImageBuffer(url: string): Promise<Buffer | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
return buf.length > 0 && buf.length < 5 * 1024 * 1024 ? buf : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildDevPlanDocx(rows: DevPlanTaskExportRow[]): Promise<Buffer> {
|
||||||
|
const children: (Paragraph | Table)[] = [
|
||||||
|
new Paragraph({ text: '开发计划任务导出', heading: HeadingLevel.HEADING_1 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
children.push(
|
||||||
|
new Paragraph({
|
||||||
|
text: `${row.taskNo} [${row.type}] ${row.status}`,
|
||||||
|
heading: HeadingLevel.HEADING_2,
|
||||||
|
}),
|
||||||
|
new Paragraph({ children: [new TextRun(row.content)] }),
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun(`创建人:${row.creatorName || '—'} 创建:${row.createdAt}`),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (row.supportTicketNo) {
|
||||||
|
children.push(new Paragraph({ children: [new TextRun(`关联工单:${row.supportTicketNo}`)] }));
|
||||||
|
}
|
||||||
|
if (row.completedAt) {
|
||||||
|
children.push(new Paragraph({ children: [new TextRun(`完成时间:${row.completedAt}`)] }));
|
||||||
|
}
|
||||||
|
for (const url of row.attachmentUrls) {
|
||||||
|
const img = await fetchImageBuffer(url);
|
||||||
|
if (img) {
|
||||||
|
children.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new ImageRun({
|
||||||
|
data: img,
|
||||||
|
transformation: { width: 320, height: 240 },
|
||||||
|
type: 'png',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.push(new Paragraph({ children: [new TextRun(url)] }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.push(new Paragraph({ text: '' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = new Document({
|
||||||
|
sections: [{ children }],
|
||||||
|
});
|
||||||
|
return Packer.toBuffer(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildDevPlanXlsx(rows: DevPlanTaskExportRow[]): Promise<Buffer> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet('开发任务');
|
||||||
|
sheet.addRow([...EXPORT_HEADERS]);
|
||||||
|
for (const row of rows) {
|
||||||
|
sheet.addRow(rowToCells(row));
|
||||||
|
}
|
||||||
|
sheet.columns.forEach((col, i) => {
|
||||||
|
col.width = i === 3 || i === 8 ? 40 : 16;
|
||||||
|
});
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
return Buffer.from(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildDevPlanPdf(rows: DevPlanTaskExportRow[]): Promise<Buffer> {
|
||||||
|
const fontPath = resolvePdfFontPath();
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const doc = new PDFDocument({ size: 'A4', layout: 'landscape', margin: 24, bufferPages: true });
|
||||||
|
doc.on('data', (chunk) => chunks.push(chunk as Buffer));
|
||||||
|
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
doc.on('error', reject);
|
||||||
|
doc.registerFont('zh', fontPath);
|
||||||
|
doc.font('zh');
|
||||||
|
|
||||||
|
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||||
|
const colWidths = [72, 48, 48, 180, 72, 56, 78, 78, 120];
|
||||||
|
const scale = pageWidth / colWidths.reduce((s, w) => s + w, 0);
|
||||||
|
const widths = colWidths.map((w) => w * scale);
|
||||||
|
const rowHeight = 36;
|
||||||
|
let y = doc.page.margins.top;
|
||||||
|
|
||||||
|
const drawRow = (cells: string[], isHeader = false) => {
|
||||||
|
let x = doc.page.margins.left;
|
||||||
|
const height = isHeader ? 24 : rowHeight;
|
||||||
|
if (y + height > doc.page.height - doc.page.margins.bottom) {
|
||||||
|
doc.addPage({ size: 'A4', layout: 'landscape', margin: 24 });
|
||||||
|
y = doc.page.margins.top;
|
||||||
|
}
|
||||||
|
doc.fontSize(isHeader ? 8 : 7);
|
||||||
|
cells.forEach((cell, index) => {
|
||||||
|
doc.rect(x, y, widths[index], height).stroke('#dddddd');
|
||||||
|
doc.text(cell || '', x + 2, y + 4, {
|
||||||
|
width: widths[index] - 4,
|
||||||
|
height: height - 6,
|
||||||
|
lineBreak: true,
|
||||||
|
});
|
||||||
|
x += widths[index];
|
||||||
|
});
|
||||||
|
y += height;
|
||||||
|
};
|
||||||
|
|
||||||
|
drawRow([...EXPORT_HEADERS], true);
|
||||||
|
for (const row of rows) {
|
||||||
|
drawRow(rowToCells(row));
|
||||||
|
}
|
||||||
|
doc.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDevPlanExportFilename(format: DevPlanExportFormat, count: number): string {
|
||||||
|
const stamp = new Date().toISOString().slice(0, 10);
|
||||||
|
const ext = format === 'markdown' ? 'md' : format === 'docx' ? 'docx' : format;
|
||||||
|
return `开发任务导出_${stamp}_${count}条.${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function devPlanExportMimeType(format: DevPlanExportFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case 'markdown':
|
||||||
|
return 'text/markdown;charset=utf-8';
|
||||||
|
case 'docx':
|
||||||
|
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||||
|
case 'xlsx':
|
||||||
|
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
case 'pdf':
|
||||||
|
return 'application/pdf';
|
||||||
|
default:
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
UpdateDevPlanTaskDto,
|
UpdateDevPlanTaskDto,
|
||||||
UpdateDevPlanVersionDto,
|
UpdateDevPlanVersionDto,
|
||||||
DevPlanTaskBatchUpdateDto,
|
DevPlanTaskBatchUpdateDto,
|
||||||
|
DevPlanTaskExportDto,
|
||||||
} from './dto/dev-plan.dto';
|
} from './dto/dev-plan.dto';
|
||||||
|
|
||||||
@Controller('admin/dev-plan')
|
@Controller('admin/dev-plan')
|
||||||
@@ -98,6 +99,11 @@ export class AdminDevPlanController {
|
|||||||
return this.service.batchUpdateTasks(body);
|
return this.service.batchUpdateTasks(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('tasks/export')
|
||||||
|
exportTasks(@Body() body: DevPlanTaskExportDto) {
|
||||||
|
return this.service.exportTasks(body);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('versions')
|
@Get('versions')
|
||||||
listVersions(
|
listVersions(
|
||||||
@Query('status') status?: string,
|
@Query('status') status?: string,
|
||||||
|
|||||||
@@ -59,6 +59,26 @@ import type {
|
|||||||
UpdateDevPlanVersionDto,
|
UpdateDevPlanVersionDto,
|
||||||
|
|
||||||
} from './dto/dev-plan.dto';
|
} from './dto/dev-plan.dto';
|
||||||
|
import {
|
||||||
|
buildDevPlanDocx,
|
||||||
|
buildDevPlanExportFilename,
|
||||||
|
buildDevPlanMarkdown,
|
||||||
|
buildDevPlanPdf,
|
||||||
|
buildDevPlanXlsx,
|
||||||
|
devPlanExportMimeType,
|
||||||
|
mapTaskToExportRow,
|
||||||
|
type DevPlanExportFormat,
|
||||||
|
} from './admin-dev-plan-export.util';
|
||||||
|
|
||||||
|
function parseAttachmentUrls(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.map((u) => String(u).trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentUrlsInput(urls?: string[]): Prisma.InputJsonValue | undefined {
|
||||||
|
const list = (urls ?? []).map((u) => u.trim()).filter(Boolean);
|
||||||
|
return list.length ? (list as unknown as Prisma.InputJsonValue) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -132,6 +152,8 @@ export class DevPlanService {
|
|||||||
|
|
||||||
completedAt: Date | null;
|
completedAt: Date | null;
|
||||||
|
|
||||||
|
attachmentUrls?: unknown;
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
extras?: { creatorName?: string | null; supportTicketNo?: string | null },
|
extras?: { creatorName?: string | null; supportTicketNo?: string | null },
|
||||||
@@ -164,6 +186,8 @@ export class DevPlanService {
|
|||||||
|
|
||||||
completedAt: row.completedAt?.toISOString() ?? null,
|
completedAt: row.completedAt?.toISOString() ?? null,
|
||||||
|
|
||||||
|
attachmentUrls: parseAttachmentUrls(row.attachmentUrls),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -378,6 +402,8 @@ export class DevPlanService {
|
|||||||
|
|
||||||
supportTicketId: dto.supportTicketId ? BigInt(dto.supportTicketId) : null,
|
supportTicketId: dto.supportTicketId ? BigInt(dto.supportTicketId) : null,
|
||||||
|
|
||||||
|
attachmentUrls: attachmentUrlsInput(dto.attachmentUrls),
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -400,6 +426,12 @@ export class DevPlanService {
|
|||||||
|
|
||||||
if (!tasks.length) throw new BadRequestException('至少创建 1 条开发任务');
|
if (!tasks.length) throw new BadRequestException('至少创建 1 条开发任务');
|
||||||
|
|
||||||
|
const ticket = await this.prisma.commonSupportTicket.findUnique({
|
||||||
|
where: { id: ticketId },
|
||||||
|
select: { attachmentUrls: true },
|
||||||
|
});
|
||||||
|
const ticketUrls = parseAttachmentUrls(ticket?.attachmentUrls);
|
||||||
|
|
||||||
const created = await this.prisma.$transaction(
|
const created = await this.prisma.$transaction(
|
||||||
|
|
||||||
tasks.map((t) =>
|
tasks.map((t) =>
|
||||||
@@ -418,6 +450,10 @@ export class DevPlanService {
|
|||||||
|
|
||||||
supportTicketId: ticketId,
|
supportTicketId: ticketId,
|
||||||
|
|
||||||
|
attachmentUrls: attachmentUrlsInput(
|
||||||
|
t.attachmentUrls?.length ? t.attachmentUrls : ticketUrls,
|
||||||
|
),
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}),
|
}),
|
||||||
@@ -464,6 +500,10 @@ export class DevPlanService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.attachmentUrls !== undefined) {
|
||||||
|
data.attachmentUrls = attachmentUrlsInput(dto.attachmentUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await this.prisma.devPlanTask.update({ where: { id }, data });
|
await this.prisma.devPlanTask.update({ where: { id }, data });
|
||||||
@@ -1157,6 +1197,114 @@ export class DevPlanService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildTaskExportWhere(query: {
|
||||||
|
status?: string;
|
||||||
|
type?: string;
|
||||||
|
keyword?: string;
|
||||||
|
}): Prisma.DevPlanTaskWhereInput {
|
||||||
|
const where: Prisma.DevPlanTaskWhereInput = {};
|
||||||
|
if (query.status) where.status = query.status as DevPlanTaskStatus;
|
||||||
|
if (query.type) where.type = query.type as DevPlanTaskType;
|
||||||
|
if (query.keyword?.trim()) {
|
||||||
|
where.OR = [
|
||||||
|
{ taskNo: { contains: query.keyword.trim() } },
|
||||||
|
{ content: { contains: query.keyword.trim() } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportTasks(dto: {
|
||||||
|
scope: 'filter' | 'selected';
|
||||||
|
format: DevPlanExportFormat;
|
||||||
|
ids?: string[];
|
||||||
|
status?: string;
|
||||||
|
type?: string;
|
||||||
|
keyword?: string;
|
||||||
|
}) {
|
||||||
|
let rows: Array<{
|
||||||
|
id: bigint;
|
||||||
|
taskNo: string;
|
||||||
|
content: string;
|
||||||
|
type: DevPlanTaskType;
|
||||||
|
status: DevPlanTaskStatus;
|
||||||
|
creatorHqAccountId: bigint;
|
||||||
|
supportTicketId: bigint | null;
|
||||||
|
attachmentUrls: unknown;
|
||||||
|
createdAt: Date;
|
||||||
|
completedAt: Date | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
if (dto.scope === 'selected') {
|
||||||
|
if (!dto.ids?.length) throw new BadRequestException('请先勾选要导出的任务');
|
||||||
|
const ids = dto.ids.map(BigInt);
|
||||||
|
rows = await this.prisma.devPlanTask.findMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
if (rows.length !== ids.length) throw new BadRequestException('部分任务不存在');
|
||||||
|
} else {
|
||||||
|
rows = await this.prisma.devPlanTask.findMany({
|
||||||
|
where: this.buildTaskExportWhere(dto),
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length) throw new BadRequestException('没有可导出的任务');
|
||||||
|
|
||||||
|
const creatorIds = rows.map((r) => r.creatorHqAccountId);
|
||||||
|
const ticketIds = rows.map((r) => r.supportTicketId).filter((id): id is bigint => id != null);
|
||||||
|
const [names, tickets] = await Promise.all([
|
||||||
|
this.loadHqNames(creatorIds),
|
||||||
|
ticketIds.length
|
||||||
|
? this.prisma.commonSupportTicket.findMany({
|
||||||
|
where: { id: { in: ticketIds } },
|
||||||
|
select: { id: true, ticketNo: true },
|
||||||
|
})
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
const ticketMap = new Map(tickets.map((t) => [String(t.id), t.ticketNo] as [string, string]));
|
||||||
|
|
||||||
|
const exportRows = rows.map((r) =>
|
||||||
|
mapTaskToExportRow({
|
||||||
|
taskNo: r.taskNo,
|
||||||
|
type: r.type,
|
||||||
|
status: r.status,
|
||||||
|
content: r.content,
|
||||||
|
supportTicketNo: r.supportTicketId != null ? ticketMap.get(String(r.supportTicketId)) ?? null : null,
|
||||||
|
creatorName: names.get(String(r.creatorHqAccountId)) ?? null,
|
||||||
|
createdAt: r.createdAt.toISOString(),
|
||||||
|
completedAt: r.completedAt?.toISOString() ?? null,
|
||||||
|
attachmentUrls: parseAttachmentUrls(r.attachmentUrls),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer: Buffer;
|
||||||
|
switch (dto.format) {
|
||||||
|
case 'markdown':
|
||||||
|
buffer = buildDevPlanMarkdown(exportRows);
|
||||||
|
break;
|
||||||
|
case 'docx':
|
||||||
|
buffer = await buildDevPlanDocx(exportRows);
|
||||||
|
break;
|
||||||
|
case 'xlsx':
|
||||||
|
buffer = await buildDevPlanXlsx(exportRows);
|
||||||
|
break;
|
||||||
|
case 'pdf':
|
||||||
|
buffer = await buildDevPlanPdf(exportRows);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestException('不支持的导出格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: buildDevPlanExportFilename(dto.format, exportRows.length),
|
||||||
|
mimeType: devPlanExportMimeType(dto.format),
|
||||||
|
contentBase64: buffer.toString('base64'),
|
||||||
|
count: exportRows.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export class CreateDevPlanTaskDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
supportTicketId?: string;
|
supportTicketId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
attachmentUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateDevPlanTaskDto {
|
export class UpdateDevPlanTaskDto {
|
||||||
@@ -52,6 +57,11 @@ export class UpdateDevPlanTaskDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(DEV_PLAN_TASK_STATUSES)
|
@IsIn(DEV_PLAN_TASK_STATUSES)
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
attachmentUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateDevPlanVersionDto {
|
export class CreateDevPlanVersionDto {
|
||||||
@@ -131,6 +141,11 @@ export class DevPlanTaskFromTicketDto {
|
|||||||
|
|
||||||
@IsIn(DEV_PLAN_TASK_TYPES)
|
@IsIn(DEV_PLAN_TASK_TYPES)
|
||||||
type!: 'BUG' | 'REQUIREMENT' | 'OPTIMIZATION';
|
type!: 'BUG' | 'REQUIREMENT' | 'OPTIMIZATION';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
attachmentUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReviewSupportTicketDto {
|
export class ReviewSupportTicketDto {
|
||||||
@@ -177,6 +192,32 @@ export class DevPlanTaskBatchUpdateDto {
|
|||||||
versionId?: string | null;
|
versionId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DevPlanTaskExportDto {
|
||||||
|
@IsIn(['filter', 'selected'])
|
||||||
|
scope!: 'filter' | 'selected';
|
||||||
|
|
||||||
|
@IsIn(['markdown', 'docx', 'xlsx', 'pdf'])
|
||||||
|
format!: 'markdown' | 'docx' | 'xlsx' | 'pdf';
|
||||||
|
|
||||||
|
@ValidateIf((o: DevPlanTaskExportDto) => o.scope === 'selected')
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@IsString({ each: true })
|
||||||
|
ids?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
type?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class BatchReviewPreviewDto {
|
export class BatchReviewPreviewDto {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import {
|
||||||
|
HqPermissionGuard,
|
||||||
|
RequireHqPermissions,
|
||||||
|
} from '../../common/guards/hq-permission.guard';
|
||||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||||
import { AdminRedeemDebugService } from './admin-redeem-debug.service';
|
import { AdminRedeemDebugService } from './admin-redeem-debug.service';
|
||||||
@@ -13,7 +17,8 @@ import {
|
|||||||
} from './dto/admin-mutate.dto';
|
} from './dto/admin-mutate.dto';
|
||||||
|
|
||||||
@Controller('admin/redeem/debug')
|
@Controller('admin/redeem/debug')
|
||||||
@UseGuards(HqAuthGuard)
|
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('benefit_debug')
|
||||||
export class AdminRedeemDebugController {
|
export class AdminRedeemDebugController {
|
||||||
constructor(private readonly service: AdminRedeemDebugService) {}
|
constructor(private readonly service: AdminRedeemDebugService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|||||||
import type { DeliveryProvider } from '@prisma/client';
|
import type { DeliveryProvider } from '@prisma/client';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { loadStorePrimaryBank } from '../../common/store/store-bank.util';
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
import type { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto';
|
import type { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto';
|
||||||
import type { UpdateDeliveryDto } from './dto/admin-mutate.dto';
|
import type { UpdateDeliveryDto } from './dto/admin-mutate.dto';
|
||||||
@@ -113,8 +114,13 @@ export class AdminRedeemService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!record) throw new NotFoundException('核销记录不存在');
|
if (!record) throw new NotFoundException('核销记录不存在');
|
||||||
|
const primaryAccount = await loadStorePrimaryBank(this.prisma, record.storeId);
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
...record,
|
...record,
|
||||||
|
store: {
|
||||||
|
...record.store,
|
||||||
|
primaryAccount,
|
||||||
|
},
|
||||||
allocations: record.allocations.map((a) => ({
|
allocations: record.allocations.map((a) => ({
|
||||||
couponId: a.couponId,
|
couponId: a.couponId,
|
||||||
amount: Number(a.amount),
|
amount: Number(a.amount),
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import {
|
||||||
|
HqPermissionGuard,
|
||||||
|
RequireHqPermissions,
|
||||||
|
} from '../../common/guards/hq-permission.guard';
|
||||||
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
||||||
import {
|
import {
|
||||||
XiaofeixiaBatchShipmentQueryDto,
|
XiaofeixiaBatchShipmentQueryDto,
|
||||||
@@ -11,7 +15,8 @@ import {
|
|||||||
|
|
||||||
/** HQ 小飞侠接口联调(仅管理端,勿对 C 端暴露) */
|
/** HQ 小飞侠接口联调(仅管理端,勿对 C 端暴露) */
|
||||||
@Controller('admin/courier/xiaofeixia')
|
@Controller('admin/courier/xiaofeixia')
|
||||||
@UseGuards(HqAuthGuard)
|
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('deliveries_debug')
|
||||||
export class AdminXiaofeixiaController {
|
export class AdminXiaofeixiaController {
|
||||||
constructor(private readonly service: AdminXiaofeixiaService) {}
|
constructor(private readonly service: AdminXiaofeixiaService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -302,8 +302,8 @@ export class AdminStoreBillController {
|
|||||||
refType: 'STORE_BILL',
|
refType: 'STORE_BILL',
|
||||||
refIdParam: 'id',
|
refIdParam: 'id',
|
||||||
})
|
})
|
||||||
confirm(@Param('id') id: string) {
|
confirm(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
||||||
return this.settlementService.confirmStoreBill(BigInt(id));
|
return this.settlementService.confirmStoreBill(BigInt(id), body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,8 +485,8 @@ export class AdminWineryBillController {
|
|||||||
refType: 'WINERY_BILL',
|
refType: 'WINERY_BILL',
|
||||||
refIdParam: 'id',
|
refIdParam: 'id',
|
||||||
})
|
})
|
||||||
confirm(@Param('id') id: string) {
|
confirm(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
||||||
return this.settlementService.confirmWineryBill(BigInt(id));
|
return this.settlementService.confirmWineryBill(BigInt(id), body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,8 +565,8 @@ export class AdminLogisticsBillController {
|
|||||||
refType: 'LOGISTICS_BILL',
|
refType: 'LOGISTICS_BILL',
|
||||||
refIdParam: 'id',
|
refIdParam: 'id',
|
||||||
})
|
})
|
||||||
confirm(@Param('id') id: string) {
|
confirm(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
||||||
return this.settlementService.confirmLogisticsBill(BigInt(id));
|
return this.settlementService.confirmLogisticsBill(BigInt(id), body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-
|
|||||||
import { AnalyticsService } from '../analytics/analytics.service';
|
import { AnalyticsService } from '../analytics/analytics.service';
|
||||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||||
|
import {
|
||||||
|
loadStorePrimaryBank,
|
||||||
|
loadStorePrimaryBanksMap,
|
||||||
|
loadWineryBankConfig,
|
||||||
|
} from '../../common/store/store-bank.util';
|
||||||
|
|
||||||
function generateBillNo(prefix: string) {
|
function generateBillNo(prefix: string) {
|
||||||
return `${prefix}${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
return `${prefix}${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||||
@@ -1043,11 +1048,20 @@ export class SettlementService {
|
|||||||
|
|
||||||
const total = rows.length;
|
const total = rows.length;
|
||||||
const slice = rows.slice((page - 1) * pageSize, page * pageSize);
|
const slice = rows.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
const storeIds = [...new Set(rows.filter((r) => r.kind === 'T1_BILL').map((r) => r.storeId))];
|
||||||
|
const bankMap = await loadStorePrimaryBanksMap(this.prisma, storeIds);
|
||||||
|
|
||||||
const redeemAmount = bills.reduce((s, b) => s + Number(b.redeemAmount), 0);
|
const redeemAmount = bills.reduce((s, b) => s + Number(b.redeemAmount), 0);
|
||||||
const payoutAmount = rows.reduce((s, r) => s + r.amount, 0);
|
const payoutAmount = rows.reduce((s, r) => s + r.amount, 0);
|
||||||
|
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
items: slice,
|
items: slice.map((r) => ({
|
||||||
|
...r,
|
||||||
|
bankAccount:
|
||||||
|
r.kind === 'T1_BILL'
|
||||||
|
? bankMap.get(String(r.storeId)) ?? null
|
||||||
|
: null,
|
||||||
|
})),
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -1112,10 +1126,11 @@ export class SettlementService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||||
return serializeBigInt(bill);
|
const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId);
|
||||||
|
return serializeBigInt({ ...bill, storeAccount });
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmStoreBill(id: bigint) {
|
async confirmStoreBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
const bill = await this.prisma.storeBill.findUnique({ where: { id } });
|
const bill = await this.prisma.storeBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可确认打款');
|
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可确认打款');
|
||||||
@@ -1124,7 +1139,11 @@ export class SettlementService {
|
|||||||
const updated = await this.prisma.$transaction(async (tx) => {
|
const updated = await this.prisma.$transaction(async (tx) => {
|
||||||
const b = await tx.storeBill.update({
|
const b = await tx.storeBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt },
|
data: {
|
||||||
|
status: 'PAID',
|
||||||
|
paidAt,
|
||||||
|
paymentRef: dto.paymentRef?.trim() || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await tx.storePayout.updateMany({
|
await tx.storePayout.updateMany({
|
||||||
where: { storeBillId: id, status: 'PENDING' },
|
where: { storeBillId: id, status: 'PENDING' },
|
||||||
@@ -1157,12 +1176,32 @@ export class SettlementService {
|
|||||||
const where = this.buildStoreBillWhere(query);
|
const where = this.buildStoreBillWhere(query);
|
||||||
const bills = await this.prisma.storeBill.findMany({
|
const bills = await this.prisma.storeBill.findMany({
|
||||||
where,
|
where,
|
||||||
include: { store: { select: { name: true, phone: true, cityName: true } } },
|
include: { store: { select: { id: true, name: true, phone: true, cityName: true } } },
|
||||||
orderBy: { billDate: 'desc' },
|
orderBy: { billDate: 'desc' },
|
||||||
});
|
});
|
||||||
const header = ['账单号', '账单日', '门店', '城市', '核销笔数', '核销金额', '结算比例', '应付金额', '状态', '打款时间'].join(',');
|
const bankMap = await loadStorePrimaryBanksMap(
|
||||||
const rows = bills.map((b) =>
|
this.prisma,
|
||||||
[
|
bills.map((b) => b.storeId),
|
||||||
|
);
|
||||||
|
const header = [
|
||||||
|
'账单号',
|
||||||
|
'账单日',
|
||||||
|
'门店',
|
||||||
|
'城市',
|
||||||
|
'核销笔数',
|
||||||
|
'核销金额',
|
||||||
|
'结算比例',
|
||||||
|
'应付金额',
|
||||||
|
'状态',
|
||||||
|
'打款时间',
|
||||||
|
'打款凭证',
|
||||||
|
'收款户名',
|
||||||
|
'收款账号',
|
||||||
|
'开户行',
|
||||||
|
].join(',');
|
||||||
|
const rows = bills.map((b) => {
|
||||||
|
const bank = bankMap.get(String(b.storeId));
|
||||||
|
return [
|
||||||
csvEscape(b.billNo),
|
csvEscape(b.billNo),
|
||||||
b.billDate.toISOString().slice(0, 10),
|
b.billDate.toISOString().slice(0, 10),
|
||||||
csvEscape(b.store.name),
|
csvEscape(b.store.name),
|
||||||
@@ -1173,8 +1212,12 @@ export class SettlementService {
|
|||||||
Number(b.payoutAmount),
|
Number(b.payoutAmount),
|
||||||
b.status,
|
b.status,
|
||||||
b.paidAt ? b.paidAt.toISOString().slice(0, 19).replace('T', ' ') : '',
|
b.paidAt ? b.paidAt.toISOString().slice(0, 19).replace('T', ' ') : '',
|
||||||
].join(','),
|
csvEscape(b.paymentRef ?? ''),
|
||||||
);
|
csvEscape(bank?.bankAccountName ?? ''),
|
||||||
|
csvEscape(bank?.bankAccountNo ?? ''),
|
||||||
|
csvEscape(bank?.bankBranch ?? ''),
|
||||||
|
].join(',');
|
||||||
|
});
|
||||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,7 +1305,15 @@ export class SettlementService {
|
|||||||
take: pageSize,
|
take: pageSize,
|
||||||
include: {
|
include: {
|
||||||
partnerAccount: {
|
partnerAccount: {
|
||||||
select: { id: true, companyName: true, name: true, phone: true },
|
select: {
|
||||||
|
id: true,
|
||||||
|
companyName: true,
|
||||||
|
name: true,
|
||||||
|
phone: true,
|
||||||
|
bankAccountName: true,
|
||||||
|
bankAccountNo: true,
|
||||||
|
bankBranch: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -1553,14 +1604,19 @@ export class SettlementService {
|
|||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
async markPartnerBillPaid(id: bigint, _dto: { paymentRef?: string } = {}) {
|
async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可标记打款');
|
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可标记打款');
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
const updated = await this.prisma.partnerBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt: new Date(), rejectReason: null },
|
data: {
|
||||||
|
status: 'PAID',
|
||||||
|
paidAt: new Date(),
|
||||||
|
rejectReason: null,
|
||||||
|
paymentRef: dto.paymentRef?.trim() || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
@@ -1588,7 +1644,17 @@ export class SettlementService {
|
|||||||
const where = this.buildPartnerBillWhere(query);
|
const where = this.buildPartnerBillWhere(query);
|
||||||
const bills = await this.prisma.partnerBill.findMany({
|
const bills = await this.prisma.partnerBill.findMany({
|
||||||
where,
|
where,
|
||||||
include: { partnerAccount: { select: { companyName: true, phone: true } } },
|
include: {
|
||||||
|
partnerAccount: {
|
||||||
|
select: {
|
||||||
|
companyName: true,
|
||||||
|
phone: true,
|
||||||
|
bankAccountName: true,
|
||||||
|
bankAccountNo: true,
|
||||||
|
bankBranch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
orderBy: { periodStart: 'desc' },
|
orderBy: { periodStart: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1605,6 +1671,10 @@ export class SettlementService {
|
|||||||
'发送时间',
|
'发送时间',
|
||||||
'确认时间',
|
'确认时间',
|
||||||
'打款时间',
|
'打款时间',
|
||||||
|
'打款凭证',
|
||||||
|
'收款户名',
|
||||||
|
'收款账号',
|
||||||
|
'开户行',
|
||||||
'驳回理由',
|
'驳回理由',
|
||||||
].join(',');
|
].join(',');
|
||||||
const rows = bills.map((b) =>
|
const rows = bills.map((b) =>
|
||||||
@@ -1621,6 +1691,10 @@ export class SettlementService {
|
|||||||
b.sentAt ? b.sentAt.toISOString().slice(0, 10) : '',
|
b.sentAt ? b.sentAt.toISOString().slice(0, 10) : '',
|
||||||
b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '',
|
b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '',
|
||||||
b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '',
|
b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '',
|
||||||
|
csvEscape(b.paymentRef ?? ''),
|
||||||
|
csvEscape(b.partnerAccount.bankAccountName ?? ''),
|
||||||
|
csvEscape(b.partnerAccount.bankAccountNo ?? ''),
|
||||||
|
csvEscape(b.partnerAccount.bankBranch ?? ''),
|
||||||
csvEscape(b.rejectReason ?? ''),
|
csvEscape(b.rejectReason ?? ''),
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
@@ -1752,10 +1826,11 @@ export class SettlementService {
|
|||||||
include: { items: { orderBy: { paidAt: 'desc' } } },
|
include: { items: { orderBy: { paidAt: 'desc' } } },
|
||||||
});
|
});
|
||||||
if (!bill) throw new NotFoundException('酒厂对账单不存在');
|
if (!bill) throw new NotFoundException('酒厂对账单不存在');
|
||||||
return serializeBigInt(bill);
|
const wineryBank = await loadWineryBankConfig(this.prisma);
|
||||||
|
return serializeBigInt({ ...bill, wineryBank });
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmWineryBill(id: bigint) {
|
async confirmWineryBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
const bill = await this.prisma.wineryBill.findUnique({ where: { id } });
|
const bill = await this.prisma.wineryBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('酒厂对账单不存在');
|
if (!bill) throw new NotFoundException('酒厂对账单不存在');
|
||||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可确认打款');
|
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可确认打款');
|
||||||
@@ -1764,7 +1839,11 @@ export class SettlementService {
|
|||||||
}
|
}
|
||||||
const updated = await this.prisma.wineryBill.update({
|
const updated = await this.prisma.wineryBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt: new Date() },
|
data: {
|
||||||
|
status: 'PAID',
|
||||||
|
paidAt: new Date(),
|
||||||
|
paymentRef: dto.paymentRef?.trim() || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
}
|
}
|
||||||
@@ -1795,6 +1874,7 @@ export class SettlementService {
|
|||||||
include: { items: true },
|
include: { items: true },
|
||||||
orderBy: { billDate: 'desc' },
|
orderBy: { billDate: 'desc' },
|
||||||
});
|
});
|
||||||
|
const wineryBank = await loadWineryBankConfig(this.prisma);
|
||||||
|
|
||||||
const header = [
|
const header = [
|
||||||
'账单号',
|
'账单号',
|
||||||
@@ -1806,6 +1886,11 @@ export class SettlementService {
|
|||||||
'应付',
|
'应付',
|
||||||
'支付时间',
|
'支付时间',
|
||||||
'账单状态',
|
'账单状态',
|
||||||
|
'打款凭证',
|
||||||
|
'收款户名',
|
||||||
|
'开户银行',
|
||||||
|
'开户支行',
|
||||||
|
'收款账号',
|
||||||
].join(',');
|
].join(',');
|
||||||
const rows: string[] = [];
|
const rows: string[] = [];
|
||||||
const statusLabel = (b: { status: string; wineryAmount: Prisma.Decimal | number }) => {
|
const statusLabel = (b: { status: string; wineryAmount: Prisma.Decimal | number }) => {
|
||||||
@@ -1814,6 +1899,13 @@ export class SettlementService {
|
|||||||
if (b.status === 'UNPAID') return '未打款';
|
if (b.status === 'UNPAID') return '未打款';
|
||||||
return b.status;
|
return b.status;
|
||||||
};
|
};
|
||||||
|
const bankCols = (b: { paymentRef?: string | null }) => [
|
||||||
|
csvEscape(b.paymentRef ?? ''),
|
||||||
|
csvEscape(wineryBank.bankAccountName ?? ''),
|
||||||
|
csvEscape(wineryBank.bankName ?? ''),
|
||||||
|
csvEscape(wineryBank.bankBranch ?? ''),
|
||||||
|
csvEscape(wineryBank.bankAccountNo ?? ''),
|
||||||
|
];
|
||||||
for (const b of bills) {
|
for (const b of bills) {
|
||||||
if (b.items.length === 0) {
|
if (b.items.length === 0) {
|
||||||
rows.push(
|
rows.push(
|
||||||
@@ -1827,6 +1919,7 @@ export class SettlementService {
|
|||||||
Number(b.wineryAmount),
|
Number(b.wineryAmount),
|
||||||
'',
|
'',
|
||||||
statusLabel(b),
|
statusLabel(b),
|
||||||
|
...bankCols(b),
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -1843,6 +1936,7 @@ export class SettlementService {
|
|||||||
Number(item.wineryAmount),
|
Number(item.wineryAmount),
|
||||||
item.paidAt.toISOString().slice(0, 19).replace('T', ' '),
|
item.paidAt.toISOString().slice(0, 19).replace('T', ' '),
|
||||||
statusLabel(b),
|
statusLabel(b),
|
||||||
|
...bankCols(b),
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2275,11 +2369,17 @@ export class SettlementService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmLogisticsBill(id: bigint) {
|
async confirmLogisticsBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
const bill = await this.prisma.logisticsBill.findUnique({ where: { id } });
|
const bill = await this.prisma.logisticsBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('物流对账单不存在');
|
if (!bill) throw new NotFoundException('物流对账单不存在');
|
||||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未结算账单可确认');
|
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未结算账单可确认');
|
||||||
|
|
||||||
|
const paidData = {
|
||||||
|
status: 'PAID' as const,
|
||||||
|
paidAt: new Date(),
|
||||||
|
paymentRef: dto.paymentRef?.trim() || null,
|
||||||
|
};
|
||||||
|
|
||||||
if (bill.settlementMethod === 'PREPAID') {
|
if (bill.settlementMethod === 'PREPAID') {
|
||||||
const deducted = await this.prisma.$transaction(async (tx) => {
|
const deducted = await this.prisma.$transaction(async (tx) => {
|
||||||
const result = await this.fulfillmentProviderService.deductPrepaid(
|
const result = await this.fulfillmentProviderService.deductPrepaid(
|
||||||
@@ -2295,7 +2395,7 @@ export class SettlementService {
|
|||||||
}
|
}
|
||||||
return tx.logisticsBill.update({
|
return tx.logisticsBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt: new Date() },
|
data: paidData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return serializeBigInt(deducted);
|
return serializeBigInt(deducted);
|
||||||
@@ -2303,7 +2403,7 @@ export class SettlementService {
|
|||||||
|
|
||||||
const updated = await this.prisma.logisticsBill.update({
|
const updated = await this.prisma.logisticsBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt: new Date() },
|
data: paidData,
|
||||||
});
|
});
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
}
|
}
|
||||||
@@ -2331,7 +2431,16 @@ export class SettlementService {
|
|||||||
const bills = await this.prisma.logisticsBill.findMany({
|
const bills = await this.prisma.logisticsBill.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
fulfillmentProvider: { select: { code: true, name: true } },
|
fulfillmentProvider: {
|
||||||
|
select: {
|
||||||
|
code: true,
|
||||||
|
name: true,
|
||||||
|
bankAccountName: true,
|
||||||
|
bankName: true,
|
||||||
|
bankBranch: true,
|
||||||
|
bankAccountNo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
items: true,
|
items: true,
|
||||||
},
|
},
|
||||||
orderBy: { periodStart: 'desc' },
|
orderBy: { periodStart: 'desc' },
|
||||||
@@ -2349,15 +2458,28 @@ export class SettlementService {
|
|||||||
'物流费',
|
'物流费',
|
||||||
'发货时间',
|
'发货时间',
|
||||||
'账单状态',
|
'账单状态',
|
||||||
|
'打款凭证',
|
||||||
|
'收款户名',
|
||||||
|
'开户银行',
|
||||||
|
'开户支行',
|
||||||
|
'收款账号',
|
||||||
].join(',');
|
].join(',');
|
||||||
const rows: string[] = [];
|
const rows: string[] = [];
|
||||||
for (const b of bills) {
|
for (const b of bills) {
|
||||||
|
const p = b.fulfillmentProvider;
|
||||||
|
const bankCols = [
|
||||||
|
csvEscape(b.paymentRef ?? ''),
|
||||||
|
csvEscape(p.bankAccountName ?? ''),
|
||||||
|
csvEscape(p.bankName ?? ''),
|
||||||
|
csvEscape(p.bankBranch ?? ''),
|
||||||
|
csvEscape(p.bankAccountNo ?? ''),
|
||||||
|
];
|
||||||
if (b.items.length === 0) {
|
if (b.items.length === 0) {
|
||||||
rows.push(
|
rows.push(
|
||||||
[
|
[
|
||||||
csvEscape(b.billNo),
|
csvEscape(b.billNo),
|
||||||
csvEscape(b.fulfillmentProvider.code),
|
csvEscape(p.code),
|
||||||
csvEscape(b.fulfillmentProvider.name),
|
csvEscape(p.name),
|
||||||
b.periodStart.toISOString().slice(0, 10),
|
b.periodStart.toISOString().slice(0, 10),
|
||||||
b.periodEnd.toISOString().slice(0, 10),
|
b.periodEnd.toISOString().slice(0, 10),
|
||||||
b.settlementMethod,
|
b.settlementMethod,
|
||||||
@@ -2366,6 +2488,7 @@ export class SettlementService {
|
|||||||
Number(b.logisticsAmount),
|
Number(b.logisticsAmount),
|
||||||
'',
|
'',
|
||||||
b.status,
|
b.status,
|
||||||
|
...bankCols,
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -2374,8 +2497,8 @@ export class SettlementService {
|
|||||||
rows.push(
|
rows.push(
|
||||||
[
|
[
|
||||||
csvEscape(b.billNo),
|
csvEscape(b.billNo),
|
||||||
csvEscape(b.fulfillmentProvider.code),
|
csvEscape(p.code),
|
||||||
csvEscape(b.fulfillmentProvider.name),
|
csvEscape(p.name),
|
||||||
b.periodStart.toISOString().slice(0, 10),
|
b.periodStart.toISOString().slice(0, 10),
|
||||||
b.periodEnd.toISOString().slice(0, 10),
|
b.periodEnd.toISOString().slice(0, 10),
|
||||||
b.settlementMethod,
|
b.settlementMethod,
|
||||||
@@ -2384,6 +2507,7 @@ export class SettlementService {
|
|||||||
Number(item.logisticsAmount),
|
Number(item.logisticsAmount),
|
||||||
item.shippedAt.toISOString().slice(0, 19).replace('T', ' '),
|
item.shippedAt.toISOString().slice(0, 19).replace('T', ' '),
|
||||||
b.status,
|
b.status,
|
||||||
|
...bankCols,
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user