feat: 客户更新接口支持局部字段,并补充开发/SQL 规范说明
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
aa4c4a28e6
commit
8258ae746e
55
README.md
55
README.md
@ -122,6 +122,61 @@ SimpleAdmin.Api/
|
|||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 开发规范(强制)
|
||||||
|
|
||||||
|
更完整的分层 / 路由 / DTO 约定见 `开发复刻说明.md`、`design.md`。下列为新增代码与合入共享分支的硬性标准,评审与上传前必须满足。
|
||||||
|
|
||||||
|
### 1. 写接口必须写注释
|
||||||
|
|
||||||
|
对外暴露的 Application API(`ApplicationService` 上的 `[HttpGet]` / `[HttpPost]` 等方法)**必须**有 XML / 中文注释,至少说明:
|
||||||
|
|
||||||
|
- 接口用途(做什么)
|
||||||
|
- 关键入参含义与约束(可写在方法注释或 DTO 属性注释上)
|
||||||
|
- 关键返回语义或业务失败场景(如抛 `BusinessException` 的条件)
|
||||||
|
|
||||||
|
DTO、领域常量、非显而易见的私有辅助方法也应补注释,避免「只有路由名能猜业务」。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// 批量创建仓储代发订单:建单并绑定库存/包裹,产品须为分组 wms。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">仓库、产品编码与店铺订单绑定明细。</param>
|
||||||
|
[HttpPost("/order/platform-store-order/batch-create-fulfill-wms")]
|
||||||
|
public async Task<PlatformStoreOrderBatchCreateLogisticsOutput> BatchCreateFulfillWmsAsync(
|
||||||
|
PlatformStoreOrderBatchCreateFulfillInput input)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 写 SQL 必须走 ORM(SqlSugar)
|
||||||
|
|
||||||
|
业务读写、列表筛选、联表、聚合、更新**一律通过 SqlSugar / Repository**,禁止在 Application / Domain 服务里手写拼装 SQL 字符串或 `Ado.SqlQuery` 作为常规实现。
|
||||||
|
|
||||||
|
| 允许 | 禁止(业务代码) |
|
||||||
|
|------|------------------|
|
||||||
|
| `Repository` / `ISugarQueryable` / 表达式 `Where` | 服务内大段 `"SELECT ... " +` 拼接 |
|
||||||
|
| 实体 + 仓储封装复杂查询 | 绕过仓储直接 ADO 执行业务 SQL |
|
||||||
|
| `sql/*.sql` 仅作建库 / 存量 DDL 迁移 | 用脚本替代日常业务 CRUD |
|
||||||
|
|
||||||
|
存量 DDL、字典初始化等脚本放在 `sql/`(说明见 `sql/README.md`),与运行时 ORM 查询分离;**运行时查询与写入必须 ORM**。
|
||||||
|
|
||||||
|
### 3. 上传代码前先同步远端
|
||||||
|
|
||||||
|
`test`、`master` 等共享分支会有其他人提交。**推送前必须先拉取远端,再上传**,避免覆盖或漏掉别人的提交。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git pull --ff-only origin <当前分支>
|
||||||
|
git push origin <当前分支>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 工作区有未提交改动时,先暂存(stash)或提交,再同步,同步完成后再恢复。
|
||||||
|
- 本地已有未推送提交、远端也有新提交时,先 rebase 或 merge 并解决冲突,确认与远端对齐后再 `push`。
|
||||||
|
- 禁止在未同步远端的情况下直接 `push` 共享分支。
|
||||||
|
|
||||||
|
### 4. 相关约定速查
|
||||||
|
|
||||||
|
- **禁止**新建库级唯一索引(`UNIQUE KEY`):见 `.cursor/rules/no-unique-indexes.mdc`,业务唯一性在 Application 校验。
|
||||||
|
- 路由显式声明;分页统一 `totalCount` + `items`;勿把 Entity 直接返回前端。
|
||||||
|
|
||||||
## Fake 框架特性
|
## Fake 框架特性
|
||||||
|
|
||||||
### 自动 API 生成
|
### 自动 API 生成
|
||||||
|
|||||||
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
OMS / 共用库的建表脚本已统一到仓库根目录 `db.sql`。
|
OMS / 共用库的建表脚本已统一到仓库根目录 `db.sql`。
|
||||||
|
|
||||||
|
## 职责边界
|
||||||
|
|
||||||
|
本目录**只放 DDL / 存量迁移 / 字典种子**等库结构与一次性数据脚本。
|
||||||
|
|
||||||
|
- **运行时业务查询与写入必须走 SqlSugar ORM(仓储)**,不要在 Application 里用手写 SQL 替代 CRUD。
|
||||||
|
- 完整编码标准见仓库根目录 `README.md` →「开发规范」。
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
- 新环境:直接执行 `db.sql`(会 DROP 后重建,勿在有生产数据的库上执行)
|
- 新环境:直接执行 `db.sql`(会 DROP 后重建,勿在有生产数据的库上执行)
|
||||||
- 已有库:用 diff 对比 `db.sql` 与现库结构后手工 ALTER;本目录 `migrate_all.sql` 为存量增量迁移合集(可重复执行段已做列/表存在判断;揽收 Direction / FeePush 为裸 ALTER,已执行过请跳过)
|
- 已有库:用 diff 对比 `db.sql` 与现库结构后手工 ALTER;本目录 `migrate_all.sql` 为存量增量迁移合集(可重复执行段已做列/表存在判断;揽收 Direction / FeePush 为裸 ALTER,已执行过请跳过)
|
||||||
- 数据权限表(`user_org_perm` 等)若 RBAC 与 OMS 分库,请只在 RBAC 库执行 `db.sql` 末尾对应段落
|
- 数据权限表(`user_org_perm` 等)若 RBAC 与 OMS 分库,请只在 RBAC 库执行 `db.sql` 末尾对应段落
|
||||||
|
- 单次增量迁移命名:`migrate_<主题>.sql`,尽量可重复执行(先判断列/表/字典是否已存在)
|
||||||
|
|||||||
@ -15,67 +15,64 @@ public class UpdateCustomerInput
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 客户编号。
|
/// 客户编号;不传或空则保留原值(编辑页编号只读,允许局部更新省略)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
|
||||||
[StringLength(32)]
|
[StringLength(32)]
|
||||||
public string CustomerCode { get; set; } = string.Empty;
|
public string? CustomerCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 客户名称。
|
/// 客户名称;不传或空则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
public string CustomerName { get; set; } = string.Empty;
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 手机号。
|
/// 手机号;不传或空则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
|
||||||
[StringLength(16)]
|
[StringLength(16)]
|
||||||
public string Phone { get; set; } = string.Empty;
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 邮箱。
|
/// 邮箱;不传则保留原值,传空串可清空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
[StringLength(64)]
|
[StringLength(64)]
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信头像地址。
|
/// 微信头像地址;不传则保留原值,传空串可清空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StringLength(255)]
|
[StringLength(255)]
|
||||||
public string? WechatAvatar { get; set; }
|
public string? WechatAvatar { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信昵称。
|
/// 微信昵称;不传则保留原值,传空串可清空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StringLength(64)]
|
[StringLength(64)]
|
||||||
public string? WechatNickname { get; set; }
|
public string? WechatNickname { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 账户余额。
|
/// 账户余额;不传则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Range(typeof(decimal), "0", "999999999999")]
|
[Range(typeof(decimal), "0", "999999999999")]
|
||||||
public decimal Balance { get; set; }
|
public decimal? Balance { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 总额度。
|
/// 总额度;不传则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Range(typeof(decimal), "0", "999999999999")]
|
[Range(typeof(decimal), "0", "999999999999")]
|
||||||
public decimal TotalQuota { get; set; }
|
public decimal? TotalQuota { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已用额度。
|
/// 已用额度;不传则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Range(typeof(decimal), "0", "999999999999")]
|
[Range(typeof(decimal), "0", "999999999999")]
|
||||||
public decimal UsedQuota { get; set; }
|
public decimal? UsedQuota { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 状态。
|
/// 状态;不传则保留原值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CustomerStatus Status { get; set; } = CustomerStatus.Normal;
|
public CustomerStatus? Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 备注。
|
/// 备注。
|
||||||
|
|||||||
@ -329,10 +329,17 @@ public class CustomerService : ApplicationService
|
|||||||
public async Task UpdateAsync(UpdateCustomerInput input)
|
public async Task UpdateAsync(UpdateCustomerInput input)
|
||||||
{
|
{
|
||||||
var entity = await GetCustomerOrThrowAsync(input.Id);
|
var entity = await GetCustomerOrThrowAsync(input.Id);
|
||||||
var customerCode = NormalizeRequiredText(input.CustomerCode, "客户编号");
|
// 未传字段保留原值,避免局部更新(如只改名称)把编号/余额/状态冲成空或默认值
|
||||||
var customerName = NormalizeRequiredText(input.CustomerName, "客户名称");
|
var customerCode = string.IsNullOrWhiteSpace(input.CustomerCode)
|
||||||
var phone = NormalizeRequiredText(input.Phone, "手机号");
|
? entity.CustomerCode
|
||||||
var email = NormalizeOptionalText(input.Email);
|
: NormalizeRequiredText(input.CustomerCode, "客户编号");
|
||||||
|
var customerName = string.IsNullOrWhiteSpace(input.CustomerName)
|
||||||
|
? entity.CustomerName
|
||||||
|
: NormalizeRequiredText(input.CustomerName, "客户名称");
|
||||||
|
var phone = string.IsNullOrWhiteSpace(input.Phone)
|
||||||
|
? entity.Phone
|
||||||
|
: NormalizeRequiredText(input.Phone, "手机号");
|
||||||
|
var email = input.Email != null ? NormalizeOptionalText(input.Email) : entity.Email;
|
||||||
|
|
||||||
await EnsureCustomerIdentityUniqueForUpdateAsync(entity.Id, customerCode, phone, email);
|
await EnsureCustomerIdentityUniqueForUpdateAsync(entity.Id, customerCode, phone, email);
|
||||||
|
|
||||||
@ -340,18 +347,65 @@ public class CustomerService : ApplicationService
|
|||||||
entity.CustomerName = customerName;
|
entity.CustomerName = customerName;
|
||||||
entity.Phone = phone;
|
entity.Phone = phone;
|
||||||
entity.Email = email;
|
entity.Email = email;
|
||||||
entity.WechatAvatar = NormalizeOptionalText(input.WechatAvatar);
|
if (input.WechatAvatar != null)
|
||||||
entity.WechatNickname = NormalizeOptionalText(input.WechatNickname);
|
{
|
||||||
entity.Balance = input.Balance;
|
entity.WechatAvatar = NormalizeOptionalText(input.WechatAvatar);
|
||||||
entity.TotalQuota = input.TotalQuota;
|
}
|
||||||
entity.UsedQuota = input.UsedQuota;
|
|
||||||
entity.Status = input.Status;
|
if (input.WechatNickname != null)
|
||||||
entity.Remark = NormalizeOptionalText(input.Remark);
|
{
|
||||||
entity.Salesman = NormalizeOptionalText(input.Salesman);
|
entity.WechatNickname = NormalizeOptionalText(input.WechatNickname);
|
||||||
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
|
}
|
||||||
entity.CustomerGroup = input.CustomerGroup;
|
|
||||||
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
if (input.Balance.HasValue)
|
||||||
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(input.PriceTierKey);
|
{
|
||||||
|
entity.Balance = input.Balance.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.TotalQuota.HasValue)
|
||||||
|
{
|
||||||
|
entity.TotalQuota = input.TotalQuota.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.UsedQuota.HasValue)
|
||||||
|
{
|
||||||
|
entity.UsedQuota = input.UsedQuota.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Status.HasValue && input.Status.Value != CustomerStatus.None)
|
||||||
|
{
|
||||||
|
entity.Status = input.Status.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Remark != null)
|
||||||
|
{
|
||||||
|
entity.Remark = NormalizeOptionalText(input.Remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Salesman != null)
|
||||||
|
{
|
||||||
|
entity.Salesman = NormalizeOptionalText(input.Salesman);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.CustomerService != null)
|
||||||
|
{
|
||||||
|
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.CustomerGroup.HasValue)
|
||||||
|
{
|
||||||
|
entity.CustomerGroup = input.CustomerGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.MemberLevelId.HasValue)
|
||||||
|
{
|
||||||
|
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.PriceTierKey != null)
|
||||||
|
{
|
||||||
|
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(input.PriceTierKey);
|
||||||
|
}
|
||||||
|
|
||||||
await CustomerRepository.UpdateAsync(entity);
|
await CustomerRepository.UpdateAsync(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user