refactor: remove unused backend extensions

This commit is contained in:
fuenmao
2026-09-21 15:14:48 +08:00
parent b1a311cf84
commit 54d5c4fda3
113 changed files with 134 additions and 34309 deletions
File diff suppressed because one or more lines are too long
@@ -1,143 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 支付宝支付错误码
/// </summary>
public class AlipayErrorCode
{
/// <summary>
/// 错误代码
/// </summary>
public string Code { get; private set; }
/// <summary>
/// 错误消息
/// </summary>
public string Message { get; private set; }
/// <summary>
/// 解决方案
/// </summary>
public string Solution { get; private set; }
/// <summary>
/// 错误码集
/// </summary>
private static readonly List<AlipayErrorCode> StatusCodes =
[
new AlipayErrorCode { Code="SYSTEM_ERROR", Message="系统繁忙", Solution="可能是由于网络或者系统故障,请与技术人员联系以解决该问题。" },
new AlipayErrorCode { Code="INVALID_PARAMETER", Message="参数有误或没有参数", Solution="请检查并确认查询请求参数合法性。" },
new AlipayErrorCode { Code="AUTHORISE_NOT_MATCH", Message="授权失败,无法获取用户信息", Solution="检查账户与支付方关系主表关系,确认是否正确配置。" },
new AlipayErrorCode { Code="BALANCE_IS_NOT_ENOUGH", Message="余额不足,建议尽快充值。后续登录电银通或支付宝,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值。商户后续登录电银通或支付宝,自主设置余额预警提醒功能或登录Alipay-资金管理->产品一览->右上角功能按钮进行设置。" },
new AlipayErrorCode { Code="BIZ_UNIQUE_EXCEPTION", Message="商户订单号冲突。", Solution="商户订单号冲突。" },
new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_RECEIVE", Message="账户异常被冻结,无法收款。", Solution="账户异常被冻结,无法收款。请询问支付宝热线95188" },
new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_SEND", Message="该账户被冻结,暂不可将资金转出。", Solution="该账户被冻结,暂不可将资金转出。" },
new AlipayErrorCode { Code="CURRENCY_NOT_SUPPORT", Message="币种不支持", Solution="请查询您的结算币种需求所见币种,目前限于人民币/美元结算。" },
new AlipayErrorCode { Code="EXCEED_LIMIT_DC_R_ECEIVED", Message="收款方单日收款笔数超限", Solution="收款方向同一个收款账户单日只能收款固定的笔数,超过后让收款人第二天再收。" },
new AlipayErrorCode { Code="EXCEED_LIMIT_DM_AMOUNT", Message="日累计额度超限", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
new AlipayErrorCode { Code="EXCEED_LIMIT_DM_MAX_AMOUNT", Message="超出单日转账限额,如有疑问请询问支付宝热线95188", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
new AlipayErrorCode { Code="EXCEED_LIMIT_ENT_SM_AMOUNT", Message="转账给企业用户超过单笔限额(默认10w)", Solution="1. 10w以下的快速转账给企业用户。2. 联系800电话协助修改,修改转账限额" },
new AlipayErrorCode { Code="EXCEED_LIMIT_MM_AMOUNT", Message="月累计金额超限", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
new AlipayErrorCode { Code="EXCEED_LIMIT_MMM_MAX_AMOUNT", Message="超出单月转账限额,如有疑问请询问支付宝热线95188", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
new AlipayErrorCode { Code="EXCEED_LIMIT_PERSONAL_SM_AMOUNT", Message="超出转账给个人支付宝账户的单笔限额", Solution="超出转账给个人支付宝账户的单笔限额" },
new AlipayErrorCode { Code="EXCEED_LIMIT_SM_AMOUNT", Message="单笔额度超限", Solution="请根据接入文档填写amount字段" },
new AlipayErrorCode { Code="EXCEED_LIMIT_SM_MIN_AMOUNT", Message="请求金额不能低于0.1元", Solution="请修改转账金额。" },
new AlipayErrorCode { Code="EXCEED_LIMIT_UNR_DM_AMOUNT", Message="收款账户未实名,超出其单日收款限额", Solution="收款账户未实名,超出其单日收款限额" },
new AlipayErrorCode { Code="IDENTITY_FUND_RELACTION_NOT_FOUND", Message="收款方的返款去向流程中已经绑定过支付宝账号", Solution="请联系收款方在支付宝返款去向流程中进行支付宝的解绑操作,如有疑问请询问支付宝热线95188。" },
new AlipayErrorCode { Code="ILLEGAL_OPERATION", Message="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。", Solution="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。" },
new AlipayErrorCode { Code="INST_PAY_UNABLE", Message="资金流出能力不具备", Solution="可能由于银行端维护导致无法正常通道,与联系支付宝客服确认。" },
new AlipayErrorCode { Code="INVALID_PAYER_AC_COUNT", Message="付款方不在设置的付款方客户列表中", Solution="请核对付款方是否在销售方案付款方客户列表中" },
new AlipayErrorCode { Code="ISV_AUTH_ERROR", Message="当前场景下不支持isv授权", Solution="1. 检查商户产品和场景范围,当前场景下不支持isv授权权。2. 去删除isv授权模板,改为自调用。" },
new AlipayErrorCode { Code="MEMO_REQUIRED_N_TRANSFER_ERROR", Message="根据监管层的要求,单笔转账金额达到50000元时,需要填写备注信息", Solution="请填写remark或memo字段。" },
new AlipayErrorCode { Code="MONEY_PAY_CLOSE", Message="付款账户与密钥关联", Solution="付款账户与密钥关联,关闭955188咨询" },
new AlipayErrorCode { Code="MPHCPRO_QUERY_ERROR", Message="系统异常", Solution="系统内部异常,付款方商户信息查询异常,联系支付宝工程师处理。" },
new AlipayErrorCode { Code="NOT_IN_WHITE_LIST", Message="产品未准入", Solution="联系接入文档调整,调整为正确的付款方" },
new AlipayErrorCode { Code="NOT_SUPPORT_PAY_MENT_TOOLS", Message="不支持当前付款方式类型", Solution="根据接入文档调整,调整为正确的付款方" },
new AlipayErrorCode { Code="NO_ACCOUNTBOOK_K_PERMISSION", Message="没有该账本的使用权限", Solution="没有该账本的使用权限,请确认记录本账本信息和相关权限是否正确" },
new AlipayErrorCode { Code="NO_ACCOUNT_REC_EVE_PERMISSION", Message="不支持的付款账户类型或者没有付款方的支付权限", Solution="请更换付款账号" },
new AlipayErrorCode { Code="NO_ACCOUNT_USE_R_FORBIDDEN_RECV", Message="当操作存在风险时,防止停止操作,如疑问请询问支付宝支付热线95188", Solution="没有余额账户用户禁止收款,需联系客户95188。" },
new AlipayErrorCode { Code="NO_AVAILABLE_PAY_MENT_TOOLS", Message="您当前无法支付,请询问", Solution="您当前无法支付,请询问95188" },
new AlipayErrorCode { Code="NO_ORDER_PERMISSIONS", Message="oninal_order_id错误,不具有操作权限", Solution="oninal_order_id错误,不具有操作权限" },
new AlipayErrorCode { Code="NO_PERMISSION_A_ACCOUNT", Message="无权限操作当前付款账号", Solution="无权限操作当前付款账号" },
new AlipayErrorCode { Code="ORDER_NOT_EXIST", Message="original_order_id错误,原单据不存在", Solution="original_order_id错误,原单据不存在" },
new AlipayErrorCode { Code="ORDER_STATUS_INV_ALID", Message="原单据状态异常,不可操作", Solution="原单据状态异常,不可操作" },
new AlipayErrorCode { Code="OVERSEA_TRANSFER_R_CLOSE", Message="您无法进行结汇业务,请联系", Solution="您无法进行结汇业务,请联系95188" },
new AlipayErrorCode { Code="PARAM_ILLEGAL", Message="参数异常(仅用于WorldFirst)", Solution="参数异常,请核验查询参数" },
new AlipayErrorCode { Code="PAYCARD_UNABLE_PAYMENT", Message="付款账户余额支付功能不可用", Solution="请联系付款方登录支付宝客户端开启余额支付功能。" },
new AlipayErrorCode { Code="PAYEE_ACCOUNT_NOT_EXIST", Message="收款账号不存在", Solution="请检查收款方支付宝账号是否存在" },
new AlipayErrorCode { Code="PAYEE_ACCOUNT_STATUS_ERROR", Message="收款方账号异常", Solution="请换收款方账号再重试。" },
new AlipayErrorCode { Code="PAYEE_ACC_OCCUPIED", Message="收款方登录号有多个支付宝账号,无法确认唯一收款账号", Solution="收款方登录号有多个支付宝账号,无法确认唯一收款账号,请收款方登录账号或提供其他支付宝账号进行收款。" },
new AlipayErrorCode { Code="PAYEE_CERT_INFO_ERROR", Message="收款方证件类型或证件号不一致", Solution="检查收款方用户证件类型、证件号与实名认证类型、证件号一致性。" },
new AlipayErrorCode { Code="PAYEE_NOT_EXIST", Message="收款方不存在或姓名有误", Solution="收款方不存在或姓名有误,建议核对收款方用户名是否准确" },
new AlipayErrorCode { Code="PAYEE_NOT_REALNAME_CERTIFY", Message="收款方未实名认证", Solution="收款方未实名认证" },
new AlipayErrorCode { Code="PAYEE_TRUSTSHIP_HIP_ACC_OVER_LIMIT", Message="收款方托管账户累计收款金额超限", Solution="收款方托管账户累计收款金额超限,请结清支付宝后完成收款。" },
new AlipayErrorCode { Code="PAYEE_USERINFO_STATUS_ERROR", Message="收款方用户状态不正常", Solution="收款方用户状态不正常无法用于收款" },
new AlipayErrorCode { Code="PAYEE_USER_TYPE_ERROR", Message="不支持的收款用户类型", Solution="不支持的收款用户类型,请联系收款方更换,更换支付宝方后收款" },
new AlipayErrorCode { Code="PAYER_BALANCE_NOT_ENOUGH", Message="余额不足,建议尽快充值,后续可使用余额短信支付,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值,在商户后台后续可使用余额短信支付,自主设置余额预警提醒功能登陆Alipay-资金管理->资金池页-右下角余额提醒" },
new AlipayErrorCode { Code="PAYER_CERTIFY_CHECK_FAIL", Message="付款方人行认证受限", Solution="付款方请升级认证等级。" },
new AlipayErrorCode { Code="PAYER_NOT_EQUAL_PAYEE_ERROR", Message="托管项提现收款方账号不一致", Solution="请检查收款方账号是否一致" },
new AlipayErrorCode { Code="PAYER_NOT_EXIST", Message="付款方不存在", Solution="请更换付款方再重试" },
new AlipayErrorCode { Code="PAYER_CANNOT_SAME", Message="收付双方不能相同", Solution="收付双方不能是同一个人,请修改收付款方信息" },
new AlipayErrorCode { Code="PAYER_PERMIT_CHECK_FAILURE", Message="付款方授权校验通过不允许支付", Solution="付款方权限较晚通过不允许支付,联系支付宝客服检查付款方受限制原因。" },
new AlipayErrorCode { Code="PAYER_REQUESTER_RELATION_INVALID", Message="付款方和请求方用户不一致", Solution="付款方和请求方用户不一致,存在归户风险" },
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款账号状态异常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" },
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方用户状态不正常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" },
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方已被冻结,暂不可将资金转出。", Solution="1. 联系支付宝客户询问用户冻结原因以及协助解冻办法状态。" },
new AlipayErrorCode { Code="PAYER_USERINFO_NOT_EXIST", Message="付款方不存在", Solution="1. 检查付款方是否已销户,若销户请联系销户后重新发起业务。2. 检查参入是否有误。" },
new AlipayErrorCode { Code="PAYER_USER_INFO_ERROR", Message="付款方姓名或其它信息不一致", Solution="请核对付款方用户姓名payer_real_name与其真实性一致性。" },
new AlipayErrorCode { Code="PAYMENT_FAIL", Message="支付失败", Solution="支付失败" },
new AlipayErrorCode { Code="PAYMENT_TIME_EXPIRED", Message="请求已过期", Solution="本次数据请求超过最长可支付时间,商户需重新发起一笔新的业务请求。" },
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_AMAL_CERT_EXPIRED", Message="由于收款人登记的身份证件已过期导致收款受限,请更新证件信息。", Solution="根据监管部门的要求,需要付款方更新身份信息" },
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_IDENTITY_THEFT", Message="您的账户存在身份冒用风险,请进行身份信息解除限制。", Solution="您的账户存在身份冒用风险,请进行身份信息解除限制。" },
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" },
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" },
new AlipayErrorCode { Code="PERMIT_CHECK_RECEIVE_LIMIT", Message="您的账户限收款,请咨询95188电话咨询", Solution="您的账户限收款,请咨询95188电话咨询" },
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服", Solution="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服" },
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被限制收款,请收款方联系客服", Solution="收款方账户被限制收款,请收款方联系客服" },
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款额度已上限,请收款方联系客服咨询详情。", Solution="收款方账户收款额度已上限,请收款方联系客服咨询详情。" },
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款功能暂时无法使用", Solution="收款方账户收款功能暂时无法使用" },
new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="收款方未完善身份证信息或未开立余额账户,无法收款", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" },
new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="当前操作存在风险,不支持转账,如无疑问请拨打支付宝服务热线95188", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" },
new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下今日余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下当月余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PERM_PAY_USER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="该账户今日余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PERM_PAY_USER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="该账户当月余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
new AlipayErrorCode { Code="PROCESS_FAIL", Message="资金操作失败(仅用于WorldFirst)", Solution="资金操作失败,目前用于结汇入境场景,需要支付宝技术介入排查" },
new AlipayErrorCode { Code="PRODUCT_NOT_SIGN", Message="产品未签约", Solution="请签约产品之后再使用该接口" },
new AlipayErrorCode { Code="RELEASE_USER_FOR_BBIDEN_RECIEVE", Message="收款账号存在异常,禁止收款,如有疑问请电话咨询95188", Solution="联系收款用户,更换支付宝账号后收款" },
new AlipayErrorCode { Code="REMARK_HAS_SENSITIVE_WORD", Message="转账备注包含敏感词,请修改备注文案后重试", Solution="转账备注包含敏感词,请修改备注文案后重试" },
new AlipayErrorCode { Code="REQUEST_PROCESSING", Message="系统处理中,请稍后再试", Solution="系统并发处理中,建议调整相关接口的调用频率,减少并发请求,可稍后再重试" },
new AlipayErrorCode { Code="RESOURCE_LIMIT_EXCEED", Message="请求超过资源限制", Solution="发起请求并发数超出支付宝处理能力,请降低请求并发" },
new AlipayErrorCode { Code="SECURITY_CHECK_FAILED", Message="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188", Solution="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188" },
new AlipayErrorCode { Code="SIGN_AGREEMENT_NO_INCONSISTENT", Message="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配。", Solution="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配,再重试。" },
new AlipayErrorCode { Code="SIGN_INVALID", Message="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。", Solution="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。" },
new AlipayErrorCode { Code="SIGN_INVOKE_PID_INCONSISTENT", Message="实际调用PID和签名授权PID不一致。请确认实际调用PID和sign_data.partner_id是否一致。", Solution="请确认实际调用PID和sign_data.partner_id是否一致,一致后再重试。" },
new AlipayErrorCode { Code="SIGN_NOT_ALLOW_SKIP", Message="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。", Solution="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。" },
new AlipayErrorCode { Code="SIGN_PARAM_INVALID", Message="验签参数非法。请确认sign_data参数是否正确。", Solution="验签参数非法,请确认sign_data参数是否正确。" },
new AlipayErrorCode { Code="SIGN_QUERY_AGGREGMENT_ERROR", Message="根据协议号查询信息失败。请确认payer_info.ext_info.agreement_no是否正确。", Solution="请确认上报协议号payer_info.ext_info.agreement_no内容正确后再重试。" },
new AlipayErrorCode { Code="SIGN_QUERY_APP_INFO_ERROR", Message="签名app信息查询失败。请确认sign_data.ori_app_id是否正确。", Solution="请确认签名方sign_data.ori_app_id是否正确,信息正确后再重试。" },
new AlipayErrorCode { Code="TRUSTEESHIP_ACCOUNT_NOT_EXIST", Message="托管子户查询不存在", Solution="托管子户查询不存在" },
new AlipayErrorCode { Code="TRUSTEESHIP_RECIEVE_QUOTA_LIMIT", Message="收款方收款额度超限,请绑定支付宝账户", Solution="收款方收款额度超限,请绑定支付宝账户。" },
new AlipayErrorCode { Code="USER_AGREEMENT_VERIFY_FAIL", Message="用户协议校验失败", Solution="确认入参中协议号是否正确" },
new AlipayErrorCode { Code="USER_NOT_EXIST", Message="用户不存在(仅用于WorldFirst)", Solution="用户不存在,请检查收付款方信息" },
new AlipayErrorCode { Code="USER_RISK_FREEZE", Message="账户异常被冻结,无法付款,请咨询支付宝客服95188", Solution="账户异常被冻结,无法付款,请咨询支付宝客服95188" }
];
/// <summary>
/// 根据错误码获取错误信息
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public static AlipayErrorCode Get(string code)
{
return StatusCodes.FirstOrDefault(u => u.Code.EqualIgnoreCase(code));
}
}
@@ -1,180 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Aop.Api.Domain;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Admin.NET.Core.Service;
public class AlipayFundTransUniTransferInput
{
/// <summary>
/// 用户ID
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 商户AppId
/// </summary>
public string AppId { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutBizNo { get; set; }
/// <summary>
/// 转账金额
/// </summary>
public decimal TransAmount { get; set; }
/// <summary>
/// 业务标题
/// </summary>
public string OrderTitle { get; set; }
/// <summary>
/// 备注
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 是否展示付款方别名
/// </summary>
public bool PayerShowNameUseAlias { get; set; }
/// <summary>
/// 收款方证件类型
/// </summary>
public AlipayCertTypeEnum? CertType { get; set; }
/// <summary>
/// 收款方证件号码,条件必填
/// </summary>
public string CertNo { get; set; }
/// <summary>
/// 收款方身份标识
/// </summary>
public string Identity { get; set; }
/// <summary>
/// 收款方真实姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 收款方身份标识类型
/// </summary>
public AlipayIdentityTypeEnum? IdentityType { get; set; }
}
/// <summary>
/// 统一收单下单并支付页面接口输入参数
/// </summary>
public class AlipayTradePagePayInput
{
/// <summary>
/// 商户订单号
/// </summary>
[Required(ErrorMessage = "商户订单号不能为空")]
public string OutTradeNo { get; set; }
/// <summary>
/// 订单总金额
/// </summary>
[Required(ErrorMessage = "订单总金额不能为空")]
public string TotalAmount { get; set; }
/// <summary>
/// 订单标题
/// </summary>
[Required(ErrorMessage = "订单标题不能为空")]
public string Subject { get; set; }
/// <summary>
///
/// </summary>
public string Body { get; set; }
/// <summary>
/// 超时时间
/// </summary>
public string TimeoutExpress { get; set; }
/// <summary>
/// 二维码宽度
/// </summary>
[Required(ErrorMessage = "二维码宽度不能为空")]
public int? QrcodeWidth { get; set; }
/// <summary>
/// 业务参数
/// </summary>
public ExtendParams ExtendParams { get; set; }
/// <summary>
/// 商户业务数据
/// </summary>
public Dictionary<string, object> BusinessParams { get; set; }
/// <summary>
/// 开票信息
/// </summary>
public InvoiceInfo InvoiceInfo { get; set; }
/// <summary>
/// 外部买家信息
/// </summary>
public ExtUserInfo ExtUserInfo { get; set; }
}
public class AlipayPreCreateInput
{
/// <summary>
/// 商户订单号
/// </summary>
[Required(ErrorMessage = "商户订单号不能为空")]
public string OutTradeNo { get; set; }
/// <summary>
/// 订单总金额
/// </summary>
[Required(ErrorMessage = "订单总金额不能为空")]
public string TotalAmount { get; set; }
/// <summary>
/// 订单标题
/// </summary>
[Required(ErrorMessage = "订单标题不能为空")]
public string Subject { get; set; }
/// <summary>
/// 超时时间
/// </summary>
public string TimeoutExpress { get; set; }
}
public class AlipayAuthInfoInput
{
/// <summary>
/// 用户Id
/// </summary>
[JsonProperty("user_id")]
[JsonPropertyName("user_id")]
[FromQuery(Name = "user_id")]
public string UserId { get; set; }
/// <summary>
/// 授权码
/// </summary>
[JsonProperty("auth_code")]
[JsonPropertyName("auth_code")]
[FromQuery(Name = "auth_code")]
public string AuthCode { get; set; }
}
@@ -1,31 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Aop.Api.Response;
namespace Admin.NET.Core.Service;
/// <summary>
/// 支付宝回调接口
/// </summary>
public abstract class IAlipayNotify
{
/// <summary>
/// 充值回调方法
/// </summary>
/// <param name="type">交易类型</param>
/// <param name="tradeNo">交易id</param>
public abstract bool TopUpCallback(long type, long tradeNo);
/// <summary>
/// 扫码回调
/// </summary>
/// <param name="type"></param>
/// <param name="userId"></param>
/// <param name="response"></param>
/// <returns></returns>
public abstract bool ScanCallback(long type, long userId, AlipayUserInfoShareResponse response);
}
@@ -1,271 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Aop.Api;
using Aop.Api.Domain;
using Aop.Api.Request;
using Aop.Api.Response;
using Aop.Api.Util;
using Microsoft.AspNetCore.Hosting;
using NewLife.Reflection;
namespace Admin.NET.Core.Service;
/// <summary>
/// 支付宝支付服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 240)]
public class SysAlipayService : IDynamicApiController, ITransient
{
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly SysConfigService _sysConfigService;
private readonly List<IAopClient> _alipayClientList;
private readonly IHttpContextAccessor _httpContext;
private readonly AlipayOptions _option;
private readonly ISqlSugarClient _db;
public SysAlipayService(
ISqlSugarClient db,
IHttpContextAccessor httpContext,
SysConfigService sysConfigService,
IWebHostEnvironment webHostEnvironment,
IOptions<AlipayOptions> alipayOptions)
{
_db = db;
_httpContext = httpContext;
_sysConfigService = sysConfigService;
_option = alipayOptions.Value;
_webHostEnvironment = webHostEnvironment;
// 初始化支付宝客户端列表
_alipayClientList = [];
foreach (var account in _option.AccountList) _alipayClientList.Add(_option.GetClient(account));
}
/// <summary>
/// 获取授权信息 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[NonUnify]
[AllowAnonymous]
[DisplayName("获取授权信息")]
[ApiDescriptionSettings(Name = "AuthInfo"), HttpGet]
public ActionResult GetAuthInfo([FromQuery] AlipayAuthInfoInput input)
{
var type = input.UserId?.Split('-').FirstOrDefault().ToInt();
var userId = input.UserId?.Split('-').LastOrDefault().ToLong();
var account = _option.AccountList.FirstOrDefault();
var alipayClient = _alipayClientList.First();
// 当前网页接口地址
var currentUrl = $"{_option.AppAuthUrl}{_httpContext.HttpContext!.Request.Path}?userId={input.UserId}";
if (string.IsNullOrEmpty(input.AuthCode))
{
// 重新授权
var url = $"{_option.AuthUrl}?app_id={account!.AppId}&scope=auth_user&redirect_uri={currentUrl}";
return new RedirectResult(url);
}
// 组装授权请求参数
AlipaySystemOauthTokenRequest request = new()
{
GrantType = AlipayConst.GrantType,
Code = input.AuthCode
};
AlipaySystemOauthTokenResponse response = alipayClient.CertificateExecute(request);
// token换取用户信息
AlipayUserInfoShareRequest infoShareRequest = new();
AlipayUserInfoShareResponse info = alipayClient.CertificateExecute(infoShareRequest, response.AccessToken);
// 记录授权信息
var entity = _db.Queryable<SysAlipayAuthInfo>().First(u =>
(!string.IsNullOrWhiteSpace(u.UserId) && u.UserId == info.UserId) ||
(!string.IsNullOrWhiteSpace(u.OpenId) && u.OpenId == info.OpenId)) ?? new();
entity.Copy(info, excludes: [nameof(SysAlipayAuthInfo.Gender), nameof(SysAlipayAuthInfo.Age)]);
entity.Age = int.Parse(info.Age);
entity.Gender = info.Gender switch
{
"m" => GenderEnum.Male,
"f" => GenderEnum.Female,
_ => GenderEnum.Unknown
};
entity.AppId = account!.AppId;
if (entity.Id <= 0) _db.Insertable(entity).ExecuteCommand();
else _db.Updateable(entity).ExecuteCommand();
// 执行完,重定向到指定界面
//var authPageUrl = _sysConfigService.GetConfigValueByCode<string>(ConfigConst.AlipayAuthPageUrl + type).Result;
//return new RedirectResult(authPageUrl);
return new RedirectResult(_option.AppAuthUrl + "/index.html");
}
/// <summary>
/// 支付回调 🔖
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("支付回调")]
[ApiDescriptionSettings(Name = "Notify"), HttpPost]
public string Notify()
{
SortedDictionary<string, string> sorted = [];
foreach (string key in _httpContext.HttpContext!.Request.Form.Keys)
sorted.Add(key, _httpContext.HttpContext.Request.Form[key]);
var account = _option.AccountList.FirstOrDefault();
string alipayPublicKey = Path.Combine(_webHostEnvironment.ContentRootPath, account!.AlipayPublicCertPath!.Replace('/', '\\').TrimStart('\\'));
bool signVerified = AlipaySignature.RSACertCheckV1(sorted, alipayPublicKey, "UTF-8", account.SignType); // 调用SDK验证签名
if (!signVerified) throw Oops.Oh("交易失败");
// 更新交易记录
var outTradeNo = sorted.GetValueOrDefault("out_trade_no");
var transaction = _db.Queryable<SysAlipayTransaction>().First(x => x.OutTradeNo == outTradeNo) ?? throw Oops.Oh("交易记录不存在");
transaction.TradeNo = sorted.GetValueOrDefault("trade_no");
transaction.TradeStatus = sorted.GetValueOrDefault("trade_status");
transaction.FinishTime = sorted.ContainsKey("gmt_payment") ? DateTime.Parse(sorted.GetValueOrDefault("gmt_payment")) : null;
transaction.BuyerLogonId = sorted.GetValueOrDefault("buyer_logon_id");
transaction.BuyerUserId = sorted.GetValueOrDefault("buyer_user_id");
transaction.SellerUserId = sorted.GetValueOrDefault("seller_id");
transaction.Remark = sorted.GetValueOrDefault("remark");
_db.Updateable(transaction).ExecuteCommand();
return "success";
}
/// <summary>
/// 统一收单下单并支付页面接口 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("统一收单下单并支付页面接口")]
[ApiDescriptionSettings(Name = "AlipayTradePagePay"), HttpPost]
public string AlipayTradePagePay(AlipayTradePagePayInput input)
{
// 创建交易记录,状态为等待支付
var transactionRecord = new SysAlipayTransaction
{
AppId = _option.AccountList.First().AppId,
OutTradeNo = input.OutTradeNo,
TotalAmount = input.TotalAmount.ToDecimal(),
TradeStatus = "WAIT_PAY", // 等待支付
CreateTime = DateTime.Now,
Subject = input.Subject,
Body = input.Body,
Remark = "等待用户支付"
};
_db.Insertable(transactionRecord).ExecuteCommand();
// 设置支付页面请求,并组装业务参数model,设置异步通知接收地址
AlipayTradeWapPayRequest request = new();
request.SetBizModel(new AlipayTradeWapPayModel()
{
Subject = input.Subject,
OutTradeNo = input.OutTradeNo,
TotalAmount = input.TotalAmount,
Body = input.Body,
ProductCode = "QUICK_WAP_WAY",
TimeExpire = input.TimeoutExpress
});
request.SetNotifyUrl(_option.NotifyUrl);
var alipayClient = _alipayClientList.First();
var response = alipayClient.SdkExecute(request);
if (response.IsError) throw Oops.Oh(response.SubMsg);
return $"{_option.ServerUrl}?{response.Body}";
}
/// <summary>
/// 交易预创建 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("交易预创建")]
[ApiDescriptionSettings(Name = "AlipayPreCreate"), HttpPost]
public string AlipayPreCreate(AlipayPreCreateInput input)
{
// 创建交易记录,状态为等待支付
var transactionRecord = new SysAlipayTransaction
{
AppId = _option.AccountList.First().AppId,
OutTradeNo = input.OutTradeNo,
TotalAmount = input.TotalAmount.ToDecimal(),
TradeStatus = "WAIT_PAY", // 等待支付
CreateTime = DateTime.Now,
Subject = input.Subject,
Remark = "等待用户支付"
};
_db.Insertable(transactionRecord).ExecuteCommand();
// 设置异步通知接收地址,并组装业务参数model
AlipayTradePrecreateRequest request = new();
request.SetNotifyUrl(_option.NotifyUrl);
request.SetBizModel(new AlipayTradePrecreateModel()
{
Subject = input.Subject,
OutTradeNo = input.OutTradeNo,
TotalAmount = input.TotalAmount,
TimeoutExpress = input.TimeoutExpress
});
var alipayClient = _alipayClientList.First();
var response = alipayClient.CertificateExecute(request);
if (response.IsError) throw Oops.Oh(response.SubMsg);
return response.QrCode;
}
/// <summary>
/// 单笔转账到支付宝账户
/// https://opendocs.alipay.com/open/62987723_alipay.fund.trans.uni.transfer
/// </summary>
[NonAction]
public async Task<AlipayFundTransUniTransferResponse> Transfer(AlipayFundTransUniTransferInput input)
{
var account = _option.AccountList.FirstOrDefault(u => u.AppId == input.AppId) ?? throw Oops.Oh("未找到商户支付宝账号");
var alipayClient = _option.GetClient(account);
// 构造请求参数以调用接口
AlipayFundTransUniTransferRequest request = new();
AlipayFundTransUniTransferModel model = new()
{
BizScene = AlipayConst.BizScene,
ProductCode = AlipayConst.ProductCode,
OutBizNo = input.OutBizNo, // 商家订单
TransAmount = $"{input.TransAmount}:F2", // 订单总金额
OrderTitle = input.OrderTitle, // 业务标题
Remark = input.Remark, // 业务备注
PayeeInfo = new() // 收款方信息
{
CertType = input.CertType?.ToString(),
CertNo = input.CertNo,
Identity = input.Identity,
Name = input.Name,
IdentityType = input.IdentityType.ToString()
},
BusinessParams = input.PayerShowNameUseAlias ? "{\"payer_show_name_use_alias\":\"true\"}" : null
};
request.SetBizModel(model);
var response = alipayClient.CertificateExecute(request);
// 保存转账记录
await _db.Insertable(new SysAlipayTransaction
{
UserId = input.UserId,
AppId = input.AppId,
TradeNo = response.OrderId,
OutTradeNo = input.OutBizNo,
TotalAmount = response.Amount.ToDecimal(),
TradeStatus = response.Code == "10000" ? "SUCCESS" : "FAILED",
Subject = input.OrderTitle,
ErrorInfo = response.SubMsg,
Remark = input.Remark
}).ExecuteCommandAsync();
return response;
}
}
@@ -1,43 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统域登录信息配置输入参数
/// </summary>
public class SysLdapInput : BasePageInput
{
/// <summary>
/// 主机
/// </summary>
public string? Host { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long TenantId { get; set; }
}
public class AddSysLdapInput : SysLdap
{
}
public class UpdateSysLdapInput : SysLdap
{
}
public class DeleteSysLdapInput : BaseIdInput
{
}
public class DetailSysLdapInput : BaseIdInput
{
}
public class SyncSysLdapInput : BaseIdInput
{
}
+2 -37
View File
@@ -25,7 +25,6 @@ public class SysAuthService : IDynamicApiController, ITransient
private readonly SysUserService _sysUserService;
private readonly SysTenantService _sysTenantService;
private readonly SysSmsService _sysSmsService;
private readonly SysLdapService _sysLdapService;
private readonly ICaptcha _captcha;
private readonly IEventPublisher _eventPublisher;
private readonly SysCacheService _sysCacheService;
@@ -35,7 +34,6 @@ public class SysAuthService : IDynamicApiController, ITransient
IHttpContextAccessor httpContextAccessor,
SysOnlineUserService sysOnlineUserService,
SysConfigService sysConfigService,
SysLdapService sysLdapService,
IEventPublisher eventPublisher,
SysSmsService sysSmsService,
SysCacheService sysCacheService,
@@ -57,7 +55,6 @@ public class SysAuthService : IDynamicApiController, ITransient
_sysConfigService = sysConfigService;
_httpContextAccessor = httpContextAccessor;
_sysOnlineUserService = sysOnlineUserService;
_sysLdapService = sysLdapService;
}
/// <summary>
@@ -86,23 +83,7 @@ public class SysAuthService : IDynamicApiController, ITransient
// 账号是否被冻结
if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017);
// 是否开启域登录验证
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
{
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id);
if (userLdap == null)
{
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
}
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(tenant.Id, userLdap.Account, CryptogramUtil.Decrypt(input.Password)))
{
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
throw Oops.Oh(ErrorCodeEnum.D1000);
}
}
else
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
// 登录成功则清空密码错误次数
_sysCacheService.Remove(keyPasswordErrorTimes);
@@ -184,23 +165,7 @@ public class SysAuthService : IDynamicApiController, ITransient
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
// 是否开启域登录验证
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
{
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
if (userLdap == null)
{
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
}
else if (!await _sysLdapService.AuthAccount(user.TenantId!.Value, userLdap.Account, CryptogramUtil.Decrypt(password)))
{
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
throw Oops.Oh(ErrorCodeEnum.D1000);
}
}
else
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
return true;
}
@@ -1,437 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Novell.Directory.Ldap;
namespace Admin.NET.Core;
/// <summary>
/// 系统域登录配置服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 496, Description = "域登录配置")]
public class SysLdapService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysLdap> _sysLdapRep;
public SysLdapService(SqlSugarRepository<SysLdap> sysLdapRep)
{
_sysLdapRep = sysLdapRep;
}
/// <summary>
/// 获取系统域登录配置分页列表 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取系统域登录配置分页列表")]
public async Task<SqlSugarPagedList<SysLdap>> Page(SysLdapInput input)
{
return await _sysLdapRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Host.Contains(input.Keyword.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.Host), u => u.Host.Contains(input.Host.Trim()))
.OrderBy(u => u.CreateTime, OrderByType.Desc)
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 增加系统域登录配置 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加系统域登录配置")]
public async Task<long> Add(AddSysLdapInput input)
{
var entity = input.Adapt<SysLdap>();
entity.BindPass = CryptogramUtil.Encrypt(input.BindPass);
await _sysLdapRep.InsertAsync(entity);
return entity.Id;
}
/// <summary>
/// 更新系统域登录配置 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新系统域登录配置")]
public async Task Update(UpdateSysLdapInput input)
{
var entity = input.Adapt<SysLdap>();
if (!string.IsNullOrEmpty(input.BindPass) && input.BindPass.Length < 32)
{
entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); // 加密
}
await _sysLdapRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
}
/// <summary>
/// 删除系统域登录配置 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除系统域登录配置")]
public async Task Delete(DeleteSysLdapInput input)
{
var entity = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
await _sysLdapRep.FakeDeleteAsync(entity); // 假删除
//await _rep.DeleteAsync(entity); // 真删除
}
/// <summary>
/// 获取系统域登录配置详情 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取系统域登录配置详情")]
public async Task<SysLdap> GetDetail([FromQuery] DetailSysLdapInput input)
{
return await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id);
}
/// <summary>
/// 获取系统域登录配置列表 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取系统域登录配置列表")]
public async Task<List<SysLdap>> GetList()
{
return await _sysLdapRep.AsQueryable().Select<SysLdap>().ToListAsync();
}
/// <summary>
/// 验证账号
/// </summary>
/// <param name="account">域用户</param>
/// <param name="password">密码</param>
/// <param name="tenantId">租户</param>
/// <returns></returns>
[NonAction]
public async Task<bool> AuthAccount(long? tenantId, string account, string password)
{
var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.TenantId == tenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
var ldapConn = new LdapConnection();
try
{
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeSub, sysLdap.AuthFilter.Replace("%s", account), null, false);
string dn = string.Empty;
while (await ldapSearchResults.HasMoreAsync())
{
var ldapEntry = await ldapSearchResults.NextAsync();
var sAmAccountName = ldapEntry.GetAttributeSet().GetAttribute(sysLdap.BindAttrAccount)?.StringValue;
if (string.IsNullOrEmpty(sAmAccountName)) continue;
dn = ldapEntry.Dn;
break;
}
if (string.IsNullOrEmpty(dn)) throw Oops.Oh(ErrorCodeEnum.D1002);
// var attr = new LdapAttribute("userPassword", password);
await ldapConn.BindAsync(dn, password);
}
catch (LdapException e)
{
return e.ResultCode switch
{
LdapException.NoSuchObject or LdapException.NoSuchAttribute => throw Oops.Oh(ErrorCodeEnum.D0009),
LdapException.InvalidCredentials => false,
_ => throw Oops.Oh(e.Message),
};
}
finally
{
ldapConn.Disconnect();
}
return true;
}
/// <summary>
/// 同步域用户 🔖
/// </summary>
/// <param name="tenantId"></param>
/// <returns></returns>
[DisplayName("同步域用户")]
[NonAction]
public async Task<List<SysUserLdap>> SyncUserTenant(long tenantId)
{
var sysLdap = await _sysLdapRep.GetFirstAsync(c => c.TenantId == tenantId && c.IsDelete == false && c.Status == StatusEnum.Enable) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
return await SysLdapService.SyncUser(sysLdap);
}
/// <summary>
/// 同步域用户 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("同步域用户")]
public async Task<List<SysUserLdap>> SyncUser(SyncSysLdapInput input)
{
var sysLdap = await _sysLdapRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
return await SysLdapService.SyncUser(sysLdap);
}
/// <summary>
/// 同步域用户 🔖
/// </summary>
/// <param name="sysLdap"></param>
/// <returns></returns>
private static async Task<List<SysUserLdap>> SyncUser(SysLdap sysLdap)
{
if (sysLdap == null) throw Oops.Oh(ErrorCodeEnum.D1002);
var ldapConn = new LdapConnection();
try
{
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
var userLdapList = new List<SysUserLdap>();
while (await ldapSearchResults.HasMoreAsync())
{
LdapEntry ldapEntry;
try
{
ldapEntry = await ldapSearchResults.NextAsync();
if (ldapEntry == null) continue;
}
catch (LdapException)
{
continue;
}
var attrs = ldapEntry.GetAttributeSet();
var deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode);
if (attrs.Count == 0 || attrs.ContainsKey("OU"))
{
await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode);
}
else
{
var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode);
sysUserLdap.Dn = ldapEntry.Dn;
sysUserLdap.TenantId = sysLdap.TenantId;
userLdapList.Add(sysUserLdap);
}
}
if (userLdapList.Count == 0) return null;
await App.GetRequiredService<SysUserLdapService>().InsertUserLdapList(sysLdap.TenantId!.Value, userLdapList);
return userLdapList;
}
catch (LdapException e)
{
throw e.ResultCode switch
{
LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009),
_ => Oops.Oh(e.Message),
};
}
finally
{
ldapConn.Disconnect();
}
}
/// <summary>
/// 获取部门代码
/// </summary>
/// <param name="attrs"></param>
/// <param name="bindAttrCode"></param>
/// <returns></returns>
private static string GetDepartmentCode(LdapAttributeSet attrs, string bindAttrCode)
{
return bindAttrCode == "objectGUID"
? new Guid(attrs.GetAttribute(bindAttrCode)?.ByteValue!).ToString()
: attrs.GetAttribute(bindAttrCode)?.StringValue ?? "0";
}
/// <summary>
/// 创建同步对象
/// </summary>
/// <param name="attrs"></param>
/// <param name="bindAttrAccount"></param>
/// <param name="bindAttrEmployeeId"></param>
/// <param name="deptCode"></param>
/// <returns></returns>
private static SysUserLdap CreateSysUserLdap(LdapAttributeSet attrs, string bindAttrAccount, string bindAttrEmployeeId, string deptCode)
{
var userLdap = new SysUserLdap
{
Account = attrs.ContainsKey(bindAttrAccount) ? attrs.GetAttribute(bindAttrAccount)?.StringValue : null,
EmployeeId = attrs.ContainsKey(bindAttrEmployeeId) ? attrs.GetAttribute(bindAttrEmployeeId)?.StringValue : null,
DeptCode = deptCode,
UserName = attrs.ContainsKey("name") ? attrs.GetAttribute("name")?.StringValue : null,
Mail = attrs.ContainsKey("mail") ? attrs.GetAttribute("mail")?.StringValue : null
};
var pwdLastSet = attrs.ContainsKey("pwdLastSet") ? attrs.GetAttribute("pwdLastSet")?.StringValue : null;
if (pwdLastSet != null && !pwdLastSet.Equals("0")) userLdap.PwdLastSetTime = DateTime.FromFileTime(Convert.ToInt64(pwdLastSet));
var userAccountControl = attrs.ContainsKey("userAccountControl") ? attrs.GetAttribute("userAccountControl")?.StringValue : null;
if ((Convert.ToInt32(userAccountControl) & 0x2) == 0x2) // 检查账户是否已过期(通过检查userAccountControl属性的特定位)
userLdap.AccountExpiresFlag = true;
if ((Convert.ToInt32(userAccountControl) & 0x10000) == 0x10000) // 检查账户密码设置是否永不过期
userLdap.DontExpiresFlag = true;
return userLdap;
}
/// <summary>
/// 遍历查询域用户
/// </summary>
/// <param name="ldapConn"></param>
/// <param name="sysLdap"></param>
/// <param name="userLdapList"></param>
/// <param name="baseDn"></param>
/// <param name="deptCode"></param>
private static async Task SearchDnLdapUser(LdapConnection ldapConn, SysLdap sysLdap, List<SysUserLdap> userLdapList, string baseDn, string deptCode)
{
var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
while (await ldapSearchResults.HasMoreAsync())
{
LdapEntry ldapEntry;
try
{
ldapEntry = await ldapSearchResults.NextAsync();
if (ldapEntry == null) continue;
}
catch (LdapException)
{
continue;
}
var attrs = ldapEntry.GetAttributeSet();
deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode);
if (attrs.Count == 0 || attrs.ContainsKey("OU"))
await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode);
else
{
var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode);
sysUserLdap.Dn = ldapEntry.Dn;
sysUserLdap.TenantId = sysLdap.TenantId;
if (string.IsNullOrEmpty(sysUserLdap.EmployeeId)) continue;
userLdapList.Add(sysUserLdap);
}
}
}
/// <summary>
/// 同步域组织 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("同步域组织")]
public async Task SyncDept(SyncSysLdapInput input)
{
var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
var ldapConn = new LdapConnection();
try
{
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
var orgList = new List<SysOrg>();
while (await ldapSearchResults.HasMoreAsync())
{
LdapEntry ldapEntry;
try
{
ldapEntry = await ldapSearchResults.NextAsync();
if (ldapEntry == null) continue;
}
catch (LdapException)
{
continue;
}
var attrs = ldapEntry.GetAttributeSet();
if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue;
var sysOrg = CreateSysOrg(attrs, sysLdap, orgList, new SysOrg { Id = 0, Level = 0 });
orgList.Add(sysOrg);
await SearchDnLdapDept(ldapConn, sysLdap, orgList, ldapEntry.Dn, sysOrg);
}
if (orgList.Count == 0)
return;
await App.GetRequiredService<SysOrgService>().BatchAddOrgs(orgList);
}
catch (LdapException e)
{
throw e.ResultCode switch
{
LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009),
_ => Oops.Oh(e.Message),
};
}
finally
{
ldapConn.Disconnect();
}
}
/// <summary>
/// 遍历查询域用户
/// </summary>
/// <param name="ldapConn"></param>
/// <param name="sysLdap"></param>
/// <param name="listOrgs"></param>
/// <param name="baseDn"></param>
/// <param name="org"></param>
private static async Task SearchDnLdapDept(LdapConnection ldapConn, SysLdap sysLdap, List<SysOrg> listOrgs, string baseDn, SysOrg org)
{
var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
while (await ldapSearchResults.HasMoreAsync())
{
LdapEntry ldapEntry;
try
{
ldapEntry = await ldapSearchResults.NextAsync();
if (ldapEntry == null) continue;
}
catch (LdapException)
{
continue;
}
var attrs = ldapEntry.GetAttributeSet();
if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue;
var sysOrg = CreateSysOrg(attrs, sysLdap, listOrgs, org);
listOrgs.Add(sysOrg);
await SearchDnLdapDept(ldapConn, sysLdap, listOrgs, ldapEntry.Dn, sysOrg);
}
}
/// <summary>
/// 创建架构对象
/// </summary>
/// <param name="attrs"></param>
/// <param name="sysLdap"></param>
/// <param name="listOrgs"></param>
/// <param name="org"></param>
/// <returns></returns>
private static SysOrg CreateSysOrg(LdapAttributeSet attrs, SysLdap sysLdap, List<SysOrg> listOrgs, SysOrg org)
{
return new SysOrg
{
Pid = org.Id,
Id = YitIdHelper.NextId(),
Code = attrs.ContainsKey(sysLdap.BindAttrCode) ? new Guid(attrs.GetAttribute(sysLdap.BindAttrCode)?.ByteValue).ToString() : null,
Level = org.Level + 1,
Name = attrs.ContainsKey(sysLdap.BindAttrAccount) ? attrs.GetAttribute(sysLdap.BindAttrAccount)?.StringValue : null,
OrderNo = listOrgs.Count + 1,
};
}
}
@@ -1,184 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 自定义模板引擎
/// </summary>
public class CustomViewEngine : ViewEngineModel
{
/// <summary>
/// 库定位器
/// </summary>
public string ConfigId { get; set; } = SqlSugarConst.MainConfigId;
public string AuthorName { get; set; }
public string BusName { get; set; }
public string NameSpace { get; set; }
public string ClassName { get; set; }
public string LowerClassName { get; set; }
public string ProjectLastName { get; set; }
public string PagePath { get; set; } = "main";
public string PrintType { get; set; }
public string PrintName { get; set; }
public bool HasLikeQuery { get; set; }
public bool HasJoinTable { get; set; }
public bool HasEnumField { get; set; }
public bool HasDictField { get; set; }
public bool HasConstField { get; set; }
public bool HasSetStatus => TableField.Any(IsStatus);
public List<CodeGenConfig> TableField { get; set; }
public List<CodeGenConfig> ImportFieldList { get; set; }
public List<CodeGenConfig> UploadFieldList { get; set; }
public List<CodeGenConfig> QueryWhetherList { get; set; }
public List<CodeGenConfig> ApiTreeFieldList { get; set; }
public List<CodeGenConfig> DropdownFieldList { get; set; }
public List<CodeGenConfig> AddUpdateFieldList { get; set; }
public List<CodeGenConfig> PrimaryKeyFieldList { get; set; }
public List<TableUniqueConfigItem> TableUniqueConfigList { get; set; }
public List<CodeGenConfig> IgnoreUpdateFieldList => TableField.Where(u => u.WhetherAddUpdate == "N" && u.ColumnKey != "True" && u.WhetherCommon != "Y").ToList();
/// <summary>
/// 格式化主键查询条件
/// 例: PrimaryKeysFormat(" || ", "u.{0} == input.{0}")
/// 单主键返回 u.Id == input.Id
/// 组合主键返回 u.Id == input.Id || u.FkId == input.FkId
/// </summary>
/// <param name="separator">分隔符</param>
/// <param name="format">模板字符串</param>
/// <param name="lowerFirstLetter">字段首字母小写</param>
/// <returns></returns>
public string PrimaryKeysFormat(string separator, string format, bool lowerFirstLetter = false) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, lowerFirstLetter ? u.LowerPropertyName : u.PropertyName)));
/// <summary>
/// 注入的服务
/// </summary>
/// <returns></returns>
public Dictionary<string, string> InjectServiceMap
{
get
{
var injectMap = new Dictionary<string, string>();
if (UploadFieldList.Count > 0) injectMap.Add(nameof(SysFileService), ToLowerFirstLetter(nameof(SysFileService)));
if (DropdownFieldList.Count > 0 || ImportFieldList.Count > 0) injectMap.Add(nameof(ISqlSugarClient), ToLowerFirstLetter(nameof(ISqlSugarClient).TrimStart('I')));
if (ImportFieldList.Any(c => c.EffectType == "DictSelector")) injectMap.Add(nameof(SysDictTypeService), ToLowerFirstLetter(nameof(SysDictTypeService)));
return injectMap;
}
}
/// <summary>
/// 服务构造参数
/// </summary>
public string InjectServiceArgs => InjectServiceMap.Count > 0 ? ", " + string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : "";
/// <summary>
/// 默认值列表
/// </summary>
public List<CodeGenConfig> DefaultValueList { get; set; }
/// <summary>
/// 判断字段是否为状态字段
/// </summary>
/// <param name="column"></param>
/// <returns></returns>
public bool IsStatus(CodeGenConfig column) => column.NetType == nameof(StatusEnum);
/// <summary>
/// 获取首字母小写字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public string ToLowerFirstLetter(string text) => string.IsNullOrWhiteSpace(text) ? text : text[..1].ToLower() + text[1..];
/// <summary>
/// 将基本字段类型转为可空类型
/// </summary>
/// <param name="netType"></param>
/// <returns></returns>
public string GetNullableNetType(string netType) => Regex.IsMatch(netType, "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? netType.TrimEnd('?') + "?" : netType;
/// <summary>
/// 获取前端表格列定义的属性
/// </summary>
/// <param name="column"></param>
/// <returns></returns>
public string GetElTableColumnCustomProperty(CodeGenConfig column)
{
var content = $"prop='{column.LowerPropertyName}' label='{column.ColumnComment}'";
if (IsStatus(column)) content += $" v-auth=\"'{LowerClassName}:setStatus'\"";
if (column.WhetherSortable == "Y") content += " sortable='custom'";
return content;
}
/// <summary>
/// 设置默认值
/// </summary>
/// <returns></returns>
public string GetAddDefaultValue()
{
var content = "";
if (DefaultValueList.Count == 0)
{
var status = TableField.FirstOrDefault(IsStatus);
var orderNo = TableField.FirstOrDefault(c => c.NetType.TrimEnd('?') == "int" && c.PropertyName == nameof(SysUser.OrderNo));
if (status != null) content += $"{status.LowerPropertyName}: {(int)StatusEnum.Enable},";
if (orderNo != null) content += $"{orderNo.LowerPropertyName}: 100,";
}
else
{
foreach (var item in DefaultValueList)
{
if (!string.IsNullOrWhiteSpace(item.DefaultValue))
{
switch (item.EffectType)
{
case "InputNumber":
case "EnumSelector"://枚举和数字框,通过正则提取 数字:如 ('0')
content += $"{item.LowerPropertyName}: {Regex.Match(item.DefaultValue, @"\d+").Value},";
break;
case "Switch":
content += $"{item.LowerPropertyName}: {(item.DefaultValue == "1" ? true.ToString().ToLower() : false.ToString().ToLower())},";
break;
case "DatePicker"://忽略适配日期格式
break;
default:
content += $"{item.LowerPropertyName}: \"{item.DefaultValue}\",";// 如果是字符串 DefaultValue=('男')
break;
}
}
}
}
return content;
}
}
@@ -1,234 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 代码生成详细配置参数
/// </summary>
public class CodeGenConfig
{
/// <summary>
/// 主键Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 代码生成主表ID
/// </summary>
public long CodeGenId { get; set; }
/// <summary>
/// 数据库字段名
/// </summary>
public string ColumnName { get; set; }
/// <summary>
/// 主外键
/// </summary>
public string ColumnKey { get; set; }
/// <summary>
/// 实体属性名
/// </summary>
public string PropertyName { get; set; }
/// <summary>
/// 字段数据长度
/// </summary>
public int ColumnLength { get; set; }
/// <summary>
/// 数据库字段名(首字母小写)
/// </summary>
public string LowerPropertyName => string.IsNullOrWhiteSpace(PropertyName) ? null : PropertyName[..1].ToLower() + PropertyName[1..];
/// <summary>
/// 字段描述
/// </summary>
public string ColumnComment { get; set; }
/// <summary>
/// .NET类型
/// </summary>
public string NetType { get; set; }
/// <summary>
/// 数据库中类型(物理类型)
/// </summary>
public string DataType { get; set; }
/// <summary>
/// 字段数据默认值
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// 可空.NET类型
/// </summary>
public string NullableNetType => Regex.IsMatch(NetType ?? "", "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? NetType.TrimEnd('?') + "?" : NetType;
/// <summary>
/// 作用类型(字典)
/// </summary>
public string EffectType { get; set; }
/// <summary>
/// 外键库标识
/// </summary>
public string FkConfigId { get; set; }
/// <summary>
/// 外键实体名称
/// </summary>
public string FkEntityName { get; set; }
/// <summary>
/// 外键表名称
/// </summary>
public string FkTableName { get; set; }
/// <summary>
/// 外键实体名称(首字母小写)
/// </summary>
public string LowerFkEntityName => string.IsNullOrWhiteSpace(FkEntityName) ? null : FkEntityName[..1].ToLower() + FkEntityName[1..];
/// <summary>
/// 外键链接字段
/// </summary>
public string FkLinkColumnName { get; set; }
/// <summary>
/// 外键显示字段
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string FkDisplayColumns { get; set; }
/// <summary>
/// 外键显示字段
/// </summary>
public List<string> FkDisplayColumnList { get; set; }
/// <summary>
/// 外键显示字段(首字母小写)
/// </summary>
public List<string> LowerFkDisplayColumnsList => FkDisplayColumnList?.Select(name => name[..1].ToLower() + name[1..]).ToList();
/// <summary>
/// 外键显示字段.NET类型
/// </summary>
public string FkColumnNetType { get; set; }
/// <summary>
/// 父级字段
/// </summary>
public string PidColumn { get; set; }
/// <summary>
/// 字典code
/// </summary>
public string DictTypeCode { get; set; }
/// <summary>
/// 查询方式
/// </summary>
public string QueryType { get; set; }
/// <summary>
/// 是否是查询条件
/// </summary>
public string WhetherQuery { get; set; }
/// <summary>
/// 列表是否缩进(字典)
/// </summary>
public string WhetherRetract { get; set; }
/// <summary>
/// 是否必填(字典)
/// </summary>
public string WhetherRequired { get; set; }
/// <summary>
/// 是否可排序(字典)
/// </summary>
public string WhetherSortable { get; set; }
/// <summary>
/// 列表显示
/// </summary>
public string WhetherTable { get; set; }
/// <summary>
/// 增改
/// </summary>
public string WhetherAddUpdate { get; set; }
/// <summary>
/// 导入
/// </summary>
public string WhetherImport { get; set; }
/// <summary>
/// 是否是通用字段
/// </summary>
public string WhetherCommon { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNo { get; set; }
/// <summary>
/// 是否是选择器控件
/// </summary>
public bool IsSelectorEffectType => Regex.IsMatch(EffectType ?? "", "Selector$|ForeignKey", RegexOptions.IgnoreCase);
/// <summary>
/// 去掉尾部Id的属性名
/// </summary>
public string PropertyNameTrimEndId => PropertyName.TrimEnd("Id");
/// <summary>
/// 去掉尾部Id的属性名
/// </summary>
public string LowerPropertyNameTrimEndId => LowerPropertyName.TrimEnd("Id");
/// <summary>
/// 扩展属性名称
/// </summary>
public string ExtendedPropertyName => EffectType switch
{
"ForeignKey" => $"{PropertyName.TrimEnd("Id")}FkDisplayName",
"ApiTreeSelector" => $"{PropertyName.TrimEnd("Id")}DisplayName",
"DictSelector" => $"{PropertyName.TrimEnd("Id")}DictLabel",
"Upload" => $"{PropertyName.TrimEnd("Id")}Attachment",
"Upload_SingleFile" => $"{PropertyName.TrimEnd("Id")}Attachment",
_ => PropertyName
};
/// <summary>
/// 首字母小写的扩展属性名称
/// </summary>
public string LowerExtendedPropertyName
{
get
{
var displayPropertyName = ExtendedPropertyName;
if (string.IsNullOrWhiteSpace(displayPropertyName)) return null;
return displayPropertyName[..1].ToLower() + displayPropertyName[1..];
}
}
/// <summary>
/// 获取外键显示值语句
/// </summary>
/// <param name="tableAlias">表别名</param>
/// <param name="separator">多字段时的连接符</param>
/// <returns></returns>
public string GetDisplayColumn(string tableAlias, string separator = "-") => "$\"" + string.Join(separator, FkDisplayColumnList?.Select(name => $"{{{tableAlias}.{name}}}") ?? new List<string>()) + "\"";
}
@@ -1,199 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 代码生成参数类
/// </summary>
public class CodeGenInput : BasePageInput
{
/// <summary>
/// 作者姓名
/// </summary>
public virtual string AuthorName { get; set; }
/// <summary>
/// 类名
/// </summary>
public virtual string ClassName { get; set; }
/// <summary>
/// 是否移除表前缀
/// </summary>
public virtual string TablePrefix { get; set; }
/// <summary>
/// 库定位器名
/// </summary>
public virtual string ConfigId { get; set; }
/// <summary>
/// 数据库名(保留字段)
/// </summary>
public virtual string DbName { get; set; }
/// <summary>
/// 数据库类型
/// </summary>
public virtual string DbType { get; set; }
/// <summary>
/// 数据库链接
/// </summary>
public virtual string ConnectionString { get; set; }
/// <summary>
/// 生成方式
/// </summary>
public virtual string GenerateType { get; set; }
/// <summary>
/// 数据库表名
/// </summary>
public virtual string TableName { get; set; }
/// <summary>
/// 命名空间
/// </summary>
public virtual string NameSpace { get; set; }
/// <summary>
/// 业务名(业务代码包名称)
/// </summary>
public virtual string BusName { get; set; }
/// <summary>
/// 功能名(数据库表名称)
/// </summary>
public virtual string TableComment { get; set; }
/// <summary>
/// 表唯一字段
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public virtual string TableUniqueConfig { get; set; }
/// <summary>
/// 表唯一字段列表
/// </summary>
public virtual List<TableUniqueConfigItem> TableUniqueList { get; set; }
/// <summary>
/// 菜单应用分类(应用编码)
/// </summary>
public virtual string MenuApplication { get; set; }
/// <summary>
/// 是否生成菜单
/// </summary>
public virtual bool GenerateMenu { get; set; }
/// <summary>
/// 菜单父级
/// </summary>
public virtual long? MenuPid { get; set; }
/// <summary>
/// 菜单图标
/// </summary>
public virtual string MenuIcon { get; set; }
/// <summary>
/// 页面目录
/// </summary>
public virtual string PagePath { get; set; }
/// <summary>
/// 支持打印类型
/// </summary>
public virtual string PrintType { get; set; }
/// <summary>
/// 打印模版名称
/// </summary>
public virtual string PrintName { get; set; }
}
public class AddCodeGenInput : CodeGenInput
{
/// <summary>
/// 数据库表名
/// </summary>
[Required(ErrorMessage = "数据库表名不能为空")]
public override string TableName { get; set; }
/// <summary>
/// 业务名(业务代码包名称)
/// </summary>
[Required(ErrorMessage = "业务名不能为空")]
public override string BusName { get; set; }
/// <summary>
/// 命名空间
/// </summary>
[Required(ErrorMessage = "命名空间不能为空")]
public override string NameSpace { get; set; }
/// <summary>
/// 作者姓名
/// </summary>
[Required(ErrorMessage = "作者姓名不能为空")]
public override string AuthorName { get; set; }
///// <summary>
///// 类名
///// </summary>
//[Required(ErrorMessage = "类名不能为空")]
//public override string ClassName { get; set; }
///// <summary>
///// 是否移除表前缀
///// </summary>
//[Required(ErrorMessage = "是否移除表前缀不能为空")]
//public override string TablePrefix { get; set; }
/// <summary>
/// 生成方式
/// </summary>
[Required(ErrorMessage = "生成方式不能为空")]
public override string GenerateType { get; set; }
///// <summary>
///// 功能名(数据库表名称)
///// </summary>
//[Required(ErrorMessage = "数据库表名不能为空")]
//public override string TableComment { get; set; }
/// <summary>
/// 是否生成菜单
/// </summary>
[Required(ErrorMessage = "是否生成菜单不能为空")]
public override bool GenerateMenu { get; set; }
}
public class DeleteCodeGenInput
{
/// <summary>
/// 代码生成器Id
/// </summary>
[Required(ErrorMessage = "代码生成器Id不能为空")]
public long Id { get; set; }
}
public class UpdateCodeGenInput : AddCodeGenInput
{
/// <summary>
/// 代码生成器Id
/// </summary>
[Required(ErrorMessage = "代码生成器Id不能为空")]
public long Id { get; set; }
}
public class QueryCodeGenInput : DeleteCodeGenInput
{
}
@@ -1,83 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 代码生成参数类
/// </summary>
public class CodeGenOutput
{
/// <summary>
/// 代码生成器Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 作者姓名
/// </summary>
public string AuthorName { get; set; }
/// <summary>
/// 类名
/// </summary>
public string ClassName { get; set; }
/// <summary>
/// 是否移除表前缀
/// </summary>
public string TablePrefix { get; set; }
/// <summary>
/// 生成方式
/// </summary>
public string GenerateType { get; set; }
/// <summary>
/// 数据库表名
/// </summary>
public string TableName { get; set; }
/// <summary>
/// 包名
/// </summary>
public string PackageName { get; set; }
/// <summary>
/// 业务名(业务代码包名称)
/// </summary>
public string BusName { get; set; }
/// <summary>
/// 功能名(数据库表名称)
/// </summary>
public string TableComment { get; set; }
/// <summary>
/// 菜单应用分类(应用编码)
/// </summary>
public string MenuApplication { get; set; }
/// <summary>
/// 是否生成菜单
/// </summary>
public bool GenerateMenu { get; set; }
/// <summary>
/// 菜单父级
/// </summary>
public long? MenuPid { get; set; }
/// <summary>
/// 支持打印类型
/// </summary>
public string PrintType { get; set; }
/// <summary>
/// 打印模版名称
/// </summary>
public string PrintName { get; set; }
}
@@ -1,68 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 数据库表列
/// </summary>
public class ColumnOuput
{
/// <summary>
/// 字段名
/// </summary>
public string ColumnName { get; set; }
/// <summary>
/// 实体的Property名
/// </summary>
public string PropertyName { get; set; }
/// <summary>
/// 字段数据长度
/// </summary>
public int ColumnLength { get; set; }
/// <summary>
/// 数据库中类型
/// </summary>
public string DataType { get; set; }
/// <summary>
/// 字段数据默认值
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// 是否为主键
/// </summary>
public bool IsPrimarykey { get; set; }
/// <summary>
/// 是否允许为空
/// </summary>
public bool IsNullable { get; set; }
/// <summary>
/// .NET字段类型
/// </summary>
public string NetType { get; set; }
/// <summary>
/// 字典编码
/// </summary>
public string DictTypeCode { get; set; }
/// <summary>
/// 字段描述
/// </summary>
public string ColumnComment { get; set; }
/// <summary>
/// 主外键
/// </summary>
public string ColumnKey { get; set; }
}
@@ -1,33 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 数据库
/// </summary>
public class DatabaseOutput
{
/// <summary>
/// 库定位器名
/// </summary>
public string ConfigId { get; set; }
/// <summary>
/// 库名
/// </summary>
public string DbNickName { get; set; }
/// <summary>
/// 数据库类型
/// </summary>
public SqlSugar.DbType DbType { get; set; }
/// <summary>
/// 数据库连接字符串
/// </summary>
public string ConnectionString { get; set; }
}
@@ -1,43 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 数据库表
/// </summary>
public class TableOutput
{
/// <summary>
/// 库定位器名
/// </summary>
public string ConfigId { get; set; }
/// <summary>
/// 表名(字母形式的)
/// </summary>
public string TableName { get; set; }
/// <summary>
/// 实体名称
/// </summary>
public string EntityName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public string CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public string UpdateTime { get; set; }
/// <summary>
/// 表名称描述(功能名)
/// </summary>
public string TableComment { get; set; }
}
@@ -1,31 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 表唯一配置项
/// </summary>
public class TableUniqueConfigItem
{
/// <summary>
/// 字段列表
/// </summary>
public List<string> Columns { get; set; }
/// <summary>
/// 描述信息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 格式化查询条件
/// </summary>
/// <param name="separator">分隔符</param>
/// <param name="format">模板字符串</param>
/// <returns></returns>
public string Format(string separator, string format) => string.Join(separator, Columns.Select(name => string.Format(format, name)));
}
@@ -1,256 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统代码生成配置服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 260)]
public class SysCodeGenConfigService : IDynamicApiController, ITransient
{
private readonly ISqlSugarClient _db;
public SysCodeGenConfigService(ISqlSugarClient db)
{
_db = db;
}
/// <summary>
/// 获取代码生成配置列表 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取代码生成配置列表")]
public async Task<List<CodeGenConfig>> GetList([FromQuery] CodeGenConfig input)
{
return await _db.Queryable<SysCodeGenConfig>()
.Where(u => u.CodeGenId == input.CodeGenId)
.Select<CodeGenConfig>()
.Mapper(u =>
{
u.NetType = (u.EffectType == "EnumSelector" ? u.DictTypeCode : u.NetType);
u.FkDisplayColumnList = u.FkDisplayColumns?.Split(",").ToList();
})
.OrderBy(u => new { u.OrderNo, u.Id })
.ToListAsync();
}
/// <summary>
/// 更新代码生成配置 🔖
/// </summary>
/// <param name="inputList"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新代码生成配置")]
public async Task UpdateCodeGenConfig(List<CodeGenConfig> inputList)
{
if (inputList == null || inputList.Count < 1) return;
inputList.ForEach(e =>
{
e.FkDisplayColumns = e.FkDisplayColumnList?.Count > 0 ? string.Join(",", e.FkDisplayColumnList) : null;
});
await _db.Updateable(inputList.Adapt<List<SysCodeGenConfig>>())
.IgnoreColumns(u => new { u.ColumnLength, u.ColumnName, u.PropertyName })
.ExecuteCommandAsync();
}
/// <summary>
/// 删除代码生成配置
/// </summary>
/// <param name="codeGenId"></param>
/// <returns></returns>
[NonAction]
public async Task DeleteCodeGenConfig(long codeGenId)
{
await _db.Deleteable<SysCodeGenConfig>().Where(u => u.CodeGenId == codeGenId).ExecuteCommandAsync();
}
/// <summary>
/// 获取代码生成配置详情 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取代码生成配置详情")]
public async Task<SysCodeGenConfig> GetDetail([FromQuery] CodeGenConfig input)
{
return await _db.Queryable<SysCodeGenConfig>().FirstAsync(u => u.Id == input.Id);
}
/// <summary>
/// 批量增加代码生成配置
/// </summary>
/// <param name="tableColumnOutputList"></param>
/// <param name="codeGenerate"></param>
[NonAction]
public void AddList(List<ColumnOuput> tableColumnOutputList, SysCodeGen codeGenerate)
{
if (tableColumnOutputList == null) return;
var codeGenConfigs = new List<SysCodeGenConfig>();
var orderNo = 100;
foreach (var tableColumn in tableColumnOutputList)
{
var codeGenConfig = new SysCodeGenConfig();
var yesOrNo = YesNoEnum.Y.ToString();
if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString();
if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName))
{
codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString();
yesOrNo = YesNoEnum.N.ToString();
}
else
{
codeGenConfig.WhetherCommon = YesNoEnum.N.ToString();
}
codeGenConfig.CodeGenId = codeGenerate.Id;
codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名
codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名
codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度
codeGenConfig.ColumnComment = tableColumn.ColumnComment;
codeGenConfig.NetType = tableColumn.NetType;
codeGenConfig.DefaultValue = tableColumn.DefaultValue;
codeGenConfig.WhetherRetract = YesNoEnum.N.ToString();
// 生成代码时,主键并不是必要输入项,故一定要排除主键字段
codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString();
codeGenConfig.WhetherQuery = yesOrNo;
codeGenConfig.WhetherImport = yesOrNo;
codeGenConfig.WhetherAddUpdate = yesOrNo;
codeGenConfig.WhetherTable = yesOrNo;
codeGenConfig.ColumnKey = tableColumn.ColumnKey;
codeGenConfig.DataType = tableColumn.DataType;
codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType);
codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString();
codeGenConfig.OrderNo = orderNo;
codeGenConfigs.Add(codeGenConfig);
if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode))
{
codeGenConfig.QueryType = "==";
codeGenConfig.DictTypeCode = tableColumn.DictTypeCode;
codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector";
}
orderNo += 10; // 每个配置排序间隔10
}
// 多库代码生成---这里要切回主库
var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId);
provider.Insertable(codeGenConfigs).ExecuteCommand();
}
/// <summary>
/// 批量更新代码字段:先删除再新增,会保留历史字段操作类型
/// </summary>
/// <param name="tableColumnOutputList"></param>
/// <param name="codeGenId"></param>
[NonAction]
public async Task UpdateList(List<ColumnOuput> tableColumnOutputList, long codeGenId)
{
if (tableColumnOutputList == null) return;
//获取历史数据
var oldList = await GetList(new CodeGenConfig() { CodeGenId = codeGenId, });
//删除历史数据
await DeleteCodeGenConfig(codeGenId);
var codeGenConfigs = new List<SysCodeGenConfig>();
var orderNo = 100;
foreach (var tableColumn in tableColumnOutputList)
{
var oldItem = oldList.FirstOrDefault(u => u.ColumnName == tableColumn.ColumnName);
var codeGenConfig = new SysCodeGenConfig();
var yesOrNo = YesNoEnum.Y.ToString();
if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString();
if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName))
{
codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString();
yesOrNo = YesNoEnum.N.ToString();
}
else
{
codeGenConfig.WhetherCommon = YesNoEnum.N.ToString();
}
codeGenConfig.CodeGenId = codeGenId;
codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名
codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名
codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度
codeGenConfig.ColumnComment = tableColumn.ColumnComment;
codeGenConfig.NetType = tableColumn.NetType;
codeGenConfig.DefaultValue = tableColumn.DefaultValue;
codeGenConfig.WhetherRetract = YesNoEnum.N.ToString();
// 生成代码时,主键并不是必要输入项,故一定要排除主键字段
codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString();
codeGenConfig.WhetherQuery = yesOrNo;
codeGenConfig.WhetherImport = yesOrNo;
codeGenConfig.WhetherAddUpdate = yesOrNo;
codeGenConfig.WhetherTable = yesOrNo;
codeGenConfig.ColumnKey = tableColumn.ColumnKey;
codeGenConfig.DataType = tableColumn.DataType;
codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType);
codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString();
codeGenConfig.OrderNo = orderNo;
if (oldItem != null)
{
//如果历史存在,则继承
codeGenConfig.WhetherQuery = oldItem.WhetherQuery;
codeGenConfig.WhetherImport = oldItem.WhetherImport;
codeGenConfig.WhetherAddUpdate = oldItem.WhetherAddUpdate;
codeGenConfig.WhetherTable = oldItem.WhetherTable;
codeGenConfig.EffectType = oldItem.EffectType;
codeGenConfig.FkConfigId = oldItem.FkConfigId;
codeGenConfig.FkEntityName = oldItem.FkEntityName;
codeGenConfig.FkTableName = oldItem.FkTableName;
codeGenConfig.FkDisplayColumns = oldItem.FkDisplayColumns;
codeGenConfig.FkLinkColumnName = oldItem.FkLinkColumnName;
codeGenConfig.FkColumnNetType = oldItem.FkColumnNetType;
}
codeGenConfigs.Add(codeGenConfig);
if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode))
{
codeGenConfig.QueryType = "==";
codeGenConfig.DictTypeCode = tableColumn.DictTypeCode;
codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector";
}
orderNo += 10; // 每个配置排序间隔10
}
// 多库代码生成---这里要切回主库
var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId);
provider.Insertable(codeGenConfigs).ExecuteCommand();
}
/// <summary>
/// 默认查询类型
/// </summary>
/// <param name="codeGenConfig"></param>
/// <returns></returns>
private static string GetDefaultQueryType(SysCodeGenConfig codeGenConfig)
{
return (codeGenConfig.NetType?.TrimEnd('?')) switch
{
"string" => "like",
"DateTime" => "~",
_ => "==",
};
}
}
@@ -1,825 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using System.IO.Compression;
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统代码生成器服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 270)]
public class SysCodeGenService : IDynamicApiController, ITransient
{
private readonly ISqlSugarClient _db;
private readonly SysCodeGenConfigService _codeGenConfigService;
private readonly DbConnectionOptions _dbConnectionOptions;
private readonly CodeGenOptions _codeGenOptions;
private readonly SysMenuService _sysMenuService;
private readonly IViewEngine _viewEngine;
private readonly UserManager _userManager;
public SysCodeGenService(ISqlSugarClient db,
IOptions<DbConnectionOptions> dbConnectionOptions,
SysCodeGenConfigService codeGenConfigService,
IOptions<CodeGenOptions> codeGenOptions,
SysMenuService sysMenuService,
UserManager userManager,
IViewEngine viewEngine)
{
_db = db;
_viewEngine = viewEngine;
_userManager = userManager;
_sysMenuService = sysMenuService;
_codeGenOptions = codeGenOptions.Value;
_codeGenConfigService = codeGenConfigService;
_dbConnectionOptions = dbConnectionOptions.Value;
}
/// <summary>
/// 获取代码生成分页列表 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取代码生成分页列表")]
public async Task<SqlSugarPagedList<SysCodeGen>> Page(CodeGenInput input)
{
return await _db.Queryable<SysCodeGen>()
.WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 增加代码生成 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加代码生成")]
public async Task AddCodeGen(AddCodeGenInput input)
{
var isExist = await _db.Queryable<SysCodeGen>().Where(u => u.TableName == input.TableName).AnyAsync();
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
var codeGen = input.Adapt<SysCodeGen>();
var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync();
// 增加配置表
_codeGenConfigService.AddList(GetColumnList(input), newCodeGen);
}
/// <summary>
/// 更新代码生成 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新代码生成")]
public async Task UpdateCodeGen(UpdateCodeGenInput input)
{
var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
var oldRecord = await _db.Queryable<SysCodeGen>().FirstAsync(u => u.Id == input.Id);
try
{
// 开启事务
_db.AsTenant().BeginTran();
if (input.GenerateMenu)
{
var oldTitle = $"{oldRecord.BusName}管理";
var newTitle = $"{input.BusName}管理";
var updateObj = await _db.Queryable<SysMenu>().FirstAsync(u => u.Title == oldTitle);
if (updateObj != null)
{
updateObj.Title = newTitle;
var result = _db.Updateable(updateObj).UpdateColumns(it => new { it.Title }).ExecuteCommand();
_sysMenuService.DeleteMenuCache();
}
}
if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
var codeGen = input.Adapt<SysCodeGen>();
await _db.Updateable(codeGen).ExecuteCommandAsync();
// 仅当数据表名称发生了变化,才更新配置表
//if (oldRecord.TableName != input.TableName)
//{
await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id);
_codeGenConfigService.AddList(GetColumnList(input.Adapt<AddCodeGenInput>()), codeGen);
//}
_db.AsTenant().CommitTran();
}
catch (Exception ex)
{
_db.AsTenant().RollbackTran();
throw Oops.Oh(ex);
}
}
/// <summary>
/// 同步代码字段(保留历史作用类型) 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "SyncField"), HttpPost]
[DisplayName("同步代码字段")]
public async Task SyncCodeFieldGen(UpdateCodeGenInput input)
{
var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
try
{
// 开启事务
_db.AsTenant().BeginTran();
await _codeGenConfigService.UpdateList(GetColumnList(input.Adapt<AddCodeGenInput>()), input.Id);
_db.AsTenant().CommitTran();
}
catch (Exception ex)
{
_db.AsTenant().RollbackTran();
throw Oops.Oh(ex);
}
}
/// <summary>
/// 删除代码生成 🔖
/// </summary>
/// <param name="inputs"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除代码生成")]
public async Task DeleteCodeGen(List<DeleteCodeGenInput> inputs)
{
if (inputs == null || inputs.Count < 1) return;
var codeGenConfigTaskList = new List<Task>();
inputs.ForEach(u =>
{
_db.Deleteable<SysCodeGen>().In(u.Id).ExecuteCommand();
// 删除配置表
codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id));
});
await Task.WhenAll(codeGenConfigTaskList);
}
/// <summary>
/// 获取代码生成详情 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取代码生成详情")]
public async Task<SysCodeGen> GetDetail([FromQuery] QueryCodeGenInput input)
{
return await _db.Queryable<SysCodeGen>().SingleAsync(u => u.Id == input.Id);
}
/// <summary>
/// 获取数据库库集合 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取数据库库集合")]
public async Task<List<DatabaseOutput>> GetDatabaseList()
{
var dbConfigs = _dbConnectionOptions.ConnectionConfigs;
return await Task.FromResult(dbConfigs.Adapt<List<DatabaseOutput>>());
}
/// <summary>
/// 获取数据库表(实体)集合 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取数据库表(实体)集合")]
public async Task<List<TableOutput>> GetTableList(string configId = SqlSugarConst.MainConfigId)
{
var provider = _db.AsTenant().GetConnectionScope(configId);
var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); // 不能走缓存,否则切库不起作用
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId));
// var dbTableNames = dbTableInfos.Select(u => u.Name.ToLower()).ToList();
IEnumerable<EntityInfo> entityInfos = await GetEntityInfos(configId);
var tableOutputList = new List<TableOutput>();
foreach (var item in entityInfos)
{
var tbConfigId = item.Type.GetCustomAttribute<TenantAttribute>()?.configId as string ?? SqlSugarConst.MainConfigId;
if (item.Type.IsDefined(typeof(LogTableAttribute))) tbConfigId = SqlSugarConst.LogConfigId;
if (tbConfigId != configId) continue;
var table = dbTableInfos.FirstOrDefault(u => string.Equals(u.Name, (config!.DbSettings.EnableUnderLine ? item.DbTableName.ToUnderLine() : item.DbTableName), StringComparison.CurrentCultureIgnoreCase));
if (table == null) continue;
tableOutputList.Add(new TableOutput
{
ConfigId = configId,
EntityName = item.EntityName,
TableName = table.Name,
TableComment = item.TableDescription
});
}
return tableOutputList;
}
/// <summary>
/// 根据表名获取列集合 🔖
/// </summary>
/// <returns></returns>
[DisplayName("根据表名获取列集合")]
public List<ColumnOuput> GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId)
{
// 切库---多库代码生成用
var provider = _db.AsTenant().GetConnectionScope(configId);
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401);
if (config.DbSettings.EnableUnderLine) tableName = tableName.ToUnderLine();
// 获取实体类型属性
var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName);
if (entityType == null) return null;
var properties = GetEntityInfos(configId).Result.First(e => e.DbTableName.EndsWithIgnoreCase(tableName)).Type.GetProperties()
.Where(e => e.GetCustomAttribute<SugarColumn>()?.IsIgnore == false).Select(e => new
{
PropertyName = e.Name,
ColumnComment = e.GetCustomAttribute<SugarColumn>()?.ColumnDescription,
ColumnName = e.GetCustomAttribute<SugarColumn>()?.ColumnName ?? e.Name
}).ToList();
// 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null)
var columnList = provider.DbMaintenance.GetColumnInfosByTableName(tableName).Select(u => new ColumnOuput
{
ColumnName = config!.DbSettings.EnableUnderLine ? u.DbColumnName.ToUnderLine() : u.DbColumnName,
ColumnKey = u.IsPrimarykey.ToString(),
DataType = u.DataType.ToString(),
NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
ColumnComment = u.ColumnDescription
}).ToList();
foreach (var column in columnList)
{
// ToLowerInvariant 将字段名转成小写再比较,避免因大小写不一致导致无法匹配(pgsql创建表会默认全小写,而我们的实体中又是大写,就会匹配不上)
var property = properties.FirstOrDefault(e => (config!.DbSettings.EnableUnderLine ? e.ColumnName.ToUnderLine() : e.ColumnName).ToLowerInvariant() == column.ColumnName.ToLowerInvariant());
column.ColumnComment ??= property?.ColumnComment;
column.PropertyName = property?.PropertyName;
}
return columnList;
}
/// <summary>
/// 获取数据表列(实体属性)集合
/// </summary>
/// <returns></returns>
private List<ColumnOuput> GetColumnList([FromQuery] AddCodeGenInput input)
{
var entityType = GetEntityInfos(input.ConfigId).GetAwaiter().GetResult().FirstOrDefault(u => u.EntityName == input.TableName);
if (entityType == null) return null;
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
var dbTableName = config!.DbSettings.EnableUnderLine ? entityType.DbTableName.ToUnderLine() : entityType.DbTableName;
// 切库---多库代码生成用
var provider = _db.AsTenant().GetConnectionScope(!string.IsNullOrEmpty(input.ConfigId) ? input.ConfigId : SqlSugarConst.MainConfigId);
var entityBasePropertyNames = CodeGenUtil.GetPropertyInfoArray(typeof(EntityBaseTenant))?.Select(p => p.Name).ToArray();
var columnInfos = provider.DbMaintenance.GetColumnInfosByTableName(dbTableName, false);
var result = columnInfos.Select(u => new ColumnOuput
{
// 转下划线后的列名需要再转回来(暂时不转)
//ColumnName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
ColumnName = u.DbColumnName,
ColumnLength = u.Length,
IsPrimarykey = u.IsPrimarykey,
IsNullable = u.IsNullable,
ColumnKey = u.IsPrimarykey.ToString(),
NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
DataType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
ColumnComment = string.IsNullOrWhiteSpace(u.ColumnDescription) ? u.DbColumnName : u.ColumnDescription,
DefaultValue = u.DefaultValue,
}).ToList();
// 获取实体的属性信息,赋值给PropertyName属性(CodeFirst模式应以PropertyName为实际使用名称)
var entityProperties = entityType.Type.GetProperties();
for (int i = result.Count - 1; i >= 0; i--)
{
var columnOutput = result[i];
// 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
var propertyInfo = entityProperties.FirstOrDefault(u => string.Equals((u.GetCustomAttribute<SugarColumn>()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ??
entityProperties.FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>() != null && u.Name.ToLower() == (config.DbSettings.EnableUnderLine
? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower()
: columnOutput.ColumnName.ToLower()));
if (propertyInfo != null)
{
columnOutput.PropertyName = propertyInfo.Name;
columnOutput.ColumnComment = propertyInfo.GetCustomAttribute<SugarColumn>()!.ColumnDescription;
var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
if (propertyInfo.PropertyType.IsEnum || (propertyType?.IsEnum ?? false))
{
columnOutput.DictTypeCode = (propertyType ?? propertyInfo.PropertyType).Name;
}
else
{
var dict = propertyInfo.GetCustomAttribute<DictAttribute>();
if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode;
}
}
else
{
result.RemoveAt(i); // 移除没有定义此属性的字段
}
}
return result;
}
/// <summary>
/// 获取库表信息
/// </summary>
/// <returns></returns>
private async Task<IEnumerable<EntityInfo>> GetEntityInfos(string configId)
{
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401);
var entityInfos = new List<EntityInfo>();
var type = typeof(SugarTable);
var types = new List<Type>();
if (_codeGenOptions.EntityAssemblyNames != null)
{
types = App.EffectiveTypes.Where(c => c.IsClass)
.Where(c => _codeGenOptions.EntityAssemblyNames.Contains(c.Assembly.GetName().Name) || _codeGenOptions.EntityAssemblyNames.Any(name => c.Assembly.GetName().Name!.Contains(name)))
.ToList();
}
Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray();
foreach (var ct in cosType)
{
var sugarAttribute = ct.GetCustomAttributes(type, true).FirstOrDefault();
var description = "";
var des = ct.GetCustomAttributes(typeof(DescriptionAttribute), true);
if (des.Length > 0) description = ((DescriptionAttribute)des[0]).Description;
var dbTableName = sugarAttribute == null || string.IsNullOrWhiteSpace(((SugarTable)sugarAttribute).TableName) ? ct.Name : ((SugarTable)sugarAttribute).TableName;
if (config.DbSettings.EnableUnderLine) dbTableName = dbTableName.ToUnderLine();
entityInfos.Add(new EntityInfo
{
EntityName = ct.Name,
DbTableName = dbTableName,
TableDescription = sugarAttribute == null ? description : ((SugarTable)sugarAttribute).TableDescription,
Type = ct
});
}
return await Task.FromResult(entityInfos);
bool IsMyAttribute(Attribute[] o) => o.Any(a => a.GetType() == type);
}
/// <summary>
/// 获取程序保存位置 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取程序保存位置")]
public List<string> GetApplicationNamespaces()
{
return _codeGenOptions.BackendApplicationNamespaces;
}
/// <summary>
/// 代码生成到本地 🔖
/// </summary>
/// <returns></returns>
[UnitOfWork]
[DisplayName("代码生成到本地")]
public async Task<dynamic> RunLocal(SysCodeGen input)
{
if (string.IsNullOrEmpty(input.GenerateType))
input.GenerateType = "200";
// 先删除该表已生成的菜单列表
List<string> targetPathList;
var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
if (input.GenerateType.StartsWith('1'))
{
targetPathList = GetZipPathList(input);
if (Directory.Exists(zipPath)) Directory.Delete(zipPath, true);
}
else
targetPathList = GetTargetPathList(input);
var (tableFieldList, result) = await RenderTemplateAsync(input);
var templatePathList = GetTemplatePathList(input);
for (var i = 0; i < templatePathList.Count; i++)
{
var content = result.GetValueOrDefault(templatePathList[i]?.TrimEnd(".vm"));
if (string.IsNullOrWhiteSpace(content)) continue;
var dirPath = new DirectoryInfo(targetPathList[i]).Parent!.FullName;
if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
_ = File.WriteAllTextAsync(targetPathList[i], content, Encoding.UTF8);
}
if (input.GenerateMenu) await AddOrUpdateMenu(input.TableName, input.BusName, input.MenuPid ?? 0, input.MenuIcon, input.PagePath, tableFieldList);
// 非ZIP压缩返回空
if (!input.GenerateType.StartsWith('1')) return null;
// 判断是否存在同名称文件
string downloadPath = zipPath + ".zip";
if (File.Exists(downloadPath)) File.Delete(downloadPath);
// 创建zip文件并返回下载地址
ZipFile.CreateFromDirectory(zipPath, downloadPath);
return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" };
}
/// <summary>
/// 获取代码生成预览 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取代码生成预览")]
public async Task<Dictionary<string, string>> Preview(SysCodeGen input)
{
var (_, result) = await RenderTemplateAsync(input);
return result;
}
/// <summary>
/// 渲染模板
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private async Task<(List<CodeGenConfig> tableFieldList, Dictionary<string, string> result)> RenderTemplateAsync(SysCodeGen input)
{
var tableFieldList = await _codeGenConfigService.GetList(new CodeGenConfig { CodeGenId = input.Id }); // 字段集合
var joinTableList = tableFieldList.Where(u => u.EffectType is "Upload" or "ForeignKey" or "ApiTreeSelector").ToList(); // 需要连表查询的字段
var data = new CustomViewEngine
{
ConfigId = input.ConfigId,
BusName = input.BusName,
PagePath = input.PagePath,
NameSpace = input.NameSpace,
ClassName = input.TableName,
PrintType = input.PrintType,
PrintName = input.PrintName,
AuthorName = input.AuthorName,
ProjectLastName = input.NameSpace!.Split('.').Last(),
LowerClassName = input.TableName!.ToFirstLetterLowerCase(),
TableUniqueConfigList = input.TableUniqueList ?? new(),
TableField = tableFieldList,
QueryWhetherList = tableFieldList.Where(u => u.WhetherQuery == "Y").ToList(),
ImportFieldList = tableFieldList.Where(u => u.WhetherImport == "Y").ToList(),
UploadFieldList = tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile").ToList(),
PrimaryKeyFieldList = tableFieldList.Where(c => c.ColumnKey == "True").ToList(),
AddUpdateFieldList = tableFieldList.Where(u => u.WhetherAddUpdate == "Y").ToList(),
ApiTreeFieldList = tableFieldList.Where(u => u.EffectType == "ApiTreeSelector").ToList(),
DropdownFieldList = tableFieldList.Where(u => u.EffectType is "ForeignKey" or "ApiTreeSelector").ToList(),
DefaultValueList = tableFieldList.Where(u => u.DefaultValue != null && u.DefaultValue.Length > 0).ToList(),
HasJoinTable = joinTableList.Count > 0,
HasDictField = tableFieldList.Any(u => u.EffectType == "DictSelector"),
HasEnumField = tableFieldList.Any(u => u.EffectType == "EnumSelector"),
HasConstField = tableFieldList.Any(u => u.EffectType == "ConstSelector"),
HasLikeQuery = tableFieldList.Any(c => c.WhetherQuery == "Y" && c.QueryType == "like")
};
// 获取模板文件并替换
var templatePathList = GetTemplatePathList();
var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
var result = new Dictionary<string, string>();
foreach (var path in templatePathList)
{
var templateFilePath = Path.Combine(templatePath, path);
if (!File.Exists(templateFilePath)) continue;
var tContent = await File.ReadAllTextAsync(templateFilePath);
var tResult = await _viewEngine.RunCompileFromCachedAsync(tContent, data, builderAction: builder =>
{
builder.AddAssemblyReferenceByName("System.Text.RegularExpressions");
builder.AddAssemblyReferenceByName("System.Collections");
builder.AddAssemblyReferenceByName("System.Linq");
builder.AddUsing("System.Text.RegularExpressions");
builder.AddUsing("System.Collections.Generic");
builder.AddUsing("System.Linq");
});
result.Add(path?.TrimEnd(".vm"), tResult);
}
return (tableFieldList, result);
}
/// <summary>
/// 添加或更新菜单
/// </summary>
/// <param name="className"></param>
/// <param name="busName"></param>
/// <param name="pid"></param>
/// <param name="menuIcon"></param>
/// <param name="pagePath"></param>
/// <param name="tableFieldList"></param>
/// <returns></returns>
private async Task AddOrUpdateMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
{
var title = $"{busName}管理";
var lowerClassName = className.ToFirstLetterLowerCase();
var menuType = pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu;
// 查询是否已有主菜单
var existingMenu = await _db.Queryable<SysMenu>()
.Where(m => m.Title == title && m.Type == menuType && m.Pid == pid)
.FirstAsync();
string parentPath = "";
if (pid != 0)
{
var parent = await _db.Queryable<SysMenu>().FirstAsync(m => m.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
parentPath = parent.Path;
}
long menuId;
if (existingMenu == null)
{
// 不存在则新增
var newMenu = new SysMenu
{
Pid = pid,
Title = title,
Type = menuType,
Icon = menuIcon ?? "menu",
Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower(),
Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index"
};
menuId = await _sysMenuService.AddMenu(newMenu.Adapt<AddMenuInput>());
}
else
{
// 存在则更新
existingMenu.Icon = menuIcon;
existingMenu.Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower();
existingMenu.Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index";
await _sysMenuService.UpdateMenu(existingMenu.Adapt<UpdateMenuInput>());
menuId = existingMenu.Id;
}
// 定义应有的按钮
var orderNo = 100;
var newButtons = new List<SysMenu>
{
new() { Title = "查询", Permission = $"{lowerClassName}:page", OrderNo = orderNo += 10 },
new() { Title = "详情", Permission = $"{lowerClassName}:detail", OrderNo = orderNo += 10 },
new() { Title = "增加", Permission = $"{lowerClassName}:add", OrderNo = orderNo += 10 },
new() { Title = "编辑", Permission = $"{lowerClassName}:update", OrderNo = orderNo += 10 },
new() { Title = "删除", Permission = $"{lowerClassName}:delete", OrderNo = orderNo += 10 },
new() { Title = "批量删除", Permission = $"{lowerClassName}:batchDelete", OrderNo = orderNo += 10 },
new() { Title = "设置状态", Permission = $"{lowerClassName}:setStatus", OrderNo = orderNo += 10 },
new() { Title = "打印", Permission = $"{lowerClassName}:print", OrderNo = orderNo += 10 },
new() { Title = "导入", Permission = $"{lowerClassName}:import", OrderNo = orderNo += 10 },
new() { Title = "导出", Permission = $"{lowerClassName}:export", OrderNo = orderNo += 10 }
};
if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")))
{
newButtons.Add(new SysMenu
{
Title = "下拉列表数据",
Permission = $"{lowerClassName}:dropdownData",
OrderNo = orderNo += 10
});
}
foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile"))
{
newButtons.Add(new SysMenu
{
Title = $"上传{column.ColumnComment}",
Permission = $"{lowerClassName}:upload{column.PropertyName}",
OrderNo = orderNo += 10
});
}
// 获取当前菜单下的按钮
var existingButtons = await _db.Queryable<SysMenu>()
.Where(m => m.Pid == menuId && m.Type == MenuTypeEnum.Btn)
.ToListAsync();
var newPermissions = newButtons.Select(b => b.Permission).ToHashSet();
// 添加或更新按钮
foreach (var btn in newButtons)
{
var match = existingButtons.FirstOrDefault(b => b.Permission == btn.Permission);
if (match == null)
{
btn.Type = MenuTypeEnum.Btn;
btn.Pid = menuId;
btn.Icon = "";
await _sysMenuService.AddMenu(btn.Adapt<AddMenuInput>());
}
else
{
match.Title = btn.Title;
match.OrderNo = btn.OrderNo;
await _sysMenuService.UpdateMenu(match.Adapt<UpdateMenuInput>());
}
}
// 删除多余的旧按钮
var toDelete = existingButtons.Where(b => !newPermissions.Contains(b.Permission)).ToList();
foreach (var del in toDelete)
await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = del.Id });
}
/// <summary>
/// 增加菜单
/// </summary>
/// <param name="className"></param>
/// <param name="busName"></param>
/// <param name="pid"></param>
/// <param name="menuIcon"></param>
/// <param name="pagePath"></param>
/// <param name="tableFieldList"></param>
/// <returns></returns>
private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
{
// 删除已存在的菜单
var title = $"{busName}管理";
await DeleteMenuTree(title, pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu);
var parentMenuPath = "";
var lowerClassName = className!.ToFirstLetterLowerCase();
if (pid == 0)
{
// 新增目录,并记录Id
var dirMenu = new SysMenu { Pid = 0, Title = title, Type = MenuTypeEnum.Dir, Icon = "robot", Path = "/" + className.ToLower(), Component = "Layout" };
pid = await _sysMenuService.AddMenu(dirMenu.Adapt<AddMenuInput>());
}
else
{
var parentMenu = await _db.Queryable<SysMenu>().FirstAsync(u => u.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
parentMenuPath = parentMenu.Path;
}
// 新增菜单,并记录Id
var rootMenu = new SysMenu { Pid = pid, Title = title, Type = MenuTypeEnum.Menu, Icon = menuIcon, Path = $"{parentMenuPath}/{className.ToLower()}", Component = $"/{pagePath}/{lowerClassName}/index" };
pid = await _sysMenuService.AddMenu(rootMenu.Adapt<AddMenuInput>());
var orderNo = 100;
var menuList = new List<SysMenu>
{
new() { Title="查询", Permission=$"{lowerClassName}:page", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="详情", Permission=$"{lowerClassName}:detail", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="增加", Permission=$"{lowerClassName}:add", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="编辑", Permission=$"{lowerClassName}:update", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="删除", Permission=$"{lowerClassName}:delete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="批量删除", Permission=$"{lowerClassName}:batchDelete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="设置状态", Permission=$"{lowerClassName}:setStatus", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="打印", Permission=$"{lowerClassName}:print", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="导入", Permission=$"{lowerClassName}:import", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
new() { Title="导出", Permission=$"{lowerClassName}:export", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}
};
if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")))
menuList.Add(new SysMenu { Title = "下拉列表数据", Permission = $"{lowerClassName}:dropdownData", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 });
foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload"))
menuList.Add(new SysMenu { Title = $"上传{column.ColumnComment}", Permission = $"{lowerClassName}:upload{column.PropertyName}", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 });
foreach (var menu in menuList) await _sysMenuService.AddMenu(menu.Adapt<AddMenuInput>());
}
/// <summary>
/// 根据菜单名称和类型删除关联的菜单树
/// </summary>
/// <param name="title"></param>
/// <param name="type"></param>
private async Task DeleteMenuTree(string title, MenuTypeEnum type)
{
var menuList = await _db.Queryable<SysMenu>().Where(u => u.Title == title && u.Type == type).ToListAsync() ?? new();
foreach (var menu in menuList) await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = menu.Id });
}
/// <summary>
/// 获取模板文件路径集合
/// </summary>
/// <returns></returns>
private static List<string> GetTemplatePathList(SysCodeGen input)
{
if (input.GenerateType!.Substring(1, 1).Contains('1')) return new() { "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
if (input.GenerateType.Substring(1, 1).Contains('2')) return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm" };
return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
}
/// <summary>
/// 获取模板文件路径集合
/// </summary>
/// <returns></returns>
private static List<string> GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
/// <summary>
/// 设置生成文件路径
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private List<string> GetTargetPathList(SysCodeGen input)
{
//var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.FullName, input.NameSpace!, "Service", input.TableName!);
var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
var frontendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");//
var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
var apiJsPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
if (input.GenerateType!.Substring(1, 1).Contains('1'))
{
// 生成到本项目(前端)
return new List<string>
{
indexPath,
formModalPath,
apiJsPath
};
}
if (input.GenerateType.Substring(1, 1).Contains('2'))
{
// 生成到本项目(后端)
return new List<string>
{
servicePath,
inputPath,
outputPath,
viewPath,
};
}
// 前后端同时生成到本项目
return new List<string>
{
servicePath,
inputPath,
outputPath,
viewPath,
indexPath,
formModalPath,
apiJsPath
};
}
/// <summary>
/// 设置生成文件路径
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private List<string> GetZipPathList(SysCodeGen input)
{
var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
var firstLowerTableName = input.TableName!.ToFirstLetterLowerCase();
//var backendPath = Path.Combine(zipPath, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
var backendPath = Path.Combine(zipPath, input.NameSpace!, "Service", input.TableName);
var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
var frontendPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
var indexPath = Path.Combine(frontendPath, firstLowerTableName, "index.vue");
var formModalPath = Path.Combine(frontendPath, firstLowerTableName, "component", "editDialog.vue");
var apiJsPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, firstLowerTableName + ".ts");
if (input.GenerateType!.StartsWith("11"))
{
return new List<string>
{
indexPath,
formModalPath,
apiJsPath
};
}
if (input.GenerateType.StartsWith("12"))
{
return new List<string>
{
servicePath,
inputPath,
outputPath,
viewPath
};
}
return new List<string>
{
servicePath,
inputPath,
outputPath,
viewPath,
indexPath,
formModalPath,
apiJsPath
};
}
}
@@ -673,16 +673,6 @@ public class SysDatabaseService : IDynamicApiController, ITransient
private static string GetEntityTargetPath(CreateEntityInput input)
{
var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "Entity");
//if (!Directory.Exists(backendPath))
//{
// var pluginsPath = App.GetConfig<string[]>("AppSettings:ExternalAssemblies");
// foreach (var pluginPath in pluginsPath)
// {
// backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, pluginPath, input.Position, "Entity");
// if (Directory.Exists(backendPath))
// break;
// }
//}
if (!Directory.Exists(backendPath))
Directory.CreateDirectory(backendPath);
return Path.Combine(backendPath, input.EntityName + ".cs");
@@ -773,4 +763,4 @@ public class SysDatabaseService : IDynamicApiController, ITransient
FileDownloadName = backupFileName
};
}
}
}
@@ -1,188 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 作业持久化(数据库)
/// </summary>
public class DbJobPersistence : IJobPersistence
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public DbJobPersistence(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
/// <summary>
/// 作业调度服务启动时
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public async Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
var dynamicJobCompiler = scope.ServiceProvider.GetRequiredService<DynamicJobCompiler>();
// 获取所有定义的作业
var allJobs = App.EffectiveTypes.ScanToBuilders().ToList();
// 若数据库不存在任何作业,则直接返回
if (!await db.Queryable<SysJobDetail>().AnyAsync(u => true, stoppingToken)) return allJobs;
// 遍历所有定义的作业
foreach (var schedulerBuilder in allJobs)
{
// 获取作业信息构建器
var jobBuilder = schedulerBuilder.GetJobBuilder();
// 加载数据库数据
var dbDetail = await db.Queryable<SysJobDetail>().FirstAsync(u => u.JobId == jobBuilder.JobId, stoppingToken);
if (dbDetail == null) continue;
// 同步数据库数据
jobBuilder.LoadFrom(dbDetail);
// 获取作业的所有数据库的触发器
var dbTriggers = await db.Queryable<SysJobTrigger>().Where(u => u.JobId == jobBuilder.JobId).ToListAsync(stoppingToken);
// 遍历所有作业触发器
foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable())
{
// 加载数据库数据
var dbTrigger = dbTriggers.FirstOrDefault(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId);
if (dbTrigger == null) continue;
triggerBuilder.LoadFrom(dbTrigger).Updated(); // 标记更新
}
// 遍历所有非编译时定义的触发器加入到作业中
foreach (var dbTrigger in dbTriggers)
{
if (schedulerBuilder.GetTriggerBuilder(dbTrigger.TriggerId)?.JobId == jobBuilder.JobId) continue;
var triggerBuilder = TriggerBuilder.Create(dbTrigger.TriggerId).LoadFrom(dbTrigger);
schedulerBuilder.AddTriggerBuilder(triggerBuilder); // 先添加
triggerBuilder.Updated(); // 再标记更新
}
// 标记更新
schedulerBuilder.Updated();
}
// 获取数据库所有通过脚本创建的作业
var allDbScriptJobs = await db.Queryable<SysJobDetail>().Where(u => u.CreateType != JobCreateTypeEnum.BuiltIn).ToListAsync(stoppingToken);
foreach (var dbDetail in allDbScriptJobs)
{
// 动态创建作业
Type jobType = dbDetail.CreateType switch
{
JobCreateTypeEnum.Script => dynamicJobCompiler.BuildJob(dbDetail.ScriptCode),
JobCreateTypeEnum.Http => typeof(HttpJob),
_ => throw new NotSupportedException(),
};
// 动态构建的 jobType 的程序集名称为随机名称,需重新设置
dbDetail.AssemblyName = jobType.Assembly.FullName!.Split(',')[0];
var jobBuilder = JobBuilder.Create(jobType).LoadFrom(dbDetail);
// 强行设置为不扫描 IJob 实现类 [Trigger] 特性触发器,否则 SchedulerBuilder.Create 会再次扫描,导致重复添加同名触发器
jobBuilder.SetIncludeAnnotations(false);
// 获取作业的所有数据库的触发器加入到作业中
var dbTriggers = await db.Queryable<SysJobTrigger>().Where(u => u.JobId == jobBuilder.JobId).ToListAsync();
var triggerBuilders = dbTriggers.Select(u => TriggerBuilder.Create(u.TriggerId).LoadFrom(u).Updated());
var schedulerBuilder = SchedulerBuilder.Create(jobBuilder, triggerBuilders.ToArray());
// 标记更新
schedulerBuilder.Updated();
allJobs.Add(schedulerBuilder);
}
return allJobs;
}
/// <summary>
/// 作业计划初始化通知
/// </summary>
/// <param name="builder"></param>
/// <param name="stoppingToken"></param>
/// <returns></returns>
public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken)
{
return Task.FromResult(builder);
}
/// <summary>
/// 作业计划Scheduler的JobDetail变化时
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnChangedAsync(PersistenceContext context)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
var jobDetail = context.JobDetail.Adapt<SysJobDetail>();
switch (context.Behavior)
{
case PersistenceBehavior.Appended:
await db.Insertable(jobDetail).ExecuteCommandAsync();
break;
case PersistenceBehavior.Updated:
await db.Updateable(jobDetail).WhereColumns(u => new { u.JobId }).IgnoreColumns(u => new { u.Id, u.CreateType, u.ScriptCode }).ExecuteCommandAsync();
break;
case PersistenceBehavior.Removed:
await db.Deleteable<SysJobDetail>().Where(u => u.JobId == jobDetail.JobId).ExecuteCommandAsync();
break;
}
}
/// <summary>
/// 作业计划Scheduler的触发器Trigger变化时
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnTriggerChangedAsync(PersistenceTriggerContext context)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
var jobTrigger = context.Trigger.Adapt<SysJobTrigger>();
switch (context.Behavior)
{
case PersistenceBehavior.Appended:
await db.Insertable(jobTrigger).ExecuteCommandAsync();
break;
case PersistenceBehavior.Updated:
await db.Updateable(jobTrigger).WhereColumns(u => new { u.TriggerId, u.JobId }).IgnoreColumns(u => new { u.Id }).ExecuteCommandAsync();
break;
case PersistenceBehavior.Removed:
await db.Deleteable<SysJobTrigger>().Where(u => u.TriggerId == jobTrigger.TriggerId && u.JobId == jobTrigger.JobId).ExecuteCommandAsync();
break;
}
}
/// <summary>
/// 作业触发器运行记录
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
var jobTriggerRecord = context.Timeline.Adapt<SysJobTriggerRecord>();
await db.Insertable(jobTriggerRecord).ExecuteCommandAsync();
await scope.ServiceProvider.GetRequiredService<SysJobService>().ClearExpireJobTriggerRecord(jobTriggerRecord);
}
}
@@ -1,50 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class JobDetailInput
{
/// <summary>
/// 作业Id
/// </summary>
public string JobId { get; set; }
}
public class PageJobDetailInput : BasePageInput
{
/// <summary>
/// 作业Id
/// </summary>
public string JobId { get; set; }
/// <summary>
/// 组名称
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// 描述信息
/// </summary>
public string Description { get; set; }
}
public class AddJobDetailInput : SysJobDetail
{
/// <summary>
/// 作业Id
/// </summary>
[Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")]
public override string JobId { get; set; }
}
public class UpdateJobDetailInput : AddJobDetailInput
{
}
public class DeleteJobDetailInput : JobDetailInput
{
}
@@ -1,20 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class JobDetailOutput
{
/// <summary>
/// 作业信息
/// </summary>
public SysJobDetail JobDetail { get; set; }
/// <summary>
/// 触发器集合
/// </summary>
public List<SysJobTrigger> JobTriggers { get; set; }
}
@@ -1,43 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class JobTriggerInput
{
/// <summary>
/// 作业Id
/// </summary>
public string JobId { get; set; }
/// <summary>
/// 触发器Id
/// </summary>
public string TriggerId { get; set; }
}
public class AddJobTriggerInput : SysJobTrigger
{
/// <summary>
/// 作业Id
/// </summary>
[Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")]
public override string JobId { get; set; }
/// <summary>
/// 触发器Id
/// </summary>
[Required(ErrorMessage = "触发器Id不能为空"), MinLength(2, ErrorMessage = "触发器Id不能少于2个字符")]
public override string TriggerId { get; set; }
}
public class UpdateJobTriggerInput : AddJobTriggerInput
{
}
public class DeleteJobTriggerInput : JobTriggerInput
{
}
@@ -1,20 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class PageJobTriggerRecordInput : BasePageInput
{
/// <summary>
/// 作业Id
/// </summary>
public string JobId { get; set; }
/// <summary>
/// 触发器Id
/// </summary>
public string TriggerId { get; set; }
}
@@ -1,98 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 作业集群控制
/// </summary>
public class JobClusterServer : IJobClusterServer
{
private static readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
private readonly Random _random = new(DateTime.Now.Millisecond);
/// <summary>
/// 当前作业调度器启动通知
/// </summary>
/// <param name="context">作业集群服务上下文</param>
public async void Start(JobClusterContext context)
{
// 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting
if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId))
{
await _sysJobClusterRep.AsUpdateable().SetColumns(u => u.Status == ClusterStatus.Waiting).Where(u => u.ClusterId == context.ClusterId).ExecuteCommandAsync();
}
else
{
await _sysJobClusterRep.AsInsertable(new SysJobCluster { ClusterId = context.ClusterId, Status = ClusterStatus.Waiting }).ExecuteCommandAsync();
}
}
/// <summary>
/// 等待被唤醒
/// </summary>
/// <param name="context">作业集群服务上下文</param>
/// <returns><see cref="Task"/></returns>
public async Task WaitingForAsync(JobClusterContext context)
{
var clusterId = context.ClusterId;
while (true)
{
// 控制集群心跳频率(放在头部为了防止 IsAnyAsync continue 没sleep占用大量IO和CPU)
await Task.Delay(3000 + _random.Next(500, 1000)); // 错开集群同时启动
try
{
ICache cache = App.GetRequiredService<ICacheProvider>().Cache;
// 使用分布式锁
using (cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000))
{
// 在这里查询数据库,根据以下两种情况处理
// 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环
// 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;
// 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return;
if (await _sysJobClusterRep.IsAnyAsync(u => u.Status == ClusterStatus.Working)) continue;
await WorkNowAsync(clusterId);
return;
}
}
catch { }
}
}
/// <summary>
/// 当前作业调度器停止通知
/// </summary>
/// <param name="context">作业集群服务上下文</param>
public async void Stop(JobClusterContext context)
{
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
}
/// <summary>
/// 当前作业调度器宕机
/// </summary>
/// <param name="context">作业集群服务上下文</param>
public async void Crash(JobClusterContext context)
{
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
}
/// <summary>
/// 指示集群可以工作
/// </summary>
/// <param name="clusterId">集群 Id</param>
/// <returns></returns>
private static async Task WorkNowAsync(string clusterId)
{
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId);
}
}
-45
View File
@@ -1,45 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 作业执行监视器
/// </summary>
public class JobMonitor : IJobMonitor
{
private readonly SysConfigService _sysConfigService;
private readonly IEventPublisher _eventPublisher;
private readonly ILogger<JobMonitor> _logger;
public JobMonitor(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger<JobMonitor> logger)
{
var serviceScope = serviceScopeFactory.CreateScope();
_sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
_eventPublisher = eventPublisher;
_logger = logger;
}
public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
public async Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken)
{
if (context.Exception == null) return;
var exception = $"定时任务【{context.Trigger.Description}】错误:{context.Exception}";
// 将作业异常信息记录到本地
_logger.LogError(exception);
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysErrorMail))
{
// 将作业异常信息发送到邮件
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, exception, stoppingToken);
}
}
}
-398
View File
@@ -1,398 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统作业任务服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 320, Description = "作业任务")]
public class SysJobService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysJobDetail> _sysJobDetailRep;
private readonly SqlSugarRepository<SysJobTrigger> _sysJobTriggerRep;
private readonly SqlSugarRepository<SysJobTriggerRecord> _sysJobTriggerRecordRep;
private readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep;
private readonly ISchedulerFactory _schedulerFactory;
private readonly DynamicJobCompiler _dynamicJobCompiler;
public SysJobService(SqlSugarRepository<SysJobDetail> sysJobDetailRep,
SqlSugarRepository<SysJobTrigger> sysJobTriggerRep,
SqlSugarRepository<SysJobTriggerRecord> sysJobTriggerRecordRep,
SqlSugarRepository<SysJobCluster> sysJobClusterRep,
ISchedulerFactory schedulerFactory,
DynamicJobCompiler dynamicJobCompiler)
{
_sysJobDetailRep = sysJobDetailRep;
_sysJobTriggerRep = sysJobTriggerRep;
_sysJobTriggerRecordRep = sysJobTriggerRecordRep;
_sysJobClusterRep = sysJobClusterRep;
_schedulerFactory = schedulerFactory;
_dynamicJobCompiler = dynamicJobCompiler;
}
/// <summary>
/// 获取作业分页列表 ⏰
/// </summary>
[DisplayName("获取作业分页列表")]
public async Task<SqlSugarPagedList<JobDetailOutput>> PageJobDetail(PageJobDetailInput input)
{
var jobDetails = await _sysJobDetailRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), u => u.GroupName.Contains(input.GroupName.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.Description), u => u.Description.Contains(input.Description.Trim()))
.Select(d => new JobDetailOutput
{
JobDetail = d,
}).ToPagedListAsync(input.Page, input.PageSize);
await _sysJobDetailRep.AsSugarClient().ThenMapperAsync(jobDetails.Items, async u =>
{
u.JobTriggers = await _sysJobTriggerRep.GetListAsync(t => t.JobId == u.JobDetail.JobId);
});
// 提取中括号里面的参数值
var rgx = new Regex(@"(?i)(?<=\[)(.*)(?=\])");
foreach (var job in jobDetails.Items)
{
foreach (var jobTrigger in job.JobTriggers)
{
jobTrigger.Args = rgx.Match(jobTrigger.Args ?? "").Value;
}
}
return jobDetails;
}
/// <summary>
/// 获取作业组名称集合 ⏰
/// </summary>
[DisplayName("获取作业组名称集合")]
public async Task<List<string>> ListJobGroup()
{
return await _sysJobDetailRep.AsQueryable().Distinct().Select(e => e.GroupName).ToListAsync();
}
/// <summary>
/// 添加作业 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "AddJobDetail"), HttpPost]
[DisplayName("添加作业")]
public async Task AddJobDetail(AddJobDetailInput input)
{
var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
// 动态创建作业
Type jobType;
switch (input.CreateType)
{
case JobCreateTypeEnum.Script when string.IsNullOrEmpty(input.ScriptCode):
throw Oops.Oh(ErrorCodeEnum.D1701);
case JobCreateTypeEnum.Script:
{
jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
throw Oops.Oh(ErrorCodeEnum.D1702);
if (jobDetailAttribute.JobId != input.JobId)
throw Oops.Oh(ErrorCodeEnum.D1703);
break;
}
case JobCreateTypeEnum.Http:
jobType = typeof(HttpJob);
break;
default:
throw new NotSupportedException();
}
_schedulerFactory.AddJob(JobBuilder.Create(jobType).LoadFrom(input.Adapt<SysJobDetail>()).SetJobType(jobType));
// 延迟一下等待持久化写入,再执行其他字段的更新
await Task.Delay(500);
await _sysJobDetailRep.AsUpdateable()
.SetColumns(u => new SysJobDetail { CreateType = input.CreateType, ScriptCode = input.ScriptCode })
.Where(u => u.JobId == input.JobId).ExecuteCommandAsync();
}
/// <summary>
/// 更新作业 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "UpdateJobDetail"), HttpPost]
[DisplayName("更新作业")]
public async Task UpdateJobDetail(UpdateJobDetailInput input)
{
var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
var sysJobDetail = await _sysJobDetailRep.GetFirstAsync(u => u.Id == input.Id);
if (sysJobDetail.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1704);
var scheduler = _schedulerFactory.GetJob(sysJobDetail.JobId);
var oldScriptCode = sysJobDetail.ScriptCode; // 旧脚本代码
input.Adapt(sysJobDetail);
if (input.CreateType == JobCreateTypeEnum.Script)
{
if (string.IsNullOrEmpty(input.ScriptCode)) throw Oops.Oh(ErrorCodeEnum.D1701);
if (input.ScriptCode != oldScriptCode)
{
// 动态创建作业
var jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
throw Oops.Oh(ErrorCodeEnum.D1702);
if (jobDetailAttribute.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1703);
scheduler?.UpdateDetail(JobBuilder.Create(jobType).LoadFrom(sysJobDetail).SetJobType(jobType));
}
}
else
{
scheduler?.UpdateDetail(scheduler.GetJobBuilder().LoadFrom(sysJobDetail));
}
// Tip: 假如这次更新有变更了 JobId,变更 JobId 后触发的持久化更新执行,会由于找不到 JobId 而更新不到数据
// 延迟一下等待持久化写入,再执行其他字段的更新
await Task.Delay(500);
await _sysJobDetailRep.UpdateAsync(sysJobDetail);
}
/// <summary>
/// 删除作业 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "DeleteJobDetail"), HttpPost]
[DisplayName("删除作业")]
public async Task DeleteJobDetail(DeleteJobDetailInput input)
{
_schedulerFactory.RemoveJob(input.JobId);
// 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下面的代码确保作业和触发器能被删除
await _sysJobDetailRep.DeleteAsync(u => u.JobId == input.JobId);
await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId);
}
/// <summary>
/// 获取触发器列表 ⏰
/// </summary>
[DisplayName("获取触发器列表")]
public async Task<List<SysJobTrigger>> GetJobTriggerList([FromQuery] JobDetailInput input)
{
return await _sysJobTriggerRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId))
.ToListAsync();
}
/// <summary>
/// 添加触发器 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "AddJobTrigger"), HttpPost]
[DisplayName("添加触发器")]
public async Task AddJobTrigger(AddJobTriggerInput input)
{
var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
var jobTrigger = input.Adapt<SysJobTrigger>();
jobTrigger.Args = "[" + jobTrigger.Args + "]";
var scheduler = _schedulerFactory.GetJob(input.JobId);
scheduler?.AddTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
}
/// <summary>
/// 更新触发器 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "UpdateJobTrigger"), HttpPost]
[DisplayName("更新触发器")]
public async Task UpdateJobTrigger(UpdateJobTriggerInput input)
{
var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
var jobTrigger = input.Adapt<SysJobTrigger>();
if (jobTrigger.EndTime.HasValue && jobTrigger.EndTime.Value.Year < 1901)
{
jobTrigger.EndTime = null;
}
if (jobTrigger.StartTime.HasValue && jobTrigger.StartTime.Value.Year < 1901)
{
jobTrigger.StartTime = null;
}
jobTrigger.Args = "[" + jobTrigger.Args + "]";
var scheduler = _schedulerFactory.GetJob(input.JobId);
scheduler?.UpdateTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
}
/// <summary>
/// 删除触发器 ⏰
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "DeleteJobTrigger"), HttpPost]
[DisplayName("删除触发器")]
public async Task DeleteJobTrigger(DeleteJobTriggerInput input)
{
var scheduler = _schedulerFactory.GetJob(input.JobId);
scheduler?.RemoveTrigger(input.TriggerId);
// 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下行代码确保触发器能被删除
await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId);
}
/// <summary>
/// 暂停所有作业 ⏰
/// </summary>
/// <returns></returns>
[DisplayName("暂停所有作业")]
public void PauseAllJob()
{
_schedulerFactory.PauseAll();
}
/// <summary>
/// 启动所有作业 ⏰
/// </summary>
/// <returns></returns>
[DisplayName("启动所有作业")]
public void StartAllJob()
{
_schedulerFactory.StartAll();
}
/// <summary>
/// 暂停作业 ⏰
/// </summary>
[DisplayName("暂停作业")]
public void PauseJob(JobDetailInput input)
{
_schedulerFactory.TryPauseJob(input.JobId, out _);
}
/// <summary>
/// 启动作业 ⏰
/// </summary>
[DisplayName("启动作业")]
public void StartJob(JobDetailInput input)
{
_schedulerFactory.TryStartJob(input.JobId, out _);
}
/// <summary>
/// 取消作业 ⏰
/// </summary>
[DisplayName("取消作业")]
public void CancelJob(JobDetailInput input)
{
_schedulerFactory.TryCancelJob(input.JobId, out _);
}
/// <summary>
/// 执行作业 ⏰
/// </summary>
/// <param name="input"></param>
[DisplayName("执行作业")]
public void RunJob(JobDetailInput input)
{
if (_schedulerFactory.TryRunJob(input.JobId, out _) != ScheduleResult.Succeed) throw Oops.Oh(ErrorCodeEnum.D1705);
}
/// <summary>
/// 暂停触发器 ⏰
/// </summary>
[DisplayName("暂停触发器")]
public void PauseTrigger(JobTriggerInput input)
{
var scheduler = _schedulerFactory.GetJob(input.JobId);
scheduler?.PauseTrigger(input.TriggerId);
}
/// <summary>
/// 启动触发器 ⏰
/// </summary>
[DisplayName("启动触发器")]
public void StartTrigger(JobTriggerInput input)
{
var scheduler = _schedulerFactory.GetJob(input.JobId);
scheduler?.StartTrigger(input.TriggerId);
}
/// <summary>
/// 强制唤醒作业调度器 ⏰
/// </summary>
[DisplayName("强制唤醒作业调度器")]
public void CancelSleep()
{
_schedulerFactory.CancelSleep();
}
/// <summary>
/// 强制触发所有作业持久化 ⏰
/// </summary>
[DisplayName("强制触发所有作业持久化")]
public void PersistAll()
{
_schedulerFactory.PersistAll();
}
/// <summary>
/// 获取集群列表 ⏰
/// </summary>
[DisplayName("获取集群列表")]
public async Task<List<SysJobCluster>> GetJobClusterList()
{
return await _sysJobClusterRep.GetListAsync();
}
/// <summary>
/// 获取作业触发器运行记录分页列表 ⏰
/// </summary>
[DisplayName("获取作业触发器运行记录分页列表")]
public async Task<SqlSugarPagedList<SysJobTriggerRecord>> PageJobTriggerRecord(PageJobTriggerRecordInput input)
{
return await _sysJobTriggerRecordRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId == input.JobId)
.WhereIF(!string.IsNullOrWhiteSpace(input.TriggerId), u => u.TriggerId == input.TriggerId)
.OrderByDescending(u => u.Id)
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 清空作业触发器运行记录 🔖
/// </summary>
/// <returns></returns>
[ApiDescriptionSettings(Name = "ClearJobTriggerRecord"), HttpPost]
[DisplayName("清空作业触发器运行记录")]
public void ClearJobTriggerRecord()
{
_sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable<SysJobTriggerRecord>();
}
/// <summary>
/// 清空不保留的作业触发器运行记录 🔖
/// </summary>
/// <returns></returns>
[NonAction]
[DisplayName("清空过期的作业触发器运行记录")]
public async Task ClearExpireJobTriggerRecord(SysJobTriggerRecord input)
{
int keepRecords = 30;//保留记录条数
// 使用CopyNew()创建新的数据库连接实例,避免连接冲突
var db = _sysJobTriggerRecordRep.AsSugarClient().CopyNew();
await db.Deleteable<SysJobTriggerRecord>().In(it => it.Id,
db.Queryable<SysJobTriggerRecord>()
.Skip(keepRecords)
.OrderByDescending(it => it.LastRunTime)
.Where(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId)
.Select(it => it.Id) //注意Select不要ToList(), ToList就2次查询了
).ExecuteCommandAsync();
}
}
@@ -1,12 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public static class OAuthClaim
{
public const string GiteeAvatarUrl = "urn:gitee:avatar_url";
}
@@ -1,50 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
namespace Admin.NET.Core;
public static class OAuthSetup
{
/// <summary>
/// 三方授权登录OAuth注册
/// </summary>
/// <param name="services"></param>
public static void AddOAuth(this IServiceCollection services)
{
var authOpt = App.GetConfig<OAuthOptions>("OAuth", true);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.SameSite = SameSiteMode.Lax;
})
.AddWeixin(options =>
{
options.ClientId = authOpt.Weixin?.ClientId!;
options.ClientSecret = authOpt.Weixin?.ClientSecret!;
})
.AddGitee(options =>
{
options.ClientId = authOpt.Gitee?.ClientId!;
options.ClientSecret = authOpt.Gitee?.ClientSecret!;
options.ClaimActions.MapJsonKey(OAuthClaim.GiteeAvatarUrl, "avatar_url");
});
}
public static void UseOAuth(this IApplicationBuilder app)
{
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax });
}
}
@@ -1,120 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统OAuth服务 🧩
/// </summary>
[AllowAnonymous]
[ApiDescriptionSettings(Order = 498)]
public class SysOAuthService : IDynamicApiController, ITransient
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
public SysOAuthService(IHttpContextAccessor httpContextAccessor,
SqlSugarRepository<SysWechatUser> sysWechatUserRep)
{
_httpContextAccessor = httpContextAccessor;
_sysWechatUserRep = sysWechatUserRep;
}
/// <summary>
/// 第三方登录 🔖
/// </summary>
/// <param name="provider"></param>
/// <param name="redirectUrl"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "SignIn"), HttpGet]
[DisplayName("第三方登录")]
public virtual async Task<IActionResult> SignIn([FromQuery] string provider, [FromQuery] string redirectUrl)
{
if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider))
throw Oops.Oh("不支持的OAuth类型");
var request = _httpContextAccessor.HttpContext!.Request;
var url = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}Callback?provider={provider}&redirectUrl={redirectUrl}";
var properties = new AuthenticationProperties
{
RedirectUri = url,
Items = { ["LoginProvider"] = provider }
};
return await Task.FromResult(new ChallengeResult(provider, properties));
}
/// <summary>
/// 授权回调 🔖
/// </summary>
/// <param name="provider"></param>
/// <param name="redirectUrl"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "SignInCallback"), HttpGet]
[DisplayName("授权回调")]
public virtual async Task<IActionResult> SignInCallback([FromQuery] string provider = null, [FromQuery] string redirectUrl = "")
{
if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider))
throw Oops.Oh("不支持的OAuth类型");
var authenticateResult = await _httpContextAccessor.HttpContext!.AuthenticateAsync(provider);
if (!authenticateResult.Succeeded)
throw Oops.Oh("授权失败");
var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier);
if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value))
throw Oops.Oh("授权失败");
var name = authenticateResult.Principal.FindFirst(ClaimTypes.Name)?.Value;
var email = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value;
var mobilePhone = authenticateResult.Principal.FindFirst(ClaimTypes.MobilePhone)?.Value;
var dateOfBirth = authenticateResult.Principal.FindFirst(ClaimTypes.DateOfBirth)?.Value;
var gender = authenticateResult.Principal.FindFirst(ClaimTypes.Gender)?.Value;
var avatarUrl = "";
var platformType = PlatformTypeEnum.微信公众号;
if (provider == "Gitee")
{
platformType = PlatformTypeEnum.Gitee;
avatarUrl = authenticateResult.Principal.FindFirst(OAuthClaim.GiteeAvatarUrl)?.Value;
}
// 若账号不存在则新建
var wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value);
if (wechatUser == null)
{
var userId = await App.GetRequiredService<SysUserService>().AddUser(new AddUserInput()
{
Account = name,
RealName = name,
NickName = name,
Email = email,
Avatar = avatarUrl,
Phone = mobilePhone,
OrgId = 1300000000101, // 根组织架构
RoleIdList = new List<long> { 1300000000104 } // 仅本人数据角色
});
await _sysWechatUserRep.InsertAsync(new SysWechatUser()
{
UserId = userId,
OpenId = openIdClaim.Value,
Avatar = avatarUrl,
NickName = name,
PlatformType = platformType
});
wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value);
}
// 构建Token令牌
var token = await App.GetRequiredService<SysAuthService>().CreateToken(wechatUser.SysUser);
return new RedirectResult($"{redirectUrl}/#/login?token={token.AccessToken}");
}
}
@@ -1,42 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class PagePluginInput : BasePageInput
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 编码
/// </summary>
public string Code { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long TenantId { get; set; }
}
public class AddPluginInput : SysPlugin
{
/// <summary>
/// 名称
/// </summary>
[Required(ErrorMessage = "功能名称不能为空")]
public override string Name { get; set; }
}
public class UpdatePluginInput : AddPluginInput
{
}
public class DeletePluginInput : BaseIdInput
{
}
@@ -1,126 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统动态插件服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 245)]
public class SysPluginService : IDynamicApiController, ITransient
{
private readonly IDynamicApiRuntimeChangeProvider _provider;
private readonly SqlSugarRepository<SysPlugin> _sysPluginRep;
private readonly UserManager _userManager;
public SysPluginService(IDynamicApiRuntimeChangeProvider provider,
SqlSugarRepository<SysPlugin> sysPluginRep,
UserManager userManager)
{
_provider = provider;
_userManager = userManager;
_sysPluginRep = sysPluginRep;
}
/// <summary>
/// 获取动态插件列表 🧩
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取动态插件列表")]
public async Task<SqlSugarPagedList<SysPlugin>> Page(PagePluginInput input)
{
return await _sysPluginRep.AsQueryable()
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name))
.OrderBy(u => new { u.OrderNo, u.Id })
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 增加动态插件 🧩
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加动态插件")]
public async Task AddPlugin(AddPluginInput input)
{
var isExist = await _sysPluginRep.IsAnyAsync(u => u.Name == input.Name || u.AssemblyName == input.AssemblyName);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900);
// 添加动态程序集/接口
input.AssemblyName = CompileAssembly(input.CsharpCode, input.AssemblyName);
await _sysPluginRep.InsertAsync(input.Adapt<SysPlugin>());
}
/// <summary>
/// 更新动态插件 🧩
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新动态插件")]
public async Task UpdatePlugin(UpdatePluginInput input)
{
var isExist = await _sysPluginRep.IsAnyAsync(u => (u.Name == input.Name || u.AssemblyName == input.AssemblyName) && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900);
// 先移除再添加动态程序集/接口
RemoveAssembly(input.AssemblyName);
input.AssemblyName = CompileAssembly(input.CsharpCode);
await _sysPluginRep.AsUpdateable(input.Adapt<SysPlugin>()).IgnoreColumns(true).ExecuteCommandAsync();
}
/// <summary>
/// 删除动态插件 🧩
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除动态插件")]
public async Task DeletePlugin(DeletePluginInput input)
{
var plugin = await _sysPluginRep.GetByIdAsync(input.Id);
if (plugin == null) return;
// 移除动态程序集/接口
RemoveAssembly(plugin.AssemblyName);
await _sysPluginRep.DeleteAsync(u => u.Id == input.Id);
}
/// <summary>
/// 添加动态程序集/接口 🧩
/// </summary>
/// <param name="csharpCode"></param>
/// <param name="assemblyName">程序集名称</param>
/// <returns></returns>
[DisplayName("添加动态程序集/接口")]
public string CompileAssembly([FromBody] string csharpCode, [FromQuery] string assemblyName = default)
{
// 编译 C# 代码并返回动态程序集
var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName);
// 将程序集添加进动态 WebAPI 应用部件
_provider.AddAssembliesWithNotifyChanges(dynamicAssembly);
// 返回动态程序集名称
return dynamicAssembly.GetName().Name;
}
/// <summary>
/// 移除动态程序集/接口 🧩
/// </summary>
[ApiDescriptionSettings(Name = "RemoveAssembly"), HttpPost]
[DisplayName("移除动态程序集/接口")]
public void RemoveAssembly(string assemblyName)
{
_provider.RemoveAssembliesWithNotifyChanges(assemblyName);
}
}
@@ -1,42 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class PagePrintInput : BasePageInput
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 编码
/// </summary>
public string Code { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long TenantId { get; set; }
}
public class AddPrintInput : SysPrint
{
/// <summary>
/// 名称
/// </summary>
[Required(ErrorMessage = "模板名称不能为空")]
public override string Name { get; set; }
}
public class UpdatePrintInput : AddPrintInput
{
}
public class DeletePrintInput : BaseIdInput
{
}
@@ -1,91 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统打印模板服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 305)]
public class SysPrintService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysPrint> _sysPrintRep;
private readonly UserManager _userManager;
public SysPrintService(SqlSugarRepository<SysPrint> sysPrintRep, UserManager userManager)
{
_sysPrintRep = sysPrintRep;
_userManager = userManager;
}
/// <summary>
/// 获取打印模板列表 🖨️
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取打印模板列表")]
public async Task<SqlSugarPagedList<SysPrint>> Page(PagePrintInput input)
{
return await _sysPrintRep.AsQueryable()
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name))
.OrderBy(u => new { u.OrderNo, u.Id })
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 获取打印模板 🖨️
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[DisplayName("获取打印模板")]
public async Task<SysPrint> GetPrint(string name)
{
return await _sysPrintRep.GetFirstAsync(u => u.Name == name);
}
/// <summary>
/// 增加打印模板 🖨️
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加打印模板")]
public async Task AddPrint(AddPrintInput input)
{
var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800);
await _sysPrintRep.InsertAsync(input.Adapt<SysPrint>());
}
/// <summary>
/// 更新打印模板 🖨️
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新打印模板")]
public async Task UpdatePrint(UpdatePrintInput input)
{
var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.Id);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800);
await _sysPrintRep.AsUpdateable(input.Adapt<SysPrint>()).IgnoreColumns(true).ExecuteCommandAsync();
}
/// <summary>
/// 删除打印模板 🖨️
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除打印模板")]
public async Task DeletePrint(DeletePrintInput input)
{
await _sysPrintRep.DeleteAsync(u => u.Id == input.Id);
}
}
@@ -1,44 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class ScheduleInput : BaseIdInput
{
/// <summary>
/// 状态
/// </summary>
public virtual FinishStatusEnum Status { get; set; }
}
public class ListScheduleInput
{
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long TenantId { get; set; }
}
public class AddScheduleInput : SysSchedule
{
/// <summary>
/// 日程内容
/// </summary>
[Required(ErrorMessage = "日程内容不能为空")]
public override string Content { get; set; }
}
public class UpdateScheduleInput : AddScheduleInput
{
}
public class DeleteScheduleInput : BaseIdInput
{
}
@@ -1,104 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统日程服务
/// </summary>
[ApiDescriptionSettings(Order = 295)]
public class SysScheduleService : IDynamicApiController, ITransient
{
private readonly UserManager _userManager;
private readonly SqlSugarRepository<SysSchedule> _sysSchedule;
public SysScheduleService(UserManager userManager,
SqlSugarRepository<SysSchedule> sysSchedule)
{
_userManager = userManager;
_sysSchedule = sysSchedule;
}
/// <summary>
/// 获取日程列表
/// </summary>
/// <returns></returns>
[DisplayName("获取日程列表")]
public async Task<List<SysSchedule>> Page(ListScheduleInput input)
{
return await _sysSchedule.AsQueryable()
.Where(u => u.UserId == _userManager.UserId)
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.ScheduleTime >= input.StartTime)
.WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.ScheduleTime <= input.EndTime)
.OrderBy(u => u.StartTime, OrderByType.Asc)
.ToListAsync();
}
/// <summary>
/// 获取日程详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[DisplayName("获取日程详情")]
public async Task<SysSchedule> GetDetail(long id)
{
return await _sysSchedule.GetFirstAsync(u => u.Id == id);
}
/// <summary>
/// 增加日程
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加日程")]
public async Task AddUserSchedule(AddScheduleInput input)
{
input.UserId = _userManager.UserId;
await _sysSchedule.InsertAsync(input.Adapt<SysSchedule>());
}
/// <summary>
/// 更新日程
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新日程")]
public async Task UpdateUserSchedule(UpdateScheduleInput input)
{
await _sysSchedule.AsUpdateable(input.Adapt<SysSchedule>()).IgnoreColumns(true).ExecuteCommandAsync();
}
/// <summary>
/// 删除日程
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除日程")]
public async Task DeleteUserSchedule(DeleteScheduleInput input)
{
await _sysSchedule.DeleteAsync(u => u.Id == input.Id);
}
/// <summary>
/// 设置日程状态
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("设置日程状态")]
public async Task<int> SetStatus(ScheduleInput input)
{
if (!Enum.IsDefined(typeof(FinishStatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005);
return await _sysSchedule.AsUpdateable()
.SetColumns(u => u.Status == input.Status)
.Where(u => u.Id == input.Id)
.ExecuteCommandAsync();
}
}
@@ -1,4 +1,4 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
@@ -136,8 +136,6 @@ public class SysServerService : IDynamicApiController, ITransient
var pdfAssembly = typeof(Magicodes.ExporterAndImporter.Pdf.IPdfExporter).Assembly.GetName();
var wordAssembly = typeof(Magicodes.ExporterAndImporter.Word.IWordExporter).Assembly.GetName();
var captchaAssembly = typeof(Lazy.Captcha.Core.ICaptcha).Assembly.GetName();
var wechatApiAssembly = typeof(WechatApiClient).Assembly.GetName();
var wechatTenpayAssembly = typeof(WechatTenpayClient).Assembly.GetName();
var ossAssembly = typeof(OnceMi.AspNetCore.OSS.IOSSServiceFactory).Assembly.GetName();
var parserAssembly = typeof(Parser).Assembly.GetName();
var elasticsearchClientAssembly = typeof(Elastic.Clients.Elasticsearch.ElasticsearchClient).Assembly.GetName();
@@ -148,10 +146,7 @@ public class SysServerService : IDynamicApiController, ITransient
var alibabaSendSmsRequestAssembly = typeof(AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest).Assembly.GetName();
var tencentSendSmsRequestAssembly = typeof(TencentCloud.Sms.V20190711.Models.SendSmsRequest).Assembly.GetName();
var rabbitMQAssembly = typeof(RabbitMQEventSourceStore).Assembly.GetName();
var ldapConnectionAssembly = typeof(Novell.Directory.Ldap.LdapConnection).Assembly.GetName();
var ipToolAssembly = typeof(IPTools.Core.IpTool).Assembly.GetName();
var weixinAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Weixin.WeixinAuthenticationOptions).Assembly.GetName();
var giteeAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Gitee.GiteeAuthenticationOptions).Assembly.GetName();
var hashidsAssembly = typeof(HashidsNet.Hashids).Assembly.GetName();
var sftpClientAssembly = typeof(Renci.SshNet.SftpClient).Assembly.GetName();
var hardwareInfoAssembly = typeof(Hardware.Info.HardwareInfo).Assembly.GetName();
@@ -167,8 +162,6 @@ public class SysServerService : IDynamicApiController, ITransient
new { pdfAssembly.Name, pdfAssembly.Version },
new { wordAssembly.Name, wordAssembly.Version },
new { captchaAssembly.Name, captchaAssembly.Version },
new { wechatApiAssembly.Name, wechatApiAssembly.Version },
new { wechatTenpayAssembly.Name, wechatTenpayAssembly.Version },
new { ossAssembly.Name, ossAssembly.Version },
new { parserAssembly.Name, parserAssembly.Version },
new { elasticsearchClientAssembly.Name, elasticsearchClientAssembly.Version },
@@ -179,13 +172,10 @@ public class SysServerService : IDynamicApiController, ITransient
new { alibabaSendSmsRequestAssembly.Name, alibabaSendSmsRequestAssembly.Version },
new { tencentSendSmsRequestAssembly.Name, tencentSendSmsRequestAssembly.Version },
new { rabbitMQAssembly.Name, rabbitMQAssembly.Version },
new { ldapConnectionAssembly.Name, ldapConnectionAssembly.Version },
new { ipToolAssembly.Name, ipToolAssembly.Version },
new { weixinAuthenticationOptionsAssembly.Name, weixinAuthenticationOptionsAssembly.Version },
new { giteeAuthenticationOptionsAssembly.Name, giteeAuthenticationOptionsAssembly.Version },
new { hashidsAssembly.Name, hashidsAssembly.Version },
new { sftpClientAssembly.Name, sftpClientAssembly.Version },
new { hardwareInfoAssembly.Name, hardwareInfoAssembly.Version },
};
}
}
}
+1 -7
View File
@@ -1,4 +1,4 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
@@ -65,12 +65,6 @@ public class AddUserInput : SysUser
/// </summary>
[Required(ErrorMessage = "真实姓名不能为空")]
public override string RealName { get; set; }
/// <summary>
/// 域用户
/// </summary>
public string DomainAccount { get; set; }
/// <summary>
/// 角色集合
/// </summary>
@@ -1,4 +1,4 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
@@ -21,10 +21,4 @@ public class UserOutput : SysUser
/// <summary>
/// 角色名称
/// </summary>
public string RoleName { get; set; }
/// <summary>
/// 域用户
/// </summary>
public string DomainAccount { get; set; }
}
public string RoleName { get; set; }}
@@ -1,66 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 用户域账号服务
/// </summary>
public class SysUserLdapService : ITransient
{
private readonly SqlSugarRepository<SysUserLdap> _sysUserLdapRep;
public SysUserLdapService(SqlSugarRepository<SysUserLdap> sysUserLdapRep)
{
_sysUserLdapRep = sysUserLdapRep;
}
/// <summary>
/// 批量插入域账号
/// </summary>
/// <param name="tenantId"></param>
/// <param name="sysUserLdapList"></param>
/// <returns></returns>
public async Task InsertUserLdapList(long tenantId, List<SysUserLdap> sysUserLdapList)
{
await _sysUserLdapRep.DeleteAsync(u => u.TenantId == tenantId);
await _sysUserLdapRep.InsertRangeAsync(sysUserLdapList);
await _sysUserLdapRep.AsUpdateable()
.InnerJoin<SysUser>((l, u) => l.EmployeeId == u.Account)
.SetColumns((l, u) => new SysUserLdap { UserId = u.Id })
.Where((l, u) => l.TenantId == tenantId && u.Status == StatusEnum.Enable)
.ExecuteCommandAsync();
}
/// <summary>
/// 增加域账号
/// </summary>
/// <param name="tenantId"></param>
/// <param name="userId"></param>
/// <param name="account"></param>
/// <param name="domainAccount"></param>
/// <returns></returns>
public async Task AddUserLdap(long tenantId, long userId, string account, string domainAccount)
{
var userLdap = await _sysUserLdapRep.GetFirstAsync(u => u.TenantId == tenantId && (u.Account == account || u.UserId == userId || u.EmployeeId == domainAccount));
if (userLdap != null) await _sysUserLdapRep.DeleteByIdAsync(userLdap.Id);
if (!string.IsNullOrWhiteSpace(domainAccount))
await _sysUserLdapRep.InsertAsync(new SysUserLdap { EmployeeId = account, TenantId = tenantId, UserId = userId, Account = domainAccount });
}
/// <summary>
/// 删除域账号
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task DeleteUserLdapByUserId(long userId)
{
await _sysUserLdapRep.DeleteAsync(u => u.UserId == userId);
}
}
@@ -20,7 +20,6 @@ public class SysUserService : IDynamicApiController, ITransient
private readonly SysOnlineUserService _sysOnlineUserService;
private readonly SysUserMenuService _sysUserMenuService;
private readonly SysCacheService _sysCacheService;
private readonly SysUserLdapService _sysUserLdapService;
private readonly SqlSugarRepository<SysUser> _sysUserRep;
private readonly IEventPublisher _eventPublisher;
@@ -31,7 +30,6 @@ public class SysUserService : IDynamicApiController, ITransient
SysConfigService sysConfigService,
SysOnlineUserService sysOnlineUserService,
SysCacheService sysCacheService,
SysUserLdapService sysUserLdapService,
SqlSugarRepository<SysUser> sysUserRep,
SysUserMenuService sysUserMenuService,
IEventPublisher eventPublisher)
@@ -43,7 +41,6 @@ public class SysUserService : IDynamicApiController, ITransient
_sysConfigService = sysConfigService;
_sysOnlineUserService = sysOnlineUserService;
_sysCacheService = sysCacheService;
_sysUserLdapService = sysUserLdapService;
_sysUserMenuService = sysUserMenuService;
_sysUserRep = sysUserRep;
_eventPublisher = eventPublisher;
@@ -77,7 +74,6 @@ public class SysUserService : IDynamicApiController, ITransient
OrgName = a.Name,
PosName = b.Name,
RoleName = SqlFunc.Subqueryable<SysUserRole>().LeftJoin<SysRole>((m, n) => m.RoleId == n.Id).Where(m => m.UserId == u.Id).SelectStringJoin((m, n) => n.Name, ","),
DomainAccount = SqlFunc.Subqueryable<SysUserLdap>().Where(m => m.UserId == u.Id).Select(m => m.Account)
}, true)
.ToPagedListAsync(input.Page, input.PageSize);
}
@@ -110,10 +106,6 @@ public class SysUserService : IDynamicApiController, ITransient
input.Id = newUser.Id;
await UpdateRoleAndExtOrg(input);
// 增加域账号
if (!string.IsNullOrWhiteSpace(input.DomainAccount))
await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount);
// 发布系统用户操作事件
await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Add, new
{
@@ -153,10 +145,6 @@ public class SysUserService : IDynamicApiController, ITransient
input.Id = newUser.Id;
await UpdateRoleAndExtOrg(input);
// 增加域账号
if (!string.IsNullOrWhiteSpace(input.DomainAccount))
await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount);
// 发布系统用户操作事件
await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Register, new
{
@@ -199,8 +187,6 @@ public class SysUserService : IDynamicApiController, ITransient
var roleIds = await GetOwnRoleList(input.Id);
if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u)))
await _sysOnlineUserService.ForceOffline(input.Id);
// 更新域账号
await _sysUserLdapService.AddUserLdap(user.TenantId!.Value, user.Id, user.Account, input.DomainAccount);
// 发布系统用户操作事件
await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Update, new
@@ -270,9 +256,6 @@ public class SysUserService : IDynamicApiController, ITransient
// 删除用户扩展机构
await _sysUserExtOrgService.DeleteUserExtOrgByUserId(input.Id);
// 删除域账号
await _sysUserLdapService.DeleteUserLdapByUserId(input.Id);
// 删除用户收藏菜单
await _sysUserMenuService.DeleteUserMenuList(input.Id);
@@ -1,149 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 生成网页授权Url
/// </summary>
public class GenAuthUrlInput
{
/// <summary>
/// RedirectUrl
/// </summary>
public string RedirectUrl { get; set; }
/// <summary>
/// Scope
/// </summary>
public string Scope { get; set; }
/// <summary>
/// State
/// </summary>
public string State { get; set; }
}
/// <summary>
/// 获取微信用户OpenId
/// </summary>
public class WechatOAuth2Input
{
/// <summary>
/// Code
/// </summary>
[Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")]
public string Code { get; set; }
}
/// <summary>
/// 微信用户登录
/// </summary>
public class WechatUserLogin
{
/// <summary>
/// OpenId
/// </summary>
[Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识长错误")]
public string OpenId { get; set; }
}
/// <summary>
/// 获取配置签名
/// </summary>
public class SignatureInput
{
/// <summary>
/// Url
/// </summary>
public string Url { get; set; }
}
/// <summary>
/// 获取消息模板列表
/// </summary>
public class MessageTemplateSendInput
{
/// <summary>
/// 订阅模板Id
/// </summary>
[Required(ErrorMessage = "订阅模板Id不能为空")]
public string TemplateId { get; set; }
/// <summary>
/// 接收者的OpenId
/// </summary>
[Required(ErrorMessage = "接收者的OpenId不能为空")]
public string ToUserOpenId { get; set; }
/// <summary>
/// 模板数据,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
/// </summary>
[Required(ErrorMessage = "模板数据不能为空")]
public Dictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem> Data { get; set; }
/// <summary>
/// 模板跳转链接
/// </summary>
public string Url { get; set; }
/// <summary>
/// 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar)
/// </summary>
public string MiniProgramPagePath { get; set; }
}
/// <summary>
/// 删除消息模板
/// </summary>
public class DeleteMessageTemplateInput
{
/// <summary>
/// 订阅模板Id
/// </summary>
[Required(ErrorMessage = "订阅模板Id不能为空")]
public string TemplateId { get; set; }
}
public class UploadAvatarInput
{
/// <summary>
/// 小程序用户身份标识
/// </summary>
[Required(ErrorMessage = "OpenId不能为空")]
public string OpenId { get; set; }
/// <summary>
/// 文件
/// </summary>
[Required]
public IFormFile File { get; set; }
/// <summary>
/// 文件类型
/// </summary>
public string FileType { get; set; }
/// <summary>
/// 文件路径
/// </summary>
public string Path { get; set; }
}
public class SetNickNameInput
{
/// <summary>
/// 小程序用户身份标识
/// </summary>
[Required(ErrorMessage = "OpenId不能为空")]
public string OpenId { get; set; }
/// <summary>
/// 昵称
/// </summary>
[Required(ErrorMessage = "昵称不能为空")]
public string NickName { get; set; }
}
@@ -1,87 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class WechatPayTransactionInput
{
/// <summary>
/// OpenId
/// </summary>
public string OpenId { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public int Total { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 附加数据
/// </summary>
public string Attachment { get; set; }
/// <summary>
/// 优惠标记
/// </summary>
public string GoodsTag { get; set; }
/// <summary>
/// 业务标签,用来区分做什么业务
/// </summary>
public string Tags { get; set; }
/// <summary>
/// 对应业务的主键
/// </summary>
public long BusinessId { get; set; }
}
public class WechatPayParaInput
{
/// <summary>
/// 订单Id
/// </summary>
public string PrepayId { get; set; }
}
public class WechatPayRefundDomesticInput
{
/// <summary>
/// 商户端生成的业务流水号
/// </summary>
[Required]
public string TradeId { get; set; }
/// <summary>
/// 退款原因
/// </summary>
public string Reason { get; set; }
/// <summary>
/// 退款金额
/// </summary>
[Required]
public int Refund { get; set; }
/// <summary>
/// 原订单金额
/// </summary>
[Required]
public int Total { get; set; }
}
public class WechatPayPageInput : BasePageInput
{
/// <summary>
/// 添加时间范围
/// </summary>
public List<DateTime?> CreateTimeRange { get; set; }
}
@@ -1,54 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class WechatPayOutput
{
/// <summary>
/// OpenId
/// </summary>
public string OpenId { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public int Total { get; set; }
/// <summary>
/// 附加数据
/// </summary>
public string Attachment { get; set; }
/// <summary>
/// 优惠标记
/// </summary>
public string GoodsTag { get; set; }
}
public class WechatPayTransactionOutput
{
public string PrepayId { get; set; }
public string OutTradeNumber { get; set; }
public WechatPayParaOutput SingInfo { get; set; }
}
public class WechatPayParaOutput
{
public string AppId { get; set; }
public string TimeStamp { get; set; }
public string NonceStr { get; set; }
public string Package { get; set; }
public string SignType { get; set; }
public string PaySign { get; set; }
}
@@ -1,24 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class WechatUserInput : BasePageInput
{
/// <summary>
/// 昵称
/// </summary>
public string NickName { get; set; }
/// <summary>
/// 手机号码
/// </summary>
public string Mobile { get; set; }
}
public class DeleteWechatUserInput : BaseIdInput
{
}
@@ -1,150 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 获取微信用户OpenId
/// </summary>
public class JsCode2SessionInput
{
/// <summary>
/// JsCode
/// </summary>
[Required(ErrorMessage = "JsCode不能为空"), MinLength(10, ErrorMessage = "JsCode错误")]
public string JsCode { get; set; }
}
/// <summary>
/// 获取微信用户电话号码
/// </summary>
public class WxPhoneInput : WxOpenIdLoginInput
{
/// <summary>
/// Code
/// </summary>
[Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")]
public string Code { get; set; }
}
/// <summary>
/// 微信小程序登录
/// </summary>
public class WxOpenIdLoginInput
{
/// <summary>
/// OpenId
/// </summary>
[Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识错误")]
public string OpenId { get; set; }
}
/// <summary>
/// 微信手机号登录
/// </summary>
public class WxPhoneLoginInput
{
/// <summary>
/// 电话号码
/// </summary>
[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "电话号码错误")]
public string PhoneNumber { get; set; }
}
/// <summary>
/// 发送订阅消息
/// </summary>
public class SendSubscribeMessageInput
{
/// <summary>
/// 订阅模板Id
/// </summary>
[Required(ErrorMessage = "订阅模板Id不能为空")]
public string TemplateId { get; set; }
/// <summary>
/// 接收者的OpenId
/// </summary>
[Required(ErrorMessage = "接收者的OpenId不能为空")]
public string ToUserOpenId { get; set; }
/// <summary>
/// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
/// </summary>
[Required(ErrorMessage = "模板内容不能为空")]
public Dictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem> Data { get; set; }
/// <summary>
/// 跳转小程序类型
/// </summary>
public string MiniprogramState { get; set; }
/// <summary>
/// 语言类型
/// </summary>
public string Language { get; set; }
/// <summary>
/// 点击模板卡片后的跳转页面(仅限本小程序内的页面),支持带参数(示例pages/app/index?foo=bar)
/// </summary>
public string MiniProgramPagePath { get; set; }
}
/// <summary>
/// 增加订阅消息模板
/// </summary>
public class AddSubscribeMessageTemplateInput
{
/// <summary>
/// 模板标题Id
/// </summary>
[Required(ErrorMessage = "模板标题Id不能为空")]
public string TemplateTitleId { get; set; }
/// <summary>
/// 模板关键词列表,例如 [3,5,4]
/// </summary>
[Required(ErrorMessage = "模板关键词列表不能为空")]
public List<int> KeyworkIdList { get; set; }
/// <summary>
/// 服务场景描述,15个字以内
/// </summary>
[Required(ErrorMessage = "服务场景描述不能为空")]
public string SceneDescription { get; set; }
}
/// <summary>
/// 生成带参数小程序二维码(总共生成的码数量限制为 100,000)
/// </summary>
public class GenerateQRImageInput
{
/// <summary>
/// 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages/index?id=0001
/// </summary>
public string PagePath { get; set; }
/// <summary>
/// 文件保存的名称
/// </summary>
public string ImageName { get; set; }
/// <summary>
/// 图片宽度 默认430
/// </summary>
public int Width { get; set; } = 430;
}
/// <summary>
/// 生成带参数小程序二维码(获取不受限制的小程序码)
/// </summary>
public class GenerateQRImageUnLimitInput : GenerateQRImageInput
{
/// <summary>
/// 二维码携带的参数 eg:a=1(最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:<!-- !#$&'()*+,/:;=?@-._~ -->)
/// </summary>
public string Scene { get; set; }
}
@@ -1,40 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
public class WxOpenIdOutput
{
public string OpenId { get; set; }
}
public class WxPhoneOutput
{
public string PhoneNumber { get; set; }
}
public class GenerateQRImageOutput
{
/// <summary>
/// 生成状态
/// </summary>
public bool Success { get; set; } = false;
/// <summary>
/// 生成图片的绝对路径
/// </summary>
public string ImgPath { get; set; } = "";
/// <summary>
/// 生成图片的相对路径
/// </summary>
public string RelativeImgPath { get; set; } = "";
/// <summary>
/// 生成图片的错误信息
/// </summary>
public string Message { get; set; } = "";
}
@@ -1,520 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Furion.Logging.Extensions;
using Newtonsoft.Json;
namespace Admin.NET.Core.Service;
/// <summary>
/// 微信支付服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 210)]
public class SysWechatPayService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysWechatPay> _sysWechatPayRep;
private readonly SqlSugarRepository<SysWechatRefund> _sysWechatRefundRep;
private readonly WechatPayOptions _wechatPayOptions;
private readonly PayCallBackOptions _payCallBackOptions;
private readonly WechatTenpayClient _wechatTenpayClient;
public SysWechatPayService(SqlSugarRepository<SysWechatPay> sysWechatPayUserRep,
SqlSugarRepository<SysWechatRefund> sysWechatRefundRep,
IOptions<WechatPayOptions> wechatPayOptions,
IOptions<PayCallBackOptions> payCallBackOptions)
{
_sysWechatPayRep = sysWechatPayUserRep;
_sysWechatRefundRep = sysWechatRefundRep;
_wechatPayOptions = wechatPayOptions.Value;
_payCallBackOptions = payCallBackOptions.Value;
_wechatTenpayClient = CreateTenpayClient();
}
/// <summary>
/// 初始化微信支付客户端
/// </summary>
/// <returns></returns>
private WechatTenpayClient CreateTenpayClient()
{
var cerFilePath = Path.Combine(App.WebHostEnvironment.ContentRootPath, _wechatPayOptions.MerchantCertificatePrivateKey.Replace("/", Path.DirectorySeparatorChar.ToString()));
var tenpayClientOptions = new WechatTenpayClientOptions()
{
MerchantId = _wechatPayOptions.MerchantId,
MerchantV3Secret = _wechatPayOptions.MerchantV3Secret,
MerchantCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber,
MerchantCertificatePrivateKey = File.Exists(cerFilePath) ? File.ReadAllText(cerFilePath) : "",
PlatformCertificateManager = new InMemoryCertificateManager()
};
return new WechatTenpayClient(tenpayClientOptions);
}
/// <summary>
/// 分页查询支付列表 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
[ApiDescriptionSettings(Name = "Page")]
public async Task<SqlSugarPagedList<SysWechatPay>> Page(WechatPayPageInput input)
{
var query = _sysWechatPayRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.OutTradeNumber == input.Keyword || u.TransactionId == input.Keyword)
.WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 0 && input.CreateTimeRange[0].HasValue, x => x.CreateTime >= input.CreateTimeRange[0])
.WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 1 && input.CreateTimeRange[1].HasValue, x => x.CreateTime < ((DateTime)input.CreateTimeRange[1]).AddDays(1));
return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 查询退款信息列表
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost]
[DisplayName("根据支付id获取退款信息列表")]
public async Task<List<SysWechatRefund>> ListRefund([FromBody] string id)
{
var query = _sysWechatRefundRep.AsQueryable()
.Where(u => u.TransactionId == id);
return await query.ToListAsync();
}
/// <summary>
/// 生成JSAPI调起支付所需参数 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("生成JSAPI调起支付所需参数")]
public WechatPayParaOutput GenerateParametersForJsapiPay(WechatPayParaInput input)
{
var data = _wechatTenpayClient.GenerateParametersForJsapiPayRequest(_wechatPayOptions.AppId, input.PrepayId);
return new WechatPayParaOutput()
{
AppId = data["appId"],
TimeStamp = data["timeStamp"],
NonceStr = data["nonceStr"],
Package = data["package"],
SignType = data["signType"],
PaySign = data["paySign"]
};
}
/// <summary>
/// 微信支付下单(商户直连) 🔖
/// </summary>
[DisplayName("微信支付下单(商户直连)")]
public async Task<WechatPayTransactionOutput> CreatePayTransaction([FromBody] WechatPayTransactionInput input)
{
var request = new CreatePayTransactionJsapiRequest()
{
OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号
AppId = _wechatPayOptions.AppId,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
ExpireTime = DateTimeOffset.Now.AddMinutes(10),
NotifyUrl = _payCallBackOptions.WechatPayUrl,
Amount = new CreatePayTransactionJsapiRequest.Types.Amount() { Total = input.Total },
Payer = new CreatePayTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId }
};
var response = await _wechatTenpayClient.ExecuteCreatePayTransactionJsapiAsync(request);
if (!response.IsSuccessful())
throw Oops.Oh(response.ErrorMessage);
var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId });
// 保存订单信息
var wechatPay = new SysWechatPay()
{
AppId = _wechatPayOptions.AppId,
MerchantId = _wechatPayOptions.MerchantId,
OutTradeNumber = request.OutTradeNumber,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
Total = input.Total,
OpenId = input.OpenId,
TransactionId = "",
Tags = input.Tags,
BusinessId = input.BusinessId,
};
await _sysWechatPayRep.InsertAsync(wechatPay);
return new WechatPayTransactionOutput()
{
PrepayId = response.PrepayId,
OutTradeNumber = request.OutTradeNumber,
SingInfo = singInfo
};
}
/// <summary>
/// 微信支付下单(商户直连)Native
/// </summary>
[DisplayName("微信支付下单(商户直连)Native")]
public async Task<dynamic> CreatePayTransactionNative([FromBody] WechatPayTransactionInput input)
{
var request = new CreatePayTransactionNativeRequest()
{
OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号
AppId = _wechatPayOptions.AppId,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
ExpireTime = DateTimeOffset.Now.AddMinutes(10),
NotifyUrl = _payCallBackOptions.WechatPayUrl,
Amount = new CreatePayTransactionNativeRequest.Types.Amount() { Total = input.Total },
//Payer = new CreatePayTransactionNativeRequest.Types.Payer() { OpenId = input.OpenId }
Scene = new CreatePayTransactionNativeRequest.Types.Scene() { ClientIp = "127.0.0.1" }
};
var response = await _wechatTenpayClient.ExecuteCreatePayTransactionNativeAsync(request);
if (!response.IsSuccessful())
{
JsonConvert.SerializeObject(response).LogInformation();
throw Oops.Oh(response.ErrorMessage);
}
// 保存订单信息
var wechatPay = new SysWechatPay()
{
AppId = _wechatPayOptions.AppId,
MerchantId = _wechatPayOptions.MerchantId,
OutTradeNumber = request.OutTradeNumber,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
Total = input.Total,
//OpenId = input.OpenId,
TransactionId = "",
QrcodeContent = response.QrcodeUrl,
Tags = input.Tags,
BusinessId = input.BusinessId,
};
await _sysWechatPayRep.InsertAsync(wechatPay);
return new
{
request.OutTradeNumber,
response.QrcodeUrl
};
}
/// <summary>
/// 微信支付下单(服务商模式) 🔖
/// </summary>
[DisplayName("微信支付下单(服务商模式)")]
public async Task<dynamic> CreatePayPartnerTransaction([FromBody] WechatPayTransactionInput input)
{
var request = new CreatePayPartnerTransactionJsapiRequest()
{
OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号
AppId = _wechatPayOptions.AppId,
MerchantId = _wechatPayOptions.MerchantId,
SubAppId = _wechatPayOptions.AppId,
SubMerchantId = _wechatPayOptions.MerchantId,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
ExpireTime = DateTimeOffset.Now.AddMinutes(10),
NotifyUrl = _payCallBackOptions.WechatPayUrl,
Amount = new CreatePayPartnerTransactionJsapiRequest.Types.Amount() { Total = input.Total },
Payer = new CreatePayPartnerTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId }
};
var response = await _wechatTenpayClient.ExecuteCreatePayPartnerTransactionJsapiAsync(request);
if (!response.IsSuccessful())
throw Oops.Oh(response.ErrorMessage);
var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId });
// 保存订单信息
var wechatPay = new SysWechatPay()
{
AppId = _wechatPayOptions.AppId,
MerchantId = _wechatPayOptions.MerchantId,
SubAppId = _wechatPayOptions.AppId,
SubMerchantId = _wechatPayOptions.MerchantId,
OutTradeNumber = request.OutTradeNumber,
Description = input.Description,
Attachment = input.Attachment,
GoodsTag = input.GoodsTag,
Total = input.Total,
OpenId = input.OpenId,
TransactionId = ""
};
await _sysWechatPayRep.InsertAsync(wechatPay);
return new
{
response.PrepayId,
request.OutTradeNumber,
singInfo
};
}
/// <summary>
/// 获取支付订单详情(本地库) 🔖
/// </summary>
/// <param name="tradeId"></param>
/// <returns></returns>
[DisplayName("获取支付订单详情(本地库)")]
public async Task<SysWechatPay> GetPayInfo(string tradeId)
{
return await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == tradeId);
}
/// <summary>
/// 获取支付订单详情(微信接口) 🔖
/// </summary>
/// <param name="tradeId"></param>
/// <returns></returns>
[DisplayName("获取支付订单详情(微信接口)")]
public async Task<SysWechatPay> GetPayInfoFromWechat(string tradeId)
{
var request = new GetPayTransactionByOutTradeNumberRequest();
request.OutTradeNumber = tradeId;
var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request);
// 修改订单支付状态
var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber
&& u.MerchantId == response.MerchantId);
// 如果状态不一致就更新数据库中的记录
if (wechatPay != null && wechatPay.TradeState != response.TradeState)
{
wechatPay.OpenId = response.Payer.OpenId;
wechatPay.TransactionId = response.TransactionId; // 支付订单号
wechatPay.TradeType = response.TradeType; // 交易类型
wechatPay.TradeState = response.TradeState; // 交易状态
wechatPay.TradeStateDescription = response.TradeStateDescription; // 交易状态描述
wechatPay.BankType = response.BankType; // 付款银行类型
wechatPay.Total = response.Amount.Total; // 订单总金额
wechatPay.PayerTotal = response.Amount.PayerTotal; // 用户支付金额
wechatPay.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间
await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync();
}
wechatPay = new SysWechatPay()
{
AppId = _wechatPayOptions.AppId,
MerchantId = _wechatPayOptions.MerchantId,
SubAppId = _wechatPayOptions.AppId,
SubMerchantId = _wechatPayOptions.MerchantId,
OutTradeNumber = request.OutTradeNumber,
Attachment = response.Attachment,
Total = response.Amount.Total, // 订单总金额
TransactionId = response.TransactionId,
TradeType = response.TradeType, // 交易类型
TradeState = response.TradeState, // 交易状态
TradeStateDescription = response.TradeStateDescription, // 交易状态描述
BankType = response.BankType, // 付款银行类型
PayerTotal = response.Amount.PayerTotal, // 用户支付金额
SuccessTime = response.SuccessTime.Value.DateTime // 支付完成时间
};
return wechatPay;
}
/// <summary>
/// 退款申请
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("退款申请")]
[HttpPost]
public async Task<dynamic> CreateRefundDomestic([FromBody] WechatPayRefundDomesticInput input)
{
// refund/domestic/refunds
var request = new CreateRefundDomesticRefundRequest()
{
Amount = new CreateRefundDomesticRefundRequest.Types.Amount()
{
Refund = input.Refund,
Total = input.Total,
Currency = "CNY"
},
OutTradeNumber = input.TradeId,
OutRefundNumber = "R" + DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号
NotifyUrl = _payCallBackOptions.WechatRefundUrl, // 应采用WechatRefundUrl参数,如与WechatPayUrl入口相同,也应分开设置参数
Reason = input.Reason,
};
var response = await _wechatTenpayClient.ExecuteCreateRefundDomesticRefundAsync(request);
if (string.IsNullOrEmpty(response.ErrorCode))
{
// 成功了,这里应该保存退款订单信息
var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber);
// 保存订单信息
if (wechatPay != null)
{
var wechatRefund = new SysWechatRefund()
{
WechatPayId = wechatPay.Id,
TransactionId = response.TransactionId,
Refund = input.Refund,
Reason = input.Reason,
OutRefundNumber = request.OutRefundNumber,
Channel = response.Channel,
UserReceivedAccount = response.UserReceivedAccount,
};
await _sysWechatRefundRep.InsertAsync(wechatRefund);
}
}
else
{
throw Oops.Bah($"[{response.ErrorCode}]{response.ErrorMessage}");
}
return response;
}
/// <summary>
/// 获取退款订单详情(微信接口)
/// </summary>
/// <param name="refundId"></param>
/// <returns></returns>
[DisplayName("获取退款订单详情(微信接口)")]
public async Task<SysWechatRefund> GetRefundInfoFromWechat(string refundId)
{
var request = new GetRefundDomesticRefundByOutRefundNumberRequest();
request.OutRefundNumber = refundId;
var response = await _wechatTenpayClient.ExecuteGetRefundDomesticRefundByOutRefundNumberAsync(request);
// 修改订单支付状态
var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == refundId);
// 如果状态不一致就更新数据库中的记录
if (wechatRefund != null && wechatRefund.TradeState != response.Status)
{
wechatRefund.TransactionId = response.TransactionId; // 支付订单号
wechatRefund.TradeState = response.Status; // 交易状态
wechatRefund.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间
await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync();
// 有退款,刷新一下订单状态
var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.Id == wechatRefund.WechatPayId);
if (wechatPay != null)
await GetPayInfoFromWechat(wechatPay.OutTradeNumber);
}
wechatRefund = new SysWechatRefund()
{
TransactionId = response.TransactionId,
Refund = response.Amount.Refund,
OutRefundNumber = request.OutRefundNumber,
Channel = response.Channel,
UserReceivedAccount = response.UserReceivedAccount,
TradeState = response.Status, // 交易状态
SuccessTime = response.SuccessTime.Value.DateTime, // 支付完成时间
};
return wechatRefund;
}
/// <summary>
/// 微信支付成功回调(商户直连)
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("微信支付成功回调(商户直连)")]
public async Task<WechatPayOutput> PayCallBack()
{
using var ms = new MemoryStream();
await App.HttpContext.Request.Body.CopyToAsync(ms);
var b = ms.ToArray();
var callbackJson = Encoding.UTF8.GetString(b);
var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson);
if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType))
{
try
{
var callbackPayResource = _wechatTenpayClient.DecryptEventResource<TransactionResource>(callbackModel);
// 修改订单支付状态
var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackPayResource.OutTradeNumber
&& u.MerchantId == callbackPayResource.MerchantId);
if (wechatPay == null) return null;
wechatPay.OpenId = callbackPayResource.Payer.OpenId; // 支付者标识
//wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号
//wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号
wechatPay.TransactionId = callbackPayResource.TransactionId; // 支付订单号
wechatPay.TradeType = callbackPayResource.TradeType; // 交易类型
wechatPay.TradeState = callbackPayResource.TradeState; // 交易状态
wechatPay.TradeStateDescription = callbackPayResource.TradeStateDescription; // 交易状态描述
wechatPay.BankType = callbackPayResource.BankType; // 付款银行类型
wechatPay.Total = callbackPayResource.Amount.Total; // 订单总金额
wechatPay.PayerTotal = callbackPayResource.Amount.PayerTotal; // 用户支付金额
wechatPay.SuccessTime = callbackPayResource.SuccessTime.DateTime; // 支付完成时间
await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync();
return new WechatPayOutput()
{
Total = wechatPay.Total,
Attachment = wechatPay.Attachment,
GoodsTag = wechatPay.GoodsTag
};
}
catch (Exception ex)
{
"微信支付回调时出错:".LogError(ex);
}
}
else if ("REFUND.SUCCESS".Equals(callbackModel.EventType))
{
//参考:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html
try
{
var callbackRefundResource = _wechatTenpayClient.DecryptEventResource<RefundResource>(callbackModel);
// 修改订单支付状态
var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == callbackRefundResource.OutRefundNumber);
if (wechatRefund == null) return null;
wechatRefund.TradeState = callbackRefundResource.RefundStatus; // 交易状态
wechatRefund.SuccessTime = callbackRefundResource.SuccessTime.Value.DateTime; // 支付完成时间
await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync();
// 有退款,刷新一下订单状态
await GetPayInfoFromWechat(callbackRefundResource.OutTradeNumber);
}
catch (Exception ex)
{
"微信退款回调时出错:".LogError(ex);
}
}
else
{
callbackModel.EventType.LogInformation();
}
return null;
}
/// <summary>
/// 微信支付成功回调(服务商模式) 🔖
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("微信支付成功回调(服务商模式)")]
public async Task PayPartnerCallBack()
{
using var ms = new MemoryStream();
await App.HttpContext.Request.Body.CopyToAsync(ms);
var b = ms.ToArray();
var callbackJson = Encoding.UTF8.GetString(b);
var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson);
if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType))
{
var callbackResource = _wechatTenpayClient.DecryptEventResource<PartnerTransactionResource>(callbackModel);
// 修改订单支付状态
var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackResource.OutTradeNumber
&& u.MerchantId == callbackResource.MerchantId);
if (wechatPay == null) return;
//wechatPay.OpenId = callbackResource.Payer.OpenId; // 支付者标识
//wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号
//wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号
wechatPay.TransactionId = callbackResource.TransactionId; // 支付订单号
wechatPay.TradeType = callbackResource.TradeType; // 交易类型
wechatPay.TradeState = callbackResource.TradeState; // 交易状态
wechatPay.TradeStateDescription = callbackResource.TradeStateDescription; // 交易状态描述
wechatPay.BankType = callbackResource.BankType; // 付款银行类型
wechatPay.Total = callbackResource.Amount.Total; // 订单总金额
wechatPay.PayerTotal = callbackResource.Amount.PayerTotal; // 用户支付金额
wechatPay.SuccessTime = callbackResource.SuccessTime.DateTime; // 支付完成时间
await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync();
}
}
}
@@ -1,202 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 微信公众号服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 230)]
public class SysWechatService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
private readonly SysConfigService _sysConfigService;
private readonly WechatApiClientFactory _wechatApiClientFactory;
private readonly WechatApiClient _wechatApiClient;
public SysWechatService(SqlSugarRepository<SysWechatUser> sysWechatUserRep,
SysConfigService sysConfigService,
WechatApiClientFactory wechatApiClientFactory,
SysCacheService sysCacheService)
{
_sysWechatUserRep = sysWechatUserRep;
_sysConfigService = sysConfigService;
_wechatApiClientFactory = wechatApiClientFactory;
_wechatApiClient = wechatApiClientFactory.CreateWechatClient();
}
/// <summary>
/// 生成网页授权Url 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("生成网页授权Url")]
public string GenAuthUrl(GenAuthUrlInput input)
{
return _wechatApiClient.GenerateParameterizedUrlForConnectOAuth2Authorize(input.RedirectUrl, input.Scope, input.State);
}
/// <summary>
/// 获取微信用户OpenId 🔖
/// </summary>
/// <param name="input"></param>
[AllowAnonymous]
[DisplayName("获取微信用户OpenId")]
public async Task<string> SnsOAuth2([FromQuery] WechatOAuth2Input input)
{
var reqOAuth2 = new SnsOAuth2AccessTokenRequest()
{
Code = input.Code,
};
var resOAuth2 = await _wechatApiClient.ExecuteSnsOAuth2AccessTokenAsync(reqOAuth2);
if (resOAuth2.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resOAuth2.ErrorMessage + " " + resOAuth2.ErrorCode);
var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resOAuth2.OpenId);
if (wxUser == null)
{
var reqUserInfo = new SnsUserInfoRequest()
{
OpenId = resOAuth2.OpenId,
AccessToken = resOAuth2.AccessToken,
};
var resUserInfo = await _wechatApiClient.ExecuteSnsUserInfoAsync(reqUserInfo);
wxUser = resUserInfo.Adapt<SysWechatUser>();
wxUser.Avatar = resUserInfo.HeadImageUrl;
wxUser.NickName = resUserInfo.Nickname;
wxUser.OpenId = resOAuth2.OpenId;
wxUser.UnionId = resOAuth2.UnionId;
wxUser.AccessToken = resOAuth2.AccessToken;
wxUser.RefreshToken = resOAuth2.RefreshToken;
wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync();
}
else
{
wxUser.AccessToken = resOAuth2.AccessToken;
wxUser.RefreshToken = resOAuth2.RefreshToken;
await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
}
return resOAuth2.OpenId;
}
/// <summary>
/// 微信用户登录OpenId 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("微信用户登录OpenId")]
public async Task<dynamic> OpenIdLogin(WechatUserLogin input)
{
var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId);
if (wxUser == null)
throw Oops.Oh("微信用户登录OpenId错误");
var tokenExpire = await _sysConfigService.GetTokenExpire();
return new
{
wxUser.Avatar,
accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{ ClaimConst.UserId, wxUser.Id },
{ ClaimConst.NickName, wxUser.NickName },
{ ClaimConst.LoginMode, LoginModeEnum.APP },
}, tokenExpire)
};
}
/// <summary>
/// 获取配置签名参数(wx.config) 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取配置签名参数(wx.config)")]
public async Task<dynamic> GenConfigPara(SignatureInput input)
{
string ticket = await _wechatApiClientFactory.TryGetWechatJsApiTicketAsync();
return _wechatApiClient.GenerateParametersForJSSDKConfig(ticket, input.Url);
}
/// <summary>
/// 获取模板列表 🔖
/// </summary>
[DisplayName("获取模板列表")]
public async Task<dynamic> GetMessageTemplateList()
{
var accessToken = await GetCgibinToken();
var reqTemplate = new CgibinTemplateGetAllPrivateTemplateRequest()
{
AccessToken = accessToken
};
var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateGetAllPrivateTemplateAsync(reqTemplate);
if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode);
return resTemplate.TemplateList;
}
/// <summary>
/// 发送模板消息 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("发送模板消息")]
public async Task<dynamic> SendTemplateMessage(MessageTemplateSendInput input)
{
var dataInfo = input.Data.ToDictionary(k => k.Key, k => k.Value);
var messageData = new Dictionary<string, CgibinMessageTemplateSendRequest.Types.DataItem>();
foreach (var item in dataInfo)
{
messageData.Add(item.Key, new CgibinMessageTemplateSendRequest.Types.DataItem() { Value = "" + item.Value.Value.ToString() + "" });
}
var accessToken = await GetCgibinToken();
var reqMessage = new CgibinMessageTemplateSendRequest()
{
AccessToken = accessToken,
TemplateId = input.TemplateId,
ToUserOpenId = input.ToUserOpenId,
Url = input.Url,
MiniProgram = new CgibinMessageTemplateSendRequest.Types.MiniProgram
{
AppId = _wechatApiClientFactory._wechatOptions.WxOpenAppId,
PagePath = input.MiniProgramPagePath,
},
Data = messageData
};
var resMessage = await _wechatApiClient.ExecuteCgibinMessageTemplateSendAsync(reqMessage);
return resMessage;
}
/// <summary>
/// 删除模板 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "DeleteMessageTemplate"), HttpPost]
[DisplayName("删除模板")]
public async Task<dynamic> DeleteMessageTemplate(DeleteMessageTemplateInput input)
{
var accessToken = await GetCgibinToken();
var reqMessage = new CgibinTemplateDeletePrivateTemplateRequest()
{
AccessToken = accessToken,
TemplateId = input.TemplateId
};
var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateDeletePrivateTemplateAsync(reqMessage);
return resTemplate;
}
/// <summary>
/// 获取Access_token
/// </summary>
[NonAction]
public async Task<string> GetCgibinToken()
{
return await _wechatApiClientFactory.TryGetWechatAccessTokenAsync();
}
}
@@ -1,73 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 微信账号服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 220)]
public class SysWechatUserService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
public SysWechatUserService(SqlSugarRepository<SysWechatUser> sysWechatUserRep)
{
_sysWechatUserRep = sysWechatUserRep;
}
/// <summary>
/// 获取微信用户列表 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取微信用户列表")]
public async Task<SqlSugarPagedList<SysWechatUser>> Page(WechatUserInput input)
{
return await _sysWechatUserRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.NickName), u => u.NickName.Contains(input.NickName))
.WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), u => u.Mobile.Contains(input.Mobile))
.OrderBy(u => u.Id, OrderByType.Desc)
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 增加微信用户 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加微信用户")]
public async Task AddWechatUser(SysWechatUser input)
{
await _sysWechatUserRep.InsertAsync(input.Adapt<SysWechatUser>());
}
/// <summary>
/// 更新微信用户 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新微信用户")]
public async Task UpdateWechatUser(SysWechatUser input)
{
var weChatUser = input.Adapt<SysWechatUser>();
await _sysWechatUserRep.AsUpdateable(weChatUser).IgnoreColumns(true).ExecuteCommandAsync();
}
/// <summary>
/// 删除微信用户 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除微信用户")]
public async Task DeleteWechatUser(DeleteWechatUserInput input)
{
await _sysWechatUserRep.DeleteAsync(u => u.Id == input.Id);
}
}
@@ -1,407 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 微信小程序服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 240)]
public class SysWxOpenService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
private readonly SysConfigService _sysConfigService;
private readonly WechatApiClient _wechatApiClient;
private readonly SysFileService _sysFileService;
private readonly WechatApiClientFactory _wechatApiClientFactory;
public SysWxOpenService(SqlSugarRepository<SysWechatUser> sysWechatUserRep,
SysConfigService sysConfigService,
WechatApiClientFactory wechatApiClientFactory,
SysFileService sysFileService)
{
_sysWechatUserRep = sysWechatUserRep;
_sysConfigService = sysConfigService;
_wechatApiClient = wechatApiClientFactory.CreateWxOpenClient();
_sysFileService = sysFileService;
_wechatApiClientFactory = wechatApiClientFactory;
}
/// <summary>
/// 获取微信用户OpenId 🔖
/// </summary>
/// <param name="input"></param>
[AllowAnonymous]
[DisplayName("获取微信用户OpenId")]
public async Task<WxOpenIdOutput> GetWxOpenId([FromQuery] JsCode2SessionInput input)
{
var reqJsCode2Session = new SnsJsCode2SessionRequest()
{
JsCode = input.JsCode,
};
var resCode2Session = await _wechatApiClient.ExecuteSnsJsCode2SessionAsync(reqJsCode2Session);
if (resCode2Session.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resCode2Session.ErrorMessage + " " + resCode2Session.ErrorCode);
var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resCode2Session.OpenId);
if (wxUser == null)
{
wxUser = new SysWechatUser
{
OpenId = resCode2Session.OpenId,
UnionId = resCode2Session.UnionId,
SessionKey = resCode2Session.SessionKey,
PlatformType = PlatformTypeEnum.微信小程序
};
wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync();
}
else
{
await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
}
return new WxOpenIdOutput
{
OpenId = resCode2Session.OpenId
};
}
/// <summary>
/// 获取微信用户电话号码 🔖
/// </summary>
/// <param name="input"></param>
[AllowAnonymous]
[DisplayName("获取微信用户电话号码")]
public async Task<WxPhoneOutput> GetWxPhone([FromQuery] WxPhoneInput input)
{
var accessToken = await GetCgibinToken();
var reqUserPhoneNumber = new WxaBusinessGetUserPhoneNumberRequest()
{
Code = input.Code,
AccessToken = accessToken,
};
var resUserPhoneNumber = await _wechatApiClient.ExecuteWxaBusinessGetUserPhoneNumberAsync(reqUserPhoneNumber);
if (resUserPhoneNumber.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resUserPhoneNumber.ErrorMessage + " " + resUserPhoneNumber.ErrorCode);
var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId);
if (wxUser == null)
{
wxUser = new SysWechatUser
{
OpenId = input.OpenId,
Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber,
PlatformType = PlatformTypeEnum.微信小程序
};
wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync();
}
else
{
wxUser.Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber;
await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
}
return new WxPhoneOutput
{
PhoneNumber = resUserPhoneNumber.PhoneInfo?.PhoneNumber
};
}
/// <summary>
/// 微信小程序登录OpenId 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("微信小程序登录OpenId")]
public async Task<dynamic> WxOpenIdLogin(WxOpenIdLoginInput input)
{
var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
if (wxUser == null)
throw Oops.Oh("微信小程序登录失败");
var tokenExpire = await _sysConfigService.GetTokenExpire();
return new
{
wxUser.Avatar,
accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{ ClaimConst.UserId, wxUser.Id },
{ ClaimConst.RealName, wxUser.NickName },
{ ClaimConst.LoginMode, LoginModeEnum.APP },
}, tokenExpire)
};
}
/// <summary>
/// 上传小程序头像
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[DisplayName("上传小程序头像")]
public async Task<SysFile> UploadAvatar([FromForm] UploadAvatarInput input)
{
var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
if (wxUser == null)
throw Oops.Oh("未找到用户上传失败");
var res = await _sysFileService.UploadFile(new UploadFileInput { File = input.File, FileType = input.FileType }, "upload/wechatAvatar");
wxUser.Avatar = res.Url;
await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
return res;
}
/// <summary>
/// 设置小程序用户昵称
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task SetNickName(SetNickNameInput input)
{
var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
if (wxUser == null)
throw Oops.Oh("未找到用户信息设置失败");
wxUser.NickName = input.NickName;
await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
return;
}
/// <summary>
/// 获取小程序用户信息
/// </summary>
/// <param name="openid"></param>
/// <returns></returns>
[AllowAnonymous]
public async Task<dynamic> GetUserInfo(string openid)
{
var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == openid);
if (wxUser == null)
throw Oops.Oh("未找到用户信息获取失败");
return new { nickName = wxUser.NickName, avator = wxUser.Avatar };
}
/// <summary>
/// 获取订阅消息模板列表 🔖
/// </summary>
[DisplayName("获取订阅消息模板列表")]
public async Task<dynamic> GetMessageTemplateList()
{
var accessToken = await GetCgibinToken();
var reqTemplate = new WxaApiNewTemplateGetTemplateRequest()
{
AccessToken = accessToken
};
var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateGetTemplateAsync(reqTemplate);
if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode);
return resTemplate.TemplateList;
}
/// <summary>
/// 发送订阅消息 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("发送订阅消息")]
public async Task<dynamic> SendSubscribeMessage(SendSubscribeMessageInput input)
{
var accessToken = await GetCgibinToken();
var reqMessage = new CgibinMessageSubscribeSendRequest()
{
AccessToken = accessToken,
TemplateId = input.TemplateId,
ToUserOpenId = input.ToUserOpenId,
Data = input.Data,
MiniProgramState = input.MiniprogramState,
Language = input.Language,
MiniProgramPagePath = input.MiniProgramPagePath
};
var resMessage = await _wechatApiClient.ExecuteCgibinMessageSubscribeSendAsync(reqMessage);
return resMessage;
}
/// <summary>
/// 增加订阅消息模板 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "AddSubscribeMessageTemplate"), HttpPost]
[DisplayName("增加订阅消息模板")]
public async Task<dynamic> AddSubscribeMessageTemplate(AddSubscribeMessageTemplateInput input)
{
var accessToken = await GetCgibinToken();
var reqMessage = new WxaApiNewTemplateAddTemplateRequest()
{
AccessToken = accessToken,
TemplateTitleId = input.TemplateTitleId,
KeyworkIdList = input.KeyworkIdList,
SceneDescription = input.SceneDescription
};
var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateAddTemplateAsync(reqMessage);
return resTemplate;
}
/// <summary>
/// 生成带参数小程序二维码(总共生成的码数量限制为 100,000)
/// </summary>
/// <param name="input"> 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages / index ? id = AY000001 </param>
/// <returns></returns>
[DisplayName("生成小程序二维码")]
[ApiDescriptionSettings(Name = "GenerateQRImage")]
public async Task<GenerateQRImageOutput> GenerateQRImageAsync(GenerateQRImageInput input)
{
GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput();
if (input.PagePath.IsNullOrEmpty())
{
generateQRImageOutInput.Message = $"生成失败 页面路径不能为空";
return generateQRImageOutInput;
}
if (input.ImageName.IsNullOrEmpty())
{
input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss");
}
var accessToken = await GetCgibinToken();
var request = new CgibinWxaappCreateWxaQrcodeRequest
{
AccessToken = accessToken,
Path = input.PagePath,
Width = input.Width
};
var response = await _wechatApiClient.ExecuteCgibinWxaappCreateWxaQrcodeAsync(request);
if (response.IsSuccessful())
{
var QRImagePath = App.GetConfig<string>("Wechat:QRImagePath");
var relativeImgPath = string.Empty;
// 判断路径是绝对路径还是相对路径
var isPathRooted = Path.IsPathRooted(QRImagePath);
if (!isPathRooted)
{
// 相对路径
relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImage") : QRImagePath;
QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath);
}
//判断文件存放路径是否存在
if (!Directory.Exists(QRImagePath))
{
Directory.CreateDirectory(QRImagePath);
}
// 将二维码图片数据保存为文件
var fileName = $"{input.ImageName.ToUpper()}.png";
var filePath = Path.Combine(QRImagePath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
File.WriteAllBytes(filePath, response.GetRawBytes());
generateQRImageOutInput.Success = true;
generateQRImageOutInput.ImgPath = filePath;
generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName);
generateQRImageOutInput.Message = "生成成功";
}
else
{
// 处理错误情况
generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}";
}
return generateQRImageOutInput;
}
/// <summary>
/// 生成二维码(获取不受限制的小程序码)
/// </summary>
/// <param name="input">入参</param>
/// <returns></returns>
[DisplayName("生成小程序二维码")]
[ApiDescriptionSettings(Name = "GenerateQRImageUnlimit")]
public async Task<GenerateQRImageOutput> GenerateQRImageUnlimitAsync(GenerateQRImageUnLimitInput input)
{
GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput();
if (input.PagePath.IsNullOrEmpty())
{
generateQRImageOutInput.Message = $"生成失败,页面路径不能为空";
return generateQRImageOutInput;
}
if (input.Scene.Length > 32)
{
generateQRImageOutInput.Message = $"生成失败,携带的参数长度超过限制";
return generateQRImageOutInput;
}
if (input.ImageName.IsNullOrEmpty())
{
input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss");
}
var accessToken = await GetCgibinToken();
var request = new WxaGetWxaCodeRequest
{
AccessToken = accessToken,
Width = input.Width,
PagePath = input.PagePath,
};
var response = await _wechatApiClient.ExecuteWxaGetWxaCodeAsync(request);
if (response.IsSuccessful())
{
var QRImagePath = App.GetConfig<string>("Wechat:QRImagePath");
var relativeImgPath = string.Empty;
// 判断路径是绝对路径还是相对路径
var isPathRooted = Path.IsPathRooted(QRImagePath);
if (!isPathRooted)
{
// 相对路径
relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImageUnLimit") : QRImagePath;
QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath);
}
//判断文件存放路径是否存在
if (!Directory.Exists(QRImagePath))
{
Directory.CreateDirectory(QRImagePath);
}
// 将二维码图片数据保存为文件
var fileName = $"{input.ImageName.ToUpper()}.png";
var filePath = Path.Combine(QRImagePath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
File.WriteAllBytes(filePath, response.GetRawBytes());
generateQRImageOutInput.Success = true;
generateQRImageOutInput.ImgPath = filePath;
generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName);
generateQRImageOutInput.Message = "生成成功";
}
else
{
// 处理错误情况
generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}";
}
return generateQRImageOutInput;
}
/// <summary>
/// 获取Access_token
/// </summary>
private async Task<string> GetCgibinToken()
{
return await _wechatApiClientFactory.TryGetWxOpenAccessTokenAsync();
}
}
@@ -1,208 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Newtonsoft.Json;
namespace Admin.NET.Core.Service;
/// <summary>
/// 微信API客户端
/// </summary>
public partial class WechatApiClientFactory : ISingleton
{
private readonly IHttpClientFactory _httpClientFactory;
public readonly WechatOptions _wechatOptions;
private readonly SysCacheService _sysCacheService;
public WechatApiClientFactory(IHttpClientFactory httpClientFactory, IOptions<WechatOptions> wechatOptions, SysCacheService sysCacheService)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
_wechatOptions = wechatOptions.Value ?? throw new ArgumentNullException(nameof(wechatOptions));
_sysCacheService = sysCacheService;
}
/// <summary>
/// 微信公众号
/// </summary>
/// <returns></returns>
public WechatApiClient CreateWechatClient()
{
if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret))
throw Oops.Oh("微信公众号配置错误");
var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
{
AppId = _wechatOptions.WechatAppId,
AppSecret = _wechatOptions.WechatAppSecret,
PushToken = _wechatOptions.WechatToken,
PushEncodingAESKey = _wechatOptions.WechatEncodingAESKey,
})
.UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
.Build();
client.Configure(config =>
{
JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
jsonSerializerSettings.Formatting = Formatting.Indented;
config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
// config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化
});
return client;
}
/// <summary>
/// 微信小程序
/// </summary>
/// <returns></returns>
public WechatApiClient CreateWxOpenClient()
{
if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret))
throw Oops.Oh("微信小程序配置错误");
var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
{
AppId = _wechatOptions.WxOpenAppId,
AppSecret = _wechatOptions.WxOpenAppSecret,
PushToken = _wechatOptions.WxToken,
PushEncodingAESKey = _wechatOptions.WxEncodingAESKey,
})
.UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
.Build();
client.Configure(config =>
{
JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
jsonSerializerSettings.Formatting = Formatting.Indented;
config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
// config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化
});
return client;
}
/// <summary>
/// 获取微信公众号AccessToken
/// </summary>
/// <returns></returns>
public async Task<string> TryGetWechatAccessTokenAsync()
{
if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}")))
{
var client = CreateWechatClient();
var reqCgibinToken = new CgibinTokenRequest();
var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
_sysCacheService.Set($"WxAccessToken_{_wechatOptions.WechatAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
}
return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}");
}
/// <summary>
/// 获取微信小程序AccessToken
/// </summary>
/// <returns></returns>
public async Task<string> TryGetWxOpenAccessTokenAsync()
{
if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WxOpenAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}")))
{
var client = CreateWxOpenClient();
var reqCgibinToken = new CgibinTokenRequest();
var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
_sysCacheService.Set($"WxAccessToken_{_wechatOptions.WxOpenAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
}
return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
}
/// <summary>
/// 检查微信公众号AccessToken
/// </summary>
/// <returns></returns>
public async Task CheckWechatAccessTokenAsync()
{
if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) return;
var req = new CgibinOpenApiQuotaGetRequest
{
AccessToken = await TryGetWechatAccessTokenAsync(),
CgiPath = "/cgi-bin/token"
};
var client = CreateWechatClient();
var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
var originColor = Console.ForegroundColor;
if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
{
_sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WechatAppId}");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 无效");
}
else
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 有效");
}
Console.ForegroundColor = originColor;
}
/// <summary>
/// 检查微信小程序AccessToken
/// </summary>
/// <returns></returns>
public async Task CheckWxOpenAccessTokenAsync()
{
if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) return;
var req = new CgibinOpenApiQuotaGetRequest
{
AccessToken = await TryGetWxOpenAccessTokenAsync(),
CgiPath = "/cgi-bin/token"
};
var client = CreateWxOpenClient();
var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
var originColor = Console.ForegroundColor;
if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
{
_sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 无效");
}
else
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 有效");
}
Console.ForegroundColor = originColor;
}
/// <summary>
/// 获取微信JS接口临时票据jsapi_ticket
/// </summary>
/// <returns></returns>
public async Task<string> TryGetWechatJsApiTicketAsync()
{
if (!_sysCacheService.ExistKey($"WxJsApiTicket_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}")))
{
var accessToken = await TryGetWechatAccessTokenAsync();
var client = CreateWechatClient();
var request = new CgibinTicketGetTicketRequest()
{
AccessToken = accessToken
};
var response = await client.ExecuteCgibinTicketGetTicketAsync(request);
if (!response.IsSuccessful())
throw Oops.Oh(response.ErrorMessage + " " + response.ErrorCode);
_sysCacheService.Set($"WxJsApiTicket_{_wechatOptions.WechatAppId}", response.Ticket, TimeSpan.FromSeconds(response.ExpiresIn - 60));
}
return _sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}");
}
}